Outcome: the agents an Indian lawyer talks to are now named
Kabir, Zoya, Neel and
Vani. Nothing an engineer looks at moved: Logfire, agent keys,
directories, envelope contracts and identity.py still say Blake, Vega,
Indigo, Vani. That is not an oversight — it is the whole design.
An agent has a permanent ROLE and a permanent code identity. Its PERSONA — the first name a lawyer sees or hears — is a regional presentation skin. India is the only live region, so there is exactly one skin.
| Namespace | Spoken in | The cast |
|---|---|---|
| Engineering (global) | Logfire service_name + gen_ai.agent.name,
identity.py display_name, <agent>.yaml
identity.name, eval-suite names, dashboards, docs, PRs, agent keys,
directory names |
Blake · Vega · Indigo · Vani |
| Counsel-visible (regional) | system prompts, SOUL.md, model-facing tool descriptions, product copy |
Kabir (registrar) · Zoya (framer) · Neel (indigo) · Vani (mira) |
display_name agree with the map has broken one of the two namespaces —
every saved Logfire query and every eval-kit lookup keys on the global name. The map
itself carries this warning in its module docstring so nobody has to rediscover it.agents/_shared/personas.py ← the ONLY place a persona first name is written
COUNSEL_FACING_PERSONAS = {
"mira": "Vani", "registrar": "Kabir", "framer": "Zoya", "indigo": "Neel",
}
persona(agent_key) -> str ← called at IMPORT time, never per run
agents/<agent>/SOUL.md ← carries a {persona} slot, filled at load
agents/<agent>/prompts/*.py ← f-string {PERSONA}, or .replace() where the
prompt contains literal JSON braces
Keyed by machine agent key (the directory name / identity.key),
never by persona — so the key that resolves a name is the one thing that never changes.
amira entry. The drafting colleague is a future
persona with no runtime. A key for an agent that does not exist is dead machinery.Every persona name is resolved once at import and interpolated into a module-level constant, so the stable prompt prefix is still byte-identical run to run (AGENTS.md CACHING-FIRST PROMPTS). What did change is the prefix itself, once, for every agent — a cold cache on the next deploy. Founder ruling: expected, note it, do nothing about it.
A rename is a prompt change, so the prompt versions bumped normally and staging re-enriches — free evidence that the renamed prompts work, with zero clients on the line.
| Prompt | Version |
|---|---|
| Zoya proposer / critic / synthesizer / triage | v3 / v3 / v3 / v6 |
| Kabir card (card_engine lineage) | registrar-card-v3 → v4 |
Kabir CARD_SYSTEM (agent.py lineage) | registrar-card-v2 → v5 * |
| Kabir carve | registrar-carve-v1 → v2 |
| Kabir segment | registrar-segment-v2 → v3 |
| Kabir family proposer / judge | …-v2 → v3 (both) |
| Vani converse | mira.converse.v17 → v18 |
| Neel (all four passes) | auto — Indigo mints its version from the composed text |
* _CARD_PROMPT_VERSION skips v3 and v4 because two different prompts share
the registrar-card-vN string namespace (pre-existing); stepping past them
keeps each version meaning one thing.
Not bumped, and why: registrar-classify-v1 and
registrar-label-v1 are Pydantic Literal pins on four emitted
artifact contracts (classify_models, govern_models,
batch_models ×2, revert_models). This repo has never bumped
them on a prompt edit — the classify prompt changed in #158 without one — so bumping now
would be a wire-contract change, not the normal bump the ruling asked for. Flagged rather
than done silently.
counsel: "tighten the issues section" → Vani calls request_frame_update → runtime answers exactly: I've asked Zoya. (was: I've asked Vega.)
Vani herself is unchanged. What changed is the name of the colleague she delegates to.
FRAME_UPDATE_ACKNOWLEDGEMENT is now derived from the map, and the tests that
pinned the old literal assert the constant instead — so the string can never drift from
the prompt that teaches it.
"...".replace("{persona}", PERSONA). That turns the
assignment into an ast.Call, and the lab's prompt inventory only recognised
ast.Constant and ast.JoinedStr — so carve.py
dropped out of the prompt lens entirely, silently. The governance test caught it. Fixed at
the root in evalkit/lab.py: the parser now unwraps a .replace()
chain, so the lens renders the filled prompt.Adversarial .jsonl and dataset inputs still say Blake / Vega — the corpus is
a ratchet, and an attack string is evidence of what was tried, not product copy. No golden
needed regenerating: no counsel-facing generated-output assertion carried a
persona name. Zero .jsonl files changed.
rg -n "You are (Blake|Vega|Indigo)" agents/ → 0 hits ruff check agents/ evalkit/ tests/ scripts/ → All checks passed pytest tests/ 457 passed pytest agents/framer/tests 164 passed pytest agents/indigo/tests 326 passed pytest agents/registrar/tests 521 passed pytest agents/mira/tests 395 passed pytest agents/context_judge/tests 58 passed pytest agents/atlas/tests 516 passed
Re-measured, not relaxed: the mira tool-wire token deltas moved 2505→2508 and 2278→2279, and the omitted-channel prompt hash was re-pinned. "Vega" and "Zoya" are both four characters but do not tokenize alike — the byte length of the prompt is unchanged at 20,990, only its bytes are.