Casola Avatar Interaction API

4. Prompt, backstory & overrides

This is the core of avatar configuration: what the avatar knows about itself, and who gets to change it.

The rule

Edit in the UI → it becomes the avatar’s default, stored and versioned. Pass as an API parameter → it overrides for that session only, and is never stored.

Both write to the same three fields — system prompt, backstory, tools — plus the config group. They differ in durability, not in power:

  Where it is written How long it lasts Visible in the dashboard
Dashboard edit A new profile row Until you change the default again Yes — with a version history
POST /api/v1/avatars/:id/profiles A new profile row Same — this is the UI’s API Yes
Mint parameter (context.*, tools, config) Nowhere One session No

An API caller can do either. POST …/profiles is the “make this the new default” call; the mint parameters are the “just for this conversation” call. Reaching for the wrong one is the mistake this page exists to prevent.

How a prompt is assembled

Every session composes one system prompt on the GPU box, in this order:

[Core]        Casola-owned safety and speech rules — always present, never overridable
<persona>     the behaviour prompt      ← system_prompt
[Backstory]   who this character is     ← backstory
[Tools]       callable tool definitions ← tools
[Memory]      per-session memory block  ← context.memory_block
[Language]    reply-language preference ← response_language

[Core] is owned by the box and ships with its code: reply length, no markdown, mirror the user’s language, safety rules. Nothing you send can weaken or replace it. That is what makes it safe to expose prompt editing to end users in a first-party UI.

The resolution ladder

For each of system_prompt, backstory and tools, independently:

1. the session parameter    (context.system_prompt / context.backstory / tools)
2. the profile              (pinned via profile_id, else the avatar's default profile)
3. the sealed version       (what the avatar was created with)

The first layer that has a value wins. Layers replace, they do not stack.

Resolution is per field. Overriding one field leaves the others resolving normally:

{ "avatar_id": "019fe463-…", "context": { "backstory": "A support specialist at Acme." } }

→ backstory from the session, system prompt from the profile, tools from the profile. Not “session layer wins everything”.

Worked example

Given avatar Lyra:

Layer system_prompt backstory tools
Sealed version (created by the wizard) “You are Lyra…” “A former archivist…”
Default profile (edited in the UI) “You are Lyra, Acme’s guide.” lookup_order
Session parameters “Speaking with a returning customer.”

Resolved for this session:

Field Value source
system_prompt “You are Lyra, Acme’s guide.” profile
backstory “Speaking with a returning customer.” session
tools lookup_order profile

Note the backstory: the profile had none, so without the session parameter it would have fallen through to the version’s “A former archivist…”. Falling through is per field and goes all the way down.

Replacing vs. adding

Two different jobs, two different parameter families. This is the second most common mistake after confusing defaults with overrides.

  Parameter Effect Cap
Replace context.system_prompt Discards the profile/version prompt entirely 40,960
  context.backstory Discards the profile/version backstory 16,384
  tools Discards stored tool config
Add extra_system_prompt Appended after whatever won 2,048
  extra_backstory Appended to the backstory block 2,048
  extra_tools Merged into whatever tools resolved

If you want “the avatar as authored, plus one sentence of context about this caller”, you want the extras — not a replacement:

{
  "avatar_id": "019fe463-…",
  "extra_system_prompt": "The caller is Dana, a Pro customer since 2024. Their last ticket was about billing.",
  "extra_backstory": "You and Dana have spoken twice before."
}

Restating the whole persona just to append a sentence is the anti-pattern extras exist to remove — it also costs you first-turn latency (see Performance). Combined extras are capped at 3,072 characters because they ride inside the session token itself.

Use a replacement when the avatar genuinely plays a different role this session — the same face and voice hosting a different scripted scenario, for example.

Profiles: the stored layer

A profile is a row holding system_prompt, backstory, tools_json and config_json. Profiles are append-only and hash-deduped: saving identical content returns the existing row rather than creating a duplicate. An avatar’s default_profile_id is what every mint without a profile_id resolves to.

Profiles are cheap by design. Editing text does not re-seal the avatar’s bundle, does not change its content_hash, and does not make the GPU fleet re-download anything — which is exactly why prompt editing is a different operation from publishing a new version.

Create a profile (this is what “Save” in the UI does)

curl -sS -X POST https://api.casola.ai/api/v1/avatars/019fe463-…/profiles \
  -H "Authorization: Bearer $CASOLA_SECRET_KEY" \
  -H "content-type: application/json" \
  -d '{
    "system_prompt": "You are Lyra, Acmes product guide. Be concise and concrete.",
    "backstory": "A former archivist who now helps people find things.",
    "tools_json": { "tools": [ { "name": "lookup_order", "description": "Look up an order by id" } ] },
    "config_json": { "greeting_enabled": true, "greeting_hold_ms": 2000 },
    "set_default": true
  }'
{
  "id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33",
  "avatar_id": "019fe463-fbd3-7000-80e8-85dad81d3fee",
  "hash": "a2271f20509d8c…",
  "hash_short": "a2271f20509d",
  "created_at": 1786249001,
  "is_default": true,
  "created": true
}
Detail Behaviour
set_default: true Makes it the avatar’s default — the dashboard sends this by default
created: true + 201 A genuinely new profile
created: false + 200 Identical content already existed; you got that row back
Omitted field Stored as null — meaning “fall through to the version”, not “empty string”

The 200 vs 201 distinction is what lets an editor tell the user “no changes — this is already version a2271f20509d” instead of silently appearing to save nothing.

The rest of the profile API

Route Method Purpose
/api/v1/avatars/:avatarId/profiles GET List (metadata + sizes, no bodies) + default_profile_id
/api/v1/avatars/:avatarId/profiles/:profileId GET One profile with its text bodies
/api/v1/avatars/:avatarId/profiles POST Append (above)
/api/v1/avatars/:avatarId/profiles/:profileId/default POST Switch the default
/api/v1/avatars/:avatarId/profiles/:profileId DELETE Delete a non-default profile

The list route returns sizes rather than bodies — a list of forty 40 KB prompts is not a list. Read one profile to prefill an editor.

// GET /api/v1/avatars/:avatarId/profiles
{
  "data": [
    {
      "id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33",
      "hash": "a2271f20509d8c…",
      "hash_short": "a2271f20509d",
      "created_at": 1786249001,
      "created_by": "usr_3f21…",
      "is_default": true,
      "sizes": { "system_prompt": 812, "backstory": 240, "tools_json": 318, "config_json": 46 }
    }
  ],
  "default_profile_id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33"
}
// GET /api/v1/avatars/:avatarId/profiles/:profileId  same fields plus the bodies
{
  "id": "019fe4c1-…",
  "avatar_id": "019fe463-…",
  "hash_short": "a2271f20509d",
  "is_default": true,
  "sizes": {  },
  "system_prompt": "You are Lyra, Acmes product guide. Be concise and concrete.",
  "backstory": "A former archivist who now helps people find things.",
  "tools_json": { "inline": [  ] },
  "config_json": { "greeting_enabled": true }
}

Switching the default is how you roll back a prompt change: profiles are immutable, so the previous one is still there.

curl -sS -X POST https://api.casola.ai/api/v1/avatars/019fe463-…/profiles/019fe4a0-…/default \
  -H "Authorization: Bearer $CASOLA_SECRET_KEY"

Deleting the current default is refused (409 profile_is_default) — switch first, then delete. Otherwise the avatar would silently fall back to its sealed version’s text, which looks like data loss to whoever is talking to it.

Pinning a profile for one session

{ "avatar_id": "019fe463-…", "profile_id": "019fe4a0-…" }

Runs a non-default profile without changing anything stored — A/B testing a prompt, or a “chat with this version” affordance in your own UI. The profile must belong to the resolved avatar (400 profile_mismatch): asking for specific text and silently getting different text is precisely the failure profiles exist to remove.

Avatars created before profiles existed

Their profile #1 is synthesized lazily from their published version’s text on first read or mint. You never see an avatar with no profile; you may see one whose profile was created later than the avatar.

Knowing what actually ran

Every mint response echoes the text layer it resolved:

"profile": { "id": "019fe4c1-…", "hash_short": "a2271f20509d", "source": "default" }
source Meaning
param You pinned it with profile_id
default The avatar’s default profile
none No profile — the sealed version’s own text ran

Log hash_short alongside your own session records and “which prompt was this user talking to?” stays answerable months later. The box additionally logs a system_prompt_final record with the full composed text, its SHA, and the winning layer per field.

Limits

Field Cap (characters)
context.system_prompt / profile system_prompt 40,960 (~10k tokens)
context.backstory / profile backstory 16,384
extra_system_prompt 2,048
extra_backstory 2,048
Combined extras 3,072
context.memory_block 4,096
context.history 40 messages / 16,384 bytes
config (serialized) 1,024 bytes

Exceeding a cap on a session parameter is a 400 — it is your input, so you should hear about it. Exceeding it on a stored profile clamps at mint with a server-side warning rather than failing the session, because stored data may have been authored by someone else at another time and a live call is the wrong place to discover it.

Performance

The composed prompt is re-sent to the language model on every turn. The [Core] block and the avatar’s own bundled persona are shared across sessions and stay warm in the model’s prefix cache; a session-level override breaks that sharing beyond [Core] for that session, so the first turn pays extra prefill proportional to the prompt size. Later turns in the same session are cached.

Practical guidance:


Next: 5. Configuration & tools →