Built-in persona base prompts move from inline DB text / base.md into prompts/personas/<slug>.md — code-owned, PR-reviewable, drift-proof. base.md / base_coordinator.md become personas/engineer.md / orchestrator.md. Prompt source is now explicit in storage instead of inferred in app logic: a new base_prompt_file column plus CHECK (base_prompt IS NOT NULL OR base_prompt_file IS NOT NULL) — two nullable columns, never both empty. Resolution is a coalesce (base_prompt else load(base_prompt_file)), frozen into the workstream stamp at creation. base_prompt_file marks a persona as built-in (code-only, un-archivable); an operator override on a built-in is allowed and wins over the file. "Inherit the kind default" is a workstream-creation act (is_default), not a persona-row state. Migration 063: - seeds reference their file (base_prompt NULL); no runtime file reads — the backfill's frozen prompt text is inlined as a point-in-time snapshot so migration history stays self-contained and reproducible. - every existing workstream is stamped by kind (creative -> writer, else the kind default), set-based (INSERT..SELECT via temp tables) with the persona column added after the bulk writes to shorten its lock window. Storage guards (both backends): operators must supply base_prompt; built-ins can't be archived or have base_prompt_file set via the API; clearing an operator persona's only source is rejected. Follow-ups reviewed alongside (#756): soft-set visibility docstring scoped to per-process; _apply_persona_snapshot / _current_persona_snapshot own the stamp round-trip; spawn approval-header args (skill/name/target_node) flattened+capped like persona; server-side tool injection generalized to replace-only (client-def gated, incl. the xAI include forwarding). Seed copy revised (researcher soft; de-costumed prose; engineer de-biased). New test_schema_parity asserts create_all matches the alembic head. Closes #683 groundwork; ruff + strict mypy clean, full suite green.
8.8 KiB
Personas
A persona is a named, reusable bundle attached to a workstream at creation that controls how its system message is composed and what capability envelope it runs with. Personas answer a recurring operational complaint: the default composition primes every session for heavy tool use, and there was no per-workstream dial to launch a "just write prose" or "evidence-first research" session.
A persona is exactly four levers — no more:
| Lever | What it does |
|---|---|
| Base prompt | Replaces the BASE module of the composed system message. Only BASE: ENV, CONTEXT, TOOLS, and POLICIES keep composing, so mandatory prompt policies ride on top of every persona. Built-in personas source their prose from a repo file; operator personas store it inline — see Where persona prompts live. |
| Tool visibility | Which tools the session advertises. Tri-state: unrestricted (tracks tool growth and MCP catalogs), no tools (the TOOLS prompt block self-suppresses and zero definitions go on the wire), or an exact set of names. Including tool_search in a set makes it soft — tools the model discovers through search join the visible set; omitting it makes the set hard (the search pathway is disabled entirely). On commercial providers a soft set costs one prompt-cache re-prime per tool_search expansion, since each expansion rewrites the wire tool set and recomposes the prompt. |
| MCP | Whether the workstream talks to MCP at all. Session-wide: off means no MCP tools for the persona's own hands or for in-process task agents, no resource/prompt catalogs, and no listener registrations. This lever expresses infrastructure intent, not behavior shaping. |
| Memory | Whether the persona's own hands get memory: recalled-memory injection into the prompt, memory-directed metacognitive nudges, and the memory tool. Task agents keep their own envelope, and compaction spill/markers are session mechanics that are never persona-gated. An exact tool set that hides memory also mutes those nudges, and the compaction-resume pointer follows recall's visibility. |
Visibility is behavior shaping, not a security boundary: any tool call that does reach the wire still clears the same approval, judge, and policy machinery as always. RBAC and tool policies remain the enforcement layers.
Snapshot semantics — resolve once, stamp forever
The persona is resolved once, at workstream creation, and stamped into
workstream_config as five keys (persona, persona_prompt,
persona_tools, persona_mcp, persona_memory). From then on the session
reads only the stamp:
- Editing or archiving a persona never changes an existing workstream.
Rehydrate, resume, and post-compaction resume all run from the stamp.
A mid-session REPL
/resumeadopts the target workstream's stamp for prompt, tools, and memory; for the MCP lever it can only narrow in place — adopting an MCP-off stamp drops the live MCP surface, while adopting an MCP-on stamp into a session whose persona dropped MCP at construction is refused with an error telling you to reopen the workstream fresh. - A workstream outlives its persona — an archived persona keeps labelling the workstreams stamped with it.
- A partial or unparseable stamp is treated as corruption: session construction fails loudly rather than silently falling back to a default envelope the operator never chose.
- Workstreams created before personas existed carry no stamp and keep
legacy behavior, byte-identical to the
engineer/orchestratordefaults below — with one exception: pre-1.7 workstreams that hadcreative_modeset are converted by migration063into fullwriterstamps, so they resume as writing sessions rather than as legacy defaults. - Forking (
resume_wson create) resumes the source's stamped persona; the fork does not re-resolve.
Seed personas
Migration 063 seeds six personas. The two per-kind defaults carry no
overrides at all, so a zero-touch launch behaves exactly as it did before
personas existed:
| Persona | Kind | Base prompt | Tools | MCP | Memory |
|---|---|---|---|---|---|
engineer (default) |
interactive | stock | unrestricted | on | on |
orchestrator (default) |
coordinator | stock | unrestricted | on | on |
scribe |
interactive | custom (faithful structuring of given material) | none | off | off |
researcher |
interactive | custom (evidence-first) | read_file, search, web_fetch, web_search, recall, memory, tool_search (soft) |
off | on |
writer |
interactive | custom (creative writing partner — replaces the removed /creative) |
none | off | on |
executive |
coordinator | custom (delegate, interrogate plans, judge outcomes) | spawn/inspect/lifecycle tools plus memory: spawn_workstream, spawn_batch, send_to_workstream, wait_for_workstream, inspect_workstream, list_workstreams, list_nodes, close_workstream, cancel_workstream, memory (hard) |
off | on |
Notes:
scribeturns memory off deliberately: recalled memories would contaminate faithful summarization with unrelated context.researcher's set is soft (includestool_search): it starts with read and evidence tools but can pull in others on demand — e.g. loadbashto run a snippet and verify a calculation. It is evidence-first, not sandboxed; any escalated tool still hits the normal approval path.- Coordinator sessions do not merge MCP today, so the MCP lever on coordinator personas is forward-compatible bookkeeping; it bites on interactive workstreams.
Where persona prompts live
Prompt source is explicit in the persona row — two nullable columns, never both empty:
base_prompt_file |
base_prompt |
Meaning |
|---|---|---|
set (e.g. scribe.md) |
— | built-in: prose lives in prompts/personas/<file>, code-owned and PR-reviewed |
| set | set | built-in with an operator override layered on top (the inline text wins) |
| — | set | operator persona, inline prose |
A CHECK forbids the both-empty row, so resolution is a plain coalesce —
base_prompt ?? load(base_prompt_file) — with no implicit "inherit the default"
branch in application logic. base_prompt_file is set only by the migration/code
(the admin API never exposes it): it marks a persona as built-in and blocks
archive, so engineer and orchestrator can't be removed. To customise a
built-in, set base_prompt on it (clear it to revert), or create your own persona.
The resolved prompt is frozen into the workstream at creation — later edits to
a built-in's file or an operator's row never change a running workstream; only new
ones pick up the change. "No persona" is not a state: every workstream is stamped,
and an empty persona= resolves to the kind's is_default (engineer /
orchestrator).
Choosing a persona
Every creation surface takes an optional persona; empty always means the kind's default (or plain legacy behavior on a database with no personas seeded):
- Web/console: the persona select on the console launcher, the server
webui's new-workstream dialog, and the dashboard composer. Selecting a
persona requires no
persona.*permission — the picker feed (GET /v1/api/personas) is authenticated-only and returns display fields. - API/SDK:
CreateWorkstreamRequest.persona(Python:create_workstream(persona=...); TypeScript:{ persona: ... }). - CLI:
turnstone --persona <name>. Unknown or disabled names error at startup.--resumeignores--personaand adopts the resumed workstream's stamp. - Coordinator spawn:
spawn_workstream/spawn_batchtake apersonaargument, validated when the coordinator prepares the spawn and re-checked by the node that creates the child (children are always interactive-kind). Omitted means the interactive default — a child never inherits its parent coordinator's persona. Sub-agents spawned viatask_agenthave no persona parameter at all; they keep their own identity and envelope.
Authoring (console)
Personas are managed in the console's Manage → Governance → Personas tab. The admin shelf exposes exactly the four levers plus the kind list, the default marker, and archive. Rules:
nameis an immutable lowercase slug; editdisplay_nameinstead.- Exactly one default per kind, storage-enforced: flipping the flag on a successor demotes the incumbent atomically, defaults are single-kind, and a default cannot be archived.
- Archive only — there is no delete verb, so every stamped workstream's provenance stays explicable.
RBAC: persona.create / persona.read / persona.write gate the admin
CRUD (/v1/api/admin/personas); all three are granted to builtin-admin
by migration 063, and other roles opt in via role permission overrides.