mirror of
https://github.com/anthropics/skills.git
synced 2026-08-02 13:05:28 +08:00
1f630fdf92
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.
126 lines
9.4 KiB
Markdown
126 lines
9.4 KiB
Markdown
# 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 1–20 roster entries. Nothing changes on `sessions.create()` — the roster is resolved from the coordinator's config.
|
||
|
||
```python
|
||
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.
|
||
|
||
```python
|
||
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`).
|