Agent teams
Fan out to sub-agents, each with its own context window, under one durable run.
In short: some questions are too broad for one agent to research in one context window. A team splits the work: a supervisor decides what to investigate, and a fixed roster of sub-agents each investigate one angle as a full run of their own — own context, own budget, own step tree. Their costs roll up into one number on the parent, so you can answer "what did this team cost" without adding up children by hand, and "which sub-agent spent it" by opening the tree.
What a team is
A team is a supervisor and a worker template, wired together as sdk: team:
- The supervisor reads the task and emits a typed list of angles to investigate —
list[SubAgentTask], each one anangleand afocus. That list is its entire output vocabulary. - The worker template is one fixed definition — a prompt, a tool list, a budget — reused for every child. The supervisor picks what to look into; the worker template decides how any one angle gets investigated, and that "how" is the same for all of them.
The split is deliberate. The supervisor is a model, and a model's output is the one thing in this system that varies at runtime — so it's confined to choosing angles from a fixed menu, nothing more. It cannot invent a new tool for a child to use, and it cannot raise a child's budget above what the worker template already grants. That's what keeps the parent budget ceiling meaningful: if the supervisor could grant itself more tools or more spend, "the team's budget is the run budget" would stop being true the first time a model decided it needed one more thing.
Each child is a full run, not a step
This is the part that's easy to get wrong by analogy with a single agent calling a sub-routine. A
child is not a tool call and not a step in the parent's trace — it's a completely independent run,
linked to the parent by parent_run_id:
- its own context window, so one angle's research can't crowd out another's or push the parent toward compaction;
- its own budget (
worker_max_iterations,worker_max_tokens), sized from the worker template, not shared with siblings; - its own step tree — every LLM call, tool call and guardrail check a child makes is its own full trace, exactly like a single-agent run's;
- its own crash recovery — a child checkpoints exactly the way any other run does.
Because a child isn't a step, it doesn't show up as a row in the parent's own trace either. The
parent's trace holds only its own work — the supervisor's planning call and the synthesis call
that follows, both real llm steps that go through the same guardrail checks and budget
accounting as any other model call in agentFast. Here's the same run from the top: everything
below is what actually lives in the parent's trace.
| llm | claude-sonnet-4-5 | supervisor: 3 angles chosen from the brief | $0.02 |
| llm | claude-sonnet-4-5 | synthesis: merged 2 of 3 angles (one failed) | $0.03 |
| Child | Run | Status | Steps | Cost |
|---|---|---|---|---|
| pricing model | run_8f2a1c | completed | 11 | $0.41 |
| competitor landscape | run_a91d07 | completed | 8 | $0.29 |
| regulatory risk | run_c3d5e2 | failed — search API timeout | 2 | $0.03 |
Those three rows are what the dashboard nests under the parent, each expandable into its own step
tree — not part of the table above. on_child_failure: continue (the default) is why synthesis
ran at all here: one dead search API didn't stop the two angles that succeeded from being
reported.
Cost rolls up, spend doesn't fan out ahead of it
Children run up to team.max_parallel at a time. Admission is checked lazily — as each slot frees,
not all upfront — because a child's usage only reaches the parent once that child actually
finishes. Deciding every child's admission in one upfront pass would judge the whole fan-out
against a parent that had spent nothing yet, and the budget ceiling could never bite. Topping up
slots as they free is what lets the parent's budget check see real accrued spend, including what
the team has already paid its children.
The moment a child finishes, its real token counts and cost — read back from the persisted run record, not the in-memory result — are added to the parent's totals. That's the whole mechanism behind "one number answers what the team cost": nothing aggregates on its own, the parent has to be told, and it's told as each child actually completes rather than guessed upfront.
When a child fails
team.on_child_failure decides what a bad angle does to the rest of the team:
| Value | Behaviour |
|---|---|
continue (default) | Synthesis runs from whatever succeeded, and the synthesis prompt is told which angles failed. One dead search API shouldn't waste the good work from three other sub-agents. |
fail | Any child failure fails the whole team run. |
Resume semantics
The parent writes its roster — the list of planned children and their status — into its own checkpoint before any child starts. That ordering is what makes resume correct: without it, a parent that dies mid-fan-out can't tell "never started" from "already finished," and a resume would either lose an angle's work or redo it.
On resume, completed children are read back from the store and never re-executed — the same exactly-once guarantee the runtime already applies to tool calls, lifted one level. A child that was still in flight when the process died keeps its run id in the roster, so resume re-attaches to that same run rather than minting a second one and paying twice for the same angle. Only children that were genuinely still pending get (re-)admitted.
A child that hits a HITL-gated tool pauses exactly like a single-agent run would — but the roster
records that entry as paused rather than failed, so on_child_failure never sees it and the
team stays resumable instead of ending. The approval id surfaces on the parent's result, in
TeamOutcome.pending_approvals, so a caller watching the parent doesn't need to already know which
child to go looking in. Resuming by that child's own run id re-attaches to it — not a fresh
run — and continues from the approved tool call, the same way any paused run resumes.
Honest limits
Children cannot spawn grandchildren. The supervisor emits a flat list[SubAgentTask] once, up
front — there's no mechanism for a child to plan further children of its own. This is what keeps
the budget ceiling and the recursion depth both bounded; open-ended spawning is out of scope for
v1.
Each child is an independent run by construction — no shared state, no message-passing between siblings while they work. If one angle's findings need to inform another's, that has to happen in synthesis, after both have finished, not during.
POST /api/chat, /api/chat/stream and /api/runs/{id}/resume all dispatch generically —
sdk: team works over HTTP exactly like any other adapter, there's no team-specific gap in the
API. The real limit is narrower: a running server is wired to exactly one agent from its own
config, so a single server can't serve both a single-agent chat bot and a research team at the
same time — that takes two servers, one per config. And the playground is a single-agent chat
UI, so a minutes-long fan-out is easier to watch as it happens in the dashboard, where children
nest under their parent, each expandable into its own step tree. See demo/research.sh for a
working example driven directly against the runtime.
Before every model call, agentFast estimates the size of the request about to be sent
(budget_would_exceed) and stops there if it would breach the ceiling — a large tool result from
the previous turn can't silently carry a child past its budget on the next one. The estimate is
a fast character-based approximation, not the provider's own tokenizer, so it can be off by a
small margin either way; the authoritative numbers still come from the provider's response
afterward. The one call that's deliberately exempt from the check is the post-budget wrap-up turn
itself — but that call is trimmed to fit the same ceiling first, by truncating bulk tool-result
text rather than the agent's own findings, so it can't reintroduce the overshoot it exists to
close out.