Files
anthropics_skills/skills/claude-api/shared/managed-agents-multiagent.md
T
Lance Martin 1f630fdf92 Update claude-api skill: Managed Agents July launch wave, partner pricing, tool-runner corrections (#1463)
Syncs the claude-api skill with the current upstream source.

## Managed Agents — five new features

- `effort` on the agent's `model` object (a level string or `{"type": "<level>"}`).
  It is agent-configuration only: setting it in a per-session `model` override is
  silently ignored.
- Optional `version` on agent update, for optimistic concurrency. Omit it for
  last-write-wins.
- `initial_events` on session create, collapsing create plus first send into one
  call. Validation is all-or-nothing and only `user.message` and
  `user.define_outcome` are accepted.
- Environment and memory-store webhooks: four `environment.*` events and three
  `memory_store.*` events.
- Event deltas on per-thread streams. Previews are thread-scoped, so a child
  thread's previews never reach the session-level stream.

## Corrections to behavior the skill already documented

- Tool output offload triggers at 100,000 characters (~25k tokens), not 100K
  tokens, and covers built-in tools rather than MCP alone.
- `system.message` works on four models, checks only the primary model, appends
  system context instead of replacing the prompt, and is accepted during a
  `requires_action` idle when it trails a tool result in the same request.
- Vault-to-MCP credential matching is normalized (scheme and host lowercased,
  default ports and trailing slashes stripped), not byte-exact.
- Multiagent depth greater than 1 is a validation error, not silently ignored.
- Outcome `interrupted` fires even when evaluation never started, and then
  carries an empty-string `outcome_evaluation_start_id`.
- Deployment jitter is 15% of the run interval, floor 5s, cap 9 minutes.
- Webhooks retry three times with jittered 5-120s backoff, then drop silently.
  Auto-disable is duration-based with three named triggers.
- `session.status_terminated` means completion or error.
- `agent.thinking` is a progress signal and carries no thinking content.
- `processed_at` is already populated on first sighting for
  `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result`.
- Session creation does not provision the sandbox.
- Skill `version` applies to Anthropic-authored skills too.
- Console-created vault credentials are header-injection only, and vault
  environment-variable substitution skips secrets in URL paths.
- Console trace URLs need the real workspace ID when the API key is not in the
  Default workspace.

## Elsewhere in the skill

- Note partner pricing under the first-party price table: Microsoft Foundry
  bills at standard API rates, while Amazon Bedrock and Vertex AI are
  partner-operated with separate pricing.
- Correct the tool-runner human-in-the-loop guidance, which previously pointed
  readers at the manual loop for approval gates the runner's per-turn hooks
  already cover.
- Repoint links away from the retired documentation hosts.
- Dedupe eagerly loaded SKILL.md content that the per-section files already
  cover.
2026-07-22 14:20:09 -04:00

9.4 KiB
Raw Blame History

Managed Agents — Multiagent Sessions

A coordinator agent can delegate to other agents within one session. All agents share the container and filesystem; each runs in its own thread — a context-isolated event stream with its own conversation history, model, system prompt, tools, MCP servers, and skills (from that agent's own config). Threads are persistent: the coordinator can send a follow-up to a subagent it called earlier and that subagent retains its prior turns.

The SDK sets the managed-agents-2026-04-01 beta header automatically on all client.beta.{agents,sessions}.* calls; no additional header is required for multiagent.


Declare the roster on the coordinator

multiagent is a top-level field on agents.create() / agents.update()not a tools[] entry. agents lists 120 roster entries. Nothing changes on sessions.create() — the roster is resolved from the coordinator's config.

orchestrator = client.beta.agents.create(
    name="Engineering Lead",
    model="claude-opus-4-8",
    system="You coordinate engineering work. Delegate code review to the reviewer and test writing to the test agent.",
    tools=[{"type": "agent_toolset_20260401"}],
    multiagent={
        "type": "coordinator",
        "agents": [
            reviewer.id,                                            # bare string — latest version
            {"type": "agent", "id": test_writer.id, "version": 4},  # pinned version
            {"type": "self"},                                       # the coordinator itself
        ],
    },
)

session = client.beta.sessions.create(agent=orchestrator.id, environment_id=env.id)
Roster entry Shape Notes
String shorthand "agent_abc123" References the latest version of a stored agent.
Agent reference {type: "agent", id, version?} Omit version to pin the latest at coordinator save time.
Self {type: "self"} The coordinator can spawn copies of itself.

If the session was created with agent_with_overrides (see shared/managed-agents-core.md → Override agent configuration for a session), those overrides apply to the coordinator and its self copies. Roster agents referenced by ID always use their own as-created configuration — overrides do not propagate to them.

Up to 20 unique agents in the roster; the coordinator may spawn multiple copies of each. One level of delegation only — and it is enforced rather than silently flattened: rostering an agent that itself carries a multiagent.agents roster fails the create or update with a validation error.


Threads

The session-level event stream is the primary thread — it shows the coordinator's trace plus a condensed view of subagent activity (thread status transitions and cross-thread messages, not every subagent tool call). Drill into a specific subagent via the per-thread endpoints:

Operation HTTP SDK (client.beta.sessions.threads.*)
List threads GET /v1/sessions/{sid}/threads .list(session_id)
Retrieve one GET /v1/sessions/{sid}/threads/{tid} .retrieve(thread_id, session_id=...)
Archive POST /v1/sessions/{sid}/threads/{tid}/archive .archive(thread_id, session_id=...)
List thread events GET /v1/sessions/{sid}/threads/{tid}/events .events.list(thread_id, session_id=...)
Stream thread events GET /v1/sessions/{sid}/threads/{tid}/stream .events.stream(thread_id, session_id=...)

Each SessionThread carries id, status (running | idle | rescheduling | terminated), agent (a resolved snapshot of the agent config — id, name, model, system, tools, skills, mcp_servers, version), parent_thread_id (null for the primary thread, which is included in the list), archived_at, and optional stats/usage. Session status aggregates thread statuses — if any thread is running, session.status is running. Max 25 concurrent threads. When draining a per-thread stream, break on session.thread_status_idle (and check its stop_reason as you would for the session-level idle).


Multiagent events (on the session stream)

Event Payload highlights Meaning
session.thread_created session_thread_id, agent_name A new thread was created.
session.thread_status_running session_thread_id, agent_name Thread started activity.
session.thread_status_idle session_thread_id, agent_name, stop_reason Thread is awaiting input. Inspect stop_reason (same shape as session.status_idle.stop_reason).
session.thread_status_rescheduled session_thread_id, agent_name Thread is rescheduling after a retryable error.
session.thread_status_terminated session_thread_id, agent_name Thread was archived or hit a terminal error.
agent.thread_message_sent to_session_thread_id, to_agent_name, content This thread sent a message to another thread. On the primary stream: the coordinator sent a task or follow-up to an agent.
agent.thread_message_received from_session_thread_id, from_agent_name, content A message arrived on this thread from another. On the primary stream: an agent sent a report or question to the coordinator.

Direction is relative to the thread whose stream carries the event, not to the coordinator. The same delegated task is an agent.thread_message_sent on the primary stream and an agent.thread_message_received on the child's own stream. Reading _received as "a subagent finished" is wrong once you're reading a child stream.


Previewing a subagent's text

Each thread's stream accepts the same event_deltas[] parameter as the session-level stream, so you can watch a subagent's text as the model generates it:

GET /v1/sessions/{sid}/threads/{tid}/stream?event_deltas%5B%5D=agent.message

Previews are thread-scoped. A child's previews are delivered only on that child's stream and never cross-posted to the session-level stream, whose previews stay scoped to the primary thread. So watching a subagent live means opening its thread stream — the session stream will not show it, no matter what you pass.

⚠️ Only plain assistant text previews. A subagent's reply to its coordinator rides agent.thread_message_sent and is never previewed. A worker that does nothing but report back therefore streams no deltas at all, even with a correct opt-in on the right thread. To get a live preview out of a subagent, its prompt has to make it write the answer as a plain assistant message in its own thread first, and only then report to the coordinator. Run one accumulator per connection, and exit the read loop on session.thread_status_idle. Opt-in, accumulate, and reconcile details: shared/managed-agents-events.md → Live previews.


Tool permissions and custom tools from subagent threads

When a subagent needs your client (an always_ask confirmation, or a custom tool result), the request is cross-posted to the primary thread with session_thread_id identifying the originating thread — so you only need to watch the session stream. Reply with user.tool_confirmation (carrying tool_use_id) or user.custom_tool_result (carrying custom_tool_use_id), and echo the session_thread_id from the originating event (the SDK param type and docstring expect it). The server also routes by the tool-use ID, so the echo is belt-and-suspenders rather than load-bearing — but include it.

for event_id in stop.event_ids:
    pending = events_by_id[event_id]
    confirmation = {
        "type": "user.tool_confirmation",
        "tool_use_id": event_id,
        "result": "allow",
    }
    if pending.session_thread_id is not None:
        confirmation["session_thread_id"] = pending.session_thread_id
    client.beta.sessions.events.send(session.id, events=[confirmation])

The same pattern applies to user.custom_tool_result.


Interrupting and archiving threads

  • user.interrupt without session_thread_id interrupts every non-archived thread in the session, including the primary — it is not a primary-only stop. Pass session_thread_id to target one thread.
  • Against a child thread blocked on requires_action, the interrupt closes each pending tool call with an error tool result ("Tool execution was interrupted before completion. Please retry.") and re-emits session.thread_status_idle with stop_reason: end_turn directly — the model is not sampled. Against a thread already idle, the interrupt is a no-op.
  • Archive requires the thread to be idle, and requires_action counts as idle — a thread parked on a pending tool call can be archived directly. Only a running thread must be interrupted first.

Pitfalls

  • Don't put the roster on sessions.create() or in tools[]. multiagent is a top-level agent field; update the coordinator, then start a session that references it.
  • Don't assume shared context. Threads share the filesystem but not conversation history or tools. If the coordinator needs a subagent to act on something, it must say so in the delegated message (or write it to disk).
  • Depth > 1 is a validation error. Rostering an agent that itself carries a multiagent.agents roster fails the create or update — only the session's coordinator delegates.

For per-language bindings beyond Python, WebFetch https://platform.claude.com/docs/en/managed-agents/multi-agent.md (see shared/live-sources.md).