Multi-Agent Systems
A coordinator agent delegates to specialist sub-agents via one
tool: agent_spawn.agent (alias Agent).
Requires coordinator role. When spawn is enabled, kv is also
injected for session-tree key-value sharing.
role: coordinator - the one value that changes behavior
agents[].role is open text (see Agents) - worker,
specialist, assistant, or a made-up label like developer are
all just descriptive. The exact string coordinator is the single
exception: it's what gates the automatic specialist-prompt injection
below, and what the compiler checks when it needs to guess an
entry_agent for a mode that requires one. Spelling it any other
way (Coordinator, coordinater, ...) silently gets you a
non-delegating agent with no error - the string match is exact.
delegate_to - required for the coordinator to know its team
delegate_to: [id, id, ...] on the coordinator is not optional
decoration - it's what actually builds the coordinator's own
specialist roster. Without it, the coordinator has no idea which
specialists exist or what each one is for, even if agent_spawn is
granted: the coordinator's context only gets a specialist list when
role: coordinator and delegate_to is non-empty - for each id
in delegate_to, it pulls that agent's own specialty: string and
hands the coordinator an {id, specialty} pair to work from.
Each entry can also be a mapping instead of a bare id, to attach
per-relationship delegation instructions - text the daemon injects
into the coordinator's own prompt automatically, right under that
specialist's line, instead of the user hand-writing "when to delegate"
logic into the main system_prompt:
delegate_to:
- id: billing_agent
instructions: "Delegate here whenever the question mentions invoices or payments."
- support_agent # bare id form still works, mixable in the same list
The compiler validates both ends of this: every id in delegate_to
(bare or {id, ...} form) must be a real agent in the same file (a
typo gets a "did you mean" suggestion), and the delegation graph must
be acyclic - a cycle (a delegates to b, b delegates back to a)
is a compile error. A delegate_to on an agent while agent_spawn is
never declared/granted anywhere in the app is also a compile error
(DGT-E0409): it would describe specialists the agent has no tool to
actually call.
At runtime, an agent can only actually spawn an id from its own
delegate_to list - the agent/agent_spawn tool call is rejected
(fail-closed) for any other target, even one that exists elsewhere in
the app. delegate_to is therefore both the prompt content and the
real authorization boundary, not just documentation for the model.
Common mistake: hand-writing the roster into system_prompt
It's tempting to just tell the coordinator about its team in prose -
system_prompt: "You have two specialists, X and Y..." - and skip
delegate_to entirely. That reads as working right up until the
first Agent(specialist="X", ...) call, which the runtime rejects
because X was never actually declared as a delegate: the prose
told the model about a specialist, but told the daemon nothing.
Put the roster in delegate_to (with specialty: and/or
instructions: for the "why"/"when") and let the daemon build the
prompt block; reserve system_prompt for how the coordinator should
behave once it has the roster, not for restating who's on the team.
Delegation chains
Nothing restricts delegate_to to the entry agent. A specialist can
itself be a coordinator with its own delegate_to and its own
sub-specialists - main delegates to triage, triage (now also
role: coordinator, with its own delegate_to) delegates further to
billing and refunds. Each link only needs agent_spawn granted
somewhere in the app (it's an app-wide gate, not per-agent) and each
coordinator's own delegate_to naming its own direct reports - a
grandparent does not need refunds in its own list just because a
descendant delegates to it.
YAML sketch
agents:
- id: coordinator
role: coordinator
delegate_to: [researcher, writer]
system_prompt: "Delegate with the agent tool. Prefer fan-out."
- id: researcher
role: specialist
specialty: "Read-only research: gathers facts and returns a summary."
system_prompt: "Answer the delegated task briefly."
- id: writer
role: specialist
specialty: "Drafts the final answer from the coordinator's brief."
system_prompt: "Write the final answer, clearly and briefly."
tools:
modules:
agent_spawn: {}
filesystem: {}
capabilities:
grant:
- module: agent_spawn
- module: filesystem
Specialists are other entries in agents[], role: specialist with
a specialty: line describing what they're for (that's the text the
coordinator actually sees via delegate_to) - not role: assistant.
Exact grant / enable wiring follows the running daemon policy builder.
agent modes (real params)
| Mode | Params |
|---|---|
| Spawn one | agent (or specialist), task (or prompt); optional memory_seed, type (isolated/fork), wait, timeout |
| Spawn many | agents: [{agent, task, ...}] |
| Wait one | run_id, wait: true |
| Wait many | run_ids: [...], wait: true |
| Status | run_id (prefer wait over polling) |
| List | list: true |
| Cancel | cancel: true, run_id (optional cancel_tree) |
Default spawn is non-blocking. There is no reassign
param in the Go handler. Catalog Spec lists only agent/task;
the live AgentToolSpec schema is what the LLM sees.
{"name": "agent", "arguments": {"agent": "researcher", "task": "Capital of France?", "wait": true}}
{"name": "agent", "arguments": {"agents": [{"agent": "researcher", "task": "A"}, {"agent": "researcher", "task": "B"}]}}
{"name": "agent", "arguments": {"run_ids": ["r1", "r2"], "wait": true}}
{"name": "agent", "arguments": {"list": true}}
type: "fork" (or inherit_context) inherits parent conversation
context; default is isolated (task + optional memory_seed only).
Related
- agent_spawn
- Flows for deterministic graphs
- Built-in tools