Skip to content

Commit b401680

Browse files
fix(agents): avoid invalid message order after summarize (#9260)
Co-authored-by: Christian Bromann <[email protected]>
1 parent 848ff2a commit b401680

File tree

3 files changed

+75
-1
lines changed

3 files changed

+75
-1
lines changed

.changeset/fancy-breads-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"langchain": patch
3+
---
4+
5+
avoid invalid message order after summarization

libs/langchain/src/agents/middleware/summarization.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,15 @@ function isSafeCutoffPoint(
316316
return true;
317317
}
318318

319+
// Prevent preserved messages from starting with AI message containing tool calls
320+
if (
321+
cutoffIndex < messages.length &&
322+
AIMessage.isInstance(messages[cutoffIndex]) &&
323+
hasToolCalls(messages[cutoffIndex])
324+
) {
325+
return false;
326+
}
327+
319328
const searchStart = Math.max(0, cutoffIndex - SEARCH_RANGE_FOR_TOOL_PAIRS);
320329
const searchEnd = Math.min(
321330
messages.length,

libs/langchain/src/agents/middleware/tests/summarization.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { summarizationMiddleware } from "../summarization.js";
1111
import { countTokensApproximately } from "../utils.js";
1212
import { createAgent } from "../../index.js";
13+
import { hasToolCalls } from "../../utils.js";
1314
import { FakeToolCallingChatModel } from "../../tests/utils.js";
1415

1516
// Mock @langchain/anthropic to test model string usage without requiring the built package
@@ -379,8 +380,67 @@ describe("summarizationMiddleware", () => {
379380
model,
380381
middleware: [middleware],
381382
});
382-
383383
const result = await agent.invoke({ messages: [] });
384384
expect(result.messages.at(-1)?.content).toBe("Mocked response");
385385
});
386+
387+
it("should not start preserved messages with AI message containing tool calls", async () => {
388+
const summarizationModel = createMockSummarizationModel();
389+
const model = createMockMainModel();
390+
391+
const middleware = summarizationMiddleware({
392+
model: summarizationModel as any,
393+
maxTokensBeforeSummary: 50, // Very low threshold to trigger summarization
394+
messagesToKeep: 2, // Keep very few messages to force problematic cutoff
395+
});
396+
397+
const agent = createAgent({
398+
model,
399+
middleware: [middleware],
400+
});
401+
402+
// Create a conversation history that would cause the problematic scenario
403+
// We need messages where an AI message with tool calls would be the first preserved message
404+
// after summarization if the cutoff isn't adjusted properly
405+
const messages = [
406+
new HumanMessage(
407+
`First message with some content to take up tokens. ${"x".repeat(100)}`
408+
),
409+
new AIMessage(`First response. ${"x".repeat(100)}`),
410+
new HumanMessage(
411+
`Second message with more content to build up tokens. ${"x".repeat(
412+
100
413+
)}`
414+
),
415+
new AIMessage(`Second response. ${"x".repeat(100)}`),
416+
// This AI message with tool calls should NOT be the first preserved message
417+
new AIMessage({
418+
content: "Let me search for information.",
419+
tool_calls: [{ id: "call_1", name: "search", args: { query: "test" } }],
420+
}),
421+
new ToolMessage({
422+
content: "Search results",
423+
tool_call_id: "call_1",
424+
}),
425+
new HumanMessage("What did you find?"),
426+
];
427+
428+
const result = await agent.invoke({ messages });
429+
430+
// Verify summarization occurred
431+
expect(result.messages[0]).toBeInstanceOf(SystemMessage);
432+
const systemPrompt = result.messages[0] as SystemMessage;
433+
expect(systemPrompt.content).toContain("## Previous conversation summary:");
434+
435+
// Verify preserved messages don't start with AI(tool calls)
436+
const preservedMessages = result.messages.filter(
437+
(m) => !SystemMessage.isInstance(m)
438+
);
439+
expect(preservedMessages.length).toBeGreaterThan(0);
440+
const firstPreserved = preservedMessages[0];
441+
// The first preserved message should not be an AI message with tool calls
442+
expect(
443+
!(AIMessage.isInstance(firstPreserved) && hasToolCalls(firstPreserved))
444+
).toBe(true);
445+
});
386446
});

0 commit comments

Comments
 (0)