Skip to content
Documentation GitHub
Agent

Role Primitive

Status: Design landing Reference epic: INK-843 ADRs: ADR-016, ADR-022

A role is the primitive for shaping how the World Agent approaches a specific invocation. It is call-scoped, structurally non-persistent, and attached as a typed input on the DSPy Signature — not as a mutation of the system prompt and not as anything written to thread history.

A Role is a value object: overlay content plus applicability metadata. The content is a string — instructions, voice, perspective, constraint framing — that the worldbuilder or the invocation site wants to impose on this particular call. The applicability metadata describes which Signatures the role should attach to; by default a role attaches to the entry Signature of the dispatched subgraph or conversation planner.

A role is not an agent identity. The World Agent has one identity (see World Agent). A role is an invocation modifier.

Multiple role sources can be active simultaneously. The planner-prompt builder resolves them into a single composed role value using a precedence chain:

  1. Call — the role explicitly attached to this specific task() dispatch or conversation prompt. Highest precedence.
  2. Thread — the role associated with the current conversation thread.
  3. Channel — the role set for the channel this conversation belongs to.
  4. Workspace — the workspace-scoped default role for agent invocations.
  5. Account — the account-level default. Lowest precedence.

Resolution produces one composed value before the dispatch. There is no runtime merging of multiple roles mid-turn. The composed value is computed once, passed once, and discarded after the turn.

The exact composition semantics — whether higher-precedence tiers block lower tiers entirely or stack on top of them — are open in the current design. The precedence order itself is fixed.

The composed role value is passed as a typed input field on the node’s DSPy Signature. This is the only attachment mechanism. It is not:

  • A mutation of the system prompt string.
  • An extra message prepended to the conversation history.
  • A side-channel property on the LangGraph state.
  • Anything written to the checkpointer or to the conversation record.

From DSPy’s perspective, the role field is just another declared input. The Signature describes it as a named field alongside other inputs (the user’s query, retrieved context, memory content, and so on). DSPy renders it into the prompt automatically, according to the field declaration. When the node returns, the input is gone.

This constraint — attachment only through the Signature input field — is settled. It is what makes call-scoping structural rather than conventional: there is no code path by which a role value leaks into thread history, because the field does not exist in the graph’s state channels and is never written to the checkpointer.

Roles are attachable at four invocation surfaces:

  • Conversation prompts. The worldbuilder sends a message; the active precedence chain resolves before the first planner node fires.
  • Skill invocations. A skill is invoked by the Skill Composer subagent; the role applies to the skill’s entry Signature.
  • Scheduled-task invocations. When the task-runner fires a scheduled task, the task’s role configuration (workspace- or account-scoped) resolves before the run begins.
  • task() dispatch. Any task() call accepts an explicit role. See Task Primitive for how roles attach at the dispatch boundary.

Workspace-scoped and account-scoped roles are stored by the Rust host and reached through MCP tool calls (role CRUD). Call- and thread-scoped roles are ephemeral; they are not stored.

Role storage is explicitly minimal: the content string, the scope tier, and a name for human reference. The World Agent does not maintain a complex role-composition database; it reads the role values through the memory client at resolution time and discards them after the turn.

It is not a permission set. Roles are prompt overlays. Capability gating is the permission system’s job (see World Agent). A role that says “act as if you have write permissions” does not grant write permissions. The permission check at the submit boundary is not affected by any role value.

It is not thread memory. Memory entries (see Agent Memory System) are durable records that survive across turns. A role is not durable; it is resolved and discarded within the turn. The boundary between “what belongs in a memory entry” and “what belongs in a role” is an open question, but the structural distinction is clear: persistence is memory’s job; call-scoping is the role’s.

It is not an agent personality product feature. Roles are a primitive. A worldbuilder-facing “give the agent a different voice for this project” experience is a product surface built on top of the primitive. That product surface is downstream; the primitive ships first.

It is not persisted in thread history. A role value never appears in the conversation record, the checkpointer, or the event log. Any replay of a conversation thread does not include role content — it includes only the observable output the role shaped.

DSPy Signatures are the declaration of a node’s LLM task — its input fields, output fields, and task description. A role is an additional input field on that declaration: named, typed as a string, present only when a role is active.

A Signature that accepts a role does not hard-code the role’s content. It declares a field and lets the resolution mechanism fill it. This means the same Signature handles all role values without modification; the role is an injected input, not a variant of the Signature.

See LLM System for how Signatures and Modules compose inside a node, and how the DSPy programming layer is separated from LangGraph orchestration.

The following are open in the current design:

  • Composition semantics. Block vs. stack: does a call-level role replace the workspace-level role entirely, or does it stack on top of it? What does “stack” mean when both specify conflicting instructions?
  • Primitive shape and naming. “Role” is the working name. Final naming may shift.
  • Thread-level role mutation. Can a role be set mid-conversation (e.g., the worldbuilder says “for the rest of this conversation, use a more critical voice”)? If so, how does the thread record the change without persisting role content?
  • It does not describe DSPy Signatures or how they are authored. See LLM System.
  • It does not describe how task() dispatches work or its shape registry. See Task Primitive.
  • It does not describe the World Agent’s identity or authority model. See World Agent.
  • It does not describe the memory tier system or durable memory entries. See Agent Memory System.
  • It does not describe how skills are invoked or composed. See Skill System.
  • It does not describe the permission system or capability gating. See MCP System.

Was this page helpful?