September 30, 2026

A fan-out is a spending decision

Subagents are the feature that demos best and costs most. Nine files audited in parallel looks like magic in a screen recording, and every one of those nine is a full model loop billed to you.

Thin amber linework on charcoal: one node branching into four parallel tracks, three complete and one still running past the end of the main track.

So we made it two settings rather than one, and the second one is the interesting half.

The capability, in Project → Capabilities. That is the permission. The models, in 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. That is deliberate: a tool whose only possible answer is "the project configured nothing" is worse than its absence, because the agent plans around it and then has to explain a failure that was never its doing.

Picking a subset is the point. Delegated work is usually the cheap half of a conversation — a sweep, a lookup, an independent verification — 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, which is one fewer decision for it to get wrong.

The status that is not in the list you would write

Five statuses, and the fifth is the one that will bite a UI:

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

A completed delegate is claimed exactly once, so its result reaches the orchestrator once — reported twice and the agent reasons about work that only happened once. After that claim, the row reads delivered.

Which means a UI keying only on finished shows a delegate as unfinished forever. Treat delivered as terminal alongside the other three. The outcome it had is preserved, so error still tells you whether it actually succeeded.

The turn can finish before its delegates do

This is the case worth handling before you ship, because the reply looks complete and is not.

ts
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, because "the model was told to mention it" is a habit and finish_reason is a fact.

A subagent still running when the turn ends is still running. Keep showing it. It reports into the next turn — or starts one, so "I'll tell you when it's done" does not depend on the user still being there.

Rendering them

onSubagents fires whenever one starts, moves, asks something, or finishes — and always with every subagent of the conversation, not just the one that moved. Render from the list and a dropped frame cannot leave your panel wrong. That pattern shows up everywhere in this API and it is worth copying in your own: send the whole collection, not the delta.

text
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. That matters more than it sounds: a progress line the model has to generate goes stale exactly when the system is under load, which is exactly when someone is watching it.

What a delegate 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:

remember and 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, with ask_parent, which stops its work until answered — and if the main agent has no answer either, it says so and the delegate proceeds on its own judgement and reports what it assumed.

subagent_*. One level deep, so a fan-out's cost is bounded rather than exponential. This one is purely about the bill.

Reading is not writing: a delegate still has recall, wiki_read and retrieval over the same documents.

Polling is the expensive failure

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 a background command finishes.

Without it, the agent polls. Each poll is a full model round-trip that learns nothing. In one transcript before we added it: eight consecutive subagent_check calls, which the user experiences as the chat looping and you experience as eight turns' worth of tokens spent discovering that something is still running.

The delegates 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 anyone else needs is the result. So sessions.list() leaves them out unless you ask:

ts
await ai.chat.sessions.list();                                      // your users' chats
await ai.chat.sessions.list(undefined, { includeSubagents: true }); // and the delegates

Every row carries parent_session_id, so you can get from a delegate back to what started it. Going the other way is one field rather than a search: a subagents[] entry carries session_id. Use that, not id — id identifies the delegate record and 404s on the session routes, which is exactly the wrong turn to take first.

Their titles are a short label (Delegate: …) rather than the task, on purpose. 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.

The number that decides what this costs

Running at once, per conversation: yours to set, clamped against the deployment's maximum of 4. Steps per subagent: 40 — a delegate that has taken fifty has misunderstood its brief and should report what it has. How long one waits for an answer: 15 minutes, after which it proceeds on its own judgement and says what it assumed.

Only the first is a real decision. Every subagent running is a turn's worth of tokens in flight, so that number is the knob that decides what a fan-out costs — and it is why this is a capability at all, rather than something on by default. A token that should not be able to multiply its own cost should not carry it.

Read next