Skip to content

Comments

fix(agents): preserve tool call/result pairing in history limiting#2557

Open
steve-rodri wants to merge 1 commit intoopenclaw:mainfrom
steve-rodri:fix/tool-result-pairing-history
Open

fix(agents): preserve tool call/result pairing in history limiting#2557
steve-rodri wants to merge 1 commit intoopenclaw:mainfrom
steve-rodri:fix/tool-result-pairing-history

Conversation

@steve-rodri
Copy link

@steve-rodri steve-rodri commented Jan 27, 2026

Problem

The limitHistoryTurns() function in src/agents/pi-embedded-runner/history.ts was 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:

LLM request rejected: messages.266.content.1: unexpected tool_use_id found in tool_result blocks: toolu_01K7ZFJdfEDqTsKuikvRRfmi. Each tool_result block must have a corresponding tool_use block in the previous message.

Solution

Modified limitHistoryTurns() to be tool-call-aware:

  1. Added hasToolCalls() helper to detect assistants with tool calls
  2. Added countFollowingToolResults() helper to count tool results
  3. Before slicing at a boundary, check if there's a toolResult message immediately before the slice point
  4. If found, walk back through consecutive tool results to find the corresponding assistant
  5. Adjust the slice point to include the complete assistant + tool results sequence

Impact

This ensures tool call/result pairs stay together when limiting history via dmHistoryLimit config, preventing the Anthropic API validation error.

Testing

  • Existing limithistoryturns tests pass
  • Fix verified to resolve the reported error

Greptile 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 subsequent toolResult messages, which can cause strict provider validation errors.

The new logic detects when the slice boundary would start immediately after one or more trailing toolResult messages and moves the slice start backward to include the corresponding assistant tool-call message as well.

Confidence Score: 4/5

  • This PR is likely safe to merge and should reduce tool pairing validation errors, with minor edge-case coverage gaps.
  • The change is localized to history slicing and doesn’t affect core execution paths beyond deciding the slice start index. Main risk is behavioral mismatch in uncommon transcript shapes (e.g., toolResult blocks not immediately adjacent to the slice boundary, or tool call detection not matching all variants), plus a small amount of dead code left behind.
  • src/agents/pi-embedded-runner/history.ts

(4/5) You can add custom instructions or style guidelines for the agent here!

Context used:

  • Context from dashboard - CLAUDE.md (source)
  • Context from dashboard - AGENTS.md (source)

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]>
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 +34 to +44
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +72 to +85
// 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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

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)toolResultcustom/metadatauser, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant