mirror of
https://github.com/anthropics/skills.git
synced 2026-08-02 13:05:28 +08:00
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.
This commit is contained in:
@@ -106,7 +106,7 @@ Use these when a managed-agents binding, behavior, or wire-level detail isn't co
|
||||
|
||||
### Anthropic CLI
|
||||
|
||||
The `ant` CLI provides terminal access to the Claude API. Every API resource is exposed as a subcommand. It is one convenient way to create agents, environments, sessions, and other resources from version-controlled YAML, and to inspect responses interactively.
|
||||
The `ant` CLI provides terminal access to the Claude API. Every API resource is exposed as a subcommand. It is the recommended way to create agents and environments from version-controlled YAML (`ant beta:agents create < agent.yaml` — see `shared/anthropic-cli.md`), and also exposes sessions and every other API resource for scripting and interactive inspection.
|
||||
|
||||
| Topic | URL | Extraction Prompt |
|
||||
| ------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
All endpoints require `x-api-key` and `anthropic-version: 2023-06-01` headers. Managed Agents endpoints additionally require the `anthropic-beta` header.
|
||||
|
||||
> Most users should define agents and environments as version-controlled YAML applied with the `ant` CLI — see `shared/anthropic-cli.md`. The endpoints below are the underlying API that the CLI and SDKs drive.
|
||||
|
||||
## Beta Headers
|
||||
|
||||
```
|
||||
@@ -42,7 +44,7 @@ All resources are under the `beta` namespace. Python and TypeScript share identi
|
||||
|
||||
**Agent shorthand:** `agent` on session create accepts three forms — a bare string (`agent="agent_abc123"`, latest version), a pinned reference `{type: "agent", id, version}`, or `{type: "agent_with_overrides", id, version?, model?, system?, tools?, mcp_servers?, skills?}` to override those fields for this session only (see `shared/managed-agents-core.md` → Override agent configuration for a session).
|
||||
|
||||
**Model shorthand:** `model` on agent create accepts either a bare string (`model="claude-opus-4-8"` — uses `standard` speed) or the full config object (`{id: "claude-opus-4-8", speed: "fast"}`). Note: `speed: "fast"` is supported only on Opus 4.8 and Opus 4.7. Opus 4.7 fast mode is deprecated; after removal, `speed: "fast"` on Opus 4.7 returns an error. Opus 4.8 is the durable fast-capable tier.
|
||||
**Model shorthand:** `model` on agent create accepts either a bare string (`model="claude-opus-4-8"` — uses `standard` speed) or the full config object, which takes `speed` and `effort` alongside `id`: `{id: "claude-opus-4-8", speed: "fast"}`, `{id: "claude-opus-4-8", effort: "high"}`. `effort` accepts a level string (`low`/`medium`/`high`/`xhigh`/`max`) or `{type: "<level>"}`, and is **agent-configuration only** — an `effort` inside a per-session `model` override is ignored. See `shared/managed-agents-core.md` → Effort on the agent model. Note: `speed: "fast"` is supported only on Opus 4.8 and Opus 4.7. Opus 4.7 fast mode is deprecated; after removal, `speed: "fast"` on Opus 4.7 returns an error. Opus 4.8 is the durable fast-capable tier.
|
||||
|
||||
---
|
||||
|
||||
@@ -55,7 +57,7 @@ All resources are under the `beta` namespace. Python and TypeScript share identi
|
||||
| `GET` | `/v1/agents` | ListAgents | List agents |
|
||||
| `POST` | `/v1/agents` | CreateAgent | Create a saved agent configuration |
|
||||
| `GET` | `/v1/agents/{agent_id}` | GetAgent | Get agent details |
|
||||
| `POST` | `/v1/agents/{agent_id}` | UpdateAgent | Update agent configuration |
|
||||
| `POST` | `/v1/agents/{agent_id}` | UpdateAgent | Update agent configuration. `version` is **optional**: supply it (≥ 1) for optimistic concurrency — a mismatch returns 409 — or omit it for an unconditional last-write-wins update. |
|
||||
| `POST` | `/v1/agents/{agent_id}/archive` | ArchiveAgent | Archive an agent. Makes it **read-only**; existing sessions continue, new sessions cannot reference it. No unarchive — this is the terminal state. |
|
||||
| `GET` | `/v1/agents/{agent_id}/versions` | ListAgentVersions | List agent versions |
|
||||
|
||||
@@ -232,7 +234,7 @@ Immutable per-mutation snapshots (`memver_...`) — the audit and rollback surfa
|
||||
```json
|
||||
{
|
||||
"name": "string (required, 1-256 chars)",
|
||||
"model": "claude-opus-4-8 (required — bare string, or {id, speed} object)",
|
||||
"model": "claude-opus-4-8 (required — bare string, or {id, speed?, effort?} object)",
|
||||
"description": "string (optional, up to 2048 chars)",
|
||||
"system": "string (optional, up to 100,000 chars)",
|
||||
"tools": [
|
||||
@@ -281,6 +283,9 @@ Immutable per-mutation snapshots (`memver_...`) — the audit and rollback surfa
|
||||
"checkout": { "type": "branch", "name": "main" }
|
||||
}
|
||||
],
|
||||
"initial_events": [
|
||||
{ "type": "user.message", "content": [{ "type": "text", "text": "Review the auth module." }] }
|
||||
],
|
||||
"vault_ids": ["vlt_abc123 (optional — vault credentials: MCP auth + environment variables)"],
|
||||
"metadata": {
|
||||
"key": "value"
|
||||
@@ -288,7 +293,9 @@ Immutable per-mutation snapshots (`memver_...`) — the audit and rollback surfa
|
||||
}
|
||||
```
|
||||
|
||||
> The `agent` field accepts a string ID, `{type: "agent", id, version}`, or `{type: "agent_with_overrides", id, version?, ...}` for session-local overrides of `model`/`system`/`tools`/`mcp_servers`/`skills`. Outside the overrides form, those fields live on the agent, not here.
|
||||
> The `agent` field accepts a string ID, `{type: "agent", id, version}`, or `{type: "agent_with_overrides", id, version?, ...}` for session-local overrides of `model`/`system`/`tools`/`mcp_servers`/`skills`. Outside the overrides form, those fields live on the agent, not here. An `effort` inside a `model` override is ignored — set it on the agent.
|
||||
>
|
||||
> **`initial_events`** (optional, max 50) sends events at creation and starts the agent loop in the same call. Only `user.message` and `user.define_outcome` are accepted — no `system.message`, and none of the tool-result kinds. Validation is all-or-nothing. See `shared/managed-agents-core.md` → Seeding a session with `initial_events`.
|
||||
>
|
||||
> **`checkout`** accepts `{type: "branch", name: "..."}` or `{type: "commit", sha: "..."}`. Omit for the repo's default branch.
|
||||
|
||||
@@ -347,7 +354,7 @@ Immutable per-mutation snapshots (`memver_...`) — the audit and rollback surfa
|
||||
}
|
||||
```
|
||||
|
||||
> `system.message` events (update the system prompt between turns) use the same envelope with `type: "system.message"` — Claude Opus 4.8 only; see `shared/managed-agents-events.md` § Updating the system prompt mid-session.
|
||||
> `system.message` events (append system-level context for this turn and later ones) use the same envelope with `type: "system.message"` — supported on Claude Opus 4.8, Claude Sonnet 5, Claude Fable 5, and Claude Mythos 5, checked against the agent's *primary* model only; see `shared/managed-agents-events.md` § Adding system context mid-session.
|
||||
|
||||
### Define Outcome Event
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ for await (const event of stream) {
|
||||
|
||||
## 2. `processed_at` — queued vs processed
|
||||
|
||||
Every event on the stream carries `processed_at` (ISO 8601). For client-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`, `user.custom_tool_result`) it's `null` when the event has been queued but not yet picked up by the agent, and populated once the agent processes it. The same event appears on the stream twice — once with `processed_at: null`, once with a timestamp.
|
||||
Every event on the stream carries `processed_at` (ISO 8601), set when the event finishes processing. For client-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`) it's `null` while the event is queued behind earlier ones, and populated once the agent processes it — so the same event appears on the stream twice, once with `null` and once with a timestamp.
|
||||
|
||||
**Three event types skip the queued phase:** `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result` are processed on receipt and echoed back with `processed_at` already populated. A pending → acknowledged UI that assumes "first sighting is always `null`" will never clear for these — treat a populated `processed_at` on first sighting as immediately acknowledged.
|
||||
|
||||
```ts
|
||||
for await (const event of stream) {
|
||||
@@ -198,7 +200,11 @@ for await (const event of stream) {
|
||||
if (event.type === 'agent.custom_tool_use' && event.name === 'linear_graphql') {
|
||||
const result = await linear.request(event.input.query, event.input.vars) // host's key
|
||||
await client.beta.sessions.events.send(session.id, {
|
||||
events: [{ type: 'user.custom_tool_result', tool_use_id: event.id, result }],
|
||||
events: [{
|
||||
type: 'user.custom_tool_result',
|
||||
custom_tool_use_id: event.id,
|
||||
content: [{ type: 'text', text: JSON.stringify(result) }],
|
||||
}],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,19 +41,19 @@ rescheduling → running ↔ idle → terminated
|
||||
| `idle` | Agent has finished the current task, and is awaiting input. It's either waiting for input to continue working via a `user.message` or blocked awaiting a `user.custom_tool_result` or `user.tool_confirmation`. The `stop_reason` attached contains more information about why the Agent has stopped working. |
|
||||
| `running` | Session has starting running, and the Agent is actively doing work. |
|
||||
| `rescheduling` | Session is (re)scheduling after a retryable error has occurred, ready to be picked up by the orchestration system. |
|
||||
| `terminated` | Session has terminated, entering an irreversible and unusable state. |
|
||||
| `terminated` | Session has ended and is in an irreversible, unusable state — **either on completion or because of an unrecoverable error**. Terminated does not by itself mean failure; fetch the session to tell the two apart. |
|
||||
|
||||
- Events can be sent when the session is `running` or `idle`. Messages are queued and processed in order.
|
||||
- The agent transitions `idle → running` when it receives a new event, then back to `idle` when done.
|
||||
- Errors surface as `session.error` events in the stream, not as a status value.
|
||||
|
||||
Every session has a live trace view in the Anthropic Console at `https://platform.claude.com/workspaces/default/sessions/{session_id}`. Print this URL immediately after creating a session so the user can watch tool calls and messages stream in real time. The `default` workspace segment auto-resolves to the session's actual workspace on load, so you don't need the workspace id.
|
||||
Every session has a live trace view in the Anthropic Console at `https://platform.claude.com/workspaces/{workspace}/sessions/{session_id}`. Print this URL immediately after creating a session so the user can watch tool calls and messages stream in real time. **`{workspace}` is the workspace the API key belongs to** — use `default` only when that's the org's Default workspace. The session response does **not** include a workspace field and the Console has no workspace-agnostic session route, so for non-default workspaces substitute the workspace's ID (visible in the Console URL bar, or expose it as a config value alongside the API key). A `default` link to a session that lives in another workspace lands on a **"Session not found"** page — the **Search workspaces** button there will locate it, but it is not an automatic redirect.
|
||||
|
||||
### Built-in session features
|
||||
|
||||
- **Context compaction** — if you approach max context, the API automatically condenses session history to keep the interaction going
|
||||
- **Prompt caching** — historical repeated tokens are cached, reducing processing time and cost
|
||||
- **Extended thinking** — on by default, returned as `agent.thinking` events
|
||||
- **Extended thinking** — on by default; `agent.thinking` events signal thinking progress and carry no thinking content
|
||||
|
||||
### Session operations
|
||||
|
||||
@@ -124,7 +124,7 @@ const session = await client.beta.sessions.create(
|
||||
);
|
||||
```
|
||||
|
||||
> 💡 **Watch it live in Console.** While developing, print a link so you can click through to the session UI instead of parsing the event stream: `https://platform.claude.com/workspaces/default/sessions/{session.id}`. The `default` workspace segment auto-resolves to the session's actual workspace on load, so you don't need to know the workspace id. Include this `print`/`console.log` in code you generate for users who are iterating locally.
|
||||
> 💡 **Watch it live in Console.** While developing, print a link so you can click through to the session UI instead of parsing the event stream: `https://platform.claude.com/workspaces/{workspace}/sessions/{session.id}`. Use `default` for `{workspace}` only when the API key belongs to the org's Default workspace; otherwise substitute the workspace's ID (the session response does not carry it — read it from the Console URL bar or make it a config value). Include this `print`/`console.log` in code you generate for users who are iterating locally.
|
||||
|
||||
**Session creation parameters:**
|
||||
|
||||
@@ -134,15 +134,38 @@ const session = await client.beta.sessions.create(
|
||||
| `environment_id`| string | **Yes** | Environment ID |
|
||||
| `title` | string | No | Human-readable name (appears in logs/dashboards) |
|
||||
| `resources` | array | No | Files, GitHub repos, or memory stores, attached to the container at startup. Memory stores are session-create-only (not addable via `resources.add()`). |
|
||||
| `initial_events`| array | No | Events to send at creation, processed in order — collapses create + first send into one call. See § Seeding a session with `initial_events` below. |
|
||||
| `vault_ids` | array | No | Vault IDs (`vlt_*`) — MCP credentials with auto-refresh + `environment_variable` secrets substituted at egress. See `shared/managed-agents-tools.md` → Vaults. |
|
||||
| `metadata` | object | No | User-provided key-value pairs |
|
||||
|
||||
#### Seeding a session with `initial_events`
|
||||
|
||||
Creating a session without `initial_events` registers the session in `idle` and starts no work; the sandbox is provisioned when the session first needs it. Passing a **non-empty** `initial_events` array starts the agent loop in the same call — the session is **created directly in `running`**, never passing through `idle`. A client that waits for an `idle → running` transition to know work began will wait forever; check `status` on the create response instead.
|
||||
|
||||
```python
|
||||
session = client.beta.sessions.create(
|
||||
agent=AGENT_ID,
|
||||
environment_id=ENVIRONMENT_ID,
|
||||
initial_events=[
|
||||
{"type": "user.message", "content": [{"type": "text", "text": "Review the auth module."}]},
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
- **Only `user.message` and `user.define_outcome` are accepted**, max **50** events. The tool-result kinds (`user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`) are rejected because no agent turn exists yet, and `user.interrupt` because there is no turn to stop. Unlike a scheduled deployment's `initial_events`, a session's does **not** accept `system.message`.
|
||||
- Each event is validated and persisted before the create response returns, in list order, with a server-assigned ID — exactly as if you had posted it to the send-events endpoint immediately after creation. Per-event content rules are the same as on that endpoint.
|
||||
- **The events are not echoed on the create response.** Read them back with `sessions.events.list(session.id)` if you need their server-assigned IDs.
|
||||
- **Validation is all-or-nothing:** if any event fails, the whole request is rejected and no session is created. An empty list is equivalent to omitting the field.
|
||||
- Rejections: more than one `user.define_outcome` → 400; a `user.define_outcome` without a `rubric` → 400; more than 100 file-sourced `document` content blocks across the whole list → 400; a request body over 32 MB → 413.
|
||||
|
||||
An outcome-driven session is therefore a single call — pass one `user.define_outcome` in `initial_events` instead of creating the session and then sending the event (see `shared/managed-agents-outcomes.md`).
|
||||
|
||||
**Agent configuration fields** (passed to `agents.create()`, not `sessions.create()`):
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------- | -------- | -------- | ---------------------------------------------- |
|
||||
| `name` | string | **Yes** | Human-readable name (1-256 chars) |
|
||||
| `model` | string or object | **Yes** | Claude model ID (bare string, or `{id, speed}` object). All Claude 4.5+ models supported. |
|
||||
| `model` | string or object | **Yes** | Claude model ID (bare string, or an object taking `id`, `speed`, and `effort`). All Claude 4.5+ models supported. See § Effort on the agent model below. |
|
||||
| `system` | string | No | System prompt — defines the agent's behavior (up to 100K chars) |
|
||||
| `tools` | array | No | Encompasses three kinds: (1) pre-built Claude Agent tools (`agent_toolset_20260401`), (2) MCP tools (`mcp_toolset`), and (3) custom client-side tools. Max 128. |
|
||||
| `mcp_servers` | array | No | MCP server connections — standardized third-party capabilities (e.g. GitHub, Asana). Max 20, unique names. See `shared/managed-agents-tools.md` → MCP Servers. |
|
||||
@@ -164,7 +187,7 @@ The API is **flat** — `model`, `system`, `tools` etc. are top-level fields, no
|
||||
| Field | Type | Required | Description |
|
||||
| ------------------ | -------- | -------- | -------------------------------------------------- |
|
||||
| `name` | string | Yes | Human-readable name |
|
||||
| `model` | string | Yes | Claude model ID |
|
||||
| `model` | string or object | Yes | Claude model ID — bare string, or `{id, speed?, effort?}` |
|
||||
| `system` | string | No | System prompt |
|
||||
| `tools` | array | No | Agent toolset / MCP toolset / custom tools |
|
||||
| `mcp_servers` | array | No | MCP server connections |
|
||||
@@ -189,10 +212,27 @@ The agent is a **persistent resource**, not a per-run parameter. The intended pa
|
||||
|
||||
> **Recommended — define agents and environments as YAML + apply via the `ant` CLI.** The split is **CLI for the control plane, SDK for the data plane**: agents and environments are relatively static resources you manage with `ant` (version-controlled YAML, applied from CI); sessions are dynamic and driven by your application through the SDK. See `shared/anthropic-cli.md` → *Version-controlled Managed Agents resources* for the `ant beta:agents create < agent.yaml` / `update --version N` flow. The SDK `agents.create()` call shown elsewhere in this doc is the in-code equivalent — use it when you need to provision programmatically, but prefer the YAML flow for anything a human maintains.
|
||||
|
||||
### Effort on the agent model
|
||||
|
||||
Pass `model` as an object to set the effort level: `{"id": "claude-opus-4-8", "effort": "high"}`. `effort` accepts a level string (`low`, `medium`, `high`, `xhigh`, `max`) or an object such as `{"type": "high"}`. The create/update response echoes it in object form and fills in omitted `model` fields with their defaults.
|
||||
|
||||
> ⚠️ **Effort is agent configuration only.** An `effort` set inside a per-session `model` override is **not applied** — the session runs at the agent's effort. To change effort you must update the agent (or point the session at a different agent). This is the one field where the override form silently does nothing rather than erroring.
|
||||
|
||||
The same object form carries `speed` for fast mode: `{"id": "claude-opus-4-8", "speed": "fast"}`.
|
||||
|
||||
### Versioning
|
||||
|
||||
Each `POST /v1/agents/{id}` (update) creates a new immutable version (numeric timestamp, e.g. `1772585501101368014`). The agent's history is append-only — you can't edit a past version.
|
||||
|
||||
**`version` on update is optional.** Supply it for optimistic concurrency, or omit it to apply the update unconditionally:
|
||||
|
||||
| `version` | Behavior | Fits |
|
||||
|---|---|---|
|
||||
| Supplied (must be ≥ 1) | 409 if it doesn't match the agent's current version — **even when the fields you send already equal the stored values**. Re-read and retry. | Interactive callers; the recommended default |
|
||||
| Omitted | Applies unconditionally. The most recent update silently replaces any concurrent one, with no error to either caller. | Declarative apply loops — e.g. a CI job syncing checked-in agent definitions, where the loop owns the agent |
|
||||
|
||||
**Update semantics.** Omitted fields are preserved. Scalar fields (`model`, `system`, `name`, `description`) are replaced; `system` and `description` can be cleared with `null`, while `model` and `name` cannot. Array fields (`tools`, `mcp_servers`, `skills`) are replaced wholesale — `null` or `[]` clears them. **`effort` is the exception inside a `model` object you supply:** if the model `id` is unchanged, omitting `effort` leaves the stored level alone; if you change the `id`, an omitted `effort` resets to the new model's default.
|
||||
|
||||
**Why version:**
|
||||
- **Reproducibility** — pin a session to a known-good config: `{type: "agent", id, version: 3}`
|
||||
- **Safe iteration** — update the agent without breaking sessions already running on the old version
|
||||
@@ -252,8 +292,8 @@ session = client.beta.sessions.create(
|
||||
|
||||
Each overridable field follows tri-state rules:
|
||||
- **Omit** → the session inherits the value from the referenced agent version.
|
||||
- **`null` (or `[]` for list fields)** → the session runs with that field cleared. Applies in full to `system`, `mcp_servers`, `skills`. Two exceptions: `model` is never clearable (`model: null` → 400 `agent_model_required`); clearing `tools` returns 400 when the session's effective `skills` is non-empty (skills require the `read` tool), otherwise `tools: null` / `tools: []` clears.
|
||||
- **A value** → replaces the agent's value **in full**. Overrides never merge — a `tools` override must list every tool the session should have.
|
||||
- **`null` (or `[]` for list fields)** → the session runs with that field cleared. Applies in full to `system` and `skills`. Three exceptions: `model` is never clearable (`model: null` → 400 `agent_model_required`); clearing `tools` returns 400 when the session's effective `skills` is non-empty (skills require the `read` tool); and clearing `mcp_servers` returns 400 when the effective `tools` still contains an `mcp_toolset` referencing one of the agent's servers — override `tools` in the same request to drop those entries, then clear `mcp_servers`.
|
||||
- **A value** → replaces the agent's value **in full**. Overrides never merge — a `tools` override must list every tool the session should have. One exception: an `effort` level inside a `model` override is **not applied** (set it on the agent instead — see § Effort on the agent model).
|
||||
|
||||
Overrides are session-local: they do **not** modify the agent resource or create a new agent version. The response's `agent` object reflects the post-override configuration, while its `id` and `version` still identify the base agent — so you can trace a session back to its base. In multiagent sessions, overrides apply to the coordinator and its `{type: "self"}` copies; roster agents referenced by ID always use their own as-created configuration (see `shared/managed-agents-multiagent.md`).
|
||||
|
||||
@@ -261,7 +301,7 @@ Overrides are session-local: they do **not** modify the agent resource or create
|
||||
|
||||
`sessions.update()` can change `agent.tools`, `agent.mcp_servers` (including permission policies), and `vault_ids` on an **existing** session. This is a **session-local override** — it does not create a new agent version and does not propagate back to the agent object. The provided arrays are **full replacements**; to append one tool, `GET` the session, modify, and `POST` back. The session must be `idle` — interrupt first if running.
|
||||
|
||||
Only `tools` and `mcp_servers` can change after a session is created — to run with a `model`, `system`, or `skills` other than the agent's values, use `agent_with_overrides` at create time (above). The agent's configured `system` field is fixed for the session's lifetime; you can still **replace the effective system prompt between turns** by sending a `system.message` event (see `shared/managed-agents-events.md` § Updating the system prompt mid-session).
|
||||
Only `tools` and `mcp_servers` can change after a session is created — to run with a `model`, `system`, or `skills` other than the agent's values, use `agent_with_overrides` at create time (above). The agent's configured `system` field is fixed for the session's lifetime; you can still **append system-level context between turns** by sending a `system.message` event (see `shared/managed-agents-events.md` § Adding system context mid-session).
|
||||
|
||||
```python
|
||||
client.beta.sessions.update(
|
||||
|
||||
@@ -61,7 +61,7 @@ To run tool execution in **your own infrastructure** instead of Anthropic's, set
|
||||
|
||||
## Resources
|
||||
|
||||
Attach files, GitHub repositories, and memory stores to a session. **Session creation blocks until all resources are mounted** — the container won't go `running` until every file and repo is in place. Max **999 file resources** per session. Multiple GitHub repositories per session are supported. For `type: "memory_store"` resources (persistent cross-session memory — max 8 per session), see `shared/managed-agents-memory.md`.
|
||||
Attach files, GitHub repositories, and memory stores to a session. Resources are resolved during session creation, so a bad `file_id` or an unreachable repo surfaces on the create call rather than mid-run. Creating a session does **not** by itself start work or provision the sandbox — without `initial_events` the session is only registered, and the sandbox comes up when the session first needs it (see `shared/managed-agents-core.md` → Seeding a session with `initial_events`). Max **999 file resources** per session. Multiple GitHub repositories per session are supported. For `type: "memory_store"` resources (persistent cross-session memory — max 8 per session), see `shared/managed-agents-memory.md`.
|
||||
|
||||
### File Uploads (input — host → agent)
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ Send events to a session via `POST /v1/sessions/{id}/events`.
|
||||
| `user.tool_confirmation` | Approve/deny a tool call (when `always_ask` policy) |
|
||||
| `user.custom_tool_result` | Provide result for a custom tool call |
|
||||
| `user.define_outcome` | Start a rubric-graded iterate loop — see `shared/managed-agents-outcomes.md` |
|
||||
| `system.message` | Update the agent's system prompt between turns — **Claude Opus 4.8 only**; see § Updating the system prompt mid-session |
|
||||
| `system.message` | Append privileged system-level context for this turn and every turn after it; see § Adding system context mid-session |
|
||||
|
||||
#### Updating the system prompt mid-session (`system.message`)
|
||||
#### Adding system context mid-session (`system.message`)
|
||||
|
||||
Unlike the `system` field on the agent definition (fixed at session creation), a `system.message` event changes the system prompt **as the session progresses** — a different persona, revised constraints, or runtime-fetched context that should shape behavior going forward:
|
||||
The `system` field on the agent definition sets the top-level system prompt and is fixed for the session's lifetime. A `system.message` event **appends** to the session's system context as a `role: "system"` turn — it does not replace that prompt. The content applies to the accompanying turn and all subsequent turns. Use it for a different persona, revised constraints, or runtime-fetched context that should shape behavior going forward:
|
||||
|
||||
```python
|
||||
client.beta.sessions.events.send(
|
||||
@@ -35,8 +35,8 @@ client.beta.sessions.events.send(
|
||||
|
||||
Constraints:
|
||||
|
||||
- **Claude Opus 4.8 only.** If any model configured on the agent does not support mid-conversation system injection, the event is rejected with a `model_does_not_support_mid_conversation_system` validation error.
|
||||
- **Cannot be sent while the session is idle with `stop_reason: requires_action`** (blocked on `user.custom_tool_result` / `user.tool_confirmation`).
|
||||
- **Model-gated: Claude Opus 4.8, Claude Sonnet 5, Claude Fable 5, and Claude Mythos 5.** Only the agent's **primary** model is checked — `system.message` lands on the primary thread only, so subagent models are not considered. On an unsupported primary model the event is rejected with a `model_does_not_support_mid_conversation_system` validation error.
|
||||
- **While the session is idle with `stop_reason: requires_action`** (blocked on `user.custom_tool_result` / `user.tool_confirmation`), a `system.message` is accepted **only when it trails a tool result event in the same request**. Sent on its own — or alongside a `user.message` — it is rejected until the pending tool events are resolved.
|
||||
- `content` accepts 1–1000 text items.
|
||||
|
||||
### Receiving Events
|
||||
@@ -47,7 +47,7 @@ Three methods:
|
||||
2. **Polling**: `GET /v1/sessions/{id}/events` — paginated event list (query params: `limit` default 1000, `page`). **Returns immediately** — this is a plain paginated GET, not a long-poll.
|
||||
3. **Webhooks**: Anthropic POSTs session state transitions to your HTTPS endpoint — thin payloads (IDs only), HMAC-signed, Console-registered. See `shared/managed-agents-webhooks.md`.
|
||||
|
||||
All **persisted** events carry `id`, `type`, and `processed_at` (ISO 8601; `null` if not yet processed by the agent). The stream-only `event_start` / `event_delta` preview events (see § Live previews) carry only the `id` of the event they preview.
|
||||
All **persisted** events carry `id`, `type`, and `processed_at` (ISO 8601), set when the event finishes processing. On events you send, `processed_at` is `null` while the event is still queued behind earlier ones — **except** `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result`, which are processed on receipt and echoed back with `processed_at` already populated. The stream-only `event_start` / `event_delta` preview events (see § Live previews) carry only the `id` of the event they preview.
|
||||
|
||||
> ⚠️ **Robust polling (raw HTTP).** If you bypass the SDK and roll your own poll loop, don't rely on `requests` or `httpx` timeouts as wall-clock caps — they're **per-chunk** read timeouts, reset every time a byte arrives. A trickling response (heartbeats, a wedged chunked-encoding body, a misbehaving proxy) can keep the call blocked indefinitely even with `timeout=(5, 60)` or `httpx.Timeout(120)`. Neither library has a "total wall-clock" timeout built in. For a hard deadline: track `time.monotonic()` at the loop level and break/cancel if a single request exceeds your budget (e.g. via a watchdog thread, or `asyncio.wait_for()` around async httpx). **Prefer the SDK** — `client.beta.sessions.events.stream()` and `client.beta.sessions.events.list()` handle timeout + retry sanely.
|
||||
>
|
||||
@@ -60,7 +60,7 @@ Event types use dot notation, grouped by namespace:
|
||||
| Event Type | Description |
|
||||
| --- | --- |
|
||||
| `agent.message` | Agent text output |
|
||||
| `agent.thinking` | Extended thinking blocks |
|
||||
| `agent.thinking` | Progress signal that the agent is thinking — it does **not** carry the thinking content |
|
||||
| `agent.tool_use` | Agent used a built-in tool (`agent_toolset_20260401`) |
|
||||
| `agent.tool_result` | Result from a built-in tool |
|
||||
| `agent.mcp_tool_use` | Agent used an MCP tool |
|
||||
@@ -70,7 +70,7 @@ Event types use dot notation, grouped by namespace:
|
||||
| `session.status_idle` | Agent has finished the current task, and is awaiting input. It's either waiting for input to continue working via a `user.message` or blocked awaiting a `user.custom_tool_result` or `user.tool_confirmation`. The `stop_reason` attached contains more information about why the Agent has stopped working. |
|
||||
| `session.status_running` | Session has starting running, and the Agent is actively doing work. |
|
||||
| `session.status_rescheduled` | Session is (re)scheduling after a retryable error has occurred, ready to be picked up by the orchestration system. |
|
||||
| `session.status_terminated` | Session has terminated, entering an irreversible and unusable state. |
|
||||
| `session.status_terminated` | Session ended and is irreversibly unusable — **on completion or on error**, not error-only. |
|
||||
| `session.error` | Error occurred during processing |
|
||||
| `span.model_request_start` | Model inference started |
|
||||
| `span.model_request_end` | Model inference completed |
|
||||
@@ -89,7 +89,9 @@ Stream-only delta preview events (`event_start`, `event_delta`) are the one exce
|
||||
|
||||
By default, assistant text reaches the stream as buffered `agent.message` events — emitted only after the model request that produced them finishes. **Live previews** let you render that text incrementally while the model is still generating. The buffered `agent.message` is always the authoritative record; a client that ignores previews still receives a complete, correct stream. The wire format is **not** Messages-API streaming: the delta type is `content_delta`, not `content_block_delta`, so Messages-API accumulator code does not carry over unchanged.
|
||||
|
||||
**Opt in per stream connection** by adding the `event_deltas[]` query parameter to `GET /v1/sessions/{id}/events/stream`, repeated once per event type to preview. Accepted values: `agent.message`, `agent.thinking` (any other value → 400). Only the session-level stream supports it — per-thread streams (`/threads/{tid}/stream`) reject the parameter.
|
||||
**Opt in per stream connection** by adding the `event_deltas[]` query parameter, repeated once per event type to preview. Accepted values: `agent.message`, `agent.thinking` — any other value returns a 400, as does a request with more than 100 values. **Both stream endpoints accept it:** the session-level stream (`GET /v1/sessions/{id}/events/stream`) and each session thread's own stream (`GET /v1/sessions/{sid}/threads/{tid}/stream`). In a shell, quote the URL or percent-encode the brackets as `%5B%5D` — bare `[]` is a glob pattern.
|
||||
|
||||
**Previews are thread-scoped.** A connection previews only the thread it is reading. A child thread's previews are delivered on that child's stream and are *never* cross-posted to the session-level stream, whose previews stay scoped to the primary thread. To watch a subagent's text as the model generates it, open that subagent's thread stream — see `shared/managed-agents-multiagent.md`. Run one accumulator instance per connection.
|
||||
|
||||
```python
|
||||
stream = client.beta.sessions.events.stream(
|
||||
@@ -105,15 +107,25 @@ When a previewed event begins, the stream emits an `event_start` carrying the up
|
||||
{"type": "event_delta", "event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "Here is the summary"}}}
|
||||
```
|
||||
|
||||
`event_start` and `event_delta` have no `id` or `processed_at` of their own — the only identifier they carry is the `id` of the event they preview. For `agent.thinking`, **only** the `event_start` is emitted (a "thinking has started" signal) — no deltas follow; read content from the buffered `agent.thinking` event.
|
||||
`event_start` and `event_delta` have no `id` or `processed_at` of their own — the only identifier they carry is the `id` of the event they preview. For `agent.thinking`, **only** the `event_start` is emitted (a "thinking has started" signal) — no deltas follow, and the buffered `agent.thinking` that concludes the preview carries no thinking content either. It is a progress signal, not a content carrier; there is nothing to read out of it.
|
||||
|
||||
**Accumulate-and-reconcile pattern.** Treat the preview as a scratch buffer keyed by `(event_id, index)`. On `event_start`, create an empty entry for the announced `id`. On each `event_delta`, append `delta.content.text` to `(event_id, delta.index)` and render the running text. When the buffered `agent.message` arrives, match it by `id`, **discard the accumulated preview**, and render the message's content instead. The identifiers always line up: `event_start.event.id`, every `event_delta.event_id`, and the buffered event's `id` are the same value. On a normal turn the order is fixed: `session.status_running` → `span.model_request_start` → `event_start` → `event_delta`* → buffered `agent.message` → `span.model_request_end`. If the turn errors or is interrupted the buffered event may never arrive, but `span.model_request_end` still does — close any unreconciled preview when you see it. Python/TypeScript/Go SDKs ship an accumulator helper that implements this; in other SDKs apply the manual pattern to the generated event types.
|
||||
|
||||
**Two guarantees the pattern relies on:** concatenating a preview's deltas in arrival order, keyed by `(event_id, index)`, yields a *prefix* of `content[index].text` in the buffered event (a prefix, not necessarily the whole text — deltas may be shed under load); and a connection emits at most one `event_start` per `event_id`, with the buffered event as the last thing that connection delivers for that `id`.
|
||||
|
||||
**Limitations:**
|
||||
- **Best effort** — under load the server may shed deltas for an event; you receive a contiguous prefix and then no further deltas for that event. The buffered `agent.message` still arrives complete. Never treat an accumulated preview as final.
|
||||
- **No replay on reconnect** — deltas are delivered only to the connection that opted in, while it's open. After a drop, follow the consolidation pattern in § Reconnecting after a dropped stream — the history fetch returns any buffered events emitted during the gap; missed deltas cannot be re-requested.
|
||||
- **Primary thread, text only** — tool use, tool results, MCP results, and subagent-thread activity are never previewed.
|
||||
- **Never persisted** — `event_start` / `event_delta` exist only on the live SSE stream, never in `GET /v1/sessions/{id}/events`.
|
||||
- **No replay on reconnect** — deltas are delivered only to the connection that opted in, while it's open; this holds for the session-level stream and each thread stream alike. A connection opened after a model request started receives no deltas for that in-flight event. After a drop, follow the consolidation pattern in § Reconnecting after a dropped stream — the history fetch returns any buffered events emitted during the gap; missed deltas cannot be re-requested.
|
||||
- **One thread, text only** — previews cover assistant text on the thread the connection is reading. Tool use, tool results, MCP results, and activity on any *other* thread are never previewed on that connection.
|
||||
- **Never persisted** — `event_start` / `event_delta` exist only on the live SSE stream, never in `GET /v1/sessions/{id}/events` or any thread's event history.
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
| You see | What it means |
|
||||
| --- | --- |
|
||||
| Buffered events but no `event_start` / `event_delta` | This connection didn't opt in (`event_deltas[]` is per connection, not per session), or the turn ran on a different thread. List `GET /v1/sessions/{sid}/threads` to find which one ran. |
|
||||
| 404 on the stream URL | Wrong path or ID, or the request carries no managed-agents beta header — the thread endpoints are beta-gated, so without it they don't exist. The thread path is `/threads/{tid}/stream`, **not** `/threads/{tid}/events/stream` (which doesn't exist) and not `/events/stream` (session level only). |
|
||||
| 400 naming `event_deltas` | Only `agent.message` and `agent.thinking` are accepted, max 100 values. |
|
||||
|
||||
---
|
||||
|
||||
@@ -182,16 +194,20 @@ Events can be sent up to the Session at any time. There is no need to wait on a
|
||||
|
||||
### Interrupt
|
||||
|
||||
An `interrupt` event **jumps the queue** (ahead of any pending user messages) and forces the session into `idle`. Use this for "stop" / "nevermind" / "cancel" commands:
|
||||
A `user.interrupt` event **jumps the queue** (ahead of any pending user messages) and forces the session into `idle`. Use this for "stop" / "nevermind" / "cancel" commands:
|
||||
|
||||
```ts
|
||||
await client.beta.sessions.events.send(sessionId, {
|
||||
events: [{ type: 'interrupt' }],
|
||||
events: [{ type: 'user.interrupt' }],
|
||||
});
|
||||
```
|
||||
|
||||
The agent stops mid-task. It does not see the interrupt as a message — it just halts. Send a follow-up `user` event to explain what to do instead. If an outcome is active, the interrupt also marks `span.outcome_evaluation_end.result: "interrupted"` (see `shared/managed-agents-outcomes.md`).
|
||||
|
||||
**The interrupted turn ends with `stop_reason: end_turn`** — the same value a turn that finishes on its own carries. There is no interruption-specific stop reason, so a drain loop can't distinguish the two from `stop_reason` alone; track that you sent the interrupt.
|
||||
|
||||
**In a multiagent session, omitting `session_thread_id` interrupts every non-archived thread, including the primary** — it is not primary-only. Pass `session_thread_id` to stop one thread. See `shared/managed-agents-multiagent.md`.
|
||||
|
||||
> **Note**: Interrupt events may have empty IDs in the current implementation. When troubleshooting, use the `processed_at` timestamp along with surrounding event IDs.
|
||||
|
||||
### Event payloads
|
||||
|
||||
@@ -6,6 +6,8 @@ Sessions are ephemeral by default — when one ends, anything the agent learned
|
||||
|
||||
Every mutation to a memory produces an immutable **memory version** (`memver_...`), giving you an audit trail and point-in-time rollback/redact.
|
||||
|
||||
> ⚠️ **Never store credentials, API keys, or tokens in memory stores.** Memories persist across sessions and are returned verbatim into future contexts — a key written once is replayed into every later session that mounts the store. Use vault `environment_variable` credentials instead (`shared/managed-agents-tools.md` → Vaults). If a secret has already been written, delete the memory and redact the affected versions (see "Redact a version" below).
|
||||
|
||||
## Object model
|
||||
|
||||
| Object | ID prefix | Scope | Notes |
|
||||
|
||||
@@ -37,7 +37,7 @@ session = client.beta.sessions.create(agent=orchestrator.id, environment_id=env.
|
||||
|
||||
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** — depth > 1 is ignored.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -66,8 +66,24 @@ Each `SessionThread` carries `id`, `status` (`running` | `idle` | `rescheduling`
|
||||
| `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` | Coordinator sent a follow-up to another thread. |
|
||||
| `agent.thread_message_received` | `from_session_thread_id`, `from_agent_name`, `content` | An agent delivered its result to the coordinator. |
|
||||
| `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.
|
||||
|
||||
---
|
||||
|
||||
@@ -92,10 +108,18 @@ 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 ignored.** A subagent's own `multiagent` roster (if any) doesn't cascade — only the session's coordinator delegates.
|
||||
- **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`).
|
||||
|
||||
@@ -49,7 +49,7 @@ Usually zero or one question:
|
||||
- `user.define_outcome` + rubric — when §2 settled on an Outcome; the harness iterates and grades until the rubric passes.
|
||||
- **Scheduled shape?** Skip per-session kickoff entirely — create a **deployment** (`deployments.create()` with `schedule` + `initial_events`); each firing creates the session autonomously. See `shared/managed-agents-scheduled-deployments.md`.
|
||||
|
||||
Mechanics to bake into the runtime code: session creation blocks until resources mount (bad mounts surface there, before tokens); open the event stream *before* sending the kickoff; break on `session.status_terminated`, or `session.status_idle` with a terminal `stop_reason` — anything except `requires_action` (`shared/managed-agents-client-patterns.md` Pattern 5); usage lands on `span.model_request_end`; artifacts land in `/mnt/session/outputs/` (`files.list({scope_id: session.id, ...})`).
|
||||
Mechanics to bake into the runtime code: session creation resolves resources (a bad mount surfaces there, before tokens) but does not itself provision the sandbox; open the event stream *before* sending the kickoff; break on `session.status_terminated`, or `session.status_idle` with a terminal `stop_reason` — anything except `requires_action` (`shared/managed-agents-client-patterns.md` Pattern 5); usage lands on `span.model_request_end`; artifacts land in `/mnt/session/outputs/` (`files.list({scope_id: session.id, ...})`).
|
||||
|
||||
## 5. Integrate — emit the code
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ The SDK sets the `managed-agents-2026-04-01` beta header automatically on all `c
|
||||
|
||||
Outcomes are not a field on `sessions.create()`. You create a normal session, then send a `user.define_outcome` event. The agent starts working on receipt — **do not also send a `user.message`** to kick it off.
|
||||
|
||||
You can collapse both calls into one by passing a single `user.define_outcome` in the session's `initial_events` array — same event, same rules, one round trip (see `shared/managed-agents-core.md` → Seeding a session with `initial_events`). More than one `user.define_outcome` in that array, or one without a `rubric`, rejects the whole create with a 400.
|
||||
|
||||
```python
|
||||
session = client.beta.sessions.create(
|
||||
agent=AGENT_ID,
|
||||
@@ -62,7 +64,7 @@ These appear on the standard event stream (`sessions.events.stream` / `.list`) a
|
||||
| `needs_revision` | Agent starts another iteration. |
|
||||
| `max_iterations_reached` | No further grader cycles. Agent may run one final revision, then session → `idle`. |
|
||||
| `failed` | Session → `idle`. Rubric fundamentally doesn't match the task (e.g. description and rubric contradict). |
|
||||
| `interrupted` | Only emitted if `_start` had already fired before a `user.interrupt` arrived. |
|
||||
| `interrupted` | Emitted whenever a `user.interrupt` arrives while an outcome is active — **even if evaluation hadn't started**. In that case `outcome_evaluation_start_id` is an empty string rather than an event ID, so don't use it as a lookup key without checking. |
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ Every session references a pre-created `/v1/agents` object. Create the agent onc
|
||||
|
||||
If you're about to write `sessions.create()` with `model`, `system`, or `tools` on the session body — **stop**. Those fields live on `agents.create()`. The session takes a *pointer* only.
|
||||
|
||||
**When generating code, separate setup from runtime.** `agents.create()` belongs in a setup script (or a guarded `if agent_id is None:` block), not at the top of the hot path. If the user's code calls `agents.create()` on every invocation, they're accumulating orphaned agents and paying the create latency for nothing. The correct shape is: create once → persist the ID (config file, env var, secrets manager) → every run loads the ID and calls `sessions.create()`.
|
||||
**When generating code, separate setup from runtime.** `agents.create()` belongs in a setup script (or a guarded `if agent_id is None:` block), not at the top of the hot path. If the user's code calls `agents.create()` on every invocation, they're accumulating orphaned agents and paying the create latency for nothing. The correct shape is: define the agent as a version-controlled YAML manifest, apply it once with `ant beta:agents create < agent.yaml` (or a guarded setup script — see `shared/anthropic-cli.md`), persist the returned ID (config file, env var, secrets manager), and have every run load the ID and call `sessions.create()`.
|
||||
|
||||
**To change the agent's behavior, use `POST /v1/agents/{id}` — don't create a new one.** Each update bumps the version; running sessions keep their pinned version, new sessions get the latest (or pin explicitly via `{type: "agent", id, version}`). See `shared/managed-agents-core.md` → Agents → Versioning. To change `tools`/`mcp_servers`/`vault_ids` on **one running session** without touching the agent object, use `sessions.update()` — see `shared/managed-agents-core.md` → Updating the agent configuration mid-session.
|
||||
|
||||
@@ -29,7 +29,7 @@ Managed Agents is in beta. The SDK sets required beta headers automatically:
|
||||
| `skills-2025-10-02` | Skills API (for managing custom skill definitions) |
|
||||
| `files-api-2025-04-14` | Files API for file uploads |
|
||||
|
||||
**Which beta header goes where:** The SDK sets `managed-agents-2026-04-01` automatically on `client.beta.{agents,environments,sessions,vaults,memory_stores,deployments,deployment_runs}.*` calls, and `files-api-2025-04-14` / `skills-2025-10-02` automatically on `client.beta.files.*` / `client.beta.skills.*` calls. You do NOT need to add the Skills or Files beta header when calling Managed Agents endpoints. **Exception — session-scoped file listing:** `client.beta.files.list({scope_id: session.id})` is a Files endpoint that takes a Managed Agents parameter, so it needs **both** headers. Pass `betas: ["managed-agents-2026-04-01"]` explicitly on that call (the SDK adds the Files header; you add the Managed Agents one). See `shared/managed-agents-environments.md` → Session outputs.
|
||||
**Which beta header goes where:** The SDK sets `managed-agents-2026-04-01` automatically on `client.beta.{agents,environments,sessions,vaults,memory_stores,deployments,deployment_runs}.*` calls, and `files-api-2025-04-14` / `skills-2025-10-02` automatically on `client.beta.files.*` / `client.beta.skills.*` calls. You do NOT need to add the Skills or Files beta header when calling Managed Agents endpoints. On raw HTTP the Managed Agents header **grants Files API access on its own**, so uploading a file for use as a session resource does not need `files-api-2025-04-14` alongside it. (Direct Skills API calls over cURL do still need `skills-2025-10-02`; the `ant` CLI and the SDKs send it for you.) **Exception — session-scoped file listing:** `client.beta.files.list({scope_id: session.id})` is a Files endpoint that takes a Managed Agents parameter, so it needs **both** headers. Pass `betas: ["managed-agents-2026-04-01"]` explicitly on that call (the SDK adds the Files header; you add the Managed Agents one). See `shared/managed-agents-environments.md` → Session outputs.
|
||||
|
||||
|
||||
## Reading Guide
|
||||
|
||||
@@ -9,7 +9,7 @@ Requires the `managed-agents-2026-04-01` beta header (the SDK sets it automatica
|
||||
A deployment bundles everything a session needs (agent, environment, optional files / GitHub / memory stores / vaults) plus a `schedule` and the `initial_events` that kick off each run:
|
||||
|
||||
- `agent` and `environment_id` are required — same shapes as `sessions.create` (see `shared/managed-agents-core.md`).
|
||||
- `initial_events` must contain the starting `user.message`.
|
||||
- `initial_events` must contain at least one starting event — a `user.message` **or** a `user.define_outcome`. (A deployment's `initial_events` also accepts `system.message`, which a session's does not.)
|
||||
- `schedule` takes a cron `expression` and an IANA `timezone`. Minute-level granularity is the maximum.
|
||||
|
||||
```bash
|
||||
@@ -71,7 +71,7 @@ The response is a deployment object (`depl_` ID prefix). Check `schedule.upcomin
|
||||
}
|
||||
```
|
||||
|
||||
Deployments may apply up to **10 seconds of jitter** to distribute load. Maximum **1000 scheduled deployments per organization** (contact Anthropic support for more).
|
||||
`upcoming_runs_at` reflects the exact configured schedule, but **execution is jittered to distribute load: up to 15% of the interval between runs, floored at 5 seconds and capped at 9 minutes.** An hourly deployment can therefore fire up to 9 minutes late; don't build a downstream deadline that assumes the listed timestamp. Maximum **1000 scheduled deployments per organization** (contact Anthropic support for more).
|
||||
|
||||
### Cron and timezone semantics
|
||||
|
||||
@@ -139,7 +139,7 @@ Raw HTTP: `POST /v1/deployments/{deployment_id}/pause` (likewise `/unpause`, `/a
|
||||
|
||||
- **Rate-limited:** recorded immediately as a `session_rate_limited` run, **no retry** — the schedule simply tries again at the next occurrence. (Rate limits on API calls *inside* a session are handled by the session itself.)
|
||||
- **Other failed runs** (e.g. `environment_archived`, `vault_not_found`, `service_unavailable`): the run records the `error.type` — monitor runs and fix the referenced resource, or pause the deployment.
|
||||
- **Agent archived or deleted:** the deployment is automatically **archived** (terminal) and no further sessions are created.
|
||||
- **Agent archived:** the deployment is automatically **archived** (terminal) in the same operation. **Agent deleted:** the next scheduled trigger detects the missing agent and archives the deployment then. Either way no deployment run is recorded, and no further sessions are created.
|
||||
|
||||
## Manual runs
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Control when server-executed tools (agent toolset + MCP) run automatically vs wa
|
||||
| Policy | Behavior |
|
||||
|---|---|
|
||||
| `always_allow` | Tool executes automatically (default) |
|
||||
| `always_ask` | Session emits `session.status_idle` and pauses until you send a `tool_confirmation` event |
|
||||
| `always_ask` | Session emits `session.status_idle` and pauses until you send a `user.tool_confirmation` event |
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -88,8 +88,8 @@ Control when server-executed tools (agent toolset + MCP) run automatically vs wa
|
||||
**Responding to `always_ask`:** Send a `user.tool_confirmation` event with `tool_use_id` from the triggering `agent_tool_use`/`mcp_tool_use` event:
|
||||
|
||||
```json
|
||||
{ "type": "tool_confirmation", "tool_use_id": "sevt_abc123", "result": "allow" }
|
||||
{ "type": "tool_confirmation", "tool_use_id": "sevt_def456", "result": "deny", "message": "Read .env.example instead" }
|
||||
{ "type": "user.tool_confirmation", "tool_use_id": "sevt_abc123", "result": "allow" }
|
||||
{ "type": "user.tool_confirmation", "tool_use_id": "sevt_def456", "result": "deny", "message": "Read .env.example instead" }
|
||||
```
|
||||
|
||||
The optional `message` on a deny is delivered to the agent so it can adjust its approach.
|
||||
@@ -184,7 +184,7 @@ This keeps secrets out of reusable agent definitions. Each vault credential is t
|
||||
|
||||
> 💡 **Changing tools/MCP servers on a running session:** `sessions.update()` can replace `agent.tools`, `agent.mcp_servers`, and `vault_ids` while the session is `idle` — a session-local override that doesn't touch the agent object. See `shared/managed-agents-core.md` → Updating the agent configuration mid-session.
|
||||
|
||||
**Large MCP tool outputs.** If an MCP tool returns more than **100K tokens**, the output is automatically offloaded to a file in the sandbox — the agent receives a truncated preview plus the file path and can `read` the full content. No configuration required.
|
||||
**Large tool outputs.** If a tool returns more than **100,000 characters (roughly 25,000 tokens)**, the output is automatically offloaded to a file in the sandbox — the agent receives a truncated preview plus the file path and can `read` the full content. No configuration required. The threshold is in *characters*, not tokens, and applies to built-in agent tools as well as MCP tools.
|
||||
|
||||
**Invalid vault credentials don't block session creation.** If a vault credential is invalid for a declared MCP server, the session still creates successfully; a `session.error` event describes the MCP auth failure, and auth retries on the next `session.status_idle` → `session.status_running` transition.
|
||||
|
||||
@@ -194,7 +194,7 @@ This keeps secrets out of reusable agent definitions. Each vault credential is t
|
||||
|
||||
**Vaults** store credentials that Anthropic manages on your behalf. Two credential categories:
|
||||
|
||||
- **MCP credentials** (`mcp_oauth`, `static_bearer`) — keyed by `mcp_server_url`. When the agent connects to a server at that URL, the token is injected automatically. `mcp_oauth` tokens are auto-refreshed via the standard OAuth 2.0 `refresh_token` grant. This is the only way to authenticate MCP servers.
|
||||
- **MCP credentials** (`mcp_oauth`, `static_bearer`) — keyed by `mcp_server_url`. When the agent connects to a server at that URL, the token is injected automatically. **Matching is normalized, not byte-exact:** scheme and host are lowercased, and default ports and trailing slashes are stripped, so host casing, an explicit default port, or a trailing slash won't break the match. A different path, subdomain, or *non-default* port will. If nothing matches, the connection is attempted unauthenticated. `mcp_oauth` tokens are auto-refreshed via the standard OAuth 2.0 `refresh_token` grant. This is the only way to authenticate MCP servers.
|
||||
- **Environment variables** (`environment_variable`) — keyed by `secret_name` (the env var name). The sandbox sees only an **opaque placeholder**; the real secret is substituted into the outbound request **at egress**. Use this for any service that authenticates through an environment variable: CLIs (`aws`, `gcloud`, `stripe`), SDKs, or direct `curl` calls from the `bash` tool.
|
||||
|
||||
Secret fields you supply (`token`, `access_token`, `refresh_token`, `client_secret`, `secret_value`) are write-only — never returned in API responses.
|
||||
@@ -205,7 +205,7 @@ Vaults store credentials; those credentials **never enter the sandbox**. This is
|
||||
|
||||
- **MCP tool calls** are routed through an Anthropic-side proxy that fetches the credential from the vault and adds it to the outbound request.
|
||||
- **Git operations on attached GitHub repositories** (`git pull`, `git push`, GitHub REST calls) are routed through a git proxy that injects the `github_repository` resource's `authorization_token` the same way.
|
||||
- **Environment-variable credentials** appear in the sandbox as an opaque placeholder; the real value replaces the placeholder at egress, on requests to the credential's allowed hosts only.
|
||||
- **Environment-variable credentials** appear in the sandbox as an opaque placeholder; the real value replaces the placeholder at egress, on requests to the credential's allowed hosts only. Substitution covers request **headers and body only** — a secret embedded in the **URL path** is never substituted, so path-secret endpoints (e.g. Slack incoming-webhook URLs) can't be vaulted; use header-based auth instead (for Slack: a bot token in `Authorization` via `chat.postMessage`).
|
||||
|
||||
**When vault credentials don't fit** (e.g. self-hosted sandboxes — `environment_variable` is not yet supported there), **register a custom tool:** the agent emits `agent.custom_tool_use`, your orchestrator (which already holds the credential) executes the call and returns `user.custom_tool_result` over the same authenticated event stream. No public endpoint is exposed; the sandbox never sees the secret. See `shared/managed-agents-client-patterns.md` → Pattern 9.
|
||||
|
||||
@@ -280,6 +280,8 @@ Omit `refresh` entirely if you only have an access token with no refresh capabil
|
||||
|
||||
A credential must have at least one location enabled; a create or update that would disable both returns 400, as does explicit `null` for the object or either field (omit instead). The response always returns both fields with their resolved values.
|
||||
|
||||
> ⚠️ **Credentials created in the Console are header-only by default** — unlike the API, where omitting the field enables both. If your client sends the secret in the request body (a form-encoded token request, for example), the placeholder passes through literally and the service rejects it with its own authentication error. Tick body injection in the Console form, or `POST` the credential with `{"injection_location": {"body": true}}`.
|
||||
|
||||
> ⚠️ **Two networking layers, both required.** `networking.allowed_hosts` on the credential controls which requests *use the secret*, not which requests are *allowed*. The agent must also be able to reach the domain at the **environment level** (`unrestricted`, or the host listed in the environment's `allowed_hosts` — see `shared/managed-agents-environments.md`). A domain missing from either layer means the secret-substituted request fails.
|
||||
|
||||
> ⚠️ **Client-side validation caveat.** Substitution happens at egress, not inside the sandbox — clients that validate the credential *format* locally before making a network request (e.g. a CLI that checks the key starts with `sk-`) will see the opaque placeholder and may fail at startup. If a client rejects the credential before any network call, that's why.
|
||||
@@ -350,7 +352,9 @@ agent = client.beta.agents.create(
|
||||
|---|---|---|
|
||||
| `type` | `"anthropic"` | `"custom"` |
|
||||
| `skill_id` | Skill name (e.g. `"xlsx"`, `"docx"`, `"pptx"`, `"pdf"`) | Skill ID from Skills API (e.g. `"skill_abc123"`) |
|
||||
| `version` | — | `"latest"` or a specific version number |
|
||||
| `version` | `"latest"` or a specific version number | `"latest"` or a specific version number |
|
||||
|
||||
`version` is optional on **both** kinds and defaults to `"latest"` — it is not custom-skill-only.
|
||||
|
||||
### Skills API
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ Console → **Manage → Webhooks**. There is no programmatic endpoint-managemen
|
||||
| Field | Constraint |
|
||||
|---|---|
|
||||
| URL | HTTPS on port 443, publicly resolvable hostname |
|
||||
| Event types | Subscribe per `data.type` — you only receive subscribed types (plus test events) |
|
||||
| Event types | Subscribe per `data.type` — an endpoint receives only the types it is subscribed to |
|
||||
| Signing secret | `whsec_`-prefixed, 32 bytes, **shown once at creation** — store it |
|
||||
|
||||
---
|
||||
|
||||
## Verify the signature
|
||||
|
||||
Every delivery is HMAC-signed. **Use the SDK's `client.beta.webhooks.unwrap()`** — it verifies the signature, rejects payloads more than ~5 minutes old, and returns the parsed event. It reads the `whsec_` secret from `ANTHROPIC_WEBHOOK_SIGNING_KEY`.
|
||||
Every delivery carries the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. **Use the SDK's `client.beta.webhooks.unwrap()`** — it verifies the signature, rejects payloads more than ~5 minutes old, and returns the parsed event. It reads the `whsec_` secret from `ANTHROPIC_WEBHOOK_SIGNING_KEY`. Pass the headers through untouched; don't hand-roll verification against a single `X-Webhook-Signature` header, which is not the wire format.
|
||||
|
||||
```python
|
||||
import anthropic
|
||||
@@ -63,7 +63,7 @@ Pass the **raw request body** to `unwrap()` — frameworks that re-serialize JSO
|
||||
```json
|
||||
{
|
||||
"type": "event",
|
||||
"id": "event_01ABC...",
|
||||
"id": "whe_9d5c1f7e...",
|
||||
"created_at": "2026-03-18T14:05:22Z",
|
||||
"data": {
|
||||
"type": "session.status_idled",
|
||||
@@ -74,7 +74,9 @@ Pass the **raw request body** to `unwrap()` — frameworks that re-serialize JSO
|
||||
}
|
||||
```
|
||||
|
||||
Switch on `data.type`, fetch the resource by `data.id`, return any **2xx** to acknowledge. `created_at` is when the *state transition* happened, not when the webhook fired.
|
||||
Switch on `data.type`, fetch the resource by `data.id`, return any **2xx** to acknowledge. `created_at` is when the *event occurred*, not when the delivery was attempted — the `webhook-timestamp` header is the clock for the attempt (see Delivery behavior).
|
||||
|
||||
The top-level `id` is the same value as the `webhook-id` header, and it is per *event*, not per delivery — every retry carries it unchanged. Dedupe on it.
|
||||
|
||||
---
|
||||
|
||||
@@ -85,16 +87,20 @@ Switch on `data.type`, fetch the resource by `data.id`, return any **2xx** to ac
|
||||
| `session.status_scheduled` | Session created and ready to accept events |
|
||||
| `session.status_run_started` | Agent execution kicked off (every transition to `running`) |
|
||||
| `session.status_idled` | Agent awaiting input (tool approval, custom tool result, or next message) |
|
||||
| `session.status_terminated` | Session hit a terminal error |
|
||||
| `session.status_rescheduled` | A transient error occurred; the session is retrying automatically |
|
||||
| `session.status_terminated` | Session ended — **on completion or on error**, not error-only |
|
||||
| `session.thread_created` | Multiagent: coordinator opened a new subagent thread |
|
||||
| `session.thread_idled` | Multiagent: a subagent thread is waiting for input |
|
||||
| `session.thread_terminated` | A thread ended — child completed its work, or the thread was archived. **Child threads only**; the primary thread's end surfaces as `session.status_terminated` |
|
||||
| `session.outcome_evaluation_ended` | Outcome grader finished one iteration |
|
||||
| `session.updated` | Session properties changed (name, configuration) |
|
||||
| `session.deleted` | Session permanently deleted — no object left to fetch; treat the event itself as final |
|
||||
| `vault.archived` | Vault was archived |
|
||||
| `vault.created` | Vault was created |
|
||||
| `vault.deleted` | Vault was deleted |
|
||||
| `vault_credential.archived` | Vault credential was archived |
|
||||
| `vault.deleted` | Vault was deleted — a `vault_credential.deleted` also fires per underlying credential. No object left to fetch; treat the event itself as final |
|
||||
| `vault_credential.archived` | Credential archived, directly or via vault archival |
|
||||
| `vault_credential.created` | Vault credential was created |
|
||||
| `vault_credential.deleted` | Vault credential was deleted |
|
||||
| `vault_credential.deleted` | Credential deleted, directly or via vault deletion. No object left to fetch; treat the event itself as final |
|
||||
| `vault_credential.refresh_failed` | MCP OAuth vault credential failed to refresh |
|
||||
| `agent.created` | Agent created |
|
||||
| `agent.updated` | A new agent version was published. Updates that do not create a new version do **not** fire this. |
|
||||
@@ -109,6 +115,15 @@ Switch on `data.type`, fetch the resource by `data.id`, return any **2xx** to ac
|
||||
| `deployment_run.started` | A **scheduled** run started. Manual runs do **not** emit `deployment_run.*` events. |
|
||||
| `deployment_run.succeeded` | Scheduled run created its session. Same `data.id` (the run ID) as the run's `.started` event — fetch the deployment run for its `session_id`, then subscribe to the session events to follow the work. |
|
||||
| `deployment_run.failed` | Scheduled run did not create a session. Same `data.id` as the run's `.started` event — fetch the deployment run for `error.type` / `error.message`. |
|
||||
| `environment.created` | Environment created |
|
||||
| `environment.updated` | Environment updated with at least one changed field. A no-op update emits nothing. |
|
||||
| `environment.archived` | Environment archived. Re-archiving an already-archived environment emits nothing. |
|
||||
| `environment.deleted` | Environment deleted, including delete of an already-archived one. No object left to fetch; treat the event itself as final |
|
||||
| `memory_store.created` | Memory store created — by you, or by an Anthropic-operated process that clones one of your stores |
|
||||
| `memory_store.archived` | Memory store archived. Re-archiving an already-archived store emits nothing. |
|
||||
| `memory_store.deleted` | Memory store deleted, including delete of an already-archived one. Cascades to its memories and versions **without** per-memory events — this single event is the signal. No object left to fetch; treat it as final |
|
||||
|
||||
> **There is deliberately no `memory_store.updated`.** Individual memories and memory versions emit no webhook events at all, and neither do an environment's self-hosted work items. If you need per-memory change tracking, poll the memory-versions endpoints (`shared/managed-agents-memory.md`).
|
||||
|
||||
> These are **webhook** `data.type` values — a separate namespace from SSE event types (`session.status_idle`, `span.outcome_evaluation_end`, etc. in `shared/managed-agents-events.md`). Don't reuse SSE constants in webhook handlers.
|
||||
|
||||
@@ -116,8 +131,13 @@ Switch on `data.type`, fetch the resource by `data.id`, return any **2xx** to ac
|
||||
|
||||
## Delivery behavior & pitfalls
|
||||
|
||||
- **No ordering guarantee.** `session.status_idled` may arrive before `session.outcome_evaluation_ended` even if the evaluation finished first. Sort by envelope `created_at` if order matters.
|
||||
- **Retries carry the same `event.id`.** At least one retry on non-2xx. Dedupe on `event.id`.
|
||||
- **3xx is failure.** Redirects are not followed — update the URL in Console if your endpoint moves.
|
||||
- **Auto-disable** after ~20 consecutive failed deliveries, or immediately if the hostname resolves to a private IP or returns a redirect. Re-enable manually in Console.
|
||||
- **Duplicates.** An endpoint can receive the same event more than once; every attempt carries the same top-level `event.id` (= the `webhook-id` header). Dedupe on it.
|
||||
- **Subscription scope.** An event reaches only endpoints subscribed to its type **at the moment it is emitted**. An event emitted while nothing was subscribed is never delivered, and subscribing later does not backfill — subscribe before you need the type.
|
||||
- **No ordering guarantee.** Events are not delivered in occurrence order: `session.status_idled` may arrive before `session.outcome_evaluation_ended`, and a `.deleted` can arrive before the `.archived` for the same resource. **Drive state from the resource you fetch, not from arrival order.**
|
||||
- **Retries: up to three attempts** per endpoint per event, with jittered exponential backoff between 5 and 120 seconds. A response that triggers auto-disable is never retried. **After the last attempt fails the event is dropped** — not queued, and with no signal that it was lost. Webhooks are not a durable log: if you must observe every transition, reconcile by listing or fetching the resource.
|
||||
- **`webhook-timestamp` is re-stamped on every attempt**, so retries don't fail the SDK's five-minute freshness check. It times the *delivery attempt*; use the payload's `created_at` for when the event occurred.
|
||||
- **Auto-disable — three triggers**, each setting `disabled_reason`, all reversible from Console (events emitted while disabled are **not** replayed):
|
||||
- A `3xx` response. Redirects are never followed; disables immediately, on the first attempt. Reason: `auto-disabled: endpoint URL returned a redirect (3xx)`.
|
||||
- The URL resolves to a non-public IP at connect time. Disables immediately. Reason: `auto-disabled: endpoint URL resolved to an invalid address`.
|
||||
- Continuous failure for a sustained period. Reason: `auto-disabled after sustained delivery failures`. **The trigger is duration, not a delivery count** — a single `2xx` resets the window, so one flaky event can't disable the endpoint.
|
||||
- **Thin payload is intentional.** Don't expect `stop_reason`, `outcome_evaluations`, credential secrets, etc. on the webhook body — fetch the resource.
|
||||
|
||||
@@ -59,9 +59,25 @@ Any `tool_choice` value can also include `"disable_parallel_tool_use": true` to
|
||||
|
||||
### Tool Runner vs Manual Loop
|
||||
|
||||
**Tool Runner (Recommended):** The SDK's tool runner handles the agentic loop automatically — it calls the API, detects tool use requests, executes your tool functions, feeds results back to Claude, and repeats until Claude stops calling tools. Available in Python, TypeScript, Java, Go, Ruby, and PHP SDKs (beta). The Python SDK also provides MCP conversion helpers (`anthropic.lib.tools.mcp`) to convert MCP tools, prompts, and resources for use with the tool runner — see `python/claude-api/tool-use.md` for details.
|
||||
**Tool Runner (Recommended):** The SDK's tool runner handles the agentic loop automatically — it calls the API, detects tool use requests, executes your tool functions, feeds results back to Claude, and repeats until Claude stops calling tools. Available in Python, TypeScript, Java, Go, Ruby, PHP, and C# SDKs (beta). The Python SDK also provides MCP conversion helpers (`anthropic.lib.tools.mcp`) to convert MCP tools, prompts, and resources for use with the tool runner — see `python/claude-api/tool-use.md` for details. **Default to the tool runner** for any custom-tool agent.
|
||||
|
||||
**Manual Agentic Loop:** Use when you need fine-grained control over the loop (e.g., custom logging, conditional tool execution, human-in-the-loop approval). Loop until `stop_reason == "end_turn"`, always append the full `response.content` to preserve tool_use blocks, and ensure each `tool_result` includes the matching `tool_use_id`.
|
||||
**The tool runner is not a black box — "I need control" is rarely a reason to drop to the manual loop.** Each iteration yields the assistant message *before* the tools run and lets you intervene, so most "fine-grained control" needs are covered without hand-writing the loop:
|
||||
|
||||
- **Human-in-the-loop approval / gating** — gate in the tool's run function (return a "user declined" result instead of executing), or inspect the tool call in the yielded message and override the pending request with `set_messages_params()` / `setMessagesParams()` / `append_messages()` / `pushMessages()` to allow or deny *before* the tool executes. The runner runs your function automatically only if you don't intervene.
|
||||
- **Error interception** — inspect the tool result before it returns to Claude (`generate_tool_call_response()` / `generateToolResponse()`); stop early or handle it yourself.
|
||||
- **Result modification** — mutate the tool result before it goes back (e.g. add `cache_control` for prompt caching, or transform the output).
|
||||
- **Per-turn retries / param changes** — e.g. bump `max_tokens` and re-run a truncated turn; bound the whole loop with `max_iterations`.
|
||||
- **Streaming and automatic compaction** are both supported.
|
||||
|
||||
These hooks are SDK helper features, not separate API parameters — for the exact method names and worked examples, WebFetch the per-language SDK repo listed in `shared/live-sources.md` → *Claude API SDK Repositories* (the tool-runner helpers live in each repo's `tools.md` / `helpers.md`). The bundled `python/claude-api/tool-use.md` and `typescript/claude-api/tool-use.md` show the basic tool-runner setup.
|
||||
|
||||
**Don't drop to a manual loop because of these misconceptions:**
|
||||
|
||||
- The tool runner does not require Zod/Pydantic — `betaTool()` (TS) and `@beta_tool` (Python) accept raw JSON Schema; other SDKs use plain structs/maps/classes.
|
||||
- The runner makes detecting the final turn *easier*, not harder — iteration ends when Claude stops calling tools, and the last yielded message is the final response. Most SDKs also offer a one-shot variant (`runner.until_done()` / `runner.runUntilDone()` / `RunToCompletion()`).
|
||||
- Confirmation/approval gates work with the runner (see Security below).
|
||||
|
||||
**Manual Agentic Loop:** Reach for this only when you want to own the *entire* loop — you need control the runner does not expose (e.g., a custom transport, request shapes the SDK cannot build, per-token streaming on SDKs whose runner does not support it), you'd rather not take the beta dependency, or your control flow doesn't fit the runner's per-turn hooks (e.g. interleaving unrelated work mid-loop). Approval gates, logging, interception, result modification, and conditional execution do **not** require it — the tool runner covers those (above). Loop until `stop_reason == "end_turn"`, always append the full `response.content` to preserve tool_use blocks, and ensure each `tool_result` includes the matching `tool_use_id`.
|
||||
|
||||
**Stop reasons for server-side tools:** When using server-side tools (code execution, web search, etc.), the API runs a server-side sampling loop. If this loop reaches its default limit of 10 iterations, the response will have `stop_reason: "pause_turn"`. To continue, re-send the user message and assistant response and make another API request — the server will resume where it left off. Do NOT add an extra user message like "Continue." — the API detects the trailing `server_tool_use` block and knows to resume automatically.
|
||||
|
||||
@@ -78,9 +94,11 @@ if response.stop_reason == "pause_turn":
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** the SDK tool runners do not auto-resume `pause_turn` (as of `@anthropic-ai/sdk` 0.110.0 / `anthropic` 0.116.0) — a paused turn ends the runner and is returned as the final message, with no error. In TypeScript you can resume inside the iteration body (push the paused assistant turn back onto the runner); in Python the runner cannot be resumed mid-loop — restart a new runner with the paused turn appended, or handle `pause_turn` in a manual loop. See each language's `tool-use.md` for the pattern.
|
||||
|
||||
Set a `max_continuations` limit (e.g., 5) to prevent infinite loops. For the full guide, see: `https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons`
|
||||
|
||||
> **Security:** The tool runner executes your tool functions automatically whenever Claude requests them. For tools with side effects (sending emails, modifying databases, financial transactions), validate inputs within your tool functions and consider requiring confirmation for destructive operations. Use the manual agentic loop if you need human-in-the-loop approval before each tool execution.
|
||||
> **Security:** The tool runner executes your tool functions automatically whenever Claude requests them. For tools with side effects (sending emails, modifying databases, financial transactions), validate inputs and gate destructive operations behind human approval. **Both** the tool runner and the manual loop support this — with the tool runner, gate inside the tool's run function (prompt the user and return a "user declined" result instead of executing), or inspect the tool call in each yielded message and take over message history with `set_messages_params()` / `setMessagesParams()` to allow or deny *before* the tool runs (it executes your function automatically only if you don't intervene); with the manual loop you gate inline before calling the function.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user