Skip to main content

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:

  1. 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.
  2. 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.

The turn can finish before its delegates do

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:

statusMeaning
runningstill working
finisheddone; result holds what it reported
failedcould not do the work; error says why
stoppedcancelled, by subagent_stop or by the turn ending
deliveredterminal. 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:

WithheldWhy
remember, wiki_writeNothing 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_fileIt would hand your user a file nobody reviewed.
ask_userThere 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 with network: "none" and shell access can still curl. 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 conversationyoursProject → 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 subagent40Operator setting. A delegate that has taken fifty steps has misunderstood its brief and should report what it has.
How long one waits for an answer15 minutesOperator 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.