fix(core): drop orphaned ToolMessages in trim_messages with strategy='last'#35313
Open
Nilesh Hadalgi (nileshhadalgi016) wants to merge 1 commit intolangchain-ai:masterfrom
Conversation
…'last' When trim_messages uses strategy='last', it can return ToolMessages at the start of the result without their corresponding AIMessage (which contains the tool_calls). This produces invalid message history that causes LLM API errors. Add _drop_orphaned_tool_messages() helper that post-processes the trimmed result by scanning left-to-right, collecting tool_call IDs from AIMessages, and removing any ToolMessage whose tool_call_id doesn't match a preceding AIMessage. This only removes messages (never adds), so token limits remain respected. Fixes langchain-ai#33245
Copilot started reviewing on behalf of
Nilesh Hadalgi (nileshhadalgi016)
February 18, 2026 18:09
View session
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug in trim_messages where using strategy="last" could return invalid message histories containing orphaned ToolMessage objects without their corresponding AIMessage (which contains the tool_calls). This caused LLM API errors requiring tool messages to follow AI messages with tool calls.
Changes:
- Added
_drop_orphaned_tool_messages()helper function to identify and remove ToolMessages whose AIMessage was trimmed away - Integrated the helper into
_last_max_tokens()after re-reversing messages but before adding back system messages - Added 3 comprehensive test cases covering the bug scenario, valid tool messages, and multiple tool calls
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| libs/core/langchain_core/messages/utils.py | Adds _drop_orphaned_tool_messages() helper and integrates it into _last_max_tokens() to filter out orphaned tool messages |
| libs/core/tests/unit_tests/messages/test_utils.py | Adds 3 test cases covering orphaned tool message dropping, valid tool message preservation, and multiple tool calls scenarios |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #33245
When
trim_messagesusesstrategy="last", it can returnToolMessages at the start of the result without their correspondingAIMessage(which contains thetool_calls). This produces invalid message history that causes LLM API errors like:Root Cause
_last_max_tokensworks by reversing the messages, calling_first_max_tokens, and re-reversing. When the token budget cuts off anAIMessagethat hastool_calls, the correspondingToolMessage(s) may still be included in the result — but now appear without their parentAIMessage, making the message history invalid.Fix
Added a
_drop_orphaned_tool_messages()helper that post-processes the trimmed result:tool_callIDs fromAIMessagesToolMessagewhosetool_call_iddoes not match a precedingAIMessageThis approach:
_last_max_tokens, after the result is re-reversedTests Added
test_trim_messages_last_drops_orphaned_tool_messages— reproduces the exact bug from the issuetest_trim_messages_last_keeps_valid_tool_messages— ensures valid tool message pairs are preservedtest_trim_messages_last_multiple_tool_calls— tests AIMessage with multiple tool_calls where both ToolMessages should be droppedAll 157 existing tests in
test_utils.pycontinue to pass with zero regressions.