Follow Discord
Sweep 09 Oct 2026 · 17:27Z Build v2.1.296 517 read Stable v2.1.287 Latest v2.1.296 Next v2.1.296 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · api

events-and-streaming changedmanaged-agents/events-and-streaming

Nearest release: v2.1.294, published an hour after this site recorded the change. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Recorded here
Lines+1,050added
Lines−1,927removed
From line 10 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits19to this page, all time

## Stream events ### Reconnect without missing events ## Send events ## Respond when the session goes idle ## Answer tool calls that pause the session ### Return a custom tool result ### Confirm a tool call ## Resume an idle session ## Interrupt the agent ## List past events ## Send system messages ### Supported models ### System messages while a tool call is pending ## Next steps ## Integrating events ## Event deltas ### Opt in to previews ### Accumulate and reconcile ### Preview session thread events ### Limitations ### Troubleshoot previews ## Additional scenarios ### Handling custom tool calls ### Tool confirmation ### Resuming an idle session ### Reaching a session budget ### Sending system messages ### Tracking usage ## Console observability ## Debugging tips

The whole hunk

from line 10, old and new numbered
/
lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 10
1010 betaHeader: managed-agents-2026-04-01
1111---
1212 
13Communication with Claude Managed Agents is event-based. You send user events to the agent, and receive agent and session events back to track status.
13Communication with Claude Managed Agents is event-based. You open a stream to follow a session, send events to direct it, and answer it when it pauses for input.
1414 
1515## Event types
1616 
17Events flow in two directions.
18 
19* **User events** and **system events** are what you send to the agent: `user.*` events start a session and steer it as it progresses; `system.message` appends system-level context that applies to the accompanying turn and all subsequent turns.
20* **Session events**, **span events**, and **agent events** are sent to you for observability into your session state and agent progress. Stream connections that opt in also receive [event deltas](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#event-deltas).
21 
22Session, span, agent, user, and system event type strings follow a `{domain}.{action}` naming convention. The stream-only delta preview events (`event_start`, `event_delta`) are the exception. See [Event types](https://platform.claude.com/docs/en/managed-agents/reference#event-types) in the reference for the full catalog. [Webhook event types](https://platform.claude.com/docs/en/managed-agents/webhooks#supported-event-types) are separate, and some of their names differ from the stream's (for example, `session.status_idled` rather than `session.status_idle`).
23 
24Every persisted event includes a `processed_at` timestamp set when the event finishes processing. On events you send, `processed_at` is null while the event is still queued behind earlier events. The exceptions are `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result`, which are processed on receipt and echoed back with `processed_at` already populated.
25 
26## Integrating events
27 
28<Tabs>
29 <Tab title="Sending events">
30 Send a `user.message` event to start or continue the agent's work:
31 
32 <CodeGroup>
33 ```bash cURL
34 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
35 -H "x-api-key: $ANTHROPIC_API_KEY" \
36 -H "anthropic-version: 2023-06-01" \
37 -H "anthropic-beta: managed-agents-2026-04-01" \
38 -H "content-type: application/json" \
39 -d @- <<'EOF'
40 {
41 "events": [
42 {
43 "type": "user.message",
44 "content": [
45 {"type": "text", "text": "Analyze the performance of the sort function in utils.py"}
46 ]
47 }
48 ]
49 }
50 EOF
51 ```
52 
53 ```bash CLI
54 ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
55 events:
56 - type: user.message
57 content:
58 - type: text
59 text: Analyze the performance of the sort function in utils.py
60 YAML
61 ```
62 
63 ```python Python
64 client.beta.sessions.events.send(
65 session.id,
66 events=[
67 {
68 "type": "user.message",
69 "content": [
70 {
71 "type": "text",
72 "text": "Analyze the performance of the sort function in utils.py",
73 },
74 ],
75 },
76 ],
77 )
78 ```
79 
80 ```typescript TypeScript
81 await client.beta.sessions.events.send(session.id, {
82 events: [
83 {
84 type: "user.message",
85 content: [
86 {
87 type: "text",
88 text: "Analyze the performance of the sort function in utils.py",
89 },
90 ],
91 },
92 ],
93 });
94 ```
95 
96 ```csharp C#
97 await client.Beta.Sessions.Events.Send(session.ID, new()
98 {
99 Events =
100 [
101 new BetaManagedAgentsUserMessageEventParams
102 {
103 Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
104 Content =
105 [
106 new BetaManagedAgentsTextBlock
107 {
108 Type = BetaManagedAgentsTextBlockType.Text,
109 Text = "Analyze the performance of the sort function in utils.py",
110 },
111 ],
112 },
113 ],
114 });
115 ```
116 
117 ```go Go
118 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
119 Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
120 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
121 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
122 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
123 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
124 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
125 Text: "Analyze the performance of the sort function in utils.py",
126 },
127 }},
128 },
129 }},
130 }); err != nil {
131 panic(err)
132 }
133 ```
134 
135 ```java Java
136 client.beta().sessions().events().send(
137 session.id(),
138 EventSendParams.builder()
139 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
140 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
141 .addTextContent("Analyze the performance of the sort function in utils.py")
142 .build())
143 .build());
144 ```
145 
146 ```php PHP
147 $client->beta->sessions->events->send(
148 $session->id,
149 events: [
150 [
151 'type' => 'user.message',
152 'content' => [
153 [
154 'type' => 'text',
155 'text' => 'Analyze the performance of the sort function in utils.py',
156 ],
157 ],
158 ],
159 ],
160 );
161 ```
162 
163 ```ruby Ruby
164 client.beta.sessions.events.send_(
165 session.id,
166 events: [
167 {
168 type: "user.message",
169 content: [
170 {
171 type: "text",
172 text: "Analyze the performance of the sort function in utils.py"
173 }
174 ]
175 }
176 ]
177 )
178 ```
179 </CodeGroup>
180 
181 Send a `user.interrupt` event to stop the agent mid-execution, then follow up with a `user.message` event to redirect it:
182 
183 <CodeGroup>
184 ```bash cURL
185 # Agent is currently analyzing a file...
186 # Interrupt with a new direction:
187 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
188 -H "x-api-key: $ANTHROPIC_API_KEY" \
189 -H "anthropic-version: 2023-06-01" \
190 -H "anthropic-beta: managed-agents-2026-04-01" \
191 -H "content-type: application/json" \
192 -d @- <<'EOF'
193 {
194 "events": [
195 {"type": "user.interrupt"},
196 {
197 "type": "user.message",
198 "content": [
199 {"type": "text", "text": "Instead, focus on fixing the bug in line 42."}
200 ]
201 }
202 ]
203 }
204 EOF
205 ```
206 
207 ```bash CLI
208 # Agent is currently analyzing a file...
209 # Interrupt with a new direction:
210 ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
211 events:
212 - type: user.interrupt
213 - type: user.message
214 content:
215 - type: text
216 text: Instead, focus on fixing the bug in line 42.
217 YAML
218 ```
219 
220 ```python Python
221 # Agent is currently analyzing a file...
222 # Interrupt with a new direction:
223 client.beta.sessions.events.send(
224 session.id,
225 events=[
226 {"type": "user.interrupt"},
227 {
228 "type": "user.message",
229 "content": [
230 {
231 "type": "text",
232 "text": "Instead, focus on fixing the bug in line 42.",
233 },
234 ],
235 },
236 ],
237 )
238 ```
239 
240 ```typescript TypeScript
241 // Agent is currently analyzing a file...
242 // Interrupt with a new direction:
243 await client.beta.sessions.events.send(session.id, {
244 events: [
245 { type: "user.interrupt" },
246 {
247 type: "user.message",
248 content: [
249 {
250 type: "text",
251 text: "Instead, focus on fixing the bug in line 42.",
252 },
253 ],
254 },
255 ],
256 });
257 ```
258 
259 ```csharp C#
260 // Agent is currently analyzing a file...
261 // Interrupt with a new direction:
262 await client.Beta.Sessions.Events.Send(session.ID, new()
263 {
264 Events =
265 [
266 new BetaManagedAgentsUserInterruptEventParams
267 {
268 Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
269 },
270 new BetaManagedAgentsUserMessageEventParams
271 {
272 Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
273 Content =
274 [
275 new BetaManagedAgentsTextBlock
276 {
277 Type = BetaManagedAgentsTextBlockType.Text,
278 Text = "Instead, focus on fixing the bug in line 42.",
279 },
280 ],
281 },
282 ],
283 });
284 ```
285 
286 ```go Go
287 // Agent is currently analyzing a file...
288 // Interrupt with a new direction:
289 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
290 Events: []anthropic.BetaManagedAgentsEventParamsUnion{
291 {
292 OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{
293 Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
294 },
295 },
296 {
297 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
298 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
299 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
300 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
301 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
302 Text: "Instead, focus on fixing the bug in line 42.",
303 },
304 }},
305 },
306 },
307 },
308 }); err != nil {
309 panic(err)
310 }
311 ```
312 
313 ```java Java
314 // Agent is currently analyzing a file...
315 // Interrupt with a new direction:
316 client.beta().sessions().events().send(
317 session.id(),
318 EventSendParams.builder()
319 .addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
320 .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
321 .build())
322 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
323 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
324 .addTextContent("Instead, focus on fixing the bug in line 42.")
325 .build())
326 .build());
327 ```
328 
329 ```php PHP
330 // Agent is currently analyzing a file...
331 // Interrupt with a new direction:
332 $client->beta->sessions->events->send(
333 $session->id,
334 events: [
335 ['type' => 'user.interrupt'],
336 [
337 'type' => 'user.message',
338 'content' => [
339 [
340 'type' => 'text',
341 'text' => 'Instead, focus on fixing the bug in line 42.',
342 ],
343 ],
344 ],
345 ],
346 );
347 ```
348 
349 ```ruby Ruby
350 # Agent is currently analyzing a file...
351 # Interrupt with a new direction:
352 client.beta.sessions.events.send_(
353 session.id,
354 events: [
355 {type: "user.interrupt"},
356 {
357 type: "user.message",
358 content: [
359 {type: "text", text: "Instead, focus on fixing the bug in line 42."}
360 ]
361 }
362 ]
363 )
364 ```
365 </CodeGroup>
366 
367 The call returns as soon as the events are queued, and the interrupt's `processed_at` stays null until the agent applies it. A model response in progress stops immediately. The interrupt can take longer to apply while tool calls are running, and the session stays `running` until it does. The `user.interrupt` event then appears on the stream, and the interrupted turn ends with a `session.status_idle` event. Its `stop_reason` is `end_turn`, the same value as a turn that finishes on its own; there is no stop reason specific to interruption. The agent starts its next turn with the `user.message` you sent after the interrupt.
368 </Tab>
369 
370 <Tab title="Streaming events">
371 Stream events from the session to receive real-time updates as the agent works. Only events emitted after the stream is opened are delivered, so open the stream before sending events to avoid a race condition.
372 
373 <CodeGroup>
374 ```bash cURL
375 # Open the stream first, then send the user message
376 exec {stream}< <(
377 curl --fail-with-body -sS -N \
378 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
379 -H "x-api-key: $ANTHROPIC_API_KEY" \
380 -H "anthropic-version: 2023-06-01" \
381 -H "anthropic-beta: managed-agents-2026-04-01" \
382 -H "content-type: application/json" \
383 -H "accept: text/event-stream"
384 )
385 
386 curl --fail-with-body -sS \
387 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
388 -H "x-api-key: $ANTHROPIC_API_KEY" \
389 -H "anthropic-version: 2023-06-01" \
390 -H "anthropic-beta: managed-agents-2026-04-01" \
391 -H "content-type: application/json" \
392 -d @- >/dev/null <<'EOF'
393 {
394 "events": [
395 {
396 "type": "user.message",
397 "content": [{"type": "text", "text": "Summarize the repo README"}]
398 }
399 ]
400 }
401 EOF
402 
403 while IFS= read -r -u "$stream" event_line; do
404 [[ $event_line == data:* ]] || continue
405 event_json=${event_line#data: }
406 case $(jq -r '.type' <<<"$event_json") in
407 agent.message)
408 jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
409 ;;
410 session.status_idle)
411 break
412 ;;
413 session.error)
414 printf '\n[Error: %s]\n' "$(jq -r '.error.message // "unknown"' <<<"$event_json")"
415 break
416 ;;
417 esac
418 done
419 exec {stream}<&-
420 ```
421 
422 ```bash CLI
423 # This workflow does not translate well to a one-off shell command.
424 # Use one of the SDK examples in this code group instead.
425 ```
426 
427 ```python Python
428 # Open the stream first, then send the user message
429 with client.beta.sessions.events.stream(session.id) as stream:
430 client.beta.sessions.events.send(
431 session.id,
432 events=[
433 {
434 "type": "user.message",
435 "content": [{"type": "text", "text": "Summarize the repo README"}],
436 },
437 ],
438 )
439 
440 for event in stream:
441 match event.type:
442 case "agent.message":
443 for block in event.content:
444 if block.type == "text":
445 print(block.text, end="")
446 case "session.status_idle":
447 break
448 case "session.error":
449 error_message = event.error.message if event.error else "unknown"
450 print(f"\n[Error: {error_message}]")
451 break
452 ```
453 
454 ```typescript TypeScript
455 // Open the stream first, then send the user message
456 const stream = await client.beta.sessions.events.stream(session.id);
457 await client.beta.sessions.events.send(session.id, {
458 events: [
459 {
460 type: "user.message",
461 content: [{ type: "text", text: "Summarize the repo README" }]
462 }
463 ]
464 });
465 
466 events: for await (const event of stream) {
467 switch (event.type) {
468 case "agent.message":
469 for (const block of event.content) {
470 if (block.type === "text") {
471 process.stdout.write(block.text);
472 }
473 }
474 break;
475 case "session.status_idle":
476 break events;
477 case "session.error":
478 console.log(`\n[Error: ${event.error?.message ?? "unknown"}]`);
479 break events;
480 }
481 }
482 ```
483 
484 ```csharp C#
485 // Open the stream first, then send the user message
486 using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
487 await client.Beta.Sessions.Events.Send(session.ID, new()
488 {
489 Events =
490 [
491 new BetaManagedAgentsUserMessageEventParams
492 {
493 Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
494 Content =
495 [
496 new BetaManagedAgentsTextBlock
497 {
498 Type = BetaManagedAgentsTextBlockType.Text,
499 Text = "Summarize the repo README",
500 },
501 ],
502 },
503 ],
504 });
505 
506 await foreach (var streamEvent in stream.Enumerate())
507 {
508 if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
509 {
510 foreach (var block in message.Content)
511 {
512 if (block.Value is BetaManagedAgentsTextBlock textBlock)
513 {
514 Console.Write(textBlock.Text);
515 }
516 }
517 }
518 else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
519 {
520 break;
521 }
522 else if (streamEvent.Value is BetaManagedAgentsSessionErrorEvent error)
523 {
524 Console.WriteLine($"\n[Error: {error.Error?.Message ?? "unknown"}]");
525 break;
526 }
527 }
528 ```
529 
530 ```go Go
531 // Open the stream first, then send the user message
532 stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
533 defer stream.Close()
534 
535 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
536 Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
537 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
538 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
539 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
540 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
541 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
542 Text: "Summarize the repo README",
543 },
544 }},
545 },
546 }},
547 }); err != nil {
548 panic(err)
549 }
550 
551 events:
552 for stream.Next() {
553 switch event := stream.Current().AsAny().(type) {
554 case anthropic.BetaManagedAgentsAgentMessageEvent:
555 // concrete-typed list: BetaManagedAgentsTextBlock
556 for _, block := range event.Content {
557 fmt.Print(block.Text)
558 }
559 case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
560 break events
561 case anthropic.BetaManagedAgentsSessionErrorEvent:
562 fmt.Printf("\n[Error: %s]\n", cmp.Or(event.Error.Message, "unknown"))
563 break events
564 }
565 }
566 if err := stream.Err(); err != nil {
567 panic(err)
568 }
569 ```
570 
571 ```java Java
572 // Open the stream first, then send the user message
573 try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
574 client.beta().sessions().events().send(
575 session.id(),
576 EventSendParams.builder()
577 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
578 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
579 .addTextContent("Summarize the repo README")
580 .build())
581 .build()
582 );
583 
584 Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator;
585 events:
586 for (var event : events) {
587 switch (event.type().value()) {
588 case AGENT_MESSAGE -> event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
589 case SESSION_STATUS_IDLE -> {
590 break events;
591 }
592 case SESSION_ERROR -> {
593 // The `message` field spans all error variants; read it from the raw JSON.
594 var errorMessage =
595 event.asSessionError().error()._json().orElse(null) instanceof JsonObject json
596 ? json.values().get("message").asStringOrThrow()
597 : "unknown";
598 IO.println("\n[Error: " + errorMessage + "]");
599 break events;
600 }
601 }
602 }
603 }
604 ```
605 
606 ```php PHP
607 // Open the stream first, then send the user message
608 $stream = $client->beta->sessions->events->streamStream($session->id);
609 $client->beta->sessions->events->send(
610 $session->id,
611 events: [
612 [
613 'type' => 'user.message',
614 'content' => [['type' => 'text', 'text' => 'Summarize the repo README']],
615 ],
616 ],
617 );
618 
619 foreach ($stream as $event) {
620 match (true) {
621 $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent => array_walk(
622 $event->content,
623 static fn ($block) => $block instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock ? print($block->text) : null,
624 ),
625 $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsSessionErrorEvent => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'),
626 default => null,
627 };
628 if ($event->type === 'session.status_idle' || $event->type === 'session.error') {
629 break;
630 }
631 }
632 $stream->close();
633 ```
634 
635 ```ruby Ruby
636 # Open the stream first, then send the user message
637 stream = client.beta.sessions.events.stream_events(session.id)
638 
639 client.beta.sessions.events.send_(
640 session.id,
641 events: [{
642 type: "user.message",
643 content: [{type: "text", text: "Summarize the repo README"}]
644 }]
645 )
646 
647 stream.each do |event|
648 case event
649 when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
650 event.content.each { print it.text }
651 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
652 break
653 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionErrorEvent
654 puts "\n[Error: #{event.error&.message || "unknown"}]"
655 break
656 else
657 # ignore other event types
658 end
659 end
660 ```
661 </CodeGroup>
662 
663 To reconnect to an existing session without missing events:
664 
665 1. Open a new stream.
666 2. List the full event history to seed a set of seen event IDs.
667 3. Tail the live stream, skipping any events already returned by the history list.
668 
669 <CodeGroup>
670 ```bash cURL
671 exec {stream}< <(
672 curl --fail-with-body -sS -N \
673 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
674 -H "x-api-key: $ANTHROPIC_API_KEY" \
675 -H "anthropic-version: 2023-06-01" \
676 -H "anthropic-beta: managed-agents-2026-04-01" \
677 -H "content-type: application/json" \
678 -H "accept: text/event-stream"
679 )
680 
681 # Stream is open and buffering. List history before tailing live.
682 declare -A seen_event_ids
683 while IFS= read -r event_id; do
684 seen_event_ids[$event_id]=1
685 done < <(
686 curl --fail-with-body -sS \
687 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
688 -H "x-api-key: $ANTHROPIC_API_KEY" \
689 -H "anthropic-version: 2023-06-01" \
690 -H "anthropic-beta: managed-agents-2026-04-01" \
691 -H "content-type: application/json" | jq -r '.data[].id'
692 )
693 
694 # Tail live events, skipping anything already seen
695 while IFS= read -r -u "$stream" event_line; do
696 [[ $event_line == data:* ]] || continue
697 event_json=${event_line#data: }
698 event_id=$(jq -r '.id' <<<"$event_json")
699 [[ -n ${seen_event_ids[$event_id]+seen} ]] && continue
700 seen_event_ids[$event_id]=1
701 case $(jq -r '.type' <<<"$event_json") in
702 agent.message)
703 jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
704 ;;
705 session.status_idle)
706 break
707 ;;
708 esac
709 done
710 exec {stream}<&-
711 ```
712 
713 ```bash CLI
714 # This workflow does not translate well to a one-off shell command.
715 # Use one of the SDK examples in this code group instead.
716 ```
717 
718 ```python Python
719 with client.beta.sessions.events.stream(session.id) as stream:
720 # Stream is open and buffering. List history before tailing live.
721 history = client.beta.sessions.events.list(session.id)
722 seen_event_ids = {past_event.id for past_event in history}
723 
724 # Tail live events, skipping anything already seen
725 for event in stream:
726 if event.type == "event_start" or event.type == "event_delta":
727 # Delta previews aren't enabled on this connection.
728 continue
729 if event.id in seen_event_ids:
730 continue
731 seen_event_ids.add(event.id)
732 match event.type:
733 case "agent.message":
734 for block in event.content:
735 if block.type == "text":
736 print(block.text, end="")
737 case "session.status_idle":
738 break
739 ```
740 
741 ```typescript TypeScript
742 const seenEventIds = new Set<string>();
743 const stream = await client.beta.sessions.events.stream(session.id);
744 
745 // Stream is open and buffering. List history before tailing live.
746 for await (const event of client.beta.sessions.events.list(session.id)) {
747 seenEventIds.add(event.id);
748 }
749 
750 // Tail live events, skipping anything already seen
751 tail: for await (const event of stream) {
752 // Preview events (event_start/event_delta) carry no top-level id
753 if (event.type === "event_start" || event.type === "event_delta") continue;
754 if (seenEventIds.has(event.id)) continue;
755 seenEventIds.add(event.id);
756 switch (event.type) {
757 case "agent.message":
758 for (const block of event.content) {
759 if (block.type === "text") {
760 process.stdout.write(block.text);
761 }
762 }
763 break;
764 case "session.status_idle":
765 break tail;
766 }
767 }
768 ```
769 
770 ```csharp C#
771 using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
772 
773 // Stream is open and buffering. List history before tailing live.
774 HashSet<string> seenEventIds = [];
775 var history = await client.Beta.Sessions.Events.List(session.ID);
776 await foreach (var pastEvent in history.Paginate())
777 {
778 seenEventIds.Add(pastEvent.ID);
779 }
780 
781 // Tail live events, skipping anything already seen
782 await foreach (var streamEvent in stream.Enumerate())
783 {
784 if (!seenEventIds.Add(streamEvent.ID))
785 {
786 continue;
787 }
788 if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
789 {
790 foreach (var block in message.Content)
791 {
792 if (block.Value is BetaManagedAgentsTextBlock textBlock)
793 {
794 Console.Write(textBlock.Text);
795 }
796 }
797 }
798 else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
799 {
800 break;
801 }
802 }
803 ```
804 
805 ```go Go
806 stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
807 defer stream.Close()
808 
809 // Stream is open and buffering. List history before tailing live.
810 seenEventIDs := map[string]struct{}{}
811 history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{})
812 for history.Next() {
813 seenEventIDs[history.Current().ID] = struct{}{}
814 }
815 if err := history.Err(); err != nil {
816 panic(err)
817 }
818 
819 // Tail live events, skipping anything already seen
820 tail:
821 for stream.Next() {
822 event := stream.Current()
823 if _, seen := seenEventIDs[event.ID]; seen {
824 continue
825 }
826 seenEventIDs[event.ID] = struct{}{}
827 switch event := event.AsAny().(type) {
828 case anthropic.BetaManagedAgentsAgentMessageEvent:
829 // concrete-typed list: BetaManagedAgentsTextBlock
830 for _, block := range event.Content {
831 fmt.Print(block.Text)
832 }
833 case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
834 break tail
835 }
836 }
837 if err := stream.Err(); err != nil {
838 panic(err)
839 }
840 ```
841 
842 ```java Java
843 try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
844 // Stream is open and buffering. List history before tailing live.
845 // Every event variant carries `id`; read it from the raw JSON to dedup across variants.
846 var seenEventIds = new HashSet<String>();
847 for (var pastEvent : client.beta().sessions().events().list(session.id()).autoPager()) {
848 if (pastEvent._json().orElseThrow() instanceof JsonObject json) {
849 seenEventIds.add(json.values().get("id").asStringOrThrow());
850 }
851 }
852 
853 // Tail live events; Set.add returns false for already-seen IDs, skipping the replay.
854 stream.stream()
855 .filter(event -> event._json().orElseThrow() instanceof JsonObject json
856 && seenEventIds.add(json.values().get("id").asStringOrThrow()))
857 .takeWhile(event -> !event.isSessionStatusIdle())
858 .filter(BetaManagedAgentsStreamSessionEvents::isAgentMessage)
859 .forEach(event -> event.asAgentMessage().content()
860 .forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))));
861 }
862 ```
863 
864 ```php PHP
865 $stream = $client->beta->sessions->events->streamStream($session->id);
866 
867 // Stream is open and buffering. List history before tailing live.
868 $seenEventIds = [];
869 foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) {
870 $seenEventIds[$event->id] = true;
871 }
872 
873 // Tail live events, skipping anything already seen
874 foreach ($stream as $event) {
875 if (isset($seenEventIds[$event->id])) {
876 continue;
877 }
878 $seenEventIds[$event->id] = true;
879 match (true) {
880 $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent => array_walk(
881 $event->content,
882 static fn ($block) => $block instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock ? print($block->text) : null,
883 ),
884 default => null,
885 };
886 if ($event->type === 'session.status_idle') {
887 break;
888 }
889 }
890 $stream->close();
891 ```
892 
893 ```ruby Ruby
894 stream = client.beta.sessions.events.stream_events(session.id)
895 
896 # Stream is open and buffering. List history before tailing live.
897 seen_event_ids = Set.new
898 client.beta.sessions.events.list(session.id).auto_paging_each { seen_event_ids << it.id }
899 
900 # Tail live events, skipping anything already seen — Set#add? returns nil for duplicates
901 stream.each do |event|
902 next unless seen_event_ids.add?(event.id)
903 case event
904 when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
905 event.content.each { print it.text }
906 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
907 break
908 else
909 # ignore other event types
910 end
911 end
912 ```
913 </CodeGroup>
914 </Tab>
915 
916 <Tab title="Listing past events">
917 Retrieve the full event history for a session:
918 
919 <CodeGroup>
920 ```bash cURL
921 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
922 -H "x-api-key: $ANTHROPIC_API_KEY" \
923 -H "anthropic-version: 2023-06-01" \
924 -H "anthropic-beta: managed-agents-2026-04-01" \
925 -H "content-type: application/json"
926 ```
927 
928 ```bash CLI
929 ant beta:sessions:events list --session-id "$SESSION_ID" --format jsonl
930 ```
931 
932 ```python Python
933 events = client.beta.sessions.events.list(session.id)
934 for event in events.data:
935 print(f"[{event.type}] {event.processed_at}")
936 ```
937 
938 ```typescript TypeScript
939 const events = await client.beta.sessions.events.list(session.id);
940 for (const event of events.data) {
941 console.log(`[${event.type}] ${event.processed_at}`);
942 }
943 ```
944 
945 ```csharp C#
946 var events = await client.Beta.Sessions.Events.List(session.ID);
947 foreach (var sessionEvent in events.Items)
948 {
949 Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
950 }
951 ```
952 
953 ```go Go
954 events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{})
955 if err != nil {
956 panic(err)
957 }
958 for _, event := range events.Data {
959 fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
960 }
961 ```
962 
963 ```java Java
964 var events = client.beta().sessions().events().list(session.id());
965 for (var event : events.data()) {
966 var eventJson = event._json().orElseThrow().convert(JsonNode.class);
967 var processedAt = eventJson.path("processed_at");
968 IO.println("[" + eventJson.get("type").asText() + "] "
969 + (processedAt.isTextual() ? processedAt.asText() : "null"));
970 }
971 ```
972 
973 ```php PHP
974 $events = $client->beta->sessions->events->list($session->id);
975 foreach ($events->data as $event) {
976 $processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'null';
977 echo "[{$event->type}] {$processedAt}\n";
978 }
979 ```
980 
981 ```ruby Ruby
982 events = client.beta.sessions.events.list(session.id)
983 events.data.each { puts "[#{it.type}] #{it.processed_at}" }
984 ```
985 </CodeGroup>
986 
987 Pass a `types` filter to return only specific event types:
988 
989 <CodeGroup>
990 ```bash cURL
991 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true&types[]=agent.tool_use&types[]=agent.tool_result" \
992 -H "x-api-key: $ANTHROPIC_API_KEY" \
993 -H "anthropic-version: 2023-06-01" \
994 -H "anthropic-beta: managed-agents-2026-04-01"
995 ```
996 
997 ```bash CLI
998 ant beta:sessions:events list --session-id "$SESSION_ID" \
999 --type agent.tool_use --type agent.tool_result \
1000 --format jsonl
1001 ```
1002 
1003 ```python Python
1004 events = client.beta.sessions.events.list(
1005 session.id,
1006 types=["agent.tool_use", "agent.tool_result"],
1007 )
1008 for event in events.data:
1009 print(f"[{event.type}] {event.processed_at}")
1010 ```
1011 
1012 ```typescript TypeScript
1013 const events = await client.beta.sessions.events.list(session.id, {
1014 types: ["agent.tool_use", "agent.tool_result"],
1015 });
1016 for (const event of events.data) {
1017 console.log(`[${event.type}] ${event.processed_at}`);
1018 }
1019 ```
1020 
1021 ```csharp C#
1022 var events = await client.Beta.Sessions.Events.List(session.ID, new()
1023 {
1024 Types = ["agent.tool_use", "agent.tool_result"],
1025 });
1026 foreach (var sessionEvent in events.Items)
1027 {
1028 Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
1029 }
1030 ```
1031 
1032 ```go Go
1033 events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{
1034 Types: []anthropic.BetaManagedAgentsSessionEventType{
1035 anthropic.BetaManagedAgentsSessionEventTypeAgentToolUse,
1036 anthropic.BetaManagedAgentsSessionEventTypeAgentToolResult,
1037 },
1038 })
1039 if err != nil {
1040 panic(err)
1041 }
1042 for _, event := range events.Data {
1043 fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
1044 }
1045 ```
1046 
1047 ```java Java
1048 var events = client.beta().sessions().events().list(
1049 session.id(),
1050 EventListParams.builder()
1051 .addType(BetaManagedAgentsSessionEventType.AGENT_TOOL_USE)
1052 .addType(BetaManagedAgentsSessionEventType.AGENT_TOOL_RESULT)
1053 .build());
1054 for (var event : events.data()) {
1055 event.agentToolUse().ifPresent(toolUse ->
1056 IO.println("[" + toolUse.type() + "] " + toolUse.processedAt()));
1057 event.agentToolResult().ifPresent(toolResult ->
1058 IO.println("[" + toolResult.type() + "] " + toolResult.processedAt()));
1059 }
1060 ```
1061 
1062 ```php PHP
1063 // In PHP, pass the types you want on EventListParams; see the Anthropic PHP SDK.
1064 ```
1065 
1066 ```ruby Ruby
1067 events = client.beta.sessions.events.list(
1068 session.id,
1069 types: ["agent.tool_use", "agent.tool_result"]
1070 )
1071 events.data.each { puts "[#{it.type}] #{it.processed_at}" }
1072 ```
1073 </CodeGroup>
1074 </Tab>
1075</Tabs>
1076 
1077## Event deltas
1078 
1079By default, the agent's response text reaches the stream as buffered `agent.message` events, each emitted only after the model request that produced it finishes. Event deltas let you render that text incrementally, as a live preview, while the model is still generating it. A preview is not the response: previews are a best-effort display aid, and the buffered `agent.message` is always the authoritative record. A client that ignores previews still receives a complete, correct stream.
1080 
1081### Opt in to previews
1082 
1083Previews are opt-in per stream connection. Add the `event_deltas[]` query parameter to the stream you're reading, repeating it once for each event type you want previewed. Because `[]` is a shell glob pattern, quote the URL whenever you build the request in a shell; the examples percent-encode the brackets as `%5B%5D`, which also works. Both stream endpoints accept the parameter: the session-level stream at `GET /v1/sessions/{session_id}/events/stream`, and each [session thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration)'s own stream at `GET /v1/sessions/{session_id}/threads/{thread_id}/stream`. The accepted values are `agent.message` and `agent.thinking`; any other value returns a 400 error, as does a request with more than 100 values. A subagent's previews appear on [that subagent's own thread stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#preview-session-thread-events).
1084 
1085When a previewed event begins, the stream emits an `event_start` carrying the upcoming event's type and `id`:
1086 
1087```json
1088{
1089 "type": "event_start",
1090 "event": {
1091 "type": "agent.message",
1092 "id": "sevt_01abc..."
1093 }
1094}
1095```
1096 
1097For `agent.message`, the start is followed by `event_delta` events carrying incremental text. Each delta names the event it extends in `event_id` and the content block it extends in `delta.index`:
1098 
1099```json
1100{
1101 "type": "event_delta",
1102 "event_id": "sevt_01abc...",
1103 "delta": {
1104 "type": "content_delta",
1105 "index": 0,
1106 "content": {
1107 "type": "text",
1108 "text": "Here is the summary"
1109 }
1110 }
1111}
1112```
1113 
1114When an `agent.thinking` event is previewed, only the `event_start` is emitted. No `event_delta` events follow, and the buffered `agent.thinking` event that concludes the preview carries no thinking content; it is a progress signal, not a content carrier.
1115 
1116Unlike persisted events, `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.
1117 
1118<Note>
1119 Event deltas use a different wire format from [Streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming), and the difference is intentional. A previewed `agent.message` gets a single `event_start` followed only by `event_delta` events. There are no per-content-block start or stop events and no stop event for the previewed event itself. The delta type is `content_delta`, not `content_block_delta`. Accumulator code written for the Messages API does not carry over unchanged.
1120</Note>
1121 
1122### Accumulate and reconcile
1123 
1124Every SDK that supports event deltas includes an accumulator helper that handles the `index` bookkeeping for you. The Go, Java, Ruby, and C# helpers also key the accumulating preview by the event's `id`; with the Python, TypeScript, and PHP helpers you keep that map yourself and fold each delta into the entry for its `id`. The manual pattern also works in every language when you need custom bookkeeping: apply it to the generated event types.
1125 
1126In the manual pattern, treat the preview as a scratch buffer and the buffered event as the record. Key the buffer by `(event_id, index)`. Reconcile per model request: a turn opens with a single `session.status_running` event, then on a turn that completes normally each model request produces, in order, `span.model_request_start`, `event_start`, the `event_delta` events, the buffered `agent.message`, and finally [`span.model_request_end`](https://platform.claude.com/docs/en/managed-agents/reference#event-types) (in the Span events tab). On the wire, this is the previewed portion of that sequence, interleaved with the connection's other buffered events:
1127 
1128```text wrap
1129event_start {"event": {"type": "agent.message", "id": "sevt_01abc..."}}
1130event_delta {"event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "..."}}}
1131...
1132agent.message {"id": "sevt_01abc...", "content": [...]}
1133```
1134 
1135The `event_delta` line repeats once per text fragment. Process each event as it arrives:
1136 
11371. On `event_start`, note the announced `id`. The identifiers always line up: `event_start.event.id`, every `event_delta.event_id`, and the buffered `agent.message`'s `id` are the same value.
11382. On each `event_delta`, append `delta.content.text` to the entry at `(event_id, delta.index)` and render the running text. The first delta for an `index` creates that entry.
11393. When the buffered `agent.message` arrives, match it by `id`, discard the accumulated preview, and render the message's content instead.
11404. On `span.model_request_end`, close any preview that has not been reconciled by its buffered event. No more deltas are coming for it. If the turn errors or is interrupted, the buffered event might never arrive; `span.model_request_end` still does.
1141 
1142Guarantees the pattern relies on:
1143 
1144* Concatenating a preview's deltas in arrival order, keyed by `(event_id, index)`, gives a prefix of `content[index].text` in the buffered event (a prefix, not necessarily the whole text, because deltas might be shed under load).
1145* A connection emits at most one `event_start` per `event_id`, and the buffered event is the last thing that connection delivers for that `id`.
17Events flow in two directions: you send events to the agent, and the session sends events back to you.
18 
19* **Events you send:** `user.*` events start a session and steer it as it progresses. [`system.message`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#send-system-messages) events append system-level context.
20* **Events you receive:** Session events, span events, and agent events report the session's state and the agent's progress.
21 
22These event type strings follow a `{domain}.{action}` naming convention. See [Event types](https://platform.claude.com/docs/en/managed-agents/reference#event-types) in the reference for the full catalog.
23 
24[Webhook event types](https://platform.claude.com/docs/en/managed-agents/webhooks#supported-event-types) are separate, and some of their names differ from the stream's. For example, webhooks use `session.status_idled` rather than `session.status_idle`.
25 
26## Stream events
27 
28Stream events from the session to receive real-time updates as the agent works. A stream delivers only the events emitted after it opens, so open the stream before you send events to avoid a race condition.
114629 
114730<CodeGroup>
114831 ```bash cURL
1149 # Opt in to agent.message previews via event_deltas, then accumulate manually.
32 # Open the stream first, then send the user message
115033 exec {stream}< <(
115134 curl --fail-with-body -sS -N \
1152 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true&event_deltas%5B%5D=agent.message" \
35 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
115336 -H "x-api-key: $ANTHROPIC_API_KEY" \
115437 -H "anthropic-version: 2023-06-01" \
115538 -H "anthropic-beta: managed-agents-2026-04-01" \
39 -H "content-type: application/json" \
115640 -H "accept: text/event-stream"
115741 )
115842 
from line 51
116751 "events": [
116852 {
116953 "type": "user.message",
1170 "content": [{"type": "text", "text": "In one short sentence, describe what an event delta is."}]
54 "content": [{"type": "text", "text": "Summarize the repo README"}]
117155 }
117256 ]
117357 }
117458 EOF
117559 
1176 # Accumulate deltas keyed by (message id, content index); the final
1177 # agent.message carries the full text, so it replaces every preview for that id.
1178 declare -A preview
117960 while IFS= read -r -u "$stream" event_line; do
118061 [[ $event_line == data:* ]] || continue
118162 event_json=${event_line#data: }
118263 case $(jq -r '.type' <<<"$event_json") in
1183 event_start)
1184 preview_id=$(jq -r '.event.id' <<<"$event_json")
1185 printf '[event_start id=%s]\n' "$preview_id"
64 agent.message)
65 jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
118666 ;;
1187 event_delta)
1188 preview_key=$(jq -r '.event_id + ":" + (.delta.index | tostring)' <<<"$event_json")
1189 preview[$preview_key]+=$(jq -r '.delta.content.text' <<<"$event_json")
1190 printf '[event_delta] %s\n' "${preview[$preview_key]}"
67 session.status_idle)
68 break
119169 ;;
70 session.error)
71 printf '\n[Error: %s]\n' "$(jq -r '.error.message // "unknown"' <<<"$event_json")"
72 break
73 ;;
74 esac
75 done
76 exec {stream}<&-
77 ```
78 
79 ```bash CLI
80 # This workflow does not translate well to a one-off shell command.
81 # Use one of the SDK examples in this code group instead.
82 ```
83 
84 ```python Python
85 # Open the stream first, then send the user message
86 with client.beta.sessions.events.stream(session.id) as stream:
87 client.beta.sessions.events.send(
88 session.id,
89 events=[
90 {
91 "type": "user.message",
92 "content": [{"type": "text", "text": "Summarize the repo README"}],
93 },
94 ],
95 )
96 
97 for event in stream:
98 match event.type:
99 case "agent.message":
100 for block in event.content:
101 if block.type == "text":
102 print(block.text, end="")
103 case "session.status_idle":
104 break
105 case "session.error":
106 error_message = event.error.message if event.error else "unknown"
107 print(f"\n[Error: {error_message}]")
108 break
109 ```
110 
111 ```typescript TypeScript
112 // Open the stream first, then send the user message
113 const stream = await client.beta.sessions.events.stream(session.id);
114 await client.beta.sessions.events.send(session.id, {
115 events: [
116 {
117 type: "user.message",
118 content: [{ type: "text", text: "Summarize the repo README" }]
119 }
120 ]
121 });
122 
123 events: for await (const event of stream) {
124 switch (event.type) {
125 case "agent.message":
126 for (const block of event.content) {
127 if (block.type === "text") {
128 process.stdout.write(block.text);
129 }
130 }
131 break;
132 case "session.status_idle":
133 break events;
134 case "session.error":
135 console.log(`\n[Error: ${event.error?.message ?? "unknown"}]`);
136 break events;
137 }
138 }
139 ```
140 
141 ```csharp C#
142 // Open the stream first, then send the user message
143 using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
144 await client.Beta.Sessions.Events.Send(session.ID, new()
145 {
146 Events =
147 [
148 new BetaManagedAgentsUserMessageEventParams
149 {
150 Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
151 Content =
152 [
153 new BetaManagedAgentsTextBlock
154 {
155 Type = BetaManagedAgentsTextBlockType.Text,
156 Text = "Summarize the repo README",
157 },
158 ],
159 },
160 ],
161 });
162 
163 await foreach (var streamEvent in stream.Enumerate())
164 {
165 if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
166 {
167 foreach (var block in message.Content)
168 {
169 if (block.Value is BetaManagedAgentsTextBlock textBlock)
170 {
171 Console.Write(textBlock.Text);
172 }
173 }
174 }
175 else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
176 {
177 break;
178 }
179 else if (streamEvent.Value is BetaManagedAgentsSessionErrorEvent error)
180 {
181 Console.WriteLine($"\n[Error: {error.Error?.Message ?? "unknown"}]");
182 break;
183 }
184 }
185 ```
186 
187 ```go Go
188 // Open the stream first, then send the user message
189 stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
190 defer stream.Close()
191 
192 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
193 Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
194 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
195 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
196 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
197 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
198 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
199 Text: "Summarize the repo README",
200 },
201 }},
202 },
203 }},
204 }); err != nil {
205 panic(err)
206 }
207 
208 events:
209 for stream.Next() {
210 switch event := stream.Current().AsAny().(type) {
211 case anthropic.BetaManagedAgentsAgentMessageEvent:
212 // concrete-typed list: BetaManagedAgentsTextBlock
213 for _, block := range event.Content {
214 fmt.Print(block.Text)
215 }
216 case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
217 break events
218 case anthropic.BetaManagedAgentsSessionErrorEvent:
219 fmt.Printf("\n[Error: %s]\n", cmp.Or(event.Error.Message, "unknown"))
220 break events
221 }
222 }
223 if err := stream.Err(); err != nil {
224 panic(err)
225 }
226 ```
227 
228 ```java Java
229 // Open the stream first, then send the user message
230 try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
231 client.beta().sessions().events().send(
232 session.id(),
233 EventSendParams.builder()
234 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
235 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
236 .addTextContent("Summarize the repo README")
237 .build())
238 .build()
239 );
240 
241 Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator;
242 events:
243 for (var event : events) {
244 switch (event.type().value()) {
245 case AGENT_MESSAGE -> event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
246 case SESSION_STATUS_IDLE -> {
247 break events;
248 }
249 case SESSION_ERROR -> {
250 // The `message` field spans all error variants; read it from the raw JSON.
251 var errorMessage =
252 event.asSessionError().error()._json().orElse(null) instanceof JsonObject json
253 ? json.values().get("message").asStringOrThrow()
254 : "unknown";
255 IO.println("\n[Error: " + errorMessage + "]");
256 break events;
257 }
258 }
259 }
260 }
261 ```
262 
263 ```php PHP
264 // Open the stream first, then send the user message
265 $stream = $client->beta->sessions->events->streamStream($session->id);
266 $client->beta->sessions->events->send(
267 $session->id,
268 events: [
269 [
270 'type' => 'user.message',
271 'content' => [['type' => 'text', 'text' => 'Summarize the repo README']],
272 ],
273 ],
274 );
275 
276 foreach ($stream as $event) {
277 match (true) {
278 $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent => array_walk(
279 $event->content,
280 static fn ($block) => $block instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock ? print($block->text) : null,
281 ),
282 $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsSessionErrorEvent => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'),
283 default => null,
284 };
285 if ($event->type === 'session.status_idle' || $event->type === 'session.error') {
286 break;
287 }
288 }
289 $stream->close();
290 ```
291 
292 ```ruby Ruby
293 # Open the stream first, then send the user message
294 stream = client.beta.sessions.events.stream_events(session.id)
295 
296 client.beta.sessions.events.send_(
297 session.id,
298 events: [{
299 type: "user.message",
300 content: [{type: "text", text: "Summarize the repo README"}]
301 }]
302 )
303 
304 stream.each do |event|
305 case event
306 when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
307 event.content.each { print it.text }
308 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
309 break
310 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionErrorEvent
311 puts "\n[Error: #{event.error&.message || "unknown"}]"
312 break
313 else
314 # ignore other event types
315 end
316 end
317 ```
318</CodeGroup>
319 
320The session reports errors through the `session.error` event. See [Event types](https://platform.claude.com/docs/en/managed-agents/reference#event-types) for its fields.
321 
322The agent's response text arrives as buffered `agent.message` events, each emitted only after the model request that produced it finishes. To render the text while the model is still generating it, see [Preview responses with event deltas](https://platform.claude.com/docs/en/managed-agents/event-deltas).
323 
324### Reconnect without missing events
325 
326To reconnect to an existing session without missing events, combine a new stream with the event history:
327 
3281. Open a new stream.
3292. [List the full event history](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#list-past-events) to seed a set of seen event IDs.
3303. Tail the live stream, skipping any events already returned by the history list.
331 
332<CodeGroup>
333 ```bash cURL
334 exec {stream}< <(
335 curl --fail-with-body -sS -N \
336 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
337 -H "x-api-key: $ANTHROPIC_API_KEY" \
338 -H "anthropic-version: 2023-06-01" \
339 -H "anthropic-beta: managed-agents-2026-04-01" \
340 -H "content-type: application/json" \
341 -H "accept: text/event-stream"
342 )
343 
344 # Stream is open and buffering. List history before tailing live.
345 declare -A seen_event_ids
346 while IFS= read -r event_id; do
347 seen_event_ids[$event_id]=1
348 done < <(
349 curl --fail-with-body -sS \
350 "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
351 -H "x-api-key: $ANTHROPIC_API_KEY" \
352 -H "anthropic-version: 2023-06-01" \
353 -H "anthropic-beta: managed-agents-2026-04-01" \
354 -H "content-type: application/json" | jq -r '.data[].id'
355 )
356 
357 # Tail live events, skipping anything already seen
358 while IFS= read -r -u "$stream" event_line; do
359 [[ $event_line == data:* ]] || continue
360 event_json=${event_line#data: }
361 event_id=$(jq -r '.id' <<<"$event_json")
362 [[ -n ${seen_event_ids[$event_id]+seen} ]] && continue
363 seen_event_ids[$event_id]=1
364 case $(jq -r '.type' <<<"$event_json") in
1192365 agent.message)
1193 msg_id=$(jq -r '.id' <<<"$event_json")
1194 for preview_key in "${!preview[@]}"; do
1195 [[ $preview_key == "$msg_id":* ]] && unset "preview[$preview_key]"
1196 done
1197 printf '[agent.message id=%s] ' "$msg_id"
1198366 jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
1199 printf '\n'
1200 ;;
1201 span.model_request_end)
1202 for preview_key in "${!preview[@]}"; do
1203 printf '[closing unreconciled preview for %s]\n' "${preview_key%%:*}"
1204 done
1205 preview=()
1206367 ;;
1207368 session.status_idle)
1208369 break
from line 379
1218379 ```
1219380 
1220381 ```python Python
1221 # Preview snapshots, keyed by event id. accumulate_managed_agents_event folds each
1222 # event_start / event_delta into an agent.message snapshot; the buffered
1223 # agent.message replaces it.
1224 previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {}
1225 
1226 # Opt in to agent.message previews on this connection
1227 with client.beta.sessions.events.stream(
1228 session.id, event_deltas=["agent.message"]
1229 ) as stream:
1230 client.beta.sessions.events.send(
1231 session.id,
1232 events=[
1233 {
1234 "type": "user.message",
1235 "content": [{"type": "text", "text": "Describe the repo in one sentence."}],
1236 },
1237 ],
1238 )
1239 
382 with client.beta.sessions.events.stream(session.id) as stream:
383 # Stream is open and buffering. List history before tailing live.
384 history = client.beta.sessions.events.list(session.id)
385 seen_event_ids = {past_event.id for past_event in history}
386 
387 # Tail live events, skipping anything already seen
1240388 for event in stream:
389 if event.type == "event_start" or event.type == "event_delta":
390 # Delta previews aren't enabled on this connection.
391 continue
392 if event.id in seen_event_ids:
393 continue
394 seen_event_ids.add(event.id)
1241395 match event.type:
1242 case "event_start":
1243 snapshot = accumulate_managed_agents_event(None, event)
1244 if snapshot is not None:
1245 previews[event.event.id] = snapshot
1246 print(f"event_start {event.event.type} {event.event.id}")
1247 case "event_delta":
1248 preview = accumulate_managed_agents_event(previews.get(event.event_id), event)
1249 if preview is not None:
1250 previews[event.event_id] = preview
1251 text = "".join(block.text for block in preview.content)
1252 print(f"event_delta preview: {text!r}")
1253396 case "agent.message":
1254 # The buffered event is the record: it replaces and closes the preview
1255 preview = accumulate_managed_agents_event(previews.pop(event.id, None), event)
1256 text = "".join(block.text for block in preview.content)
1257 print(f"agent.message {event.id} {text!r}")
1258 case "span.model_request_end":
1259 # No more deltas are coming. Close any preview whose
1260 # buffered event never arrived.
1261 for event_id in previews:
1262 print(f"span.model_request_end closing preview for {event_id}")
1263 previews.clear()
397 for block in event.content:
398 if block.type == "text":
399 print(block.text, end="")
1264400 case "session.status_idle":
1265401 break
1266402 ```
1267403 
1268404 ```typescript TypeScript
1269 // Preview snapshots, keyed by event id. `accumulateManagedAgentsEvent`
1270 // folds event_start / event_delta previews into an agent.message snapshot.
1271 const previews = new Map<string, BetaManagedAgentsAgentMessageEvent>();
1272 
1273 // Opt in to agent.message previews for this connection only
1274 const stream = await client.beta.sessions.events.stream(session.id, {
1275 event_deltas: ["agent.message"],
1276 });
405 const seenEventIds = new Set<string>();
406 const stream = await client.beta.sessions.events.stream(session.id);
407 
408 // Stream is open and buffering. List history before tailing live.
409 for await (const event of client.beta.sessions.events.list(session.id)) {
410 seenEventIds.add(event.id);
411 }
412 
413 // Tail live events, skipping anything already seen
414 tail: for await (const event of stream) {
415 // Preview events (event_start/event_delta) carry no top-level id
416 if (event.type === "event_start" || event.type === "event_delta") continue;
417 if (seenEventIds.has(event.id)) continue;
418 seenEventIds.add(event.id);
419 switch (event.type) {
420 case "agent.message":
421 for (const block of event.content) {
422 if (block.type === "text") {
423 process.stdout.write(block.text);
424 }
425 }
426 break;
427 case "session.status_idle":
428 break tail;
429 }
430 }
431 ```
432 
433 ```csharp C#
434 using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
435 
436 // Stream is open and buffering. List history before tailing live.
437 HashSet<string> seenEventIds = [];
438 var history = await client.Beta.Sessions.Events.List(session.ID);
439 await foreach (var pastEvent in history.Paginate())
440 {
441 seenEventIds.Add(pastEvent.ID);
442 }
443 
444 // Tail live events, skipping anything already seen
445 await foreach (var streamEvent in stream.Enumerate())
446 {
447 if (!seenEventIds.Add(streamEvent.ID))
448 {
449 continue;
450 }
451 if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
452 {
453 foreach (var block in message.Content)
454 {
455 if (block.Value is BetaManagedAgentsTextBlock textBlock)
456 {
457 Console.Write(textBlock.Text);
458 }
459 }
460 }
461 else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
462 {
463 break;
464 }
465 }
466 ```
467 
468 ```go Go
469 stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
470 defer stream.Close()
471 
472 // Stream is open and buffering. List history before tailing live.
473 seenEventIDs := map[string]struct{}{}
474 history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{})
475 for history.Next() {
476 seenEventIDs[history.Current().ID] = struct{}{}
477 }
478 if err := history.Err(); err != nil {
479 panic(err)
480 }
481 
482 // Tail live events, skipping anything already seen
483 tail:
484 for stream.Next() {
485 event := stream.Current()
486 if _, seen := seenEventIDs[event.ID]; seen {
487 continue
488 }
489 seenEventIDs[event.ID] = struct{}{}
490 switch event := event.AsAny().(type) {
491 case anthropic.BetaManagedAgentsAgentMessageEvent:
492 // concrete-typed list: BetaManagedAgentsTextBlock
493 for _, block := range event.Content {
494 fmt.Print(block.Text)
495 }
496 case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
497 break tail
498 }
499 }
500 if err := stream.Err(); err != nil {
501 panic(err)
502 }
503 ```
504 
505 ```java Java
506 try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
507 // Stream is open and buffering. List history before tailing live.
508 // Every event variant carries `id`; read it from the raw JSON to dedup across variants.
509 var seenEventIds = new HashSet<String>();
510 for (var pastEvent : client.beta().sessions().events().list(session.id()).autoPager()) {
511 if (pastEvent._json().orElseThrow() instanceof JsonObject json) {
512 seenEventIds.add(json.values().get("id").asStringOrThrow());
513 }
514 }
515 
516 // Tail live events; Set.add returns false for already-seen IDs, skipping the replay.
517 stream.stream()
518 .filter(event -> event._json().orElseThrow() instanceof JsonObject json
519 && seenEventIds.add(json.values().get("id").asStringOrThrow()))
520 .takeWhile(event -> !event.isSessionStatusIdle())
521 .filter(BetaManagedAgentsStreamSessionEvents::isAgentMessage)
522 .forEach(event -> event.asAgentMessage().content()
523 .forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))));
524 }
525 ```
526 
527 ```php PHP
528 $stream = $client->beta->sessions->events->streamStream($session->id);
529 
530 // Stream is open and buffering. List history before tailing live.
531 $seenEventIds = [];
532 foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) {
533 $seenEventIds[$event->id] = true;
534 }
535 
536 // Tail live events, skipping anything already seen
537 foreach ($stream as $event) {
538 if (isset($seenEventIds[$event->id])) {
539 continue;
540 }
541 $seenEventIds[$event->id] = true;
542 match (true) {
543 $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent => array_walk(
544 $event->content,
545 static fn ($block) => $block instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock ? print($block->text) : null,
546 ),
547 default => null,
548 };
549 if ($event->type === 'session.status_idle') {
550 break;
551 }
552 }
553 $stream->close();
554 ```
555 
556 ```ruby Ruby
557 stream = client.beta.sessions.events.stream_events(session.id)
558 
559 # Stream is open and buffering. List history before tailing live.
560 seen_event_ids = Set.new
561 client.beta.sessions.events.list(session.id).auto_paging_each { seen_event_ids << it.id }
562 
563 # Tail live events, skipping anything already seen — Set#add? returns nil for duplicates
564 stream.each do |event|
565 next unless seen_event_ids.add?(event.id)
566 case event
567 when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
568 event.content.each { print it.text }
569 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
570 break
571 else
572 # ignore other event types
573 end
574 end
575 ```
576</CodeGroup>
577 
578## Send events
579 
580Send a `user.message` event to start or continue the agent's work:
581 
582<CodeGroup>
583 ```bash cURL
584 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
585 -H "x-api-key: $ANTHROPIC_API_KEY" \
586 -H "anthropic-version: 2023-06-01" \
587 -H "anthropic-beta: managed-agents-2026-04-01" \
588 -H "content-type: application/json" \
589 -d @- <<'EOF'
590 {
591 "events": [
592 {
593 "type": "user.message",
594 "content": [
595 {"type": "text", "text": "Analyze the performance of the sort function in utils.py"}
596 ]
597 }
598 ]
599 }
600 EOF
601 ```
602 
603 ```bash CLI
604 ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
605 events:
606 - type: user.message
607 content:
608 - type: text
609 text: Analyze the performance of the sort function in utils.py
610 YAML
611 ```
612 
613 ```python Python
614 client.beta.sessions.events.send(
615 session.id,
616 events=[
617 {
618 "type": "user.message",
619 "content": [
620 {
621 "type": "text",
622 "text": "Analyze the performance of the sort function in utils.py",
623 },
624 ],
625 },
626 ],
627 )
628 ```
629 
630 ```typescript TypeScript
1277631 await client.beta.sessions.events.send(session.id, {
1278632 events: [
1279633 {
1280634 type: "user.message",
1281 content: [{ type: "text", text: "Summarize the repo README" }]
1282 }
1283 ]
635 content: [
636 {
637 type: "text",
638 text: "Analyze the performance of the sort function in utils.py",
639 },
640 ],
641 },
642 ],
1284643 });
1285 
1286 deltas: for await (const event of stream) {
1287 switch (event.type) {
1288 case "event_start": {
1289 // 1. Note the announced id and open the snapshot. Deltas and the
1290 // buffered event carry the same id.
1291 const preview = accumulateManagedAgentsEvent(undefined, event);
1292 if (preview) previews.set(event.event.id, preview);
1293 console.log(`event_start ${event.event.type} ${event.event.id}`);
1294 break;
1295 }
1296 case "event_delta": {
1297 // 2. Fold the fragment into the snapshot and render it
1298 const preview = accumulateManagedAgentsEvent(previews.get(event.event_id), event);
1299 if (preview) {
1300 previews.set(event.event_id, preview);
1301 const text = preview.content
1302 .map((block) => (block.type === "text" ? block.text : ""))
1303 .join("");
1304 console.log(`event_delta preview: ${JSON.stringify(text)}`);
1305 }
1306 break;
1307 }
1308 case "agent.message": {
1309 // 3. The buffered event is the record: it replaces and closes the preview
1310 const message = accumulateManagedAgentsEvent(previews.get(event.id), event);
1311 previews.delete(event.id);
1312 const text = message.content
1313 .map((block) => (block.type === "text" ? block.text : ""))
1314 .join("");
1315 console.log(`agent.message ${event.id} ${JSON.stringify(text)}`);
1316 break;
1317 }
1318 case "span.model_request_end":
1319 // 4. No more deltas are coming. Close any preview that was never reconciled.
1320 for (const eventId of previews.keys()) {
1321 console.log(`span.model_request_end closing preview for ${eventId}`);
1322 }
1323 previews.clear();
1324 break;
1325 case "session.status_idle":
1326 break deltas;
1327 }
1328 }
1329 stream.controller.abort();
1330644 ```
1331645 
1332646 ```csharp C#
1333 // Opt in to event deltas: agent.message events are previewed as they are produced.
1334 using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(
1335 session.ID,
1336 new() { EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] }
1337 );
1338647 await client.Beta.Sessions.Events.Send(session.ID, new()
1339648 {
1340649 Events =
from line 656
1347656 new BetaManagedAgentsTextBlock
1348657 {
1349658 Type = BetaManagedAgentsTextBlockType.Text,
1350 Text = "Write a haiku about event streams.",
659 Text = "Analyze the performance of the sort function in utils.py",
1351660 },
1352661 ],
1353662 },
1354663 ],
1355664 });
1356 
1357 // Accumulate preview fragments per (event id, content index). The buffered
1358 // agent.message that follows carries the complete content, so it replaces the
1359 // accumulated preview rather than appending to it.
1360 Dictionary<string, SortedDictionary<long, string>> previews = [];
1361 
1362 await foreach (var streamEvent in stream.Enumerate())
1363 {
1364 if (streamEvent.TryPickStartEvent(out var start))
1365 {
1366 // A preview opened for the event with this id. This stream only opts in
1367 // to agent.message deltas; TryPick* returns false instead of throwing,
1368 // so other preview types (including ones added later) are skipped.
1369 if (start.Event.TryPickAgentMessage(out var preview))
1370 {
1371 Console.WriteLine($"event_start {preview.Type.Raw()} {preview.ID}");
1372 }
1373 }
1374 else if (streamEvent.TryPickDeltaEvent(out var delta))
1375 {
1376 // Insert at a new index, append at an existing one
1377 if (!previews.TryGetValue(delta.EventID, out var fragments))
1378 {
1379 previews[delta.EventID] = fragments = [];
1380 }
1381 var index = delta.Delta.Index ?? 0;
1382 fragments[index] = fragments.GetValueOrDefault(index, "") + delta.Delta.Content.Text;
1383 Console.WriteLine($"event_delta preview: {fragments[index]}");
1384 }
1385 else if (streamEvent.TryPickAgentMessageEvent(out var message))
1386 {
1387 // Deltas are best-effort: discard the preview and use the buffered event
1388 previews.Remove(message.ID);
1389 var text = string.Concat(message.Content.Select(block =>
1390 block.TryPickBetaManagedAgentsTextBlock(out var textBlock) ? textBlock.Text : ""));
1391 Console.WriteLine($"agent.message {message.ID} {text}");
1392 }
1393 else if (streamEvent.TryPickSpanModelRequestEndEvent(out _))
1394 {
1395 // No more deltas are coming; close any preview that was never reconciled.
1396 foreach (var eventId in previews.Keys)
1397 {
1398 Console.WriteLine($"span.model_request_end closing preview for {eventId}");
1399 }
1400 previews.Clear();
1401 }
1402 else if (streamEvent.TryPickSessionStatusIdleEvent(out _))
1403 {
1404 break;
1405 }
1406 }
1407665 ```
1408666 
1409667 ```go Go
1410 // Opt in to incremental previews of agent.message events
1411 stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{
1412 EventDeltas: []anthropic.BetaManagedAgentsDeltaType{
1413 anthropic.BetaManagedAgentsDeltaTypeAgentMessage,
668 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
669 Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
670 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
671 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
672 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
673 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
674 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
675 Text: "Analyze the performance of the sort function in utils.py",
676 },
677 }},
1414678 },
1415 })
1416 
1417 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
1418 Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
1419 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
1420 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
1421 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
1422 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
1423 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
1424 Text: "Write a haiku about the ocean.",
1425 },
1426 }},
1427 },
1428 }},
1429 }); err != nil {
1430 panic(err)
1431 }
1432 
1433 // The accumulator folds event_start / event_delta fragments into
1434 // per-event-id agent.message snapshots. The zero value is ready to use.
1435 var previews anthropic.BetaManagedAgentsEventAccumulator
1436 
1437 deltas:
1438 for stream.Next() {
1439 event := stream.Current()
1440 previews.Accumulate(event)
1441 
1442 switch event := event.AsAny().(type) {
1443 case anthropic.BetaManagedAgentsStartEvent:
1444 fmt.Printf("event_start %s %s\n", event.Event.Type, event.Event.ID)
1445 case anthropic.BetaManagedAgentsDeltaEvent:
1446 fmt.Printf("event_delta preview: %q\n", previews.AgentMessageText(event.EventID))
1447 case anthropic.BetaManagedAgentsAgentMessageEvent:
1448 // The buffered event carries the complete content: the accumulator
1449 // replaces the preview with it
1450 fmt.Printf("agent.message %s %q\n", event.ID, previews.AgentMessageText(event.ID))
1451 case anthropic.BetaManagedAgentsSpanModelRequestEndEvent:
1452 // No more deltas are coming for this request. The accumulator
1453 // drops its snapshots here, closing any preview that was never
1454 // reconciled by a buffered agent.message.
1455 fmt.Println("span.model_request_end no more deltas for this request")
1456 case anthropic.BetaManagedAgentsSessionStatusIdleEvent:
1457 break deltas
1458 }
1459 }
1460 if err := stream.Err(); err != nil {
1461 panic(err)
1462 }
1463 stream.Close()
679 }},
680 }); err != nil {
681 panic(err)
682 }
1464683 ```
1465684 
1466685 ```java Java
1467 // Preview text, keyed by event ID then content index. The buffered agent.message replaces it.
1468 Map<String, Map<Long, StringBuilder>> previews = new HashMap<>();
1469 
1470 // Opt in to agent.message previews on this connection
1471 try (var stream = client.beta().sessions().events().streamStreaming(
1472 session.id(),
1473 EventStreamParams.builder()
1474 .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE)
1475 .build()
1476 )) {
1477 client.beta().sessions().events().send(
1478 session.id(),
1479 EventSendParams.builder()
1480 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
1481 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
1482 .addTextContent("Describe the repo in one sentence.")
1483 .build())
1484 .build()
1485 );
1486 
1487 Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator;
1488 deltas:
1489 for (var event : events) {
1490 switch (event.type().value()) {
1491 case EVENT_START -> {
1492 if (event.asEventStart().event().isAgentMessage()) {
1493 var preview = event.asEventStart().event().asAgentMessage();
1494 IO.println("event_start " + preview.type().asString() + " " + preview.id());
1495 }
1496 }
1497 case EVENT_DELTA -> {
1498 var eventDelta = event.asEventDelta();
1499 var fragment = eventDelta.delta();
1500 var buffer = previews
1501 .computeIfAbsent(eventDelta.eventId(), _ -> new HashMap<>())
1502 .computeIfAbsent(fragment.index().orElse(0L), _ -> new StringBuilder());
1503 buffer.append(fragment.content().text());
1504 IO.println("event_delta preview: " + buffer);
1505 }
1506 case AGENT_MESSAGE -> {
1507 // The buffered event is the record: drop its preview, render its content
1508 var message = event.asAgentMessage();
1509 previews.remove(message.id());
1510 var text = message.content().stream()
1511 .flatMap(block -> block.text().stream())
1512 .map(textBlock -> textBlock.text())
1513 .collect(Collectors.joining());
1514 IO.println("agent.message " + message.id() + " " + text);
1515 }
1516 case SPAN_MODEL_REQUEST_END -> {
1517 // No more deltas are coming. Close any preview whose buffered event never arrived.
1518 previews.keySet().forEach(eventId ->
1519 IO.println("span.model_request_end closing preview for " + eventId));
1520 previews.clear();
1521 }
1522 case SESSION_STATUS_IDLE -> {
1523 break deltas;
1524 }
1525 }
1526 }
1527 }
686 client.beta().sessions().events().send(
687 session.id(),
688 EventSendParams.builder()
689 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
690 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
691 .addTextContent("Analyze the performance of the sort function in utils.py")
692 .build())
693 .build());
1528694 ```
1529695 
1530696 ```php PHP
1531 // In PHP, set eventDeltas on EventStreamParams and accumulate with Anthropic\Lib\Sessions\EventAccumulator.
697 $client->beta->sessions->events->send(
698 $session->id,
699 events: [
700 [
701 'type' => 'user.message',
702 'content' => [
703 [
704 'type' => 'text',
705 'text' => 'Analyze the performance of the sort function in utils.py',
706 ],
707 ],
708 ],
709 ],
710 );
1532711 ```
1533712 
1534713 ```ruby Ruby
1535 # Opt in to event deltas: agent.message previews stream as incremental fragments.
1536 stream = client.beta.sessions.events.stream_events(
1537 session.id,
1538 event_deltas: [Anthropic::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE]
1539 )
1540 
1541714 client.beta.sessions.events.send_(
1542715 session.id,
1543 events: [{
1544 type: "user.message",
1545 content: [{type: "text", text: "Give a one-sentence project tagline."}]
1546 }]
716 events: [
717 {
718 type: "user.message",
719 content: [
720 {
721 type: "text",
722 text: "Analyze the performance of the sort function in utils.py"
723 }
724 ]
725 }
726 ]
1547727 )
1548 
1549 # Accumulate preview fragments by (event_id, index) into explicitly mutable
1550 # (`+""`) buffers so `<<` can append in place. The buffered agent.message with
1551 # the same id is authoritative and replaces whatever the deltas built up.
1552 buffers = Hash.new do |by_event, event_id|
1553 by_event[event_id] = Hash.new { |fragments, index| fragments[index] = +"" }
1554 end
1555 
1556 stream.each do |event|
1557 case event
1558 when Anthropic::Beta::BetaManagedAgentsStartEvent
1559 puts "event_start #{event.event.type} #{event.event.id}"
1560 when Anthropic::Beta::BetaManagedAgentsDeltaEvent
1561 delta = event.delta
1562 fragment = delta.content.text
1563 buffers[event.event_id][delta.index || 0] << fragment
1564 puts "event_delta preview: #{buffers[event.event_id][delta.index || 0].inspect}"
1565 when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
1566 # Replace: drop the accumulated preview and render the complete event.
1567 buffers.delete(event.id)
1568 puts "agent.message #{event.id} #{event.content.map(&:text).join.inspect}"
1569 when Anthropic::Beta::Sessions::BetaManagedAgentsSpanModelRequestEndEvent
1570 # No more deltas are coming. Close any preview that was never reconciled.
1571 buffers.each_key { |event_id| puts "span.model_request_end closing preview for #{event_id}" }
1572 buffers.clear
1573 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
1574 break
1575 else
1576 # ignore other event types
1577 end
1578 end
1579728 ```
1580729</CodeGroup>
1581730 
1582### Preview session thread events
1583 
1584In a [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) session, every session thread has its own event stream at `GET /v1/sessions/{session_id}/threads/{thread_id}/stream`, and it takes the same `event_deltas[]` parameter with the same values. Previews are thread-scoped by design: a connection previews only the thread it's reading. A child thread's previews are delivered on that child's own 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.
1585 
1586The thread stream's path is easy to get wrong: it is `/threads/{thread_id}/stream`, not `/events/stream` (which exists only at the session level), and there is no `/threads/{thread_id}/events/stream` endpoint.
1587 
1588The preview events themselves don't change. `event_start` and `event_delta` have the same shape on a thread stream as on the session-level stream, and the [accumulate and reconcile](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#accumulate-and-reconcile) pattern applies as written. The one adjustment is bookkeeping: run one accumulator instance per stream connection.
1589 
1590<CodeGroup>
1591 ```bash cURL
1592 # List the session's threads and pick a child: child threads carry a non-null
1593 # parent_thread_id, and the primary thread's parent_thread_id is null.
1594 THREAD_ID=$(
1595 curl --fail-with-body -sS \
1596 "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads?beta=true" \
1597 -H "x-api-key: $ANTHROPIC_API_KEY" \
1598 -H "anthropic-version: 2023-06-01" \
1599 -H "anthropic-beta: managed-agents-2026-04-01" |
1600 jq -er 'first(.data[] | select(.parent_thread_id != null)).id'
1601 )
1602 
1603 # The child thread's stream takes the same event_deltas[] parameter as the
1604 # session stream. Percent-encode the brackets (%5B%5D) and quote the URL.
1605 exec {stream}< <(
1606 curl --fail-with-body -sS -N \
1607 "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true&event_deltas%5B%5D=agent.message" \
1608 -H "x-api-key: $ANTHROPIC_API_KEY" \
1609 -H "anthropic-version: 2023-06-01" \
1610 -H "anthropic-beta: managed-agents-2026-04-01" \
1611 -H "accept: text/event-stream"
1612 )
1613 
1614 while IFS= read -r -u "$stream" event_line; do
1615 [[ $event_line == data:* ]] || continue
1616 event_json=${event_line#data: }
1617 case $(jq -r '.type' <<<"$event_json") in
1618 event_delta)
1619 jq -j '.delta.content.text' <<<"$event_json"
1620 ;;
1621 agent.message)
1622 # The buffered event is the authoritative record; render its content.
1623 printf '\n'
1624 jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
1625 printf '\n'
1626 ;;
1627 session.thread_status_idle)
1628 break
1629 ;;
1630 esac
1631 done
1632 exec {stream}<&-
1633 ```
1634 
1635 ```bash CLI
1636 # List the session's threads and pick a child: child threads carry a non-null
1637 # parent_thread_id, and the primary thread's parent_thread_id is null
1638 # (--transform's #(parent_thread_id!=~null) query matches non-null values).
1639 THREAD_ID=$(ant beta:sessions:threads list \
1640 --session-id "$SESSION_ID" \
1641 --format raw --transform 'data.#(parent_thread_id!=~null).id' --raw-output)
1642 
1643 # The child thread's stream takes the same event_deltas parameter as the
1644 # session stream, one --event-delta flag per event type to preview. @tostr
1645 # re-encodes each text field as a JSON string, so every value stays on one
1646 # YAML line and jq's fromjson recovers the original text.
1647 transform='{type,frag:delta.content.text|@tostr,text:content.#(type=="text").text|@tostr}'
1648 exec {stream}< <(ant beta:sessions:threads:events stream \
1649 --session-id "$SESSION_ID" \
1650 --thread-id "$THREAD_ID" \
1651 --event-delta agent.message \
1652 --transform "$transform" \
1653 --format yaml)
1654 
1655 type=
1656 while IFS= read -r -u "$stream" line; do
1657 case "$line" in
1658 type:\ session.thread_status_idle) break ;;
1659 type:\ *) type=${line#type: } ;;
1660 frag:*)
1661 [[ $type == event_delta ]] || continue
1662 jq -j fromjson <<<"${line#frag: }" ;;
1663 text:*)
1664 [[ $type == agent.message ]] || continue
1665 # The buffered event is the authoritative record; render its content.
1666 printf '\n'
1667 jq -r fromjson <<<"${line#text: }" ;;
1668 esac
1669 done
1670 exec {stream}<&-
1671 ```
1672 
1673 ```python Python
1674 # List the session's threads and pick a child: child threads carry a non-null
1675 # parent_thread_id, and the primary thread's parent_thread_id is null.
1676 child_thread = next(
1677 thread
1678 for thread in client.beta.sessions.threads.list(session.id)
1679 if thread.parent_thread_id is not None
1680 )
1681 
1682 # The child thread's stream takes the same event_deltas parameter as the
1683 # session stream.
1684 with client.beta.sessions.threads.events.stream(
1685 child_thread.id,
1686 session_id=session.id,
1687 event_deltas=["agent.message"],
1688 ) as stream:
1689 for event in stream:
1690 match event.type:
1691 case "event_delta":
1692 print(event.delta.content.text, end="")
1693 case "agent.message":
1694 # The buffered event is the authoritative record; render its content
1695 print()
1696 for block in event.content:
1697 if block.type == "text":
1698 print(block.text, end="")
1699 print()
1700 case "session.thread_status_idle":
1701 break
1702 ```
1703 
1704 ```typescript TypeScript
1705 // List the session's threads and pick a child: child threads carry a non-null
1706 // parent_thread_id, and the primary thread's parent_thread_id is null.
1707 let childThreadId: string | undefined;
1708 for await (const thread of client.beta.sessions.threads.list(session.id)) {
1709 if (thread.parent_thread_id !== null) {
1710 childThreadId = thread.id;
1711 break;
1712 }
1713 }
1714 if (!childThreadId) throw new Error("No child thread found");
1715 
1716 // The child thread's stream takes the same event_deltas parameter as the
1717 // session stream.
1718 const stream = await client.beta.sessions.threads.events.stream(childThreadId, {
1719 session_id: session.id,
1720 event_deltas: ["agent.message"],
1721 });
1722 
1723 threadDeltas: for await (const event of stream) {
1724 switch (event.type) {
1725 case "event_delta":
1726 process.stdout.write(event.delta.content.text);
1727 break;
1728 case "agent.message": {
1729 // The buffered event is the authoritative record; render its content.
1730 process.stdout.write("\n");
1731 const text = event.content
1732 .map((block) => (block.type === "text" ? block.text : ""))
1733 .join("");
1734 console.log(text);
1735 break;
1736 }
1737 case "session.thread_status_idle":
1738 break threadDeltas;
1739 }
1740 }
1741 stream.controller.abort();
1742 ```
1743 
1744 ```csharp C#
1745 // List the session's threads and pick a child: child threads carry a non-null
1746 // parent_thread_id, and the primary thread's parent_thread_id is null.
1747 var threads = await client.Beta.Sessions.Threads.List(session.ID);
1748 var childThread = threads.Items.First(thread => thread.ParentThreadID is not null);
1749 
1750 // The child thread's stream takes the same event_deltas parameter as the
1751 // session stream.
1752 using var stream = await client.Beta.Sessions.Threads.Events.WithRawResponse.StreamStreaming(
1753 childThread.ID,
1754 new() { SessionID = session.ID, EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] }
1755 );
1756 
1757 await foreach (var streamEvent in stream.Enumerate())
1758 {
1759 if (streamEvent.TryPickDeltaEvent(out var delta))
1760 {
1761 Console.Write(delta.Delta.Content.Text);
1762 }
1763 else if (streamEvent.TryPickAgentMessageEvent(out var message))
1764 {
1765 // The buffered event is the authoritative record; render its content.
1766 Console.WriteLine();
1767 var text = string.Concat(message.Content.Select(block =>
1768 block.TryPickBetaManagedAgentsTextBlock(out var textBlock) ? textBlock.Text : ""));
1769 Console.WriteLine(text);
1770 }
1771 else if (streamEvent.TryPickSessionThreadStatusIdleEvent(out _))
1772 {
1773 break;
1774 }
1775 }
1776 ```
1777 
1778 ```go Go
1779 // List the session's threads and pick a child: child threads carry a non-null
1780 // parent_thread_id, and the primary thread's parent_thread_id is null.
1781 var childThreadID string
1782 threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionThreadListParams{})
1783 for threads.Next() {
1784 if thread := threads.Current(); thread.ParentThreadID != "" {
1785 childThreadID = thread.ID
1786 break
1787 }
1788 }
1789 if err := threads.Err(); err != nil {
1790 panic(err)
1791 }
1792 
1793 // The child thread's stream takes the same event_deltas parameter as the
1794 // session stream; run one read loop per stream connection.
1795 stream := client.Beta.Sessions.Threads.Events.StreamEvents(ctx, childThreadID, anthropic.BetaSessionThreadEventStreamParams{
1796 SessionID: session.ID,
1797 EventDeltas: []anthropic.BetaManagedAgentsDeltaType{
1798 anthropic.BetaManagedAgentsDeltaTypeAgentMessage,
1799 },
1800 })
1801 
1802 threadDeltas:
1803 for stream.Next() {
1804 switch event := stream.Current().AsAny().(type) {
1805 case anthropic.BetaManagedAgentsDeltaEvent:
1806 fmt.Print(event.Delta.Content.Text)
1807 case anthropic.BetaManagedAgentsAgentMessageEvent:
1808 // The buffered event is the authoritative record; render its content.
1809 fmt.Println()
1810 // concrete-typed list: BetaManagedAgentsTextBlock
1811 for _, block := range event.Content {
1812 fmt.Print(block.Text)
1813 }
1814 fmt.Println()
1815 case anthropic.BetaManagedAgentsSessionThreadStatusIdleEvent:
1816 break threadDeltas
1817 }
1818 }
1819 if err := stream.Err(); err != nil {
1820 panic(err)
1821 }
1822 stream.Close()
1823 ```
1824 
1825 ```java Java
1826 // List the session's threads and pick a child: child threads carry a non-null
1827 // parent_thread_id, and the primary thread's parent_thread_id is null.
1828 var childThread = client.beta().sessions().threads().list(session.id()).autoPager().stream()
1829 .filter(thread -> thread.parentThreadId().isPresent())
1830 .findFirst()
1831 .orElseThrow();
1832 
1833 // The child thread's stream takes the same event_deltas parameter as the session
1834 // stream. Its params class shares the session-level one's simple name, so qualify it.
1835 try (var stream = client.beta().sessions().threads().events().streamStreaming(
1836 childThread.id(),
1837 com.anthropic.models.beta.sessions.threads.events.EventStreamParams.builder()
1838 .sessionId(session.id())
1839 .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE)
1840 .build()
1841 )) {
1842 Iterable<BetaManagedAgentsStreamSessionThreadEvents> events = stream.stream()::iterator;
1843 threadDeltas:
1844 for (var event : events) {
1845 switch (event.type().value()) {
1846 case EVENT_DELTA -> IO.print(event.asEventDelta().delta().content().text());
1847 case AGENT_MESSAGE -> {
1848 // The buffered event is the authoritative record; render its content.
1849 IO.println();
1850 event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
1851 IO.println();
1852 }
1853 case SESSION_THREAD_STATUS_IDLE -> {
1854 break threadDeltas;
1855 }
1856 }
1857 }
1858 }
1859 ```
1860 
1861 ```php PHP
1862 // In PHP, set eventDeltas on the thread EventStreamParams and accumulate with Anthropic\Lib\Sessions\EventAccumulator.
1863 ```
1864 
1865 ```ruby Ruby
1866 # List the session's threads and pick a child: child threads carry a non-null
1867 # parent_thread_id, and the primary thread's parent_thread_id is null.
1868 child_thread = client.beta.sessions.threads.list(session.id).to_enum.find { it.parent_thread_id }
1869 
1870 # The child thread's stream takes the same event_deltas parameter as the
1871 # session stream.
1872 stream = client.beta.sessions.threads.events.stream_events(
1873 child_thread.id,
1874 session_id: session.id,
1875 event_deltas: [Anthropic::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE]
1876 )
1877 
1878 stream.each do |event|
1879 case event
1880 when Anthropic::Beta::BetaManagedAgentsDeltaEvent
1881 print event.delta.content.text
1882 when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
1883 # The buffered event is the authoritative record; render its content.
1884 puts
1885 event.content.each { print it.text }
1886 puts
1887 when Anthropic::Beta::Sessions::BetaManagedAgentsSessionThreadStatusIdleEvent
1888 break
1889 else
1890 # ignore other event types
1891 end
1892 end
1893 ```
1894</CodeGroup>
1895 
1896The read loop exits on [`session.thread_status_idle`](https://platform.claude.com/docs/en/managed-agents/reference#event-types), the event emitted when the session thread's turn finishes and the thread goes idle.
1897 
1898### Limitations
1899 
1900Previews are tuned for responsiveness. Build against these constraints:
1901 
1902* **Best effort:** Under load, the server might shed deltas for an event. When it does, you receive a contiguous prefix of the text and then no further deltas for that event. The buffered `agent.message` still arrives complete. Never treat an accumulated preview as final.
1903* **No replay on reconnect:** Deltas are delivered only to the connection that opted in, while it is open. This applies to the session-level stream and to each session thread stream alike, and a connection opened after a model request started receives no deltas for that in-flight event. If the stream drops, follow the [reconnect procedure](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) in the Streaming events tab: reopen the stream and list the event history. The history includes any buffered events emitted while you were disconnected, including the `agent.message` your preview was waiting for. There is no way to re-request missed deltas.
1904* **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 [session thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) are never previewed on that connection.
1905* **Start-only `agent.thinking`:** An `agent.thinking` preview emits only the `event_start` as a signal that a thinking block has started; no `event_delta` events follow it.
1906* **Never persisted:** `event_start` and `event_delta` exist only on the live stream. They do not appear in the session's event history (`GET /v1/sessions/{session_id}/events`) or in any session thread's event history.
1907 
1908### Troubleshoot previews
1909 
1910If the stream doesn't behave as you expect:
1911 
1912| You see | What it means |
1913| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1914| A stream with buffered events but no `event_start` or `event_delta` | The connection you're reading didn't opt in (`event_deltas[]` applies per connection, not per session), or the turn never touched the thread you're streaming. Previews are thread-scoped, so list the session's threads (`GET /v1/sessions/{session_id}/threads`) to find which one ran. |
1915| A 404 on the stream URL | The path or an ID is wrong, or the request carries no managed-agents beta header at all. The thread endpoints are beta-gated, so without the header they don't exist. |
1916| A 400 naming `event_deltas` | Only `agent.message` and `agent.thinking` are accepted. |
1917 
1918## Additional scenarios
1919 
1920### Handling custom tool calls
1921 
1922When the agent invokes a [custom tool](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools):
1923 
19241. The session emits an `agent.custom_tool_use` event containing the tool name and input.
19252. The session pauses with a `session.status_idle` event containing `stop_reason: requires_action`. The blocking event IDs are in the `stop_reason.event_ids` array.
19263. Execute the tool in your system and send a `user.custom_tool_result` event for each, passing the event ID in the `custom_tool_use_id` parameter along with the result content.
731Every event in the session's history includes a `processed_at` timestamp, set when the event finishes processing. On events you send, `processed_at` is null while the event is still queued behind earlier events. The exceptions are `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result`, which are processed on receipt and echoed back with `processed_at` already populated.
732 
733## Respond when the session goes idle
734 
735A `session.status_idle` event means the agent has stopped and is waiting for input. Its `stop_reason.type` says why:
736 
737| `stop_reason.type` | Why the session stopped | What to do |
738| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
739| `end_turn` | The agent finished its turn, or [you interrupted it](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#interrupt-the-agent). | [Send a `user.message`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#resume-an-idle-session) when you have more work for the agent. |
740| `requires_action` | One or more tool calls need an answer from you, such as a custom tool call or a confirmation request. | [Answer each blocking tool call](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#answer-tool-calls-that-pause-the-session). |
741| `budget_reached` | The session's tracked list cost reached its [budget](https://platform.claude.com/docs/en/managed-agents/budgets). | [Change or remove the budget](https://platform.claude.com/docs/en/managed-agents/budgets#resume-a-session-at-its-budget). |
742 
743No event resumes a session paused at its budget. The paused work resumes automatically when you change the budget to a value above the consumed list cost, or remove it. See [When a session reaches its budget](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget) for the events that mark the pause and the events the session still accepts.
744 
745## Answer tool calls that pause the session
746 
747A session pauses when the agent invokes a [custom tool](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools), and when a tool call needs your confirmation under a [permission policy](https://platform.claude.com/docs/en/managed-agents/permission-policies). Both pauses follow the same sequence:
748 
7491. The session emits the tool call as an `agent.custom_tool_use`, `agent.tool_use`, or `agent.mcp_tool_use` event.
7502. The session pauses with a `session.status_idle` event whose `stop_reason.type` is `requires_action`. The blocking event IDs are in the `stop_reason.event_ids` array.
7513. For each blocking event ID, send a [`user.custom_tool_result`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#return-a-custom-tool-result) or a [`user.tool_confirmation`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#confirm-a-tool-call) event.
19277524. Once all blocking events are resolved, the session transitions back to `running`.
753 
754In a multiagent session, a subagent's blocking events are cross-posted to the primary thread. See [Tool permissions and custom tools](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#tool-permissions-and-custom-tools).
755 
756### Return a custom tool result
757 
758The `agent.custom_tool_use` event contains the tool name and input. Execute the tool in your system. Then send a `user.custom_tool_result` event, passing the event ID in the `custom_tool_use_id` parameter along with the result content.
1928759 
1929760<CodeGroup>
1930761 ```bash cURL
from line 1029
21981029 ```
21991030</CodeGroup>
22001031 
2201### Tool confirmation
2202 
2203A tool call waits for your confirmation under an `always_ask` [permission policy](https://platform.claude.com/docs/en/managed-agents/permission-policies), or under `auto` when the server reaches no determination. When that happens:
2204 
22051. The session emits an `agent.tool_use` or `agent.mcp_tool_use` event.
22062. The session pauses with a `session.status_idle` event whose `stop_reason.type` is `requires_action`. The blocking event IDs are in the `stop_reason.event_ids` array.
22073. Send a `user.tool_confirmation` event for each, passing the event ID in the `tool_use_id` parameter. Set `result` to `"allow"` or `"deny"`. Use `deny_message` to explain a denial.
22084. Once all blocking events are resolved, the session transitions back to `running`.
2209 
2210Each `agent.tool_use` and `agent.mcp_tool_use` event carries `evaluated_permission` (`allow`, `ask`, or `deny`), and only events whose `evaluated_permission` is `"ask"` wait for a confirmation. Most events also carry an `evaluation` object that records which policy produced that outcome, described under [See how each call was evaluated](https://platform.claude.com/docs/en/managed-agents/permission-policies#see-how-each-call-was-evaluated). For example, a `bash` call paused under an `always_ask` policy appears on the stream as follows:
2211 
2212```json
2213{
2214 "type": "agent.tool_use",
2215 "id": "sevt_01def...",
2216 "name": "bash",
2217 "input": {
2218 "command": "pip install -r requirements.txt"
2219 },
2220 "evaluated_permission": "ask",
2221 "evaluation": {
2222 "type": "always_ask"
2223 },
2224 "processed_at": "2026-03-25T14:01:45Z"
2225}
2226```
1032### Confirm a tool call
1033 
1034Send a `user.tool_confirmation` event, passing the event ID in the `tool_use_id` parameter. Set `result` to `"allow"` or `"deny"`. See [Respond to confirmation requests](https://platform.claude.com/docs/en/managed-agents/permission-policies#respond-to-confirmation-requests) for which calls wait for a confirmation and how to explain a denial.
1035 
1036The following example approves every pending call:
22271037 
22281038<CodeGroup>
22291039 ```bash cURL
from line 1261
24511261 ```
24521262</CodeGroup>
24531263 
2454### Resuming an idle session
2455 
2456Sessions persist between interactions. Conversation history is preserved unless the session is explicitly deleted. When a session goes idle, its sandbox is checkpointed, preserving the full sandbox state, including the filesystem, installed packages, and any files the agent created. This allows you to resume cleanly from inactivity.
2457 
2458<Note>
2459 While session history is persisted until deleted, sandbox state is only preserved for 30 days after the sandbox is created. Activity does not extend this window: after 30 days the sandbox state (files, installed tools, and so on) is unrecoverable, and a resumed session starts from a fresh sandbox. If your workflow depends on sandbox contents, have the agent write important artifacts to [outputs](https://platform.claude.com/docs/en/managed-agents/define-outcomes#retrieving-deliverables) before the window ends.
2460</Note>
2461 
2462To resume a session, send a `user.message` event to it as usual:
1264## Resume an idle session
1265 
1266Sessions persist between interactions. To resume a session, send a `user.message` event to it as usual:
24631267 
24641268<CodeGroup>
24651269 ```bash cURL
from line 1427
26231427 ```
26241428</CodeGroup>
26251429 
2626### Reaching a session budget
2627 
2628A session created with a [budget](https://platform.claude.com/docs/en/managed-agents/budgets) pauses instead of overspending. When the session's tracked list cost reaches the cap, the platform pauses each thread before its next model request, and the session goes idle with a `stop_reason` of `budget_reached` rather than terminating. The request that carried the total past the cap runs to completion, so the `list_cost` reported by the `session.usage` snapshot can read [at or a fraction past the cap](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget). On the stream, the pause arrives as three events, in order:
2629 
26301. `session.thread_status_idle` with `stop_reason: budget_reached`, for each thread as it pauses.
26312. `session.usage`, a snapshot of the session's cumulative usage and tracked list cost.
26323. `session.status_idle` with `stop_reason: budget_reached`. The `session.usage` event always immediately precedes this idle.
2633 
2634A thread whose final request both crosses the cap and completes its turn reports `end_turn` on its own `session.thread_status_idle` event while the session still reports `budget_reached`; key on the session-level `stop_reason` to detect the pause.
2635 
2636While the session is at its cap, it accepts only the events that settle work already in flight: `user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`, and `user.interrupt`. Any event that would start new work, including `user.message`, is rejected with a 400 error naming that list. When a session has both a thread waiting on a tool ask and a thread paused at the cap, the session-level `stop_reason` is `requires_action`, not `budget_reached`: settling the ask doesn't trigger a model request, so respond to it as usual.
2637 
2638No event resumes a session paused at its cap. Instead, update the session's budget: changing the cap to any value above the consumed list cost, or removing the budget by updating the session with `"budget": null`, resumes the paused work automatically. See [Session budgets](https://platform.claude.com/docs/en/managed-agents/budgets) for how list cost is tracked and the full budget update semantics.
2639 
2640### Sending system messages
1430Conversation history is preserved unless the session is explicitly deleted. When a session goes idle, its sandbox is checkpointed. The checkpoint preserves the full sandbox state, including the filesystem, installed packages, and any files the agent created.
26411431 
26421432<Note>
2643 `system.message` is supported by Claude Fable 5.1, Claude Mythos 5.1, Claude Fable 5, Claude Mythos 5, Claude Opus 5.5, Claude Opus 5, Claude Opus 4.8, Claude Sonnet 5.5, and Claude Haiku 5.5. If the agent's primary model does not support mid-conversation system injection, the event is rejected with a `model_does_not_support_mid_conversation_system` validation error. Subagent models are not checked, because `system.message` lands on the primary thread only.
1433 Sandbox state is preserved for only 30 days after the sandbox is created, and activity does not extend this window. After 30 days the sandbox state (files, installed tools, and so on) is unrecoverable, and a resumed session starts from a fresh sandbox. If your workflow depends on sandbox contents, have the agent write important artifacts to [outputs](https://platform.claude.com/docs/en/managed-agents/define-outcomes#retrieving-deliverables) before the window ends.
26441434</Note>
26451435 
2646Send a `system.message` event to give the agent privileged system-level context that applies to the accompanying turn and all subsequent turns. Unlike the `system` field on the agent definition (which sets the top-level system prompt), `system.message` content is appended to the session's system context as a `role: "system"` turn rather than replacing that prompt. Use it when the agent needs updated system-level guidance mid-session: a different persona, revised constraints, or context fetched at runtime that should shape the model's behavior going forward.
1436## Interrupt the agent
1437 
1438Send a `user.interrupt` event to stop the agent mid-execution, then follow up with a `user.message` event to redirect it:
1439 
1440<CodeGroup>
1441 ```bash cURL
1442 # Agent is currently analyzing a file...
1443 # Interrupt with a new direction:
1444 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
1445 -H "x-api-key: $ANTHROPIC_API_KEY" \
1446 -H "anthropic-version: 2023-06-01" \
1447 -H "anthropic-beta: managed-agents-2026-04-01" \
1448 -H "content-type: application/json" \
1449 -d @- <<'EOF'
1450 {
1451 "events": [
1452 {"type": "user.interrupt"},
1453 {
1454 "type": "user.message",
1455 "content": [
1456 {"type": "text", "text": "Instead, focus on fixing the bug in line 42."}
1457 ]
1458 }
1459 ]
1460 }
1461 EOF
1462 ```
1463 
1464 ```bash CLI
1465 # Agent is currently analyzing a file...
1466 # Interrupt with a new direction:
1467 ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML'
1468 events:
1469 - type: user.interrupt
1470 - type: user.message
1471 content:
1472 - type: text
1473 text: Instead, focus on fixing the bug in line 42.
1474 YAML
1475 ```
1476 
1477 ```python Python
1478 # Agent is currently analyzing a file...
1479 # Interrupt with a new direction:
1480 client.beta.sessions.events.send(
1481 session.id,
1482 events=[
1483 {"type": "user.interrupt"},
1484 {
1485 "type": "user.message",
1486 "content": [
1487 {
1488 "type": "text",
1489 "text": "Instead, focus on fixing the bug in line 42.",
1490 },
1491 ],
1492 },
1493 ],
1494 )
1495 ```
1496 
1497 ```typescript TypeScript
1498 // Agent is currently analyzing a file...
1499 // Interrupt with a new direction:
1500 await client.beta.sessions.events.send(session.id, {
1501 events: [
1502 { type: "user.interrupt" },
1503 {
1504 type: "user.message",
1505 content: [
1506 {
1507 type: "text",
1508 text: "Instead, focus on fixing the bug in line 42.",
1509 },
1510 ],
1511 },
1512 ],
1513 });
1514 ```
1515 
1516 ```csharp C#
1517 // Agent is currently analyzing a file...
1518 // Interrupt with a new direction:
1519 await client.Beta.Sessions.Events.Send(session.ID, new()
1520 {
1521 Events =
1522 [
1523 new BetaManagedAgentsUserInterruptEventParams
1524 {
1525 Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
1526 },
1527 new BetaManagedAgentsUserMessageEventParams
1528 {
1529 Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage,
1530 Content =
1531 [
1532 new BetaManagedAgentsTextBlock
1533 {
1534 Type = BetaManagedAgentsTextBlockType.Text,
1535 Text = "Instead, focus on fixing the bug in line 42.",
1536 },
1537 ],
1538 },
1539 ],
1540 });
1541 ```
1542 
1543 ```go Go
1544 // Agent is currently analyzing a file...
1545 // Interrupt with a new direction:
1546 if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
1547 Events: []anthropic.BetaManagedAgentsEventParamsUnion{
1548 {
1549 OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{
1550 Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
1551 },
1552 },
1553 {
1554 OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
1555 Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
1556 Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
1557 OfText: &anthropic.BetaManagedAgentsTextBlockParam{
1558 Type: anthropic.BetaManagedAgentsTextBlockTypeText,
1559 Text: "Instead, focus on fixing the bug in line 42.",
1560 },
1561 }},
1562 },
1563 },
1564 },
1565 }); err != nil {
1566 panic(err)
1567 }
1568 ```
1569 
1570 ```java Java
1571 // Agent is currently analyzing a file...
1572 // Interrupt with a new direction:
1573 client.beta().sessions().events().send(
1574 session.id(),
1575 EventSendParams.builder()
1576 .addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
1577 .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
1578 .build())
1579 .addEvent(BetaManagedAgentsUserMessageEventParams.builder()
1580 .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
1581 .addTextContent("Instead, focus on fixing the bug in line 42.")
1582 .build())
1583 .build());
1584 ```
1585 
1586 ```php PHP
1587 // Agent is currently analyzing a file...
1588 // Interrupt with a new direction:
1589 $client->beta->sessions->events->send(
1590 $session->id,
1591 events: [
1592 ['type' => 'user.interrupt'],
1593 [
1594 'type' => 'user.message',
1595 'content' => [
1596 [
1597 'type' => 'text',
1598 'text' => 'Instead, focus on fixing the bug in line 42.',
1599 ],
1600 ],
1601 ],
1602 ],
1603 );
1604 ```
1605 
1606 ```ruby Ruby
1607 # Agent is currently analyzing a file...
1608 # Interrupt with a new direction:
1609 client.beta.sessions.events.send_(
1610 session.id,
1611 events: [
1612 {type: "user.interrupt"},
1613 {
1614 type: "user.message",
1615 content: [
1616 {type: "text", text: "Instead, focus on fixing the bug in line 42."}
1617 ]
1618 }
1619 ]
1620 )
1621 ```
1622</CodeGroup>
1623 
1624The call returns as soon as the events are queued. The interrupt then takes effect in this order:
1625 
16261. The agent applies the interrupt. Until then, the interrupt's `processed_at` stays null and the session stays `running`. A model response in progress stops immediately, but the interrupt can take longer to apply while tool calls are running.
16272. The `user.interrupt` event appears on the stream, and the interrupted turn ends with a `session.status_idle` event.
16283. The agent starts its next turn with the `user.message` you sent after the interrupt.
1629 
1630The idle event's `stop_reason.type` is `end_turn`, the same value as a turn that finishes on its own.
1631 
1632## List past events
1633 
1634Retrieve the full event history for a session:
1635 
1636<CodeGroup>
1637 ```bash cURL
1638 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
1639 -H "x-api-key: $ANTHROPIC_API_KEY" \
1640 -H "anthropic-version: 2023-06-01" \
1641 -H "anthropic-beta: managed-agents-2026-04-01" \
1642 -H "content-type: application/json"
1643 ```
1644 
1645 ```bash CLI
1646 ant beta:sessions:events list --session-id "$SESSION_ID" --format jsonl
1647 ```
1648 
1649 ```python Python
1650 events = client.beta.sessions.events.list(session.id)
1651 for event in events.data:
1652 print(f"[{event.type}] {event.processed_at}")
1653 ```
1654 
1655 ```typescript TypeScript
1656 const events = await client.beta.sessions.events.list(session.id);
1657 for (const event of events.data) {
1658 console.log(`[${event.type}] ${event.processed_at}`);
1659 }
1660 ```
1661 
1662 ```csharp C#
1663 var events = await client.Beta.Sessions.Events.List(session.ID);
1664 foreach (var sessionEvent in events.Items)
1665 {
1666 Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
1667 }
1668 ```
1669 
1670 ```go Go
1671 events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{})
1672 if err != nil {
1673 panic(err)
1674 }
1675 for _, event := range events.Data {
1676 fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
1677 }
1678 ```
1679 
1680 ```java Java
1681 var events = client.beta().sessions().events().list(session.id());
1682 for (var event : events.data()) {
1683 var eventJson = event._json().orElseThrow().convert(JsonNode.class);
1684 var processedAt = eventJson.path("processed_at");
1685 IO.println("[" + eventJson.get("type").asText() + "] "
1686 + (processedAt.isTextual() ? processedAt.asText() : "null"));
1687 }
1688 ```
1689 
1690 ```php PHP
1691 $events = $client->beta->sessions->events->list($session->id);
1692 foreach ($events->data as $event) {
1693 $processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'null';
1694 echo "[{$event->type}] {$processedAt}\n";
1695 }
1696 ```
1697 
1698 ```ruby Ruby
1699 events = client.beta.sessions.events.list(session.id)
1700 events.data.each { puts "[#{it.type}] #{it.processed_at}" }
1701 ```
1702</CodeGroup>
1703 
1704Pass a `types` filter to return only specific event types:
1705 
1706<CodeGroup>
1707 ```bash cURL
1708 curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true&types[]=agent.tool_use&types[]=agent.tool_result" \
1709 -H "x-api-key: $ANTHROPIC_API_KEY" \
1710 -H "anthropic-version: 2023-06-01" \
1711 -H "anthropic-beta: managed-agents-2026-04-01"
1712 ```
1713 
1714 ```bash CLI
1715 ant beta:sessions:events list --session-id "$SESSION_ID" \
1716 --type agent.tool_use --type agent.tool_result \
1717 --format jsonl
1718 ```
1719 
1720 ```python Python
1721 events = client.beta.sessions.events.list(
1722 session.id,
1723 types=["agent.tool_use", "agent.tool_result"],
1724 )
1725 for event in events.data:
1726 print(f"[{event.type}] {event.processed_at}")
1727 ```
1728 
1729 ```typescript TypeScript
1730 const events = await client.beta.sessions.events.list(session.id, {
1731 types: ["agent.tool_use", "agent.tool_result"],
1732 });
1733 for (const event of events.data) {
1734 console.log(`[${event.type}] ${event.processed_at}`);
1735 }
1736 ```
1737 
1738 ```csharp C#
1739 var events = await client.Beta.Sessions.Events.List(session.ID, new()
1740 {
1741 Types = ["agent.tool_use", "agent.tool_result"],
1742 });
1743 foreach (var sessionEvent in events.Items)
1744 {
1745 Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}");
1746 }
1747 ```
1748 
1749 ```go Go
1750 events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{
1751 Types: []anthropic.BetaManagedAgentsSessionEventType{
1752 anthropic.BetaManagedAgentsSessionEventTypeAgentToolUse,
1753 anthropic.BetaManagedAgentsSessionEventTypeAgentToolResult,
1754 },
1755 })
1756 if err != nil {
1757 panic(err)
1758 }
1759 for _, event := range events.Data {
1760 fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
1761 }
1762 ```
1763 
1764 ```java Java
1765 var events = client.beta().sessions().events().list(
1766 session.id(),
1767 EventListParams.builder()
1768 .addType(BetaManagedAgentsSessionEventType.AGENT_TOOL_USE)
1769 .addType(BetaManagedAgentsSessionEventType.AGENT_TOOL_RESULT)
1770 .build());
1771 for (var event : events.data()) {
1772 event.agentToolUse().ifPresent(toolUse ->
1773 IO.println("[" + toolUse.type() + "] " + toolUse.processedAt()));
1774 event.agentToolResult().ifPresent(toolResult ->
1775 IO.println("[" + toolResult.type() + "] " + toolResult.processedAt()));
1776 }
1777 ```
1778 
1779 ```php PHP
1780 // In PHP, pass the types you want on EventListParams; see the Anthropic PHP SDK.
1781 ```
1782 
1783 ```ruby Ruby
1784 events = client.beta.sessions.events.list(
1785 session.id,
1786 types: ["agent.tool_use", "agent.tool_result"]
1787 )
1788 events.data.each { puts "[#{it.type}] #{it.processed_at}" }
1789 ```
1790</CodeGroup>
1791 
1792## Send system messages
1793 
1794On a [supported model](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#supported-models), send a `system.message` event to give the agent privileged system-level context. The context applies to the accompanying turn and all subsequent turns. Use it when the agent needs updated system-level guidance mid-session, for example a different persona, revised constraints, or context fetched at runtime.
1795 
1796The event's `content` accepts 1–1000 text items. The content is appended to the session's system context as a `role: "system"` turn. It doesn't replace the top-level system prompt, which the `system` field on the agent definition sets.
26471797 
26481798<CodeGroup>
26491799 ```bash cURL
from line 1939
27891939 ```
27901940</CodeGroup>
27911941 
2792While the session is idle with `stop_reason: requires_action`, a `system.message` is accepted only when it trails a tool result event in the same request; sent on its own or with a `user.message`, it is rejected until the pending tool events are resolved. `content` accepts 1–1000 text items.
2793 
2794### Tracking usage
2795 
2796The session object includes a `usage` field with the session's cumulative usage: token counts, server tool use, active time, and the tracked list cost. Fetch the session after it goes idle to read the latest totals.
2797 
2798```json
2799{
2800 "id": "sesn_01...",
2801 "status": "idle",
2802 "usage": {
2803 "input_tokens": 5000,
2804 "output_tokens": 3200,
2805 "cache_read_input_tokens": 20000,
2806 "cache_creation": {
2807 "ephemeral_5m_input_tokens": 2000,
2808 "ephemeral_1h_input_tokens": 0
2809 },
2810 "list_cost": {
2811 "amount": "187",
2812 "currency": "USD"
2813 },
2814 "active_seconds": 342.5,
2815 "server_tool_use": {
2816 "web_search_requests": 3,
2817 "web_fetch_requests": 0
2818 }
2819 }
2820}
2821```
2822 
2823`input_tokens` reports uncached input tokens and `output_tokens` reports total output tokens across all model calls in the session. The `cache_read_input_tokens` field reports tokens read from the prompt cache, and the `cache_creation` object breaks down cache-creation tokens by cache lifetime (`ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens`). Cache entries use a 5-minute TTL by default, so back-to-back turns within that window benefit from cache reads, which reduce per-token cost.
2824 
2825`list_cost` is the session's cumulative consumption priced at public list rates, as a whole number of cents in a string, with a currency code. `active_seconds` is the cumulative time during which the session had at least one thread running; overlapping activity from concurrent threads is counted once, unlike the `active_seconds` in the session's `stats` object, which sums each thread's own active time. This deduplicated figure is the duration the session's runtime cost is priced on. `server_tool_use` counts server-executed tool requests for pricing: web search requests are priced into list cost per request, and web fetch requests carry no per-request charge and aren't metered, so `web_fetch_requests` reads `0`. Each [session thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration)'s own `usage` carries `list_cost` and `active_seconds` too. Per-thread figures are rounded independently and exclude the session's running-time cost, so they don't sum exactly to the session's `list_cost`; the session figure is the authoritative one.
2826 
2827You don't have to poll the session to observe these totals. The `session.usage` event carries the same cumulative snapshot (the `usage` object, plus the session's `budget`, which is `null` when the session has none) on the session stream and in the event history. It is emitted on idle transitions rather than on a timer: the session emits one immediately before it goes idle, whatever the stop reason, and one when a thread pauses at a [session budget](https://platform.claude.com/docs/en/managed-agents/budgets). A stream reader therefore sees the final cost of a turn, or of the work that hit a budget, without an extra fetch.
2828 
2829To enforce a spend limit, set a [session budget](https://platform.claude.com/docs/en/managed-agents/budgets) rather than polling usage and stopping the session yourself. The platform prices the session's consumption continuously and pauses each thread before its next model request once the session's list cost reaches the cap; see [Reaching a session budget](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#reaching-a-session-budget) for what that looks like on the stream.
2830 
2831## Console observability
2832 
2833The Claude Console includes a session viewer for inspecting what an agent did without writing any code. In the Console sidebar, under **Managed Agents**, select **Sessions** to see every session in the workspace with its status, agent, token usage, cost, and creation time, then select a session to open it. The session viewer is only accessible to Developers and Admins. It shows:
2834 
2835* **Timeline minimap:** A zoomable overview of the session's activity over time, with one lane per thread in [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) sessions. Select a lane to view that thread, or select a mark to jump to its event.
2836 
2837* **Transcript:** The conversation grouped by model request, including thinking, tool calls with their inputs and results, and message text as it streams. You can filter the events and copy or download them as JSON.
2838 
2839* **Inspector:** A resizable side panel with details about the session, in five tabs:
2840 
2841 * **Session** shows the session's details and metadata, its cumulative cost over time, and spend against the session's [budget](https://platform.claude.com/docs/en/managed-agents/budgets) when one is set.
2842 * **Events** lists every raw event on the current thread in the order the server sent it; select an event to see its JSON. A message that streamed while the page was open also has a **Deltas** view of its [event deltas](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#event-deltas).
2843 * **Tools** lists the tools the session's agents are configured with, along with call counts, failures, and median duration; select a tool to see its calls and jump to one in the transcript.
2844 * **Resources** lists mounted [files](https://platform.claude.com/docs/en/managed-agents/files), [repositories](https://platform.claude.com/docs/en/managed-agents/github), and [memory stores](https://platform.claude.com/docs/en/managed-agents/memory) at their container paths, including the memories in each store and the changes this session made to them, plus files the agent wrote to `/mnt/session/outputs` and the [skills](https://platform.claude.com/docs/en/managed-agents/skills) attached to the session's agents.
2845 * **Threads** lists every thread with its status, context size, and cost. Select a thread to view its details, such as the agent, model, context usage, and cost.
2846 
2847Append `?event={event_id}` to a session URL to open the session at a specific event.
2848 
2849With `ant beta:sessions connect`, you can open the same viewer from the `ant` CLI or follow the session in your terminal. See [Connect to a Managed Agents session from your terminal](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/sessions-connect).
2850 
2851## Debugging tips
2852 
2853* **Check session events:** Session errors are conveyed through the `session.error` event
2854* **Review tool results:** Tool execution failures often explain unexpected agent behavior
2855* **Track token usage:** Monitor token consumption to optimize prompts and reduce costs
2856* **Use system prompts:** Add logging instructions to the system prompt so the agent summarizes what it did and what it found
2857* **Troubleshoot previews:** If a stream that opts in to event deltas doesn't behave as you expect, see [Troubleshoot previews](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#troubleshoot-previews)
1942### Supported models
1943 
1944`system.message` is supported by these models:
1945 
1946* Claude Fable 5.1
1947* Claude Mythos 5.1
1948* Claude Fable 5
1949* Claude Mythos 5
1950* Claude Opus 5.5
1951* Claude Opus 5
1952* Claude Opus 4.8
1953* Claude Sonnet 5.5
1954* Claude Haiku 5.5
1955 
1956If the agent's primary model does not support mid-conversation system injection, the event is rejected with a `model_does_not_support_mid_conversation_system` validation error. Subagent models are not checked, because `system.message` lands on the primary thread only.
1957 
1958### System messages while a tool call is pending
1959 
1960While the session is idle with a `requires_action` stop reason, a `system.message` must trail a tool result event in the same request. Sent on its own or with a `user.message`, it is rejected until the pending tool events are resolved.
1961 
1962## Next steps
1963 
1964<CardGroup cols={2}>
1965 <Card title="Preview responses with event deltas" icon="text" href="https://platform.claude.com/docs/en/managed-agents/event-deltas">
1966 Render the agent's response text while the model is still generating it.
1967 </Card>
1968 
1969 <Card title="Inspect sessions and track usage" icon="magnifying-glass" href="https://platform.claude.com/docs/en/managed-agents/session-observability">
1970 Inspect a session in the Claude Console and read its token usage and list cost.
1971 </Card>
1972 
1973 <Card title="Subscribe to webhooks" icon="link" href="https://platform.claude.com/docs/en/managed-agents/webhooks">
1974 Get notified when major events happen without polling.
1975 </Card>
1976 
1977 <Card title="Event types" icon="book" href="https://platform.claude.com/docs/en/managed-agents/reference#event-types">
1978 Look up every event type a session can send or receive.
1979 </Card>
1980</CardGroup>
28581981 
Feedback