fix: sanitize incomplete tool calls with partialJson#2253
fix: sanitize incomplete tool calls with partialJson#2253Zedit42 wants to merge 1 commit intoopenclaw:mainfrom
Conversation
When a request is terminated mid-stream, tool call blocks may have partialJson but incomplete/missing arguments. These incomplete tool calls cause API rejections because their matching tool_result blocks become orphaned. This fix: - Adds sanitizePartialToolCalls() to remove incomplete tool calls - Updates extractToolCallsFromAssistant() to skip incomplete calls - Integrates with existing sanitizeToolUseResultPairing() flow Fixes: Tool call terminated mid-execution causes 400 error with 'unexpected tool_use_id found in tool_result blocks'
| function isIncompleteToolCall(block: ToolCallBlock): boolean { | ||
| if (!block || typeof block !== "object") return false; | ||
| // If partialJson exists and arguments is missing or incomplete, it's partial | ||
| if (typeof block.partialJson === "string" && block.partialJson) { | ||
| // Check if arguments is missing or empty | ||
| const args = block.arguments; | ||
| if (args === undefined || args === null) return true; | ||
| if (typeof args === "object" && Object.keys(args as object).length === 0) return true; | ||
| } |
There was a problem hiding this comment.
isIncompleteToolCall treats arguments: {} as incomplete whenever partialJson is present. In existing tests in this file you intentionally emit tool calls with arguments: {} as the normal shape (e.g. in pairing/guard tests), and in real transcripts some tools legitimately have empty args. With this logic, any such call that also has partialJson captured would be dropped and its result orphaned/dropped, potentially deleting valid tool interactions.
Consider narrowing the condition to only remove when arguments is truly missing/undefined (or when partialJson is present and arguments is not an object), rather than Object.keys(args).length === 0.
Also appears in src/agents/session-transcript-repair.test.ts:205-224 which currently encodes this behavior as expected, but may be asserting an unintended policy.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/session-transcript-repair.ts
Line: 20:28
Comment:
`isIncompleteToolCall` treats `arguments: {}` as incomplete whenever `partialJson` is present. In existing tests in this file you intentionally emit tool calls with `arguments: {}` as the normal shape (e.g. in pairing/guard tests), and in real transcripts some tools legitimately have empty args. With this logic, any such call that also has `partialJson` captured would be dropped and its result orphaned/dropped, potentially deleting valid tool interactions.
Consider narrowing the condition to only remove when `arguments` is truly missing/undefined (or when `partialJson` is present *and* `arguments` is not an object), rather than `Object.keys(args).length === 0`.
Also appears in `src/agents/session-transcript-repair.test.ts:205-224` which currently encodes this behavior as expected, but may be asserting an unintended policy.
How can I resolve this? If you propose a fix, please make it concise.| // Filter out incomplete tool calls | ||
| const filteredContent = content.filter((block) => { | ||
| if (!block || typeof block !== "object") return true; | ||
| const rec = block as ToolCallBlock; | ||
| if (rec.type !== "toolCall" && rec.type !== "toolUse" && rec.type !== "functionCall") { | ||
| return true; | ||
| } | ||
| if (isIncompleteToolCall(rec)) { | ||
| changed = true; | ||
| return false; | ||
| } | ||
| return true; | ||
| }); | ||
|
|
||
| if (filteredContent.length !== content.length) { | ||
| // Content was modified, create new message | ||
| out.push({ ...assistant, content: filteredContent } as AgentMessage); |
There was a problem hiding this comment.
sanitizePartialToolCalls filters out blocks with type: "functionCall" / "toolUse" / "toolCall" based solely on partialJson + args shape, without confirming the block actually has a valid id/name. If there are other block types in transcripts that reuse these type strings (or malformed blocks), this could drop non-tool content unexpectedly.
A safer guard is to only consider it a tool call candidate if id is a non-empty string (and optionally name), matching extractToolCallsFromAssistant’s criteria, before applying the incomplete heuristic.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/session-transcript-repair.ts
Line: 59:75
Comment:
`sanitizePartialToolCalls` filters out blocks with `type: "functionCall"` / `"toolUse"` / `"toolCall"` based solely on `partialJson` + args shape, without confirming the block actually has a valid `id`/`name`. If there are other block types in transcripts that reuse these type strings (or malformed blocks), this could drop non-tool content unexpectedly.
A safer guard is to only consider it a tool call candidate if `id` is a non-empty string (and optionally `name`), matching `extractToolCallsFromAssistant`’s criteria, before applying the incomplete heuristic.
How can I resolve this? If you propose a fix, please make it concise.|
Thanks for putting this together. We’re closing this PR because incomplete/partial tool-call handling has since been improved in later changes on Thanks again for the contribution. |
Problem
When a request is terminated mid-stream (e.g., timeout, user interrupt), tool call blocks may be written to the session with
partialJsonbut incomplete/missingarguments.The existing repair mechanism adds synthetic tool_result entries for these incomplete tool calls, but the Anthropic API rejects them because:
Error message:
Solution
This PR adds
sanitizePartialToolCalls()which:partialJsonbut no completeargumentsextractToolCallsFromAssistant()to skip incomplete calls (so no synthetic results are added)The function is called at the start of
sanitizeToolUseResultPairing()so incomplete tool calls are cleaned before pairing logic runs.Testing
Added tests for:
All 9 tests pass.
Greptile Overview
Greptile Summary
This PR adds a pre-pass to transcript repair that removes assistant tool-call blocks that were interrupted mid-stream (captured as
partialJsonwithout completearguments), preventing downstream pairing logic from creating/dropping tool results in a way that strict providers (Anthropic-compatible) reject. It also updates tool-call extraction to ignore these incomplete calls and adds unit tests covering removal/preservation and an integration scenario where orphaned results are dropped.The change fits into the existing
sanitizeToolUseResultPairingpipeline, which is used by session-history sanitization (e.g. insrc/agents/pi-embedded-runner/google.ts) to ensure tool calls/results are ordered and consistent before sending to model APIs.Confidence Score: 3/5
arguments: {}as incomplete whenpartialJsonexists could delete valid tool calls for tools with empty-arg schemas, which would also cause their results to be dropped. Tightening that heuristic would reduce risk.(2/5) Greptile learns from your feedback when you react with thumbs up/down!