fix(agents): preserve tool call/result pairing in history limiting#2557
fix(agents): preserve tool call/result pairing in history limiting#2557steve-rodri wants to merge 1 commit intoopenclaw:mainfrom
Conversation
Fixes issue where limitHistoryTurns() could orphan tool results by slicing conversation history between an assistant's tool calls and their results. This caused Anthropic API validation errors: 'unexpected tool_use_id found in tool_result blocks... Each tool_result block must have a corresponding tool_use block in the previous message' Changes: - Add hasToolCalls() helper to detect assistants with tool calls - Add countFollowingToolResults() helper to count tool results - Check for toolResult messages at slice boundaries - Adjust slice point to include complete assistant + toolResults sequence The fix ensures tool call/result pairs stay together when limiting history via dmHistoryLimit config. Co-Authored-By: Claude Opus 4.5 <[email protected]>
| function countFollowingToolResults(messages: AgentMessage[], startIndex: number): number { | ||
| let count = 0; | ||
| for (let i = startIndex + 1; i < messages.length; i++) { | ||
| if (messages[i].role === "toolResult") { | ||
| count++; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| return count; | ||
| } |
There was a problem hiding this comment.
countFollowingToolResults() is added but never used in this file, which looks like leftover logic from an earlier iteration. Keeping it unused adds noise and may mislead readers about intended behavior.
If it’s not needed, consider removing it; if it is needed, add the call site so the helper is justified.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/history.ts
Line: 34:44
Comment:
`countFollowingToolResults()` is added but never used in this file, which looks like leftover logic from an earlier iteration. Keeping it unused adds noise and may mislead readers about intended behavior.
If it’s not needed, consider removing it; if it is needed, add the call site so the helper is justified.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| // Only adjust if there's actually a toolResult at the boundary | ||
| if (lastUserIndex > 0 && messages[lastUserIndex - 1].role === "toolResult") { | ||
| // Walk back through consecutive tool result messages to find the assistant | ||
| let j = lastUserIndex - 1; | ||
| while (j >= 0 && messages[j].role === "toolResult") { | ||
| j--; | ||
| } | ||
|
|
||
| // If we found an assistant with tool calls immediately before the tool results, | ||
| // we need to include it to avoid breaking the tool call/result pairing. | ||
| if (j >= 0 && hasToolCalls(messages[j])) { | ||
| sliceIndex = j; | ||
| } | ||
| } |
There was a problem hiding this comment.
The boundary fix only triggers when messages[lastUserIndex - 1] is a toolResult, but tool results can be separated by non-assistant “remainder” entries (see repairToolUseResultPairing() allowing non-toolResult remainder between assistant turns). In that case, slicing at lastUserIndex can still orphan those toolResults (and their tool call) even though the message immediately before the boundary isn’t a toolResult.
This matters if your transcripts can contain e.g. assistant(toolCall) → toolResult → custom/metadata → user, because the current check won’t walk back across that non-toolResult entry to preserve the whole tool span.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/history.ts
Line: 72:85
Comment:
The boundary fix only triggers when `messages[lastUserIndex - 1]` is a `toolResult`, but tool results can be separated by non-assistant “remainder” entries (see `repairToolUseResultPairing()` allowing non-toolResult `remainder` between assistant turns). In that case, slicing at `lastUserIndex` can still orphan those toolResults (and their tool call) even though the message immediately before the boundary isn’t a `toolResult`.
This matters if your transcripts can contain e.g. `assistant(toolCall)` → `toolResult` → `custom/metadata` → `user`, because the current check won’t walk back across that non-toolResult entry to preserve the whole tool span.
How can I resolve this? If you propose a fix, please make it concise.bfc1ccb to
f92900f
Compare
Problem
The
limitHistoryTurns()function insrc/agents/pi-embedded-runner/history.tswas slicing conversation history without considering tool call/result pairing. When limiting history to the last N user turns, it could cut off tool results from their corresponding assistant tool calls, causing Anthropic API validation errors:Solution
Modified
limitHistoryTurns()to be tool-call-aware:hasToolCalls()helper to detect assistants with tool callscountFollowingToolResults()helper to count tool resultstoolResultmessage immediately before the slice pointImpact
This ensures tool call/result pairs stay together when limiting history via
dmHistoryLimitconfig, preventing the Anthropic API validation error.Testing
limithistoryturnstests passGreptile Overview
Greptile Summary
This PR updates
limitHistoryTurns()(src/agents/pi-embedded-runner/history.ts) to avoid slicing conversation history in a way that separates an assistant tool-call turn from its subsequenttoolResultmessages, which can cause strict provider validation errors.The new logic detects when the slice boundary would start immediately after one or more trailing
toolResultmessages and moves the slice start backward to include the corresponding assistant tool-call message as well.Confidence Score: 4/5
(4/5) You can add custom instructions or style guidelines for the agent here!
Context used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)