Skip to content

Commit aa40988

Browse files
daniel-lxsmrubens
andauthored
fix: handle malformed native tool calls to prevent hanging (#9758)
Co-authored-by: Matt Rubens <[email protected]>
1 parent 9a1d7a6 commit aa40988

File tree

1 file changed

+49
-5
lines changed

1 file changed

+49
-5
lines changed

src/core/task/Task.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,12 +2473,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
24732473
// Finalize the streaming tool call
24742474
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
24752475

2476+
// Get the index for this tool call
2477+
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
2478+
24762479
if (finalToolUse) {
24772480
// Store the tool call ID
24782481
;(finalToolUse as any).id = event.id
24792482

24802483
// Get the index and replace partial with final
2481-
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
24822484
if (toolUseIndex !== undefined) {
24832485
this.assistantMessageContent[toolUseIndex] = finalToolUse
24842486
}
@@ -2491,6 +2493,25 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
24912493

24922494
// Present the finalized tool call
24932495
presentAssistantMessage(this)
2496+
} else if (toolUseIndex !== undefined) {
2497+
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
2498+
// We still need to mark the tool as non-partial so it gets executed
2499+
// The tool's validation will catch any missing required parameters
2500+
const existingToolUse = this.assistantMessageContent[toolUseIndex]
2501+
if (existingToolUse && existingToolUse.type === "tool_use") {
2502+
existingToolUse.partial = false
2503+
// Ensure it has the ID for native protocol
2504+
;(existingToolUse as any).id = event.id
2505+
}
2506+
2507+
// Clean up tracking
2508+
this.streamingToolCallIndices.delete(event.id)
2509+
2510+
// Mark that we have new content to process
2511+
this.userMessageContentReady = false
2512+
2513+
// Present the tool call - validation will handle missing params
2514+
presentAssistantMessage(this)
24942515
}
24952516
}
24962517
}
@@ -2611,12 +2632,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26112632
// Finalize the streaming tool call
26122633
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
26132634

2635+
// Get the index for this tool call
2636+
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
2637+
26142638
if (finalToolUse) {
26152639
// Store the tool call ID
26162640
;(finalToolUse as any).id = event.id
26172641

26182642
// Get the index and replace partial with final
2619-
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
26202643
if (toolUseIndex !== undefined) {
26212644
this.assistantMessageContent[toolUseIndex] = finalToolUse
26222645
}
@@ -2629,6 +2652,25 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
26292652

26302653
// Present the finalized tool call
26312654
presentAssistantMessage(this)
2655+
} else if (toolUseIndex !== undefined) {
2656+
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
2657+
// We still need to mark the tool as non-partial so it gets executed
2658+
// The tool's validation will catch any missing required parameters
2659+
const existingToolUse = this.assistantMessageContent[toolUseIndex]
2660+
if (existingToolUse && existingToolUse.type === "tool_use") {
2661+
existingToolUse.partial = false
2662+
// Ensure it has the ID for native protocol
2663+
;(existingToolUse as any).id = event.id
2664+
}
2665+
2666+
// Clean up tracking
2667+
this.streamingToolCallIndices.delete(event.id)
2668+
2669+
// Mark that we have new content to process
2670+
this.userMessageContentReady = false
2671+
2672+
// Present the tool call - validation will handle missing params
2673+
presentAssistantMessage(this)
26322674
}
26332675
}
26342676
}
@@ -2898,9 +2940,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
28982940
this.assistantMessageContent = parsedBlocks
28992941
}
29002942

2901-
// Only present partial blocks that were just completed (from XML parsing)
2902-
// Native tool blocks were already presented during streaming, so don't re-present them
2903-
if (partialBlocks.length > 0 && partialBlocks.some((block) => block.type !== "tool_use")) {
2943+
// Present any partial blocks that were just completed
2944+
// For XML protocol: includes both text and tool_use blocks parsed from the text stream
2945+
// For native protocol: tool_use blocks were already presented during streaming via
2946+
// tool_call_partial events, but we still need to present them if they exist (e.g., malformed)
2947+
if (partialBlocks.length > 0) {
29042948
// If there is content to update then it will complete and
29052949
// update `this.userMessageContentReady` to true, which we
29062950
// `pWaitFor` before making the next request.

0 commit comments

Comments
 (0)