@@ -10,6 +10,7 @@ import {
1010import { summarizationMiddleware } from "../summarization.js" ;
1111import { countTokensApproximately } from "../utils.js" ;
1212import { createAgent } from "../../index.js" ;
13+ import { hasToolCalls } from "../../utils.js" ;
1314import { 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