Files
turnstone/docs/personas.md
T
Patrick Buckley dbf389783e refactor(personas): file-backed built-in prompts, explicit source column
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.
2026-07-03 00:29:26 -07:00

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 /resume adopts 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 / orchestrator defaults below — with one exception: pre-1.7 workstreams that had creative_mode set are converted by migration 063 into full writer stamps, so they resume as writing sessions rather than as legacy defaults.
  • Forking (resume_ws on 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:

  • scribe turns memory off deliberately: recalled memories would contaminate faithful summarization with unrelated context.
  • researcher's set is soft (includes tool_search): it starts with read and evidence tools but can pull in others on demand — e.g. load bash to 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. --resume ignores --persona and adopts the resumed workstream's stamp.
  • Coordinator spawn: spawn_workstream / spawn_batch take a persona argument, 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 via task_agent have 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:

  • name is an immutable lowercase slug; edit display_name instead.
  • 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.