Skip to content

Conversation

@mdrxy
Copy link
Member

@mdrxy mdrxy commented Jan 5, 2026

Fixes #34282

Before: When using agents with tools (like file reading, web search, etc.), the conversation looks like this:

[User]     "Read these 10 files and summarize them"
[AI]       "I'll read all 10 files" + [tool_call: read_file x 10]
[Tool]     "Contents of file1.txt..."
[Tool]     "Contents of file2.txt..."
[Tool]     "Contents of file3.txt..."
... (7 more tool responses)

When the conversation gets too long, SummarizationMiddleware kicks in to compress older messages. The problem was:

If you asked to keep the last 6 messages, you'd get:

[Summary]  "Here's what happened before..."
[Tool]     "Contents of file5.txt..."
[Tool]     "Contents of file6.txt..."
[Tool]     "Contents of file7.txt..."
[Tool]     "Contents of file8.txt..."
[Tool]     "Contents of file9.txt..."
[Tool]     "Contents of file10.txt..."

The AI's original request to read the files ([AI] message with tool_calls) was summarized away, but the tool responses remained. This caused the error:

Error code: 400 - "No tool call found for function call output with call_id..."

Many APIs require that every tool response has a matching tool request. Without the AI message, the tool responses are "orphaned."

The fix

Now when the cutoff lands on tool messages, we move backward to include the AI message that requested those tools:

Same scenario, keeping last 6 messages:

[Summary]  "Here's what happened before..."
[AI]       "I'll read all 10 files" + [tool_call: read_file x 10]
[Tool]     "Contents of file1.txt..."
[Tool]     "Contents of file2.txt..."
... (all 10 tool responses)

The AI message is preserved along with its tool responses, keeping them paired together.

Practical examples

Example 1: Parallel tool calls

Scenario: Agent reads 10 files in parallel, summarization triggers (see above)

Example 2: Mixed conversation

Scenario: User asks question, AI uses tools, user says thanks

[User]     "What's the weather?"
[AI]       "Let me check" + [tool_call: get_weather]
[Tool]     "72F and sunny"
[AI]       "It's 72F and sunny!"
[User]     "Thanks!"

Keeping last 2 messages:

Before (Bug) After (Fix)
Only [User] "Thanks!" kept [AI] + [Tool] + [AI] + [User] all kept
Lost the weather info Tool pair preserved with response

Example 3: Multiple tool sequences

[User]     "Search for X"
[AI]       [tool_call: search]
[Tool]     "Results for X"
[User]     "Now search for Y"
[AI]       [tool_call: search]
[Tool]     "Results for Y"
[User]     "Great!"

Keeping last 3 messages: If cutoff lands on [Tool] "Results for Y", we now include [AI] [tool_call: search] to keep the pair together.

@github-actions github-actions bot added langchain `langchain` package issues & PRs fix For PRs that implement a fix and removed fix For PRs that implement a fix labels Jan 5, 2026
christian-bromann added a commit to langchain-ai/langchainjs that referenced this pull request Jan 6, 2026
Port of langchain-ai/langchain#34609

When SummarizationMiddleware triggers and the cutoff lands on a ToolMessage,
the middleware now searches backward for the AIMessage with matching tool_calls
and includes it in the preserved messages. This prevents "orphaned" tool
responses that cause API errors like:

  "No tool call found for function call output with call_id..."
Copy link
Collaborator

@ccurme ccurme left a comment

Choose a reason for hiding this comment

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

What is the difference between this and the logic we used to have here: https://github.com/langchain-ai/langchain/pull/34195/files

while cutoff_index < len(messages) and isinstance(messages[cutoff_index], ToolMessage):
cutoff_index += 1
return cutoff_index
if cutoff_index >= len(messages) or not isinstance(messages[cutoff_index], ToolMessage):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we also don't want to land on an AIMessage with tool calls, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

The previous logic advanced forward past ToolMessage objects (aggressive summarization). This approach takes the opposite approach, searching backward to include the AIMessage requesting the tools (in other words, preserve more context for the sake of atomicity)

If the cutoff lands on an AIMessage with tool_calls, the corresponding ToolMessage responses would be in the summarized portion, creating the reverse orphaning problem — tool call requests without their responses. Landing on an AIMessage with tool_calls is safe because the ToolMessage objects come after it and will be preserved together.

Base automatically changed from mdrxy/fix-summarization to master January 7, 2026 00:05
@mdrxy mdrxy requested a review from eyurtsev as a code owner January 7, 2026 00:05
@github-actions github-actions bot added core `langchain-core` package issues & PRs fix For PRs that implement a fix and removed fix For PRs that implement a fix labels Jan 7, 2026
Copy link
Collaborator

@ccurme ccurme left a comment

Choose a reason for hiding this comment

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

Are we changing the convention (summarizing less vs. more), or fixing an error mode?

If we are fixing, could you add a test that fails on master and passes here (or identify that test if it exists now)? Thanks.

assert middleware._find_safe_cutoff_point(messages, len(messages) + 5) == len(messages) + 5


def test_summarization_middleware_find_safe_cutoff_point_orphan_tool() -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This test passes on master

Copy link
Member Author

Choose a reason for hiding this comment

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

We're changing the convention (summarizing less vs. more)

I've added a test that fails on master

@mdrxy mdrxy merged commit 2b6911d into master Jan 8, 2026
50 checks passed
@mdrxy mdrxy deleted the mdrxy/fix-cutoff-summarization branch January 8, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core `langchain-core` package issues & PRs fix For PRs that implement a fix langchain `langchain` package issues & PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

After SummarizationMiddleware is triggered, agent can randomly triggers 400 - No tool call found

3 participants