@@ -2983,58 +2983,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
29832983 }
29842984 }
29852985
2986- // Finalize any remaining streaming tool calls that weren't explicitly ended
2987- // This is critical for MCP tools which need tool_call_end events to be properly
2988- // converted from ToolUse to McpToolUse via finalizeStreamingToolCall()
2989- const finalizeEvents = NativeToolCallParser . finalizeRawChunks ( )
2990- for ( const event of finalizeEvents ) {
2991- if ( event . type === "tool_call_end" ) {
2992- // Finalize the streaming tool call
2993- const finalToolUse = NativeToolCallParser . finalizeStreamingToolCall ( event . id )
2994-
2995- // Get the index for this tool call
2996- const toolUseIndex = this . streamingToolCallIndices . get ( event . id )
2997-
2998- if ( finalToolUse ) {
2999- // Store the tool call ID
3000- ; ( finalToolUse as any ) . id = event . id
3001-
3002- // Get the index and replace partial with final
3003- if ( toolUseIndex !== undefined ) {
3004- this . assistantMessageContent [ toolUseIndex ] = finalToolUse
3005- }
3006-
3007- // Clean up tracking
3008- this . streamingToolCallIndices . delete ( event . id )
3009-
3010- // Mark that we have new content to process
3011- this . userMessageContentReady = false
3012-
3013- // Present the finalized tool call
3014- presentAssistantMessage ( this )
3015- } else if ( toolUseIndex !== undefined ) {
3016- // finalizeStreamingToolCall returned null (malformed JSON or missing args)
3017- // We still need to mark the tool as non-partial so it gets executed
3018- // The tool's validation will catch any missing required parameters
3019- const existingToolUse = this . assistantMessageContent [ toolUseIndex ]
3020- if ( existingToolUse && existingToolUse . type === "tool_use" ) {
3021- existingToolUse . partial = false
3022- // Ensure it has the ID for native protocol
3023- ; ( existingToolUse as any ) . id = event . id
3024- }
3025-
3026- // Clean up tracking
3027- this . streamingToolCallIndices . delete ( event . id )
3028-
3029- // Mark that we have new content to process
3030- this . userMessageContentReady = false
3031-
3032- // Present the tool call - validation will handle missing params
3033- presentAssistantMessage ( this )
3034- }
3035- }
3036- }
3037-
30382986 // Create a copy of current token values to avoid race conditions
30392987 const currentTokens = {
30402988 input : inputTokens ,
@@ -3282,6 +3230,61 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32823230 // the case, `presentAssistantMessage` relies on these blocks either
32833231 // to be completed or the user to reject a block in order to proceed
32843232 // and eventually set userMessageContentReady to true.)
3233+
3234+ // Finalize any remaining streaming tool calls that weren't explicitly ended
3235+ // This is critical for MCP tools which need tool_call_end events to be properly
3236+ // converted from ToolUse to McpToolUse via finalizeStreamingToolCall()
3237+ const finalizeEvents = NativeToolCallParser . finalizeRawChunks ( )
3238+ for ( const event of finalizeEvents ) {
3239+ if ( event . type === "tool_call_end" ) {
3240+ // Finalize the streaming tool call
3241+ const finalToolUse = NativeToolCallParser . finalizeStreamingToolCall ( event . id )
3242+
3243+ // Get the index for this tool call
3244+ const toolUseIndex = this . streamingToolCallIndices . get ( event . id )
3245+
3246+ if ( finalToolUse ) {
3247+ // Store the tool call ID
3248+ ; ( finalToolUse as any ) . id = event . id
3249+
3250+ // Get the index and replace partial with final
3251+ if ( toolUseIndex !== undefined ) {
3252+ this . assistantMessageContent [ toolUseIndex ] = finalToolUse
3253+ }
3254+
3255+ // Clean up tracking
3256+ this . streamingToolCallIndices . delete ( event . id )
3257+
3258+ // Mark that we have new content to process
3259+ this . userMessageContentReady = false
3260+
3261+ // Present the finalized tool call
3262+ presentAssistantMessage ( this )
3263+ } else if ( toolUseIndex !== undefined ) {
3264+ // finalizeStreamingToolCall returned null (malformed JSON or missing args)
3265+ // We still need to mark the tool as non-partial so it gets executed
3266+ // The tool's validation will catch any missing required parameters
3267+ const existingToolUse = this . assistantMessageContent [ toolUseIndex ]
3268+ if ( existingToolUse && existingToolUse . type === "tool_use" ) {
3269+ existingToolUse . partial = false
3270+ // Ensure it has the ID for native protocol
3271+ ; ( existingToolUse as any ) . id = event . id
3272+ }
3273+
3274+ // Clean up tracking
3275+ this . streamingToolCallIndices . delete ( event . id )
3276+
3277+ // Mark that we have new content to process
3278+ this . userMessageContentReady = false
3279+
3280+ // Present the tool call - validation will handle missing params
3281+ presentAssistantMessage ( this )
3282+ }
3283+ }
3284+ }
3285+
3286+ // IMPORTANT: Capture partialBlocks AFTER finalizeRawChunks() to avoid double-presentation.
3287+ // Tools finalized above are already presented, so we only want blocks still partial after finalization.
32853288 const partialBlocks = this . assistantMessageContent . filter ( ( block ) => block . partial )
32863289 partialBlocks . forEach ( ( block ) => ( block . partial = false ) )
32873290
@@ -3290,16 +3293,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32903293
32913294 // No legacy streaming parser to finalize.
32923295
3293- // Present any partial blocks that were just completed.
3294- // Tool calls are typically presented during streaming via tool_call_partial events,
3295- // but we still present here if any partial blocks remain (e.g., malformed streams).
3296- if ( partialBlocks . length > 0 ) {
3297- // If there is content to update then it will complete and
3298- // update `this.userMessageContentReady` to true, which we
3299- // `pWaitFor` before making the next request.
3300- presentAssistantMessage ( this )
3301- }
3302-
33033296 // Note: updateApiReqMsg() is now called from within drainStreamInBackgroundToFindAllUsage
33043297 // to ensure usage data is captured even when the stream is interrupted. The background task
33053298 // uses local variables to accumulate usage data before atomically updating the shared state.
@@ -3324,10 +3317,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
33243317
33253318 // No legacy text-stream tool parser state to reset.
33263319
3327- // Now add to apiConversationHistory .
3328- // Need to save assistant responses to file before proceeding to
3329- // tool use since user can exit at any moment and we wouldn't be
3330- // able to save the assistant's response .
3320+ // CRITICAL: Save assistant message to API history BEFORE executing tools .
3321+ // This ensures that when new_task triggers delegation and calls flushPendingToolResultsToHistory(),
3322+ // the assistant message is already in history. Otherwise, tool_result blocks would appear
3323+ // BEFORE their corresponding tool_use blocks, causing API errors .
33313324
33323325 // Check if we have any content to process (text or tool uses)
33333326 const hasTextContent = assistantMessage . length > 0
@@ -3424,13 +3417,69 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
34243417 }
34253418 }
34263419
3420+ // Enforce new_task isolation: if new_task is called alongside other tools,
3421+ // truncate any tools that come after it and inject error tool_results.
3422+ // This prevents orphaned tools when delegation disposes the parent task.
3423+ const newTaskIndex = assistantContent . findIndex (
3424+ ( block ) => block . type === "tool_use" && block . name === "new_task" ,
3425+ )
3426+
3427+ if ( newTaskIndex !== - 1 && newTaskIndex < assistantContent . length - 1 ) {
3428+ // new_task found but not last - truncate subsequent tools
3429+ const truncatedTools = assistantContent . slice ( newTaskIndex + 1 )
3430+ assistantContent . length = newTaskIndex + 1 // Truncate API history array
3431+
3432+ // ALSO truncate the execution array (assistantMessageContent) to prevent
3433+ // tools after new_task from being executed by presentAssistantMessage().
3434+ // Find new_task index in assistantMessageContent (may differ from assistantContent
3435+ // due to text blocks being structured differently).
3436+ const executionNewTaskIndex = this . assistantMessageContent . findIndex (
3437+ ( block ) => block . type === "tool_use" && block . name === "new_task" ,
3438+ )
3439+ if ( executionNewTaskIndex !== - 1 ) {
3440+ this . assistantMessageContent . length = executionNewTaskIndex + 1
3441+ }
3442+
3443+ // Pre-inject error tool_results for truncated tools
3444+ for ( const tool of truncatedTools ) {
3445+ if ( tool . type === "tool_use" && ( tool as Anthropic . ToolUseBlockParam ) . id ) {
3446+ this . pushToolResultToUserContent ( {
3447+ type : "tool_result" ,
3448+ tool_use_id : ( tool as Anthropic . ToolUseBlockParam ) . id ,
3449+ content :
3450+ "This tool was not executed because new_task was called in the same message turn. The new_task tool must be the last tool in a message." ,
3451+ is_error : true ,
3452+ } )
3453+ }
3454+ }
3455+ }
3456+
3457+ // Save assistant message BEFORE executing tools
3458+ // This is critical for new_task: when it triggers delegation, flushPendingToolResultsToHistory()
3459+ // will save the user message with tool_results. The assistant message must already be in history
3460+ // so that tool_result blocks appear AFTER their corresponding tool_use blocks.
34273461 await this . addToApiConversationHistory (
34283462 { role : "assistant" , content : assistantContent } ,
34293463 reasoningMessage || undefined ,
34303464 )
34313465
34323466 TelemetryService . instance . captureConversationMessage ( this . taskId , "assistant" )
3467+ }
34333468
3469+ // Present any partial blocks that were just completed.
3470+ // Tool calls are typically presented during streaming via tool_call_partial events,
3471+ // but we still present here if any partial blocks remain (e.g., malformed streams).
3472+ // NOTE: This MUST happen AFTER saving the assistant message to API history.
3473+ // When new_task is in the batch, it triggers delegation which calls flushPendingToolResultsToHistory().
3474+ // If the assistant message isn't saved yet, tool_results would appear before tool_use blocks.
3475+ if ( partialBlocks . length > 0 ) {
3476+ // If there is content to update then it will complete and
3477+ // update `this.userMessageContentReady` to true, which we
3478+ // `pWaitFor` before making the next request.
3479+ presentAssistantMessage ( this )
3480+ }
3481+
3482+ if ( hasTextContent || hasToolUses ) {
34343483 // NOTE: This comment is here for future reference - this was a
34353484 // workaround for `userMessageContent` not getting set to true.
34363485 // It was due to it not recursively calling for partial blocks
@@ -4129,9 +4178,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
41294178
41304179 const shouldIncludeTools = allTools . length > 0
41314180
4132- // Parallel tool calls are disabled - feature is on hold
4133- // Previously resolved from experiments.isEnabled(..., EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS)
4134- const parallelToolCallsEnabled = false
4181+ const parallelToolCallsEnabled = state ?. experiments ?. multipleNativeToolCalls ?? false
41354182
41364183 const metadata : ApiHandlerCreateMessageMetadata = {
41374184 mode : mode ,
0 commit comments