mirror of
https://github.com/Piebald-AI/claude-code-system-prompts.git
synced 2026-05-30 05:35:24 +08:00
201 lines
9.7 KiB
Markdown
201 lines
9.7 KiB
Markdown
<!--
|
|
name: 'Data: Managed Agents events and steering'
|
|
description: Reference guide for sending and receiving events on managed agent sessions, including streaming, polling, reconnection, message queuing, interrupts, and event payload details
|
|
ccVersion: 2.1.132
|
|
-->
|
|
# Managed Agents — Events & Steering
|
|
|
|
## Events
|
|
|
|
### Sending Events
|
|
|
|
Send events to a session via `POST /v1/sessions/{id}/events`.
|
|
|
|
| Event Type | When to Send |
|
|
| ------------------------- | --------------------------------------------------- |
|
|
| `user.message` | Send a user message |
|
|
| `user.interrupt` | Interrupt the agent while it's running |
|
|
| `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` |
|
|
|
|
### Receiving Events
|
|
|
|
Three methods:
|
|
|
|
1. **Streaming (SSE)**: `GET /v1/sessions/{id}/events/stream` — real-time Server-Sent Events. **Long-lived** — the server sends periodic heartbeats to keep the connection alive.
|
|
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 received events carry `id`, `type`, and `processed_at` (ISO 8601; `null` if not yet processed by the agent).
|
|
|
|
> ⚠️ **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.
|
|
>
|
|
> If `GET /v1/sessions/{id}/events` (paginated) ever hangs after headers, you've likely hit `GET /v1/sessions/{id}/events` by mistake or a server-side stall — report it; don't treat it as a client-config problem.
|
|
|
|
### Event Types (Received)
|
|
|
|
Event types use dot notation, grouped by namespace:
|
|
|
|
| Event Type | Description |
|
|
| --- | --- |
|
|
| `agent.message` | Agent text output |
|
|
| `agent.thinking` | Extended thinking blocks |
|
|
| `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 |
|
|
| `agent.mcp_tool_result` | Result from an MCP tool |
|
|
| `agent.custom_tool_use` | Agent invoked a custom tool — session goes idle, you respond with `user.custom_tool_result` |
|
|
| `agent.thread_context_compacted` | Conversation context was compacted |
|
|
| `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.error` | Error occurred during processing |
|
|
| `span.model_request_start` | Model inference started |
|
|
| `span.model_request_end` | Model inference completed |
|
|
| `span.outcome_evaluation_start` / `_ongoing` / `_end` | Grader progress for outcome-oriented sessions — see `shared/managed-agents-outcomes.md` |
|
|
| `session.thread_created` | Subagent thread spawned (multiagent) — see `shared/managed-agents-multiagent.md` |
|
|
| `session.thread_status_running` / `_idle` / `_rescheduled` / `_terminated` | Subagent thread status transitions (multiagent). `_idle` carries `stop_reason`. |
|
|
| `agent.thread_message_sent` / `_received` | Cross-thread message, carries `to_session_thread_id` / `from_session_thread_id` (multiagent) |
|
|
|
|
The stream also echoes back user-sent events (`user.message`, `user.interrupt`, `user.tool_confirmation`, `user.custom_tool_result`, `user.define_outcome`).
|
|
|
|
---
|
|
|
|
## Steering Patterns
|
|
|
|
Practical patterns for driving a session via the events surface.
|
|
|
|
### Stream-first ordering
|
|
|
|
**Open the stream before sending events.** The stream only delivers events that occur *after* it's opened — it does not replay current state or historical events. If you send a message first and open the stream second, early events (including fast status transitions) arrive buffered in a single batch and you lose the ability to react to them in real time.
|
|
|
|
```ts
|
|
// ✅ Correct — stream and send concurrently
|
|
const [response] = await Promise.all([
|
|
streamEvents(sessionId), // opens SSE connection
|
|
sendMessage(sessionId, text),
|
|
]);
|
|
|
|
// ❌ Wrong — events before stream opens arrive as a single buffered batch
|
|
await sendMessage(sessionId, text);
|
|
const response = await streamEvents(sessionId);
|
|
```
|
|
|
|
**For full history,** use `GET /v1/sessions/{id}/events` (paginated list) — the stream only gives you live events from connection onward.
|
|
|
|
### Reconnecting after a dropped stream
|
|
|
|
**The SSE stream has no replay.** If your connection drops (httpx read timeout, network blip) and you reconnect, you only get events emitted *after* reconnection. Any events emitted during the gap are lost from the stream.
|
|
|
|
**The consolidation pattern:** on every (re)connect, overlap the stream with a history fetch and dedupe by event ID:
|
|
|
|
```python
|
|
def connect_with_consolidation(client, session_id):
|
|
# 1. Open the SSE stream first
|
|
stream = client.beta.sessions.events.stream(session_id=session_id)
|
|
|
|
# 2. Fetch history to cover any gap
|
|
history = client.beta.sessions.events.list(
|
|
session_id=session_id,
|
|
)
|
|
|
|
# 3. Yield history first, then stream — dedupe by event.id
|
|
seen = set()
|
|
for ev in history.data:
|
|
seen.add(ev.id)
|
|
yield ev
|
|
for ev in stream:
|
|
if ev.id not in seen:
|
|
seen.add(ev.id)
|
|
yield ev
|
|
```
|
|
|
|
### Message queuing
|
|
|
|
**You don't have to wait for a response before sending the next message.** User events are queued server-side and processed in order. This is useful for chat bridges where the user sends rapid follow-ups:
|
|
|
|
```ts
|
|
// All three go into one session; agent processes them in order
|
|
await sendMessage(sessionId, "Summarize the README");
|
|
await sendMessage(sessionId, "Actually also check the CONTRIBUTING guide");
|
|
await sendMessage(sessionId, "And compare the two");
|
|
// Stream once — agent responds to all three as a coherent turn
|
|
```
|
|
|
|
Events can be sent up to the Session at any time. There is no need to wait on a specific session status to enqueue new events via `client.beta.sessions.events.send()`
|
|
|
|
### 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:
|
|
|
|
```ts
|
|
await client.beta.sessions.events.send(sessionId, {
|
|
events: [{ type: '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`).
|
|
|
|
> **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
|
|
|
|
some events carry useful metadata beyond the status change itself:
|
|
|
|
`session.status_idle` — includes a `stop_reason` field which elaborates on why the session stopped and what type of further action is required by the user.
|
|
```json
|
|
{
|
|
"id": "sevt_456",
|
|
"processed_at": "2026-04-07T04:27:43.197Z",
|
|
"stop_reason": {
|
|
"event_ids": [
|
|
"sevt_123"
|
|
],
|
|
"type": "requires_action"
|
|
},
|
|
"type": "status_idle"
|
|
}
|
|
```
|
|
|
|
`span.model_request_end` contains a `model_usage` field for cost tracking and efficiency analysis:
|
|
|
|
```json
|
|
{
|
|
"type": "span.model_request_end",
|
|
"id": "sevt_456",
|
|
"is_error": false,
|
|
"model_request_start_id": "sevt_123",
|
|
"model_usage": {
|
|
"cache_creation_input_tokens": 0,
|
|
"cache_read_input_tokens": 6656,
|
|
"input_tokens": 3571,
|
|
"output_tokens": 727
|
|
},
|
|
"processed_at": "2026-04-07T04:11:32.189Z"
|
|
}
|
|
```
|
|
|
|
**`agent.thread_context_compacted`** — emitted when the conversation history was summarized to fit context. Includes `pre_compaction_tokens` so you know how much was squeezed:
|
|
|
|
```json
|
|
{
|
|
"id": "sevt_abc123",
|
|
"processed_at": "2026-03-24T14:05:15.787Z",
|
|
"type": "agent.thread_context_compacted"
|
|
}
|
|
```
|
|
|
|
### Archive
|
|
|
|
When done with a session, archive it to free resources:
|
|
|
|
```ts
|
|
await client.beta.sessions.archive(sessionId);
|
|
```
|
|
|
|
> Archiving a **session** is routine cleanup — sessions are per-run and disposable. **Do not generalize this to agents or environments**: those are persistent, reusable resources, and archiving them is permanent (no unarchive; new sessions cannot reference them). See `shared/managed-agents-overview.md` → Common Pitfalls.
|
|
|
|
|