Event deltas
By 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.
Opt in to previews
Previews 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'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.
When a previewed event begins, the stream emits an event_start carrying the upcoming event's type and id:
{
"type": "event_start",
"event": {
"type": "agent.message",
"id": "sevt_01abc..."
}
}For 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:
{
"type": "event_delta",
"event_id": "sevt_01abc...",
"delta": {
"type": "content_delta",
"index": 0,
"content": {
"type": "text",
"text": "Here is the summary"
}
}
}When 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.
Unlike 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.
Event deltas use a different wire format from Streaming messages, 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.
Accumulate and reconcile
Every 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.
In 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 (in the Span events tab). On the wire, this is the previewed portion of that sequence, interleaved with the connection's other buffered events:
event_start {"event": {"type": "agent.message", "id": "sevt_01abc..."}}
event_delta {"event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "..."}}}
...
agent.message {"id": "sevt_01abc...", "content": [...]}The event_delta line repeats once per text fragment. Process each event as it arrives:
- On
event_start, note the announcedid. The identifiers always line up:event_start.event.id, everyevent_delta.event_id, and the bufferedagent.message'sidare the same value. - On each
event_delta, appenddelta.content.textto the entry at(event_id, delta.index)and render the running text. The first delta for anindexcreates that entry. - When the buffered
agent.messagearrives, match it byid, discard the accumulated preview, and render the message's content instead. - 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_endstill does.
Guarantees the pattern relies on:
Concatenating a preview's deltas in arrival order, keyed by
(event_id, index), gives a prefix ofcontent[index].textin the buffered event (a prefix, not necessarily the whole text, because deltas might be shed under load).A connection emits at most one
event_startperevent_id, and the buffered event is the last thing that connection delivers for thatid.bash# Opt in to agent.message previews via event_deltas, then accumulate manually. exec {stream}< <( curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true&event_deltas%5B%5D=agent.message" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "accept: text/event-stream" ) curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- >/dev/null <<'EOF' { "events": [ { "type": "user.message", "content": [{"type": "text", "text": "In one short sentence, describe what an event delta is."}] } ] } EOF # Accumulate deltas keyed by (message id, content index); the final # agent.message carries the full text, so it replaces every preview for that id. declare -A preview while IFS= read -r -u "$stream" event_line; do [[ $event_line == data:* ]] || continue event_json=${event_line#data: } case $(jq -r '.type' <<<"$event_json") in event_start) preview_id=$(jq -r '.event.id' <<<"$event_json") printf '[event_start id=%s]\n' "$preview_id" ;; event_delta) preview_key=$(jq -r '.event_id + ":" + (.delta.index | tostring)' <<<"$event_json") preview[$preview_key]+=$(jq -r '.delta.content.text' <<<"$event_json") printf '[event_delta] %s\n' "${preview[$preview_key]}" ;; agent.message) msg_id=$(jq -r '.id' <<<"$event_json") for preview_key in "${!preview[@]}"; do [[ $preview_key == "$msg_id":* ]] && unset "preview[$preview_key]" done printf '[agent.message id=%s] ' "$msg_id" jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json" printf '\n' ;; span.model_request_end) for preview_key in "${!preview[@]}"; do printf '[closing unreconciled preview for %s]\n' "${preview_key%%:*}" done preview=() ;; session.status_idle) break ;; esac done exec {stream}<&-bash# This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead.python# Preview snapshots, keyed by event id. accumulate_managed_agents_event folds each # event_start / event_delta into an agent.message snapshot; the buffered # agent.message replaces it. previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {} # Opt in to agent.message previews on this connection with client.beta.sessions.events.stream( session.id, event_deltas=["agent.message"] ) as stream: client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [{"type": "text", "text": "Describe the repo in one sentence."}], }, ], ) for event in stream: match event.type: case "event_start": snapshot = accumulate_managed_agents_event(None, event) if snapshot is not None: previews[event.event.id] = snapshot print(f"event_start {event.event.type} {event.event.id}") case "event_delta": preview = accumulate_managed_agents_event(previews.get(event.event_id), event) if preview is not None: previews[event.event_id] = preview text = "".join(block.text for block in preview.content) print(f"event_delta preview: {text!r}") case "agent.message": # The buffered event is the record: it replaces and closes the preview preview = accumulate_managed_agents_event(previews.pop(event.id, None), event) text = "".join(block.text for block in preview.content) print(f"agent.message {event.id} {text!r}") case "span.model_request_end": # No more deltas are coming. Close any preview whose # buffered event never arrived. for event_id in previews: print(f"span.model_request_end closing preview for {event_id}") previews.clear() case "session.status_idle": breaktypescript// Preview snapshots, keyed by event id. `accumulateManagedAgentsEvent` // folds event_start / event_delta previews into an agent.message snapshot. const previews = new Map<string, BetaManagedAgentsAgentMessageEvent>(); // Opt in to agent.message previews for this connection only const stream = await client.beta.sessions.events.stream(session.id, { event_deltas: ["agent.message"], }); await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [{ type: "text", text: "Summarize the repo README" }] } ] }); for await (const event of stream) { if (event.type === "event_start") { // 1. Note the announced id and open the snapshot. Deltas and the // buffered event carry the same id. const preview = accumulateManagedAgentsEvent(undefined, event); if (preview) previews.set(event.event.id, preview); console.log(`event_start ${event.event.type} ${event.event.id}`); } else if (event.type === "event_delta") { // 2. Fold the fragment into the snapshot and render it const preview = accumulateManagedAgentsEvent(previews.get(event.event_id), event); if (preview) { previews.set(event.event_id, preview); const text = preview.content .map((block) => (block.type === "text" ? block.text : "")) .join(""); console.log(`event_delta preview: ${JSON.stringify(text)}`); } } else if (event.type === "agent.message") { // 3. The buffered event is the record: it replaces and closes the preview const message = accumulateManagedAgentsEvent(previews.get(event.id), event); previews.delete(event.id); const text = message.content .map((block) => (block.type === "text" ? block.text : "")) .join(""); console.log(`agent.message ${event.id} ${JSON.stringify(text)}`); } else if (event.type === "span.model_request_end") { // 4. No more deltas are coming. Close any preview that was never reconciled. for (const eventId of previews.keys()) { console.log(`span.model_request_end closing preview for ${eventId}`); } previews.clear(); } else if (event.type === "session.status_idle") { break; } } stream.controller.abort();csharp// Opt in to event deltas: agent.message events are previewed as they are produced. using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming( session.ID, new() { EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] } ); await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "Write a haiku about event streams.", }, ], }, ], }); // Accumulate preview fragments per (event id, content index). The buffered // agent.message that follows carries the complete content, so it replaces the // accumulated preview rather than appending to it. Dictionary<string, SortedDictionary<long, string>> previews = []; await foreach (var streamEvent in stream.Enumerate()) { if (streamEvent.TryPickStartEvent(out var start)) { // A preview opened for the event with this id. This stream only opts in // to agent.message deltas; TryPick* returns false instead of throwing, // so other preview types (including ones added later) are skipped. if (start.Event.TryPickAgentMessage(out var preview)) { Console.WriteLine($"event_start {preview.Type.Raw()} {preview.ID}"); } } else if (streamEvent.TryPickDeltaEvent(out var delta)) { // Insert at a new index, append at an existing one if (!previews.TryGetValue(delta.EventID, out var fragments)) { previews[delta.EventID] = fragments = []; } var index = delta.Delta.Index ?? 0; fragments[index] = fragments.GetValueOrDefault(index, "") + delta.Delta.Content.Text; Console.WriteLine($"event_delta preview: {fragments[index]}"); } else if (streamEvent.TryPickAgentMessageEvent(out var message)) { // Deltas are best-effort: discard the preview and use the buffered event previews.Remove(message.ID); var text = string.Concat(message.Content.Select(block => block.TryPickBetaManagedAgentsTextBlock(out var textBlock) ? textBlock.Text : "")); Console.WriteLine($"agent.message {message.ID} {text}"); } else if (streamEvent.TryPickSpanModelRequestEndEvent(out _)) { // No more deltas are coming; close any preview that was never reconciled. foreach (var eventId in previews.Keys) { Console.WriteLine($"span.model_request_end closing preview for {eventId}"); } previews.Clear(); } else if (streamEvent.TryPickSessionStatusIdleEvent(out _)) { break; } }go// Opt in to incremental previews of agent.message events stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{ EventDeltas: []anthropic.BetaManagedAgentsDeltaType{ anthropic.BetaManagedAgentsDeltaTypeAgentMessage, }, }) if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Write a haiku about the ocean.", }, }}, }, }}, }); err != nil { panic(err) } // The accumulator folds event_start / event_delta fragments into // per-event-id agent.message snapshots. The zero value is ready to use. var previews anthropic.BetaManagedAgentsEventAccumulator deltas: for stream.Next() { event := stream.Current() previews.Accumulate(event) switch event := event.AsAny().(type) { case anthropic.BetaManagedAgentsStartEvent: fmt.Printf("event_start %s %s\n", event.Event.Type, event.Event.ID) case anthropic.BetaManagedAgentsDeltaEvent: fmt.Printf("event_delta preview: %q\n", previews.AgentMessageText(event.EventID)) case anthropic.BetaManagedAgentsAgentMessageEvent: // The buffered event carries the complete content: the accumulator // replaces the preview with it fmt.Printf("agent.message %s %q\n", event.ID, previews.AgentMessageText(event.ID)) case anthropic.BetaManagedAgentsSpanModelRequestEndEvent: // No more deltas are coming for this request. The accumulator // drops its snapshots here, closing any preview that was never // reconciled by a buffered agent.message. fmt.Println("span.model_request_end no more deltas for this request") case anthropic.BetaManagedAgentsSessionStatusIdleEvent: break deltas } } if err := stream.Err(); err != nil { panic(err) } stream.Close()java// Preview text, keyed by event ID then content index. The buffered agent.message replaces it. Map<String, Map<Long, StringBuilder>> previews = new HashMap<>(); // Opt in to agent.message previews on this connection try (var stream = client.beta().sessions().events().streamStreaming( session.id(), EventStreamParams.builder() .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE) .build() )) { client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("Describe the repo in one sentence.") .build()) .build() ); Iterable<BetaManagedAgentsStreamSessionEvents> events = stream.stream()::iterator; for (var event : events) { if (event.isEventStart() && event.asEventStart().event().isAgentMessage()) { var preview = event.asEventStart().event().asAgentMessage(); IO.println("event_start " + preview.type().asString() + " " + preview.id()); } else if (event.isEventDelta()) { var eventDelta = event.asEventDelta(); var fragment = eventDelta.delta(); var buffer = previews .computeIfAbsent(eventDelta.eventId(), _ -> new HashMap<>()) .computeIfAbsent(fragment.index().orElse(0L), _ -> new StringBuilder()); buffer.append(fragment.content().text()); IO.println("event_delta preview: " + buffer); } else if (event.isAgentMessage()) { // The buffered event is the record: drop its preview, render its content var message = event.asAgentMessage(); previews.remove(message.id()); var text = message.content().stream() .flatMap(block -> block.text().stream()) .map(textBlock -> textBlock.text()) .collect(Collectors.joining()); IO.println("agent.message " + message.id() + " " + text); } else if (event.isSpanModelRequestEnd()) { // No more deltas are coming. Close any preview whose buffered event never arrived. previews.keySet().forEach(eventId -> IO.println("span.model_request_end closing preview for " + eventId)); previews.clear(); } else if (event.isSessionStatusIdle()) { break; } } }php// In PHP, set eventDeltas on EventStreamParams and accumulate with Anthropic\Lib\Sessions\EventAccumulator.ruby# Opt in to event deltas: agent.message previews stream as incremental fragments. stream = client.beta.sessions.events.stream_events( session.id, event_deltas: [Anthropic::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE] ) client.beta.sessions.events.send_( session.id, events: [{ type: "user.message", content: [{type: "text", text: "Give a one-sentence project tagline."}] }] ) # Accumulate preview fragments by (event_id, index) into explicitly mutable # (`+""`) buffers so `<<` can append in place. The buffered agent.message with # the same id is authoritative and replaces whatever the deltas built up. buffers = Hash.new do |by_event, event_id| by_event[event_id] = Hash.new { |fragments, index| fragments[index] = +"" } end stream.each do |event| case event.type in :event_start puts "event_start #{event.event.type} #{event.event.id}" in :event_delta delta = event.delta fragment = delta.content.text buffers[event.event_id][delta.index || 0] << fragment puts "event_delta preview: #{buffers[event.event_id][delta.index || 0].inspect}" in :"agent.message" # Replace: drop the accumulated preview and render the complete event. buffers.delete(event.id) puts "agent.message #{event.id} #{event.content.map(&:text).join.inspect}" in :"span.model_request_end" # No more deltas are coming. Close any preview that was never reconciled. buffers.each_key { |event_id| puts "span.model_request_end closing preview for #{event_id}" } buffers.clear in :"session.status_idle" break else # ignore other event types end end
Preview session thread events
In a multiagent 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.
The 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.
The 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 pattern applies as written. The one adjustment is bookkeeping: run one accumulator instance per stream connection.
# List the session's threads and pick a child: child threads carry a non-null
# parent_thread_id, and the primary thread's parent_thread_id is null.
THREAD_ID=$(
curl --fail-with-body -sS \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/threads?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" |
jq -er 'first(.data[] | select(.parent_thread_id != null)).id'
)
# The child thread's stream takes the same event_deltas[] parameter as the
# session stream. Percent-encode the brackets (%5B%5D) and quote the URL.
exec {stream}< <(
curl --fail-with-body -sS -N \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true&event_deltas%5B%5D=agent.message" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "accept: text/event-stream"
)
while IFS= read -r -u "$stream" event_line; do
[[ $event_line == data:* ]] || continue
event_json=${event_line#data: }
case $(jq -r '.type' <<<"$event_json") in
event_delta)
jq -j '.delta.content.text' <<<"$event_json"
;;
agent.message)
# The buffered event is the authoritative record; render its content.
printf '\n'
jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json"
printf '\n'
;;
session.thread_status_idle)
break
;;
esac
done
exec {stream}<&-# List the session's threads and pick a child: child threads carry a non-null
# parent_thread_id, and the primary thread's parent_thread_id is null
# (--transform's #(parent_thread_id!=~null) query matches non-null values).
THREAD_ID=$(ant beta:sessions:threads list \
--session-id "$SESSION_ID" \
--format raw --transform 'data.#(parent_thread_id!=~null).id' --raw-output)
# The child thread's stream takes the same event_deltas parameter as the
# session stream, one --event-delta flag per event type to preview. @tostr
# re-encodes each text field as a JSON string, so every value stays on one
# YAML line and jq's fromjson recovers the original text.
transform='{type,frag:delta.content.text|@tostr,text:content.#(type=="text").text|@tostr}'
exec {stream}< <(ant beta:sessions:threads:events stream \
--session-id "$SESSION_ID" \
--thread-id "$THREAD_ID" \
--event-delta agent.message \
--transform "$transform" \
--format yaml)
type=
while IFS= read -r -u "$stream" line; do
case "$line" in
type:\ session.thread_status_idle) break ;;
type:\ *) type=${line#type: } ;;
frag:*)
[[ $type == event_delta ]] || continue
jq -j fromjson <<<"${line#frag: }" ;;
text:*)
[[ $type == agent.message ]] || continue
# The buffered event is the authoritative record; render its content.
printf '\n'
jq -r fromjson <<<"${line#text: }" ;;
esac
done
exec {stream}<&-# List the session's threads and pick a child: child threads carry a non-null
# parent_thread_id, and the primary thread's parent_thread_id is null.
child_thread = next(
thread
for thread in client.beta.sessions.threads.list(session.id)
if thread.parent_thread_id is not None
)
# The child thread's stream takes the same event_deltas parameter as the
# session stream.
with client.beta.sessions.threads.events.stream(
child_thread.id,
session_id=session.id,
event_deltas=["agent.message"],
) as stream:
for event in stream:
match event.type:
case "event_delta":
print(event.delta.content.text, end="")
case "agent.message":
# The buffered event is the authoritative record; render its content
print()
for block in event.content:
if block.type == "text":
print(block.text, end="")
print()
case "session.thread_status_idle":
break// List the session's threads and pick a child: child threads carry a non-null
// parent_thread_id, and the primary thread's parent_thread_id is null.
let childThreadId: string | undefined;
for await (const thread of client.beta.sessions.threads.list(session.id)) {
if (thread.parent_thread_id !== null) {
childThreadId = thread.id;
break;
}
}
if (!childThreadId) throw new Error("No child thread found");
// The child thread's stream takes the same event_deltas parameter as the
// session stream.
const stream = await client.beta.sessions.threads.events.stream(childThreadId, {
session_id: session.id,
event_deltas: ["agent.message"],
});
for await (const event of stream) {
if (event.type === "event_delta") {
process.stdout.write(event.delta.content.text);
} else if (event.type === "agent.message") {
// The buffered event is the authoritative record; render its content.
process.stdout.write("\n");
const text = event.content
.map((block) => (block.type === "text" ? block.text : ""))
.join("");
console.log(text);
} else if (event.type === "session.thread_status_idle") {
break;
}
}
stream.controller.abort();// List the session's threads and pick a child: child threads carry a non-null
// parent_thread_id, and the primary thread's parent_thread_id is null.
var threads = await client.Beta.Sessions.Threads.List(session.ID);
var childThread = threads.Items.First(thread => thread.ParentThreadID is not null);
// The child thread's stream takes the same event_deltas parameter as the
// session stream.
using var stream = await client.Beta.Sessions.Threads.Events.WithRawResponse.StreamStreaming(
childThread.ID,
new() { SessionID = session.ID, EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] }
);
await foreach (var streamEvent in stream.Enumerate())
{
if (streamEvent.TryPickDeltaEvent(out var delta))
{
Console.Write(delta.Delta.Content.Text);
}
else if (streamEvent.TryPickAgentMessageEvent(out var message))
{
// The buffered event is the authoritative record; render its content.
Console.WriteLine();
var text = string.Concat(message.Content.Select(block =>
block.TryPickBetaManagedAgentsTextBlock(out var textBlock) ? textBlock.Text : ""));
Console.WriteLine(text);
}
else if (streamEvent.TryPickSessionThreadStatusIdleEvent(out _))
{
break;
}
} // List the session's threads and pick a child: child threads carry a non-null
// parent_thread_id, and the primary thread's parent_thread_id is null.
var childThreadID string
threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionThreadListParams{})
for threads.Next() {
if thread := threads.Current(); thread.ParentThreadID != "" {
childThreadID = thread.ID
break
}
}
if err := threads.Err(); err != nil {
panic(err)
}
// The child thread's stream takes the same event_deltas parameter as the
// session stream; run one read loop per stream connection.
stream := client.Beta.Sessions.Threads.Events.StreamEvents(ctx, childThreadID, anthropic.BetaSessionThreadEventStreamParams{
SessionID: session.ID,
EventDeltas: []anthropic.BetaManagedAgentsDeltaType{
anthropic.BetaManagedAgentsDeltaTypeAgentMessage,
},
})
threadDeltas:
for stream.Next() {
switch event := stream.Current().AsAny().(type) {
case anthropic.BetaManagedAgentsDeltaEvent:
fmt.Print(event.Delta.Content.Text)
case anthropic.BetaManagedAgentsAgentMessageEvent:
// The buffered event is the authoritative record; render its content.
fmt.Println()
// concrete-typed list: BetaManagedAgentsTextBlock
for _, block := range event.Content {
fmt.Print(block.Text)
}
fmt.Println()
case anthropic.BetaManagedAgentsSessionThreadStatusIdleEvent:
break threadDeltas
}
}
if err := stream.Err(); err != nil {
panic(err)
}
stream.Close()// List the session's threads and pick a child: child threads carry a non-null
// parent_thread_id, and the primary thread's parent_thread_id is null.
var childThread = client.beta().sessions().threads().list(session.id()).autoPager().stream()
.filter(thread -> thread.parentThreadId().isPresent())
.findFirst()
.orElseThrow();
// The child thread's stream takes the same event_deltas parameter as the session
// stream. Its params class shares the session-level one's simple name, so qualify it.
try (var stream = client.beta().sessions().threads().events().streamStreaming(
childThread.id(),
com.anthropic.models.beta.sessions.threads.events.EventStreamParams.builder()
.sessionId(session.id())
.addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE)
.build()
)) {
Iterable<BetaManagedAgentsStreamSessionThreadEvents> events = stream.stream()::iterator;
for (var event : events) {
if (event.isEventDelta()) {
IO.print(event.asEventDelta().delta().content().text());
} else if (event.isAgentMessage()) {
// The buffered event is the authoritative record; render its content.
IO.println();
event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())));
IO.println();
} else if (event.isSessionThreadStatusIdle()) {
break;
}
}
}// In PHP, set eventDeltas on the thread EventStreamParams and accumulate with Anthropic\Lib\Sessions\EventAccumulator.# List the session's threads and pick a child: child threads carry a non-null
# parent_thread_id, and the primary thread's parent_thread_id is null.
child_thread = client.beta.sessions.threads.list(session.id).to_enum.find { it.parent_thread_id }
# The child thread's stream takes the same event_deltas parameter as the
# session stream.
stream = client.beta.sessions.threads.events.stream_events(
child_thread.id,
session_id: session.id,
event_deltas: [Anthropic::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE]
)
stream.each do |event|
case event.type
in :event_delta
print event.delta.content.text
in :"agent.message"
# The buffered event is the authoritative record; render its content.
puts
event.content.each { print it.text }
puts
in :"session.thread_status_idle"
break
else
# ignore other event types
end
endThe read loop exits on session.thread_status_idle, the event emitted when the session thread's turn finishes and the thread goes idle.
Limitations
Previews are tuned for responsiveness. Build against these constraints:
- 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.messagestill arrives complete. Never treat an accumulated preview as final. - No replay on reconnect: Deltas are delivered only to the connection that opted in, while it 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 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.messageyour preview was waiting for. There is no way to re-request missed deltas. - 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 are never previewed on that connection.
- Start-only
agent.thinking: Anagent.thinkingpreview emits only theevent_startas a signal that a thinking block has started; noevent_deltaevents follow it. - Never persisted:
event_startandevent_deltaexist 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.
Troubleshoot previews
If the stream doesn't behave as you expect:
| You see | What it means |
|---|---|
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. |
| 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. |
A 400 naming event_deltas | Only agent.message and agent.thinking are accepted. |