Topic 14.5
Showing Several Agents
In one line
When an orchestrating agent hands work to specialist agents, one request holds a tree of steps. The screen must say who said what, nest the specialists' steps under the call that started them, bubble a failure up from the branch it happened in, and collapse the noise so the user reads three lines instead of thirty.
Think of it like this
A hospital ward round. The lead doctor talks to you, but behind them a nurse checked your blood pressure, a pharmacist checked your medicines and a radiologist read your scan. A good ward round tells you the result, and says who found what: "the pharmacist found two medicines that should not be taken together". You do not need every reading, but you can ask for them. A multi-agent screen works the same way: one voice for the answer, clear credit for each specialist, details on request.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Orchestrator
- The agent the user talks to. It decides which specialists to call and writes the final answer.
- Specialist (sub-agent)
- An agent with one narrow job and its own tools, called by the orchestrator.
- Agent-as-tool
- A pattern where a specialist agent is wrapped as a tool of the orchestrator.
- Hand-off
- A pattern where control of the conversation moves to another agent.
- Async generator
- A function written
async function*that canyieldseveral values over time. - Preliminary result
- A yielded tool result that is not final. The tool part has
preliminary: trueuntil the last value. - toModelOutput
- A tool option that converts the tool's output into the shorter form the model sees.
- Roll-up
- Computing one status for a branch from the statuses of everything inside it.
- toolCallId
- The unique id of one tool call. The right key for a list of tool parts.
Step by step
01The tree behind one answer
The user asks one question. Mitra calls two specialists in the same step. Each runs its own small loop with its own tool. Mitra then writes the answer. The screen needs to carry this shape.
02A specialist as a tool
The first live example wraps each specialist in a tool whose execute is an async generator. It streams the specialist, reads its UI messages and yields each one. toModelOutput gives Mitra only the specialist's last text.
On the screen, each specialist is a tool part. Its output is a message with its own parts. The page draws the tree by recursion and labels every line with an author from a table in the code.
03When a branch fails
The second live example runs two failures. In one, the specialist's own tool throws: the inner step ends in output-error, the specialist still answers, and the branch is marked "problem inside". In the other, the specialist cannot start at all: the outer tool part ends in output-error and the branch is "failed".
A roll-up function turns the nested data into one status per branch. Mitra's own line stays calm in both cases.
04Collapsing the noise
The third live example takes a saved run (the same data the first one produces) and draws it two ways: collapsed, with one line per specialist, and expanded. A failed branch opens by itself. The console counts the visible rows, which is what a reader has to scan.
05Cost, abort and depth
Each specialist is a whole agent run, with its own context and token use. Pass the abort signal into it, so Stop reaches the nested loop too. And keep nesting shallow: every level adds cost, latency and a level of indentation the reader must follow.
Try it yourself
- 1
Call only one specialist
In the first example, delete the
askRefundscall from Mitra'stoolCallslist, so the first step asks only the shipping specialist.The tree has one branch instead of two, and the page needed no change, because it draws whatever tool parts exist. (Mitra's scripted last line still talks about the return window, which is the kind of mismatch a real model would not make.)
- 2
Tell the model less
In the first example, delete the
toModelOutputoption fromasTool.Mitra now receives each specialist's whole message (all its parts) instead of a one-line text. The screen looks the same, but the model's context is larger and every later step costs more.
- 3
Open everything
In the third example, change
useState(failedInside(part))touseState(true).Both branches now open at load. The console counts do not change, because they describe the data, but the page now needs more scanning, and the failed step is no longer easier to find than the working one.
Code & diagrams
Mitra calls both specialists in one step. Each specialist's own steps arrive inside its tool part's output. Reading from the console, the tree is already in the data: no one had to merge streams.
Preview
Console
Nothing logged yet.Expected console output
askShipping (output-available)
track (output-available)
Shipping specialist says: "Parcel left the warehouse."
askRefunds (output-available)
checkWindow (output-available)
Refunds specialist says: "A return is possible for 5 more days."
Mitra says: "ORD-1042 has shipped, and you can still return it for 5 days."The same roll-up function gives two different results: a specialist whose tool failed but still answered, and a specialist that could not run at all. Mitra's own line stays calm in both cases. The first line is the SDK's own log of the specialist that could not start.
Preview
Console
Nothing logged yet.Expected console output
AI_APICallError: model provider is down
Shipping specialist: finished, but a step inside it failed (inside: Tracking system timed out)
Refunds specialist: could not run (model provider is down)
Mitra: I could not complete everything. See the details below.The function that decides what opens is the one that matters. Opening on failure means the reader never has to hunt for the red step.
Preview
Console
Nothing logged yet.Expected console output
everything open: 7 lines
everything collapsed: 3 lines
opens by itself: Refunds specialist
one line each:
Shipping specialist: 2 steps. Parcel left the warehouse.
Refunds specialist: 2 steps. I could not check the window. (a step failed)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Look only at the branch's own state
In the second example, delete the line if (inner.some((p) => p.state === "output-error" || rollup(p) !== "ok")) return "problem inside"; from rollup.
Myth vs fact
Myth
More agents make a better answer.
Fact
Each specialist is a whole run with its own context and cost. Use one when it has a job that needs its own tools or instructions, not to look sophisticated.
Myth
I should show every step of every agent.
Fact
Users want the answer and the evidence. Show one line per specialist, open details on request, and open failures by themselves.
Myth
A failure in a specialist is a failure of the whole run.
Fact
Often it is not. The orchestrator can still answer with what it has. Roll the status up per branch so the user sees what failed and what still worked.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A hand-off, where a specialist takes over the conversation, is a state change in your own app: store which agent is active for the chat, choose that agent in the route, and send a data part such as
data-handoffwithfromandtoso the screen shows a clear marker. The SDK does not need a special feature for it. - ▸
Screen readers: announce the top-level status ("Shipping specialist finished") in one
role="status"region. Do not put a live region around the nested lists, or every update inside a branch will be read aloud. - ▸
Pass the same
abortSignalinto every specialist and give each one its own step limit. A runaway specialist inside a tool call is easy to miss, because the orchestrator's own step count does not include it. - ▸
Log each specialist's usage under its own label (Topic 13.5). The bill for an orchestrated run is the sum of several runs, and the orchestrator's number alone looks small.
Remember this
- 1
The problem this solves: A single agent's steps are a list. With several agents, they are a tree: Mitra asks a shipping specialist, which calls its own tracking tool, while a refunds specialist runs at the same time. If you pour all of that into one flat chat, users cannot tell who is speaking, which step belongs to which request, or why there are forty lines of text for a two-line answer. This topic shows how to carry the structure to the screen and keep it readable.
- 2
Two patterns, which look different on screen. In agent-as-tool, the orchestrator calls a specialist like any other tool, waits for its result, and writes the final answer itself. The user talks to one voice, and the specialists are details underneath. In a hand-off, control moves: the specialist takes over the conversation for the next turns, and the screen should show a visible change of speaker ("You are now talking to the refunds team"). Agent-as-tool is simpler to build and to explain, and it is what the AI SDK pattern in this topic uses.
- 3
Agent-as-tool in the SDK. A tool's
executecan be an async generator: a function thatyields several values. Each yielded value is sent to the browser as a preliminary tool result (the tool part haspreliminary: true), and the last one is the final output. Insideexecute, you call the specialist'sagent.stream(...), read its UI messages withreadUIMessageStream, andyieldeach message. The browser receives the specialist's whole message, with its ownparts, as the tool part'soutput. So the nested steps are already in the data:part.output.parts. - 4
Keep the model's view separate from the screen's. The orchestrator model does not need a specialist's whole transcript. The tool option
toModelOutputturns the tool output into what the model sees: for example only the specialist's final text. The screen gets the full nested message and the model gets a short summary. That saves tokens and keeps the model from copying a specialist's wording or instructions. - 5
Who said what. Every part belongs to an author. For the top level, the author is the orchestrator. For a part inside a tool part's
output, the author is the specialist that tool stands for. Keep a small table from tool name to a human label (askShippingis "Shipping specialist") that you control, and use it everywhere text appears. Never take a name from the model's own words. - 6
Parallel specialists interleave. If the orchestrator calls two specialists in the same step, their updates arrive mixed together. Key every element by
toolCallId, never by position or by array index, so that an update for the shipping specialist cannot land inside the refunds specialist's box. Draw them in the order the calls were made, not the order results arrived. - 7
Failures bubble up, but only as far as they should. An error inside a specialist's own tool turns that inner step red, and the specialist may still finish with a useful answer. A specialist that crashes entirely makes the outer tool part end in
output-error. A roll-up function gives each branch one status from its children:ok,problem insideorfailed. The user sees at a glance which branch to open, and the other branches stay calm. - 8
Collapse by default, open on need. Show one line per specialist: its name, a one-line result and a count ("2 steps"). Keep the details one click away, in a control with
aria-expanded. Open a branch by itself when it failed, or when it is the one working right now. Tell screen readers about the top-level progress only: a live region that announces every nested token is unusable. Limit depth: more than two levels of nesting is a sign the design needs flattening.
Explain it without notes
How does the agent-as-tool pattern put a specialist's steps on the screen?
Why use toModelOutput for a specialist tool?
Why must parallel specialists be keyed by toolCallId?
How should the UI show a failure inside a specialist, and how does it differ from a specialist that cannot start?
Practice
Write flatten(parts, author) that returns the rows of a run as { depth, text } objects, nesting specialists' rows one level deeper.
Your design has three levels: Mitra, a refunds specialist, and a fraud-check agent inside it. List two problems this causes on screen and one way to flatten it.
Trade-offs
- ↔
Showing nested detail is transparent, but costs attention. Collapsing is calmer, but hides information that matters when something goes wrong, so open on failure.
- ↔
Agent-as-tool keeps one clear voice and is easy to reason about, but the orchestrator pays for every specialist's work. A hand-off is cheaper for long specialist conversations but needs a visible change of speaker.
Done when you can
I can wrap a specialist agent as a tool and stream its steps with
readUIMessageStream.I can draw the nested parts as a tree labelled with authors from my own table.
I can key parallel branches by
toolCallId.I can compute a roll-up status per branch and open failures by default.
I can say what to announce to screen readers and what not to.