@@ -66,11 +66,8 @@ interface AnthropicMessageWithContent {
6666type ChunkHandlerContext = {
6767 sessionId : string ;
6868 toolUseCache : ToolUseCache ;
69- /** Tool_use ids already surfaced as a `tool_call` (a permission request
70- * emits one eagerly, before the tool_use block streams). When the streamed
71- * block arrives second it refines with a `tool_call_update` instead of
72- * emitting a duplicate `tool_call`. Mutated in place; pruned at
73- * `tool_result` time alongside `toolUseCache`. */
69+ /** Tool_use ids already surfaced as a `tool_call` (permission requests emit
70+ * eagerly); the second emitter refines instead of duplicating. */
7471 emittedToolCalls ?: Set < string > ;
7572 fileContentCache : { [ key : string ] : string } ;
7673 enrichedReadCache ?: EnrichedReadCache ;
@@ -87,17 +84,11 @@ type ChunkHandlerContext = {
8784} ;
8885
8986/**
90- * The text/thinking blocks that actually streamed live as `stream_event`
91- * deltas for the message the next consolidated `assistant` will repeat, in
92- * stream order, each accumulated to its full streamed text. The consolidated
93- * handler diffs each assembled block against these and forwards only the
94- * un-streamed remainder — nothing if it streamed in full (the common case),
95- * the whole block if it never streamed (a non-streaming gateway), or just the
96- * tail if the stream was cut short mid-block. Matching on content rather than
97- * the Anthropic message id makes dedupe robust to gateways that don't carry a
98- * stable/matching id across the stream and the consolidated message. Cleared
99- * at each top-level `message_start` and again once a consolidated message
100- * consumes it, so the record stays bounded to the in-flight message.
87+ * Text/thinking actually streamed live for the in-flight message, in order.
88+ * The consolidated assistant message prefix-diffs its blocks against this and
89+ * forwards only the un-streamed remainder. Content matching (not message ids)
90+ * keeps dedupe robust to gateways whose ids don't line up; cleared per
91+ * message so it stays bounded.
10192 */
10293export interface StreamedAssistantBlocks {
10394 blocks : { index : number ; type : "text" | "thinking" ; text : string } [ ] ;
@@ -211,9 +202,6 @@ function handleToolUseChunk(
211202 ctx : ChunkHandlerContext ,
212203) : SessionUpdate | null {
213204 const alreadyCached = chunk . id in ctx . toolUseCache ;
214- // A permission request may have already surfaced this tool call to the
215- // client (see `emittedToolCalls`); if so this streamed encounter refines it
216- // rather than emitting a duplicate `tool_call`.
217205 const alreadyEmitted =
218206 alreadyCached || ctx . emittedToolCalls ?. has ( chunk . id ) === true ;
219207 ctx . toolUseCache [ chunk . id ] = chunk ;
@@ -846,11 +834,8 @@ export async function handleSystemMessage(
846834 break ;
847835 }
848836 case "informational" : {
849- // Free-form notice from the SDK (e.g. why a UserPromptSubmit/Stop hook
850- // blocked continuation). Surface the text so the user sees it instead
851- // of a silent stop. ACP's agent_message_chunk has no severity field, so
852- // fold the level into the text for the more prominent levels ('info' is
853- // transcript-only noise — leave plain).
837+ // Surface hook-blocked stops; the level is folded into the text since
838+ // agent_message_chunk has no severity field.
854839 const informationalText =
855840 message . level === "info"
856841 ? message . content
@@ -1022,26 +1007,16 @@ export async function handleStreamEvent(
10221007
10231008 const streamed = context . streamedAssistantBlocks ;
10241009 if ( streamed ) {
1025- // A new top-level message starts: clear any streamed-content residue from
1026- // a prior message that never reached its consolidated reset (a cancelled
1027- // turn breaks out before it). Block indices restart at 0 each message, so
1028- // leftover entries would otherwise collide with this message's blocks and
1029- // re-emit (or truncate) already-streamed text. Gated on
1030- // `parent_tool_use_id === null` so a subagent stream can't clear the
1031- // top-level record.
1010+ // Clear residue from a message that never reached its consolidated reset
1011+ // (e.g. a cancelled turn); indices restart per message and would collide.
10321012 if (
10331013 message . event . type === "message_start" &&
10341014 message . parent_tool_use_id === null
10351015 ) {
10361016 streamed . blocks . length = 0 ;
10371017 }
1038- // Accumulate the text/thinking actually streamed live, so the assistant
1039- // handler can diff its assembled blocks against what already reached the
1040- // client as chunks and forward only the remainder. Only top-level streams
1041- // are recorded — subagent text is never streamed and must stay filtered,
1042- // as it is internal to the tool call. Contiguous deltas of the same block
1043- // (same index and type) extend the current entry; anything else opens a
1044- // new one.
1018+ // Record only top-level streams; subagent text is never streamed and
1019+ // must stay filtered.
10451020 if (
10461021 message . parent_tool_use_id === null &&
10471022 message . event . type === "content_block_delta"
@@ -1053,11 +1028,7 @@ export async function handleStreamEvent(
10531028 : delta . type === "thinking_delta"
10541029 ? { type : "thinking" as const , text : delta . thinking }
10551030 : undefined ;
1056- // Skip empty deltas (some gateways emit empty thinking chunks):
1057- // appending "" is a no-op, but pushing a "" entry would create a block
1058- // the consolidated handler's `text.length > 0` guard can never consume,
1059- // stalling the diff cursor and re-emitting the next block as a
1060- // duplicate.
1031+ // An empty entry would stall the diff cursor in the assistant handler.
10611032 if ( chunk && chunk . text . length > 0 ) {
10621033 const index = message . event . index ;
10631034 const last = streamed . blocks [ streamed . blocks . length - 1 ] ;
@@ -1221,16 +1192,9 @@ function logSpecialMessages(
12211192 }
12221193}
12231194
1224- /**
1225- * Diffs each assistant `text`/`thinking` block against what already streamed
1226- * live as chunks (`StreamedAssistantBlocks`, in document order) and forwards
1227- * only the un-streamed remainder — nothing if it streamed in full (the common
1228- * case), the whole block if it never streamed (a non-streaming gateway), or
1229- * just the tail if the stream was cut short mid-block. Subagent assistant
1230- * content (`parent_tool_use_id !== null`) is never streamed and stays
1231- * internal to its tool call, so it is always dropped, as is everything when
1232- * no tracker is available (replay).
1233- */
1195+ // Forwards only the un-streamed remainder of each assistant text/thinking
1196+ // block: nothing, the whole block (non-streaming gateway) or a cut-short
1197+ // tail. Subagent content and tracker-less replay stay dropped.
12341198function filterAssistantContent (
12351199 message : SDKAssistantMessage ,
12361200 streamed : StreamedAssistantBlocks | undefined ,
@@ -1253,10 +1217,8 @@ function filterAssistantContent(
12531217 ) ;
12541218 }
12551219
1256- // `streamPos` walks the streamed blocks in step with the assembled
1257- // text/thinking blocks; tool_use and other blocks pass through untouched
1258- // (their own `toolUseCache` collapses the streamed/assembled pair) without
1259- // advancing it.
1220+ // streamPos walks the streamed record in step with the assembled
1221+ // text/thinking blocks; other block types pass through without advancing.
12601222 const kept : typeof content = [ ] ;
12611223 let streamPos = 0 ;
12621224 for ( const block of content ) {
@@ -1265,15 +1227,11 @@ function filterAssistantContent(
12651227 continue ;
12661228 }
12671229 const full = block . type === "text" ? block . text : block . thinking ;
1268- // Empty assembled blocks carry nothing (some gateways emit an empty
1269- // `thinking` block before the real text) — drop them.
12701230 if ( full . length === 0 ) {
12711231 continue ;
12721232 }
1273- // A streamed block of the same type whose accumulated text is a prefix of
1274- // this one was already (at least partly) delivered as chunks; consume it
1275- // and forward only what's left. A non-empty streamed text is required so
1276- // an empty/aborted streamed block doesn't swallow the assembled copy.
1233+ // A same-type streamed prefix means the block (or its head) was already
1234+ // delivered as chunks; consume it and forward only what's left.
12771235 const streamedBlock = streamed . blocks [ streamPos ] ;
12781236 if (
12791237 streamedBlock &&
@@ -1286,9 +1244,7 @@ function filterAssistantContent(
12861244 if ( remainder . length === 0 ) {
12871245 continue ;
12881246 }
1289- // Overwrite in place with just the un-streamed tail (the assembled
1290- // message isn't read again after this) so the block keeps its exact SDK
1291- // type.
1247+ // Overwrite in place so the block keeps its exact SDK type.
12921248 if ( block . type === "text" ) {
12931249 block . text = remainder ;
12941250 } else {
@@ -1297,12 +1253,8 @@ function filterAssistantContent(
12971253 kept . push ( block ) ;
12981254 continue ;
12991255 }
1300- // Not matched: never streamed (or the stream diverged from the assembled
1301- // text) — forward the block in full.
13021256 kept . push ( block ) ;
13031257 }
1304- // Consumed: reset so the next message's blocks accumulate fresh and the
1305- // record stays bounded to the in-flight message.
13061258 streamed . blocks . length = 0 ;
13071259 return kept ;
13081260}
0 commit comments