Subagents
The idea
A subagent is a run. Not a special object, not a second engine — the same loop, the same table, the same persistence, with depth > 0 and a parentRunId.
That one decision buys everything else: a subagent can use tools, can park for a human, can be resumed after an approval, and shows up in the operational store next to every other run. Nothing had to be built twice.
Turning it on
Delegation is opt-in:
export const chat = runtime.createStreamTextAgent({
name: 'chat',
model: 'gpt-4o',
subagents: true,
});The platform injects a scoped spawnSubagent tool. The model calls it with a name and instructions.
Giving children tools
runtime.createStreamTextAgent({
name: 'chat',
model: 'gpt-4o',
subagents: { tools: { sendEmail } },
tools: { sendEmail },
});Tools listed under subagents are merged into every child and wrapped exactly as the parent's are — so a child that calls a marked tool parks for approval, and is resumed where it stopped rather than restarted.
The built-in web tools work in children too: pass runtime.builtinTools([...]) under subagents.tools, and a researcher child searches and reads pages on its own, billed to the parent's run.
Named profiles
The default child is a generalist with one persona and the shared tools. When delegation should go to specialists — a page manager, a copywriter, an analyst — name them:
subagents: {
profiles: {
'page-manager': {
description: 'edits and reorders the pages of the site',
systemFn: pageManagerPrompt,
model: 'gpt-4o-mini',
tools: pageTools,
maxSteps: 20,
},
copywriter: { description: 'writes headings and body copy', system: 'You write clean copy.' },
},
},Subagents: &agentenkit.SubagentsConfig{
Profiles: map[string]agentenkit.SubagentProfile{
"page-manager": {
Description: "edits and reorders the pages of the site",
SystemFn: pageManagerPrompt,
Model: "gpt-4o-mini",
Tools: pageTools,
MaxSteps: 20,
},
"copywriter": {Description: "writes headings and body copy", System: "You write clean copy."},
},
},With profiles set, spawnSubagent's description lists the names and what each one does, and name must be one of them; an unknown name comes back to the model as a tool error, not a crash. The child takes the profile's persona (systemFn wins over system), model, tools and step cap. Everything else is unchanged: it is still a run, it still parks for approval, and it is re-entered under the same profile after one.
What a child gets
A child receives its brief, not the parent's transcript. Its turns are persisted in the same thread under its own agentId, so they are its transcript, not the conversation's. A UI that renders the main thread filters to agentId === null.
The result handed back to the parent is capped at subagentResultCapChars (default 8000).
Limits
| Setting | Default | Meaning |
|---|---|---|
subagentMaxDepth | 2 | How deep nesting may go |
subagentMaxConcurrent | 3 | Children running at once per run, at each depth |
subagentMaxSteps | 10 | Model round trips per child |
subagentResultCapChars | 8000 | Characters returned to the parent |
Depth 2 means a child may spawn a grandchild, and there it stops. Exceeding the cap is reported to the caller as a tool result, not raised as a crash.
The concurrency cap belongs to one run: another run's children never take its slots. And each depth has slots of its own. A parent keeps its slot while its child runs, so a single pool for every depth would let three children each wait for a slot only a finished child can free, and the run would never end. Waiting for a slot ends at once when the run is stopped.
Failure is a result, not a crash
A child that fails reports back to its parent instead of killing the run. A child whose model stream ended with no finish counts as failed too: its partial text is not a result. A child cut off by a user stop is recorded CANCELLED, and the stop ends the whole run. The parent's model sees a failed tool result and decides what to do — retry differently, work around it, or tell the user. That is usually what you want; an agent whose helper failed is not an agent that should stop existing.
The failure carries the reason, which matters more than it sounds: a model that invents a model name gets a clear "unknown model" rather than a silent disappearance.
The token ledger
A child's spend counts against the run's budget the moment it happens, through a ledger shared by reference. Attribution still records which agent spent what, so you can see the split — but the cap is enforced across the whole tree.
Watching them
Three run stream events describe a child's life: SUBAGENT_STARTED (subagentId, name, depth), SUBAGENT_EVENT (one of the child's own stream events, wrapped, with its subagentId) and SUBAGENT_FINISHED (status is completed, failed or cancelled, with an error when it has one). They travel on the parent run's stream. use-agentenkit turns them into a subagents array:
{subagents.map((s) => (
<div key={s.agentId} data-depth={s.depth}>
<strong>{s.name}</strong> — {s.status}
{s.error && <span>{s.error}</span>}
<pre>{s.text}</pre>
</div>
))}Those events live only as long as the run stream (streamGraceMs after the segment ends). On a finished thread the hook rebuilds each card from the durable run rows and the child's persisted turns instead, so a reload does not lose a subagent's output.