Subagents
subagents · per-turn flag enable_subagents
The agent can hand a self-contained piece of work to another agent that runs beside it, and carry on. A sweep across nine files, an independent verification of a conclusion, six unrelated lookups at once — work that is genuinely separable and long enough to be worth the hand-off.
It is the same promise a background command makes, one level up: the agent starts something, keeps working, and is told when it comes back. If the turn ends first, the report lands in the next one — or starts one, so "I'll tell you when it's done" doesn't depend on the user still being there.
Turning it on
Two things, and it needs both:
- The capability, in Project → Capabilities. Each subagent is another model loop billed to you, so this is a spending decision as much as a permission — and it is a capability precisely so a token that should not be able to multiply its own cost doesn't carry it.
- The models, in Project → LLM & limits → Subagent models. A subset of your generation models, and — beside them — how many may run at once. With the capability on and no models chosen, the agent gets no subagent tools at all: a tool whose only possible answer is "the project configured nothing" is worse than its absence, because the agent plans around it.
Picking a subset is the point. Delegated work is usually the cheap half of a conversation, and pinning it to a smaller model is how a fan-out stays affordable. Pick more than one and the agent chooses per task from exactly that list; pick one and it never sees the choice at all.
Watching them
const done = await ai.chat.stream({ message: "audit every fetcher for retry handling" }, {
onSubagents: (agents) => render(agents),
});
done.subagents;
onSubagents fires whenever one starts, moves, asks the main agent something, or
finishes — and always with every subagent of the conversation, not just the one that
moved, so render from the list and a dropped frame cannot leave your panel wrong.
Each carries a ref (a1, a2, … — stable for the conversation), the task in the
main agent's words, and a progress line of four or five words:
a1 audit services/api running searching the code: retry
a2 audit services/worker running reading a file: fetch.py
a3 check the shared http client finished reported back
progress is written from what the subagent is actually doing rather than asked of the
model, so it stays live under load — which is exactly when someone is watching it. Use
steps for movement when the phrase itself doesn't change.
A subagent that is still running when the turn ends is still running. Keep showing
it; it reports into the next turn.
When that happens the turn carries finish_reason: "subagents_incomplete", and the
turn.stopped webhook reports the same with complete: false. Nobody is waiting — the
turn is over — but the answer was written without part of what it asked for.
Check it before rendering a reply as final:
if (done.finish_reason === "subagents_incomplete") {
const waiting = done.subagents.filter((a) => a.status === "running");
banner(`Still working: ${waiting.map((a) => a.task).join(", ")}`);
}
The main agent is told the same thing and is instructed to say which part is missing rather than fill it in, but the flag is what your UI should key on.
status on each entry is authoritative, and there are five:
status | Meaning |
|---|---|
running | still working |
finished | done; result holds what it reported |
failed | could not do the work; error says why |
stopped | cancelled, by subagent_stop or by the turn ending |
delivered | terminal. Finished and already reported to the main agent |
delivered is the one worth knowing about. A completed delegate is claimed once, so that
its result is handed to the orchestrator exactly once — reported twice and the agent
reasons about work that only happened once. After that claim the row reads delivered,
so treat it as terminal alongside finished, failed and stopped: a UI that keys
only on finished shows a delegate as unfinished forever. The outcome it had is
preserved, so error still tells you whether it succeeded.
Their conversations are not in your session list
A delegate gets a real conversation — that is what gives it a transcript, a plan and a
context budget of its own — but it is a conversation nobody had, and the only part of it
anyone else needs is the result at the end. So chat.sessions.list() leaves them out:
await ai.chat.sessions.list(); // your users' chats
await ai.chat.sessions.list(undefined, { includeSubagents: true }); // and the delegates
// both answer { items, has_more, next_offset } — a page, not the whole set
Every row carries parent_session_id — null for an ordinary conversation, the delegating
turn for a delegate — so a client that does ask can tell them apart, and you can get from
a delegate back to what started it.
Going the other way is one field, not a search: a subagents[] entry carries
session_id, so a client holding a subagent event can read that delegate's transcript
directly with chat.sessions.messages(session_id). Do not use id for this — it
identifies the delegate record and 404s on the session routes, which is exactly the wrong
turn a developer takes first. Their titles are a short label (Delegate: …), not
the task: a delegated prompt routinely carries file paths and the wording used to steer
it, which is not something to put in an end user's sidebar.
What a subagent can and cannot do
It runs with your project's capabilities, on your corpus, with the same tools — that is what makes it a delegate rather than a second, weaker product. Four things are withheld, and the main agent cannot grant them back:
| Withheld | Why |
|---|---|
remember, wiki_write | Nothing durable. A delegate writing to your project's memory puts something there that the agent answerable for the conversation never saw and cannot correct. It says so in its report instead, and the main agent decides. |
send_file | It would hand your user a file nobody reviewed. |
ask_user | There is no user on the end of a delegate's turn. It asks the main agent instead (below). |
subagent_* | One level deep, so a fan-out's cost is bounded rather than exponential. |
Reading is not writing: a subagent still has recall, wiki_read and retrieval over
the same documents.
Narrowing one
The main agent may hand a subagent less than it has itself — never more:
workdir— confine it to a sub-directory of the sandbox workspace. Subagents share the conversation's sandbox, so this is a part of the same tree the main agent works in — which is also what lets it read what they produced. Enforced by the same path resolution every sandbox tool already goes through, so../gets a refusal rather than a surprise.network: "none"— takes away web search and the browser. This is an honest claim about the agent's reach, not about the sandbox's: commands still run in the same VM under your deployment's egress policy, so a subagent withnetwork: "none"and shell access can stillcurl. Use the sandbox's own network mode for a guarantee.tools— an explicit allow-list, intersected with what the main agent holds.
Waiting
The agent gets a wait tool wherever it can start work that outlives a tool call. It
blocks for up to two minutes and returns the moment a subagent or background command
finishes, so the agent stops when the work does rather than at the end of a timer.
Without it the agent polls, and each poll is a full model round-trip that learns
nothing — in one transcript, eight consecutive subagent_check calls, which is what a
user sees as the chat looping.
When it asks
A subagent that is genuinely blocked on a decision only the main agent can make uses
ask_parent, which stops its work until answered. The main agent is told at its next
step and answers with subagent_answer; if it has no answer either, it says so and the
subagent proceeds on its own judgement and reports what it assumed.
There is nothing for your UI to do here — this is one agent talking to another — but
question is on the subagent's state so you can show it rather than a status line that
has silently stopped moving.
Limits
| Running at once, per conversation | yours | Project → LLM & limits → Subagent models. Clamped against the deployment's maximum (4 by default) — you can ask for fewer, never more. Blank uses the deployment's. |
| Agent↔tool steps per subagent | 40 | Operator setting. A delegate that has taken fifty steps has misunderstood its brief and should report what it has. |
| How long one waits for an answer | 15 minutes | Operator setting. Past it the subagent proceeds on its own judgement and says what it assumed. |
The first is the one worth thinking about: every subagent running is a turn's worth of tokens in flight, so it is the knob that decides what a fan-out costs.
Subagents run as durable workflows, so a deploy or a worker dying moves the work rather
than losing it — the report arrives late rather than never. Where a deployment has no
scheduling they run in the API process instead and a restart does end them; those are
reported as failed with the reason rather than left saying running forever.