Skip to content

fix: sanitize incomplete tool calls with partialJson#2253

Closed
Zedit42 wants to merge 1 commit intoopenclaw:mainfrom
Zedit42:fix/orphan-tool-result-repair
Closed

fix: sanitize incomplete tool calls with partialJson#2253
Zedit42 wants to merge 1 commit intoopenclaw:mainfrom
Zedit42:fix/orphan-tool-result-repair

Conversation

@Zedit42
Copy link

@Zedit42 Zedit42 commented Jan 26, 2026

Problem

When a request is terminated mid-stream (e.g., timeout, user interrupt), tool call blocks may be written to the session with partialJson but incomplete/missing arguments.

The existing repair mechanism adds synthetic tool_result entries for these incomplete tool calls, but the Anthropic API rejects them because:

  1. The tool_use block exists but is malformed (partialJson without complete args)
  2. The orphaned tool_result references a tool_use_id that doesn't properly exist

Error message:

messages.36.content.5: unexpected tool_use_id found in tool_result blocks: toolu_xxx. 
Each tool_result block must have a corresponding tool_use block in the previous message.

Solution

This PR adds sanitizePartialToolCalls() which:

  • Detects tool call blocks with partialJson but no complete arguments
  • Removes these incomplete tool calls from assistant message content
  • Updates extractToolCallsFromAssistant() 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:

  • Removing tool calls with partialJson but no arguments
  • Removing tool calls with partialJson and empty arguments
  • Preserving complete tool calls (with or without partialJson)
  • Integration: incomplete tool calls removed AND their orphaned results dropped

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 partialJson without complete arguments), 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 sanitizeToolUseResultPairing pipeline, which is used by session-history sanitization (e.g. in src/agents/pi-embedded-runner/google.ts) to ensure tool calls/results are ordered and consistent before sending to model APIs.

Confidence Score: 3/5

  • Reasonably safe to merge, but the new partial-tool-call heuristic may drop valid tool calls in some real transcripts.
  • The overall approach (remove interrupted tool calls before pairing) is coherent and covered by new tests, and it integrates cleanly into the existing sanitization pipeline. Main concern is the definition of "incomplete": treating arguments: {} as incomplete when partialJson exists 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.
  • src/agents/session-transcript-repair.ts

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

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'
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +20 to +28
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +59 to +75
// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@vignesh07
Copy link
Contributor

Thanks for putting this together.

We’re closing this PR because incomplete/partial tool-call handling has since been improved in later changes on main, so this patch is now superseded.

Thanks again for the contribution.

@vignesh07 vignesh07 closed this Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants