Skip to content

Commit 3ac0647

Browse files
committed
fix(agents): avoid invalid message order after summarize
1 parent a4a60d2 commit 3ac0647

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,15 @@ function isSafeCutoffPoint(
304304
return true;
305305
}
306306

307+
// Prevent preserved messages from starting with AI message containing tool calls
308+
if (
309+
cutoffIndex < messages.length &&
310+
AIMessage.isInstance(messages[cutoffIndex]) &&
311+
hasToolCalls(messages[cutoffIndex])
312+
) {
313+
return false;
314+
}
315+
307316
const searchStart = Math.max(0, cutoffIndex - SEARCH_RANGE_FOR_TOOL_PAIRS);
308317
const searchEnd = Math.min(
309318
messages.length,

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { summarizationMiddleware } from "../summarization.js";
1010
import { countTokensApproximately } from "../utils.js";
1111
import { createAgent } from "../../index.js";
12+
import { hasToolCalls } from "../../utils.js";
1213
import { FakeToolCallingChatModel } from "../../tests/utils.js";
1314

1415
describe("summarizationMiddleware", () => {
@@ -335,4 +336,58 @@ describe("summarizationMiddleware", () => {
335336
expect(nonSystemMessages.length).toBeGreaterThanOrEqual(messagesToKeep);
336337
expect(nonSystemMessages.length).toBeLessThanOrEqual(messagesToKeep + 3); // Some buffer for safety
337338
});
339+
340+
it("should not start preserved messages with AI message containing tool calls", async () => {
341+
const summarizationModel = createMockSummarizationModel();
342+
const model = createMockMainModel();
343+
344+
const middleware = summarizationMiddleware({
345+
model: summarizationModel as any,
346+
maxTokensBeforeSummary: 50, // Very low threshold to trigger summarization
347+
messagesToKeep: 2, // Keep very few messages to force problematic cutoff
348+
});
349+
350+
const agent = createAgent({
351+
model,
352+
middleware: [middleware],
353+
});
354+
355+
// Create a conversation history that would cause the problematic scenario
356+
// We need messages where an AI message with tool calls would be the first preserved message
357+
// after summarization if the cutoff isn't adjusted properly
358+
const messages = [
359+
new HumanMessage(`First message with some content to take up tokens. ${"x".repeat(100)}`),
360+
new AIMessage(`First response. ${"x".repeat(100)}`),
361+
new HumanMessage(`Second message with more content to build up tokens. ${"x".repeat(100)}`),
362+
new AIMessage(`Second response. ${"x".repeat(100)}`),
363+
// This AI message with tool calls should NOT be the first preserved message
364+
new AIMessage({
365+
content: "Let me search for information.",
366+
tool_calls: [{ id: "call_1", name: "search", args: { query: "test" } }],
367+
}),
368+
new ToolMessage({
369+
content: "Search results",
370+
tool_call_id: "call_1",
371+
}),
372+
new HumanMessage("What did you find?"),
373+
];
374+
375+
const result = await agent.invoke({ messages });
376+
377+
// Verify summarization occurred
378+
expect(result.messages[0]).toBeInstanceOf(SystemMessage);
379+
const systemPrompt = result.messages[0] as SystemMessage;
380+
expect(systemPrompt.content).toContain("## Previous conversation summary:");
381+
382+
// Verify preserved messages don't start with AI(tool calls)
383+
const preservedMessages = result.messages.filter(
384+
(m) => !SystemMessage.isInstance(m)
385+
);
386+
expect(preservedMessages.length).toBeGreaterThan(0);
387+
const firstPreserved = preservedMessages[0];
388+
// The first preserved message should not be an AI message with tool calls
389+
expect(
390+
!(AIMessage.isInstance(firstPreserved) && hasToolCalls(firstPreserved))
391+
).toBe(true);
392+
});
338393
});

0 commit comments

Comments
 (0)