fix: handle malformed native tool calls to prevent hanging (#9758)

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
Daniel 2025-12-02 19:40:43 -05:00 committed by GitHub
parent 9a1d7a673b
commit aa40988e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2473,12 +2473,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Finalize the streaming tool call
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
// Get the index for this tool call
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
if (finalToolUse) {
// Store the tool call ID
;(finalToolUse as any).id = event.id
// Get the index and replace partial with final
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
if (toolUseIndex !== undefined) {
this.assistantMessageContent[toolUseIndex] = finalToolUse
}
@ -2491,6 +2493,25 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Present the finalized tool call
presentAssistantMessage(this)
} else if (toolUseIndex !== undefined) {
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
// We still need to mark the tool as non-partial so it gets executed
// The tool's validation will catch any missing required parameters
const existingToolUse = this.assistantMessageContent[toolUseIndex]
if (existingToolUse && existingToolUse.type === "tool_use") {
existingToolUse.partial = false
// Ensure it has the ID for native protocol
;(existingToolUse as any).id = event.id
}
// Clean up tracking
this.streamingToolCallIndices.delete(event.id)
// Mark that we have new content to process
this.userMessageContentReady = false
// Present the tool call - validation will handle missing params
presentAssistantMessage(this)
}
}
}
@ -2611,12 +2632,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Finalize the streaming tool call
const finalToolUse = NativeToolCallParser.finalizeStreamingToolCall(event.id)
// Get the index for this tool call
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
if (finalToolUse) {
// Store the tool call ID
;(finalToolUse as any).id = event.id
// Get the index and replace partial with final
const toolUseIndex = this.streamingToolCallIndices.get(event.id)
if (toolUseIndex !== undefined) {
this.assistantMessageContent[toolUseIndex] = finalToolUse
}
@ -2629,6 +2652,25 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Present the finalized tool call
presentAssistantMessage(this)
} else if (toolUseIndex !== undefined) {
// finalizeStreamingToolCall returned null (malformed JSON or missing args)
// We still need to mark the tool as non-partial so it gets executed
// The tool's validation will catch any missing required parameters
const existingToolUse = this.assistantMessageContent[toolUseIndex]
if (existingToolUse && existingToolUse.type === "tool_use") {
existingToolUse.partial = false
// Ensure it has the ID for native protocol
;(existingToolUse as any).id = event.id
}
// Clean up tracking
this.streamingToolCallIndices.delete(event.id)
// Mark that we have new content to process
this.userMessageContentReady = false
// Present the tool call - validation will handle missing params
presentAssistantMessage(this)
}
}
}
@ -2898,9 +2940,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.assistantMessageContent = parsedBlocks
}
// Only present partial blocks that were just completed (from XML parsing)
// Native tool blocks were already presented during streaming, so don't re-present them
if (partialBlocks.length > 0 && partialBlocks.some((block) => block.type !== "tool_use")) {
// Present any partial blocks that were just completed
// For XML protocol: includes both text and tool_use blocks parsed from the text stream
// For native protocol: tool_use blocks were already presented during streaming via
// tool_call_partial events, but we still need to present them if they exist (e.g., malformed)
if (partialBlocks.length > 0) {
// If there is content to update then it will complete and
// update `this.userMessageContentReady` to true, which we
// `pWaitFor` before making the next request.