@@ -130,21 +130,82 @@ export async function* transformSdkStream(
130130 }
131131 } else if ( event . toolUseEvent ) {
132132 const tc = event . toolUseEvent
133+ // Only accumulate the tool *name* into totalContent — the input JSON is
134+ // never bracket-format and can be hundreds of KB; including it causes
135+ // catastrophic backtracking in parseBracketToolCalls.
133136 if ( tc . name ) totalContent += tc . name
134- if ( tc . input ) totalContent += tc . input
135137
136138 if ( tc . toolUseId ) {
137139 if ( currentToolCall && currentToolCall . toolUseId === tc . toolUseId ) {
138140 currentToolCall . input += tc . input || ''
141+ if ( tc . input ) {
142+ const _c = convertToOpenAI (
143+ {
144+ type : 'content_block_delta' ,
145+ index : currentToolCall . blockIndex ,
146+ delta : { type : 'input_json_delta' , partial_json : tc . input }
147+ } ,
148+ conversationId ,
149+ model
150+ )
151+ if ( _c !== null ) yield _c
152+ }
139153 } else if ( tc . name ) {
140154 if ( currentToolCall ) toolCalls . push ( currentToolCall )
155+ for ( const ev of stopBlock ( streamState . textBlockIndex , streamState ) ) {
156+ const _c = convertToOpenAI ( ev , conversationId , model )
157+ if ( _c !== null ) yield _c
158+ }
159+ const blockIndex = streamState . nextBlockIndex ++
141160 currentToolCall = {
142161 toolUseId : tc . toolUseId ,
143162 name : toolNameMapper ? toolNameMapper ( tc . name ) : tc . name ,
144- input : tc . input || ''
163+ input : tc . input || '' ,
164+ stopped : false ,
165+ blockIndex
166+ }
167+ {
168+ const _c = convertToOpenAI (
169+ {
170+ type : 'content_block_start' ,
171+ index : blockIndex ,
172+ content_block : {
173+ type : 'tool_use' ,
174+ id : tc . toolUseId ,
175+ name : currentToolCall . name ,
176+ input : { }
177+ }
178+ } ,
179+ conversationId ,
180+ model
181+ )
182+ if ( _c !== null ) yield _c
183+ }
184+ if ( tc . input ) {
185+ const _c = convertToOpenAI (
186+ {
187+ type : 'content_block_delta' ,
188+ index : blockIndex ,
189+ delta : { type : 'input_json_delta' , partial_json : tc . input }
190+ } ,
191+ conversationId ,
192+ model
193+ )
194+ if ( _c !== null ) yield _c
145195 }
146196 }
147197 if ( tc . stop && currentToolCall ) {
198+ currentToolCall . stopped = true
199+ // The AI SDK dispatches tools on message_delta, not on block stop,
200+ // so closing inline is safe and lets the client show progress immediately.
201+ {
202+ const _c = convertToOpenAI (
203+ { type : 'content_block_stop' , index : currentToolCall . blockIndex } ,
204+ conversationId ,
205+ model
206+ )
207+ if ( _c !== null ) yield _c
208+ }
148209 toolCalls . push ( currentToolCall )
149210 currentToolCall = null
150211 }
@@ -172,7 +233,27 @@ export async function* transformSdkStream(
172233 }
173234
174235 if ( currentToolCall ) {
236+ // Stream cut off mid-tool-call. Close the open block so the event stream
237+ // is well-formed, then append a visible error so the model can recover.
238+ logger . debug (
239+ `[STREAM] Truncated tool call: name=${ currentToolCall . name } id=${ currentToolCall . toolUseId } inputLen=${ currentToolCall . input . length } `
240+ )
241+ if ( currentToolCall . blockIndex !== undefined ) {
242+ const _c = convertToOpenAI (
243+ { type : 'content_block_stop' , index : currentToolCall . blockIndex } ,
244+ conversationId ,
245+ model
246+ )
247+ if ( _c !== null ) yield _c
248+ }
175249 toolCalls . push ( currentToolCall )
250+ for ( const ev of createTextDeltaEvents (
251+ `\n\n[Kiro: tool call "${ currentToolCall . name } " was truncated mid-stream — the response was too large. Try a smaller operation or split the task.]` ,
252+ streamState
253+ ) ) {
254+ const _c = convertToOpenAI ( ev , conversationId , model )
255+ if ( _c !== null ) yield _c
256+ }
176257 currentToolCall = null
177258 }
178259
@@ -205,25 +286,32 @@ export async function* transformSdkStream(
205286 if ( _c !== null ) yield _c
206287 }
207288
208- const bracketToolCalls = parseBracketToolCalls ( totalContent )
289+ const bracketToolCalls = totalContent . includes ( '[Called ' )
290+ ? parseBracketToolCalls ( totalContent )
291+ : [ ]
209292 if ( bracketToolCalls . length > 0 ) {
210293 for ( const btc of bracketToolCalls ) {
211294 toolCalls . push ( {
212295 toolUseId : btc . toolUseId ,
213296 name : btc . name ,
214- input : typeof btc . input === 'string' ? btc . input : JSON . stringify ( btc . input )
297+ input : typeof btc . input === 'string' ? btc . input : JSON . stringify ( btc . input ) ,
298+ stopped : true
299+ // no blockIndex — these are emitted post-stream below
215300 } )
216301 }
217302 }
218303
219304 const dedupedToolCalls = deduplicateToolCallsByContent ( toolCalls )
220305
221- if ( dedupedToolCalls . length > 0 ) {
222- const baseIndex = streamState . nextBlockIndex
223- for ( let i = 0 ; i < dedupedToolCalls . length ; i ++ ) {
224- const tc = dedupedToolCalls [ i ]
306+ // SDK tool calls were already emitted inline (content_block_start/delta/stop
307+ // during streaming). Only bracket-format tool calls (blockIndex undefined)
308+ // need to be emitted here.
309+ const postStreamCalls = dedupedToolCalls . filter ( ( tc ) => tc . blockIndex === undefined )
310+ if ( postStreamCalls . length > 0 ) {
311+ for ( let i = 0 ; i < postStreamCalls . length ; i ++ ) {
312+ const tc = postStreamCalls [ i ]
225313 if ( ! tc ) continue
226- const blockIndex = baseIndex + i
314+ const blockIndex = streamState . nextBlockIndex ++
227315
228316 {
229317 const _c = convertToOpenAI (
@@ -259,10 +347,7 @@ export async function* transformSdkStream(
259347 {
260348 type : 'content_block_delta' ,
261349 index : blockIndex ,
262- delta : {
263- type : 'input_json_delta' ,
264- partial_json : inputJson
265- }
350+ delta : { type : 'input_json_delta' , partial_json : inputJson }
266351 } ,
267352 conversationId ,
268353 model
0 commit comments