Multiagent: watch a curriculum team work in real time
A coordinator that delegates to subagents has an observability gap. The session-level stream previews the primary thread's text as the model generates it, but a subagent's output only becomes visible after its whole turn is buffered. If the researcher runs for two minutes, you watch nothing for two minutes.
This notebook closes that gap, using four Managed Agents API features together:
- Per-thread delta streaming. Each session thread's stream accepts the same
event_deltasparameter as the session-level stream, so a subagent's text previews live on its own stream. - Initial events on session create.
sessions.createacceptsinitial_events, so a session starts working in the same call that creates it. - Effort on the agent's model.
model.effortsets how hard Claude works on each inference call, per agent. In a team, that's a per-role cost lever. - Optional version on agent update.
agents.updatetreats the current version as an optional concurrency key rather than a required one.
The team you'll build plans a one-week 7th-grade science unit: a coordinator delegates to a standards researcher (web search, high effort) and a lesson writer (no web access), then assembles the unit plan. If the multiagent coordinator pattern is new to you, start with CMA_coordinate_specialist_team.ipynb(opens in new tab). This notebook builds on those shapes.
1. Set up the client
These features ride the standard managed-agents-2026-04-01 beta header. The SDK calls in this notebook (initial_events on sessions.create, event_deltas on thread streams, model.effort) need anthropic>=0.118.0.
2. Create the two specialists
The researcher gets web search and a high effort level, because standards alignment is judgment-heavy work: it has to weigh which performance expectations actually fit the unit rather than list everything it finds. effort goes inside the model object and accepts low, medium, high, xhigh, or max (as a bare string or {"type": "high"}). Not every model accepts every level, and an invalid combination is rejected at create time.
Effort is also the team's main cost lever: higher levels let Claude spend more tokens per inference call, lower levels cap that spend. Because it's set per agent, you buy depth only for the roles that need it. In a larger team, a formatting or triage role could drop to low without touching the researcher's budget.
Set effort on the agent, not per session: an effort level inside a per-session model override isn't applied.
The web tools' configuration also accepts allowed- and blocked-domain lists, so a production deployment can fence this researcher to the standards sites it should trust. See Configuring the toolset(opens in new tab) for the config shape.
The lesson writer works only from what the coordinator hands it. Disabling the web tools keeps its lessons grounded in the researcher's vetted findings instead of whatever a fresh search returns.
The create response echoes the resolved model configuration, including fields you omitted. The researcher shows the high you set, and the writer shows the model's default effort. If effort comes back None, your organization's beta header doesn't carry the feature yet: the field is dropped, not rejected, so this echo is the place to catch it.
3. Create the coordinator
The coordinator carries the multiagent roster. The spawn and delegation tools are injected from the roster automatically, and the roster pins each child to the version that's current right now. That pinning matters later, when you update the researcher.
4. Start the session with its first message inline
Without initial_events, starting a session takes two calls: create it, then post a user.message. Seeding the first message at create time collapses that into one. The array accepts up to 50 user.message and user.define_outcome events, processed in order, and validation is all-or-nothing: if any event fails, no session is created. A non-empty list starts the agent loop in the same call, so the session comes back already moving toward running.
If you want the unit plan graded against a rubric, add a single user.define_outcome event here (it must include a rubric, and only one is allowed per create). CMA_verify_with_outcome_grader.ipynb(opens in new tab) covers that loop.
5. Watch the whole team live
Previews are thread-scoped by design. A connection previews only the thread it reads: the session-level stream previews the primary thread, and a child's previews appear only on that child's own stream at GET /v1/sessions/{session_id}/threads/{thread_id}/stream. So the pattern is one stream per thread: read the session-level stream for the coordinator, and every time a session.thread_created event announces a child (it carries session_thread_id and agent_name), attach a watcher to that thread's stream in a background thread.
Two rules shape the code:
- The preview is a scratch buffer, the buffered event is the record. Deltas are best-effort and may stop under load. The buffered
agent.messagethat follows carries the complete content. The SDK'saccumulate_managed_agents_eventhelper foldsevent_start,event_delta, and the buffered event into one snapshot, replacing the preview when the record arrives. Run one accumulator per stream connection. - No replay. A connection opened after a model request started receives no deltas for that in-flight request, and a reconnect never replays missed deltas. Attaching the watcher as soon as
session.thread_createdarrives catches the child's work from its first response onward.
Each watcher exits on session.thread_status_idle, the event emitted when the child's turn finishes.
The main loop feeds the same fold_preview for the coordinator's text, plus the coordination events that only appear on the primary thread: session.thread_created when a child spawns, agent.thread_message_sent when the coordinator hands off a task, and agent.thread_message_received when a child reports back. A tool call cross-posted from a child thread carries session_thread_id, so the loop skips those: the child's own watcher shows them.
The loop ends on any session.status_idle, printing the stop reason when it isn't end_turn: a session waiting on a tool confirmation or out of retries should end the cell, not hang it. Both loops also break on the terminated status events, so an unrecoverable session error ends the cell instead of leaving the stream open.
If a stream connects but only ever delivers buffered events, the event_deltas parameter was stripped rather than rejected: that's the signature of a beta header that doesn't carry the feature. A 404 on the thread stream URL means a wrong path or a missing managed-agents beta header (the thread endpoints are beta-gated). The path is /threads/{thread_id}/stream, not /threads/{thread_id}/events/stream.
The streaming contract in full (delta shapes, the reconciliation guarantees, and the troubleshooting table) is in events and streaming(opens in new tab).
6. Check the preview against the record
Concatenating a preview's deltas gives a prefix of the buffered event's text: a prefix, not necessarily the whole text, because deltas may be shed under load. That guarantee is what makes reconciliation a single replace. Each row below is one buffered agent.message, compared against the preview the accumulator had built when the record arrived.
7. Read the unit plan
The coordinator's prompt says to write the plan with whole-file write calls, so the full document is in the event log and the last write to the file is the final version. If your own agent patches files with edit, read the file itself instead of replaying writes.
8. Ship a prompt change without the version round trip
Suppose review feedback says every alignment claim needs the standard's code next to it. agents.update treats the agent's current version as an optional concurrency key: omit it and the update applies unconditionally, with the server incrementing the version for you. A provisioning script doesn't have to read the agent just to write it back.
Supply version when concurrent writers are possible (a mismatch returns a 409, so you always update from a known state). Omit it when a single flow owns the agent, like a CI job that syncs checked-in agent definitions.
One caveat for multiagent setups: the coordinator's roster pinned the researcher's version at coordinator create time, so existing coordinators keep delegating to the old version. Update the coordinator (its multiagent field, or any field) to re-resolve the roster against the latest child versions.