Skip to main content

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:

yaml
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​

yaml
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)​

ModeParams
Spawn oneagent (or specialist), task (or prompt); optional memory_seed, type (isolated/fork), wait, timeout
Spawn manyagents: [{agent, task, ...}]
Wait onerun_id, wait: true
Wait manyrun_ids: [...], wait: true
Statusrun_id (prefer wait over polling)
Listlist: true
Cancelcancel: 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.

jsonc
{"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).