Documentation — Subagents and delegation
Concepts

Subagents and delegation

A subagent is a separate agentd process that the agent starts to do one bounded piece of work and return a short answer. This page explains why delegation in agentd is a process tree, what crosses the boundary between parent and child, how a child lives, ends and restarts, and how the instance tier runs a whole child daemon. It is for reviewers who need the model and for builders who want to know what they are configuring; the task guide is Delegate to subagents.

Why delegation is a process

The context window is the scarcest resource an agent has. "Which of these 300 files call the auth helper without a timeout?" has an answer, but finding it in the agent's own window means paying for 300 file reads the agent never needs again, and carrying them for the rest of the run.

A subagent buys the answer without the receipts. It reads the 300 files into its own window and hands back a distilled value: at most 8,000 bytes for a string result, and 400 characters for the note the agent's conversation may receive. The search happened; its transcript stays in the child. Two more properties come with the process boundary.

An injection firewall. No protocol path carries a child's transcript up to the parent, and nothing hands the parent's transcript down: the child starts from what the caller chose to pass. Before a child exists, its narrowed set of MCP servers is checked against the lethal trifecta (untrusted input, sensitive data and egress in one agent), and a child's result is withheld from a reader that holds both sensitive and egress tools when the child may carry outside input. Security model explains both checks.

A bounded blast radius. A subagent has its own process group, memory, MCP connections and step, token and deadline budget, optional kernel limits, and, where cgroups are configured, its own cgroup leaf. Killing its process group is instant and total. A wedged child costs you a child, not the daemon.

The price is that delegation is lossy by construction, and a child is one bounded agent loop, not a durable pipeline. When the parent needs the raw material, or the work is a sequence with retries and waits, a subagent is the wrong tool; the guide's when not to delegate section lists the cases.

One flat generation of children

agentd ships as one binary. To make a child it re-executes itself with AGENTD_SUBAGENT=1 set, and the new process runs the subagent entry point instead of the daemon. The supervisor, called the reactor, owns the configuration, the credentials, the durable store and the process table, and runs no agent loop. Every agent loop runs in a child: a turn worker for a conversation's turn, a think child for preflight, compaction, think and decide, or a subagent. The supervisor's only model calls are the goal check and the approval judge, made on threads of its own with no tools.

The tree is flat. Every child is a direct child of the reactor, keyed by a node id in one table, so there is no supervision hierarchy to walk.

So when a turn worker calls subagent.run, it asks the reactor for a sibling, not a descendant: the caller does not own the child process. Each of these children sets PR_SET_PDEATHSIG(SIGKILL) as its first act, and one whose parent died during the fork notices (its parent is no longer the pid that spawned it) and exits, so a dead daemon takes its children with it. A child's stdout is a binary control channel to the reactor, and its stderr is inherited into the daemon's log.

What the child is handed

The spawn payload is the whole trust boundary. It travels as the first control frame on the child's stdin, never in argv (readable by anyone through /proc) and never in the environment. The child's environment holds only the baseline, the variables its connections' secret references, routed aws credentials and the top-level model connection's AGENTD_INTELLIGENCE_TOKEN* read, and spawn.env_passthrough; text in its conversation that spells a secret reference adds nothing (Security model).

The child getsHow it is decided
Its instructionThe caller's instruction, or a template's text with its parameters folded in
A seed transcriptOnly the context messages the caller passed; empty by default
MCP serversThe servers the caller named, from the parent's configured set; every parent server when servers is omitted
A tool grantThe tools patterns, minted by the supervisor and enforced by the child over its catalogue and its dispatch
An output contractoutput_contract prose, or output_schema folded into that prose as text
A modelThe template's model, else subagents.defaults.model, else the agent's turn model, resolved to a connection with a live credential
BudgetsSteps, tokens and a deadline, plus optional memory and CPU limits and a priority
The agent's identityWith security.aauth, the daemon's: the key file's path, the agent token the daemon holds and the tokens the tree holds for each server, so the child signs its MCP, model and A2A requests with the daemon's key and asks the Agent Provider for nothing until that token is due a refresh; a token the child earns from a server is handed back for the children after it (AAuth agent identity)

The tool grant can only narrow. A caller cannot forge or widen it through context, because the supervisor's mint replaces any grant already in the seed, and a grant the child cannot parse narrows to nothing rather than to everything. An excluded tool is unreachable, not merely unadvertised.

What the child does not get matters as much:

  • No agentd built-in tools. The child's catalogue is the tools of its granted MCP servers, any code-registered tools of an embedding build, and resource.read when those servers expose resources. It has no subagent.*, workflow.*, memory.* or a2a.* tools, so it cannot delegate onward.
  • No A2A peers. The list is empty on spawn, so a child cannot reach another agent.
  • No stored credential. The durable record keeps the model reference, not the resolved connection, and no AAuth identity; a restore resolves the model again and hands the child the running daemon's identity.

The child connects to its servers itself, and offers exactly the MCP tools the agent's registry offers a subagent on them, under the registry's names: the supervisor sends the list with the spawn. A server's allow and exclude (a referenced service's ceiling included), its ns, a profile server's contract names and tools.disabled reach the child that way, and a spawn's tools grant, a policy rule and the calls routed back to the supervisor all name the same tool.

Two calls still come back to the supervisor. With security.policies set, the supervisor names every tool of the child's servers that a policy rule could touch, and the child sends those calls up instead of making them, so the rule (and any approval it asks for) applies to the child too. An MCP server's elicitation request reaches the operator the same way.

A child acts for whoever spawned it: its records carry the spawning principal, and a tool call that names a handle (subagent.status, subagent.kill and the rest) acts only for that handle's owner. Another principal's handle reads as unknown.

One chokepoint for every spawn

Every subagent.run, from the agent's turn or from a workflow step, lands in one function in the reactor and passes ordered gates before any process exists: a non-empty instruction, a valid mode, live breadth, lifetime total, delegation depth, the spawn rate, resource pressure, the trifecta check over the narrowed servers, and well-formed memory, CPU and priority values (a priority: low spawn is shed one pressure level early). Each refusal is a tool result the model reads and can adapt to, or a failed step in a workflow, never a crash. Supervision and containment describes the gates and the pressure signals.

Depth is minted, never claimed. The reactor reads the requester's depth from its stored record and gives the child that depth plus one, so a child cannot claim to be shallower than it is. In practice depth stops at 1: a child is offered no subagent.* tool, and the limits.subagents.depth cap is the backstop behind that.

Only after the gates does the reactor mint a handle (sub-<n>), stage a subagent/<handle> record, and fork. A successful fork marks the record running and persists it at once, so a restart finds a child to re-spawn. A crash inside the fork window leaves no record, and the half-born child collapses on its own.

Four modes

ModeThe child runsThe caller gets
syncOne bounded agent loop, up to its step budget, then exitsThe result when it lands; the call parks without blocking the reactor
asyncThe same single loopA handle at once; subagent.await or a workflow wait collects
detachedThe same single loopA handle at once; nobody waits, and a crash does not revive it (a drain's stop does)
warmStays alive and runs one turn per messageA handle at once; subagent.send steers it

A warm subagent prepares its session once, runs its instruction as the first turn, then runs one turn per message sent with subagent.send. Messages queue inside the child, so a message sent mid-turn is not lost. Each turn gets a fresh step, token and deadline budget, so one expensive reaction cannot starve the session. A warm session also rides out a model outage with the all-endpoints-down backoff, where a one-shot child fails at once instead.

How a result comes back

The reactor distils before it stores. A string longer than 8,000 bytes is cut at a character boundary and marked … [truncated]; a string that parses as JSON is re-parsed, so the caller receives an object rather than a quoted blob. An object or array is not capped: it passes untouched into the record, the store and the caller's result. A child that ran out of budget still returns the last text it wrote.

Where the result lands depends on who asked. A tool caller gets {handle, status, result, error}, and so does a workflow step as its output. A plan item bound to the handle advances on its own, in every conversation. The agent's root conversation receives a note (subagent <handle> <status>: …, at most 400 characters) only when agent.wake_on includes subagent_result, which the default does not, and a warm child notes each finished turn the same way. A note, like every read-back, is withheld from a reader that holds both sensitive and egress tools when the child may carry outside input.

Lifecycle

A flat child's record moves through a small set of statuses. All but spawned and running are terminal, the set every waiter tests against: completed, failed, cancelled, refused, killed, crashed, retired, exhausted_steps, exhausted_tokens, deadline, stalled and loop_detected.

Every status a child ends with is final, its budget's included. A child that dies without reporting (killed by a signal, out of memory, torn down by the supervisor as stuck or past its deadline) ends failed with the reason "exited without a result", unless subagent.kill stopped it. The child's deadline bounds a model call in flight, so it ends deadline when the deadline passes.

Liveness is probed, not assumed. The reactor pings each child, and the child's control thread answers separately from its agent loop. A child that reports events is healthy; one that is silent but answers pings is busy (a 20-minute model call reads as busy and is left alone); one that does neither (120 seconds without events, 10 without a pong) is stuck and is torn down. Every child also has a hard deadline: its own deadline plus 60 seconds, armed once at spawn.

subagent.kill ends a child within about 7 seconds. It marks the record cancelled, with the kill's reason as its error, and sends the child a cancel frame, both at once. A child between steps (waiting for the supervisor to answer a tool call, or a warm child waiting for its next message) reads the frame and exits, and is never signalled. A child inside a model call or an MCP tool call cannot read it until that call returns, so the kill puts it on the kill ladder: 5 seconds after the kill, if it is still running, its process group gets SIGTERM, and 2 seconds later SIGKILL. Each signal is logged as subagent.kill.escalated. What the child does after the kill does not change its record: a result it reports in those seconds, or its death by a signal, leaves it cancelled, and the next start does not spawn it again. Whatever waits on the child is answered cancelled when the kill lands. The calls the child finished are charged, since it reports each as it returns; the one a signal cuts off reports nothing.

A drain lets a child finish. A sync, async or detached child runs on through a drain and records its result as usual; only one still running at lifecycle.drain_timeout, or when a second signal forces the drain, is killed. A warm child never ends on its own, so the drain cancels it as it begins. A child the drain stops is recorded parked, never failed, and the next start spawns it again — except a sync child a turn was waiting on, which is recorded cancelled: the drain stops that turn too, and its replay asks for the child again, so a parked one would run twice. Supervision and containment covers liveness and the kill ladder in full.

Restart is not resume

At startup the reactor reads every subagent/<handle> record and re-spawns each one that is not terminal, except a detached child a crash left running, which was deliberately abandoned; a detached child a drain parked comes back. The re-spawn bumps the record's attempt, and the child's model is resolved again against the live configuration, because the stored payload holds no credential. A reload resolves running children's models again too, and each applies the new connection at its next turn boundary under intelligence.swap_policy.

Two things do not survive a restart:

  • The conversation. A restored warm child replays its original instruction and seed; no transcript is persisted.
  • In-memory waiters. A sync call from a turn is gone with the process; the result reaches the parent through plan bindings and the root note only. A workflow subagent step or wait {on: subagent} is a durable step record, and it does resume.

Design delegated work to be idempotent across a restart: a re-spawned child does its work again from the start.

Limit. Handle numbers restart at 1 in every process, while restored records keep their handles, so a new spawn can take the handle of a restored record and overwrite it. Treat handles as valid for one process lifetime.

A record with durable: false (on the call, the template, subagents.defaults, or deployment-wide through store.durability.work: ephemeral) lives in memory only: it is never persisted and never restored, which suits throwaway workers.

Templates and the instance tier

A template turns "what a worker here looks like" into an operator-declared, reviewable definition. The model may instantiate a template but never author one, and subagents.allow_freeform: false makes templates the only way to spawn. The model fills only the template's declared parameters: each value is checked against its declared type, enum and required flag, then folded into the definition as data. A value is never parsed for directives, and a spawn whose values would add machinery, an author note or end matter to the text is refused. An instance-tier spawn whose value spells a secret reference ({{secret:…}} or {{secret-file:…}}) is refused too: folded into the child's configuration, it would have the child resolve a credential, and be started with its variable, at the caller's choice.

A template's tier follows from its text, so it cannot be declared wrongly:

  • Flat. An instruction with no machinery spawns the flat worker this page has described, through the same chokepoint and caps. The template defines the grant (servers, tools); its limits, mode, priority and output contract apply where the call site sets none of its own.
  • Instance. An instruction that defines machinery, such as a :::!workflow, :::!mcp, :::!stream, :::!config or :::!tools block, spawns an instance-tier child: a full agentd daemon with its own workflows, streams, waits, timers and store.

An instance child is the same binary started with a configuration file the parent composes. The parent writes the template's machinery, the delivered prose with its parameters, its own intelligence section (without its budget, its routes or any inline token, and with the template's model and budget), its service catalogue, egress posture and TLS trust, its AAuth provider and key file, a store, and an A2A listener on a unix socket in the child's own directory. Model credentials reach the child through the environment, never the file, and the child loads the AAuth key without ever creating one and fetches its own agent token for it when it starts. The parent can address the child by its handle (inst-<n>) at once, over the child's socket. The child reaches the parent only through the channel the parent hands it at spawn (below), and is composed no peer, bearer or other credential of the parent's. Neither link is AAuth-signed, so neither waits on the Agent Provider.

The child is a daemon, so its prose is its policy: it runs nothing when it spawns, and work at spawn is a :::!workflow with a once start or a first message, such as subagent.send or an a2a.send step.

Four mechanisms ride that wiring:

  • A result. With mode: sync and result: {workflow: <name>}, the spawn parks until the named workflow first completes in the child, then resolves with that run's output. agentd composes a small reporter workflow, built from ordinary nodes, that sends the reserved _instance.result command home. The child keeps running afterwards.
  • Mirrored streams. mirror_streams forwards every event of a child's stream into the parent's stream of the same name, with source instance:<handle>, so the parent's consumers see a fleet's events without knowing which child wrote them.
  • Budget metering. A durable child's token use is charged against the parent's budget: the parent reads the child's lifetime total from its manifest every five seconds, once more when the child exits and when a restarted parent finds it again, and charges the difference. What has been charged is kept on the child's record, so a restart does not charge it a second time. A non-durable child has no manifest, so its use is not metered.
  • Retirement. ttl elapsing, the until signal arriving in the child, subagent.retire, or the parent's own shutdown all take one path: the child receives SIGTERM (a parent's death reaches it the same way), stops admitting work, drains its runs, checkpoints and exits. The parent kills it only if it is still running five seconds after the parent's drain timeout. The record closes as retired, and a durable child's directory stays on disk as the audit trail. A child that exits on its own closes as completed, failed or crashed; for one that failed, the parent's instance.exited event carries the cause the child logged as it exited.

Across a parent restart, live instance children are started again from their composed configuration. Their state is durable and the handle is their identity, so a child picks up where it stopped. A non-durable child runs on a memory store and is not restarted.

A child's state belongs to the parent that spawned it. A durable child's directory is kept with the parent's durable state: under the file store's root, or, for an mcp or http store, under the default state directory in a directory named for that store. Both are keyed by store.prefix and the instance name, as the store keys the parent's state, so the same parent finds its children in every life and no other parent is given them. A parent whose store is memory or none keeps nothing across its own restart, so its children are non-durable whatever durable says. A non-durable child's directory is in a directory of the parent process's own (mode 0700), under $XDG_RUNTIME_DIR when that is the user's own 0700 directory, else under the system temporary directory. The child's directory is removed when the child exits, unless it failed or crashed: then it stays, its log in it, until the parent exits. The parent's is removed when the parent exits; a parent that does not exit normally leaves it behind, and no later process uses it. Two parents never share a child's directory, socket or store, and a new parent's inst-1 never starts on an earlier parent's state. Run a child daemon lists the paths.

How an instance child reaches its parent

At spawn the parent makes a socket pair and hands the child one end at fd 3 (AGENTD_INSTANCE_CHANNEL). The child's peer parent — what its reporter workflow and an a2a.send to parent dial — is that channel: each connection the child opens is a fresh stream passed over it, and the parent names the caller instance:<handle> by the channel it came through, before reading anything the request presents.

That principal may send the parent its two reports, _instance.result and _instance.emit, about its own handle, and nothing else. It is no operator: an admin.set, a status read, a stream, a message that is no report, or a report for another child's handle is refused, and the last is logged instance.op.refused. The child's log says instance.channel with held: true when it took its channel at start.

A uid of its own. With spawn.instance_uid and spawn.instance_gid set, and agentd running as root, each instance child starts under that uid and gid, with no capability, inside the Landlock domain as before; its own workers run under the same uid. The parent makes the child's directory the child's first: a durable child's directory under the store's root (whose ancestors must be passable by the uid, or the spawn is refused), a non-durable one's a private directory under the system temporary directory. The uid may not be 0, agentd's own, an external signer's or spawn.uid, and the load refuses one agentd cannot start a child as. Without it, a child keeps agentd's uid: it still answers its parent only as itself over its channel, but a unix A2A listener of the parent's that it found and dialed would name it the operator, since the kernel cannot tell the two apart. Security model says what a container needs.

Delegation. A child acts for no person: a delegated server is never composed into it, and a person's work reaches a delegated server only from the supervisor. A flat subagent's calls — and a run one starts — act with the person's grant only where the person's template says subagents: inherit, at the servers the subagent declared (Enterprise-managed authorization).

The template's machinery faces the same boundary as any served document: a fragment that writes operator-only configuration is refused, and so are a child's own a2a, intelligence, store and lifecycle sections (the parent composes those), MCP Events bindings and grants, and webhook starts or waits (a child has no webhook listener). Its MCP servers are checked against the parent's service catalogue and egress posture, and must not compose the trifecta. These checks run when the parent loads. The child validates the composed file again at spawn, and that is where the template's workflow steps are checked. Instruction documents explains the boundary.

What delegation does not do

  • Delegate onward. A child has no subagent.* tools and no A2A peers.
  • Detect a spinning loop. A child's loop has no repeated-call or no-progress detector; its step and token budgets are the bound.
  • Bound subagent.await. The tool's timeout argument is not applied; the child's liveness and hard deadline are the real bound. A workflow subagent step or wait {on: subagent} honours its timeout.
  • Validate an output_schema. For a subagent the schema is prose in the brief. Check the shape yourself.
  • Re-read the spawn rate. limits.subagents.rate is read at the first spawn of the process; a reload does not change it.
  • Pass skills. A spawn's skills list is accepted and not used, and a template's :::!skill block is left out of the text the child reads. Put what the child needs in its instruction or context.
  • Count subagents in metrics. agent_subagents_spawned_total and agent_subagents_exited_total count every child process, turn and think workers included. Count subagents from the subagent.* log events or subagent.list.

Next steps