Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ~33 min· topic 5 of 6

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 can yield several values over time.
Preliminary result
A yielded tool result that is not final. The tool part has preliminary: true until 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.

The tree behind one answerdiagram
Rendering diagram…

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

    Call only one specialist

    In the first example, delete the askRefunds call from Mitra's toolCalls list, 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. 2

    Tell the model less

    In the first example, delete the toModelOutput option from asTool.

    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. 3

    Open everything

    In the third example, change useState(failedInside(part)) to useState(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

Two specialists, drawn as a tree

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

Press Run to see the page.

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."
A branch fails: where does the red go?

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

Press Run to see the page.

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.
Collapse the noise, open what matters

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

Press Run to see the page.

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.

terminal
$ Console
── what you'll see ──
Shipping specialist: finished (inside: Tracking system timed out)
Refunds specialist: could not run (model provider is down)

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-handoff with from and to so 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 abortSignal into 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. 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. 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. 3

    Agent-as-tool in the SDK. A tool's execute can be an async generator: a function that yields several values. Each yielded value is sent to the browser as a preliminary tool result (the tool part has preliminary: true), and the last one is the final output. Inside execute, you call the specialist's agent.stream(...), read its UI messages with readUIMessageStream, and yield each message. The browser receives the specialist's whole message, with its own parts, as the tool part's output. So the nested steps are already in the data: part.output.parts.

  4. 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 toModelOutput turns 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. 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 (askShipping is "Shipping specialist") that you control, and use it everywhere text appears. Never take a name from the model's own words.

  6. 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. 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 inside or failed. The user sees at a glance which branch to open, and the other branches stay calm.

  8. 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

01

How does the agent-as-tool pattern put a specialist's steps on the screen?

02

Why use toModelOutput for a specialist tool?

03

Why must parallel specialists be keyed by toolCallId?

04

How should the UI show a failure inside a specialist, and how does it differ from a specialist that cannot start?

Practice

01

Write flatten(parts, author) that returns the rows of a run as { depth, text } objects, nesting specialists' rows one level deeper.

02

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.