@@ -792,3 +792,385 @@ describe("summarizeConversation with custom settings", () => {
792792 )
793793 } )
794794} )
795+
796+ describe ( "summarizeConversation with minimum token requirements" , ( ) => {
797+ // Mock ApiHandler
798+ let mockApiHandler : ApiHandler
799+ let mockCondensingApiHandler : ApiHandler
800+ const defaultSystemPrompt = "You are a helpful assistant."
801+ const taskId = "test-task-id"
802+
803+ // Sample messages for testing
804+ const sampleMessages : ApiMessage [ ] = [
805+ { role : "user" , content : "Hello" , ts : 1 } ,
806+ { role : "assistant" , content : "Hi there" , ts : 2 } ,
807+ { role : "user" , content : "How are you?" , ts : 3 } ,
808+ { role : "assistant" , content : "I'm good" , ts : 4 } ,
809+ { role : "user" , content : "What's new?" , ts : 5 } ,
810+ { role : "assistant" , content : "Not much" , ts : 6 } ,
811+ { role : "user" , content : "Tell me more" , ts : 7 } ,
812+ ]
813+
814+ beforeEach ( ( ) => {
815+ // Reset mocks
816+ vi . clearAllMocks ( )
817+
818+ // Setup mock API handler
819+ mockApiHandler = {
820+ createMessage : vi . fn ( ) ,
821+ countTokens : vi . fn ( ) ,
822+ getModel : vi . fn ( ) . mockReturnValue ( {
823+ id : "test-model" ,
824+ info : {
825+ contextWindow : 8000 ,
826+ supportsImages : true ,
827+ supportsComputerUse : true ,
828+ supportsVision : true ,
829+ maxTokens : 4000 ,
830+ supportsPromptCache : true ,
831+ maxCachePoints : 10 ,
832+ minTokensPerCachePoint : 100 ,
833+ cachableFields : [ "system" , "messages" ] ,
834+ } ,
835+ } ) ,
836+ } as unknown as ApiHandler
837+
838+ mockCondensingApiHandler = {
839+ createMessage : vi . fn ( ) ,
840+ countTokens : vi . fn ( ) ,
841+ getModel : vi . fn ( ) . mockReturnValue ( {
842+ id : "condensing-model" ,
843+ info : {
844+ contextWindow : 4000 ,
845+ supportsImages : true ,
846+ supportsComputerUse : false ,
847+ supportsVision : false ,
848+ maxTokens : 2000 ,
849+ supportsPromptCache : false ,
850+ maxCachePoints : 0 ,
851+ minTokensPerCachePoint : 0 ,
852+ cachableFields : [ ] ,
853+ } ,
854+ } ) ,
855+ } as unknown as ApiHandler
856+ } )
857+
858+ it ( "should not expand summary when minimum tokens is not specified" , async ( ) => {
859+ // Setup initial summary stream
860+ const initialStream = ( async function * ( ) {
861+ yield { type : "text" as const , text : "Short summary" }
862+ yield { type : "usage" as const , totalCost : 0.02 , outputTokens : 50 }
863+ } ) ( )
864+
865+ mockApiHandler . createMessage = vi . fn ( ) . mockReturnValueOnce ( initialStream ) as any
866+ mockApiHandler . countTokens = vi . fn ( ) . mockResolvedValue ( 100 ) as any
867+
868+ const result = await summarizeConversation (
869+ sampleMessages ,
870+ mockApiHandler ,
871+ defaultSystemPrompt ,
872+ taskId ,
873+ 1000 , // prevContextTokens
874+ false ,
875+ undefined ,
876+ undefined ,
877+ undefined , // No minimum tokens specified
878+ )
879+
880+ // Should only call createMessage once (no expansion)
881+ expect ( mockApiHandler . createMessage ) . toHaveBeenCalledTimes ( 1 )
882+ expect ( result . summary ) . toBe ( "Short summary" )
883+ expect ( result . newContextTokens ) . toBe ( 150 ) // 50 output + 100 counted
884+ expect ( result . error ) . toBeUndefined ( )
885+ } )
886+
887+ it ( "should not expand summary when current tokens already meet minimum requirement" , async ( ) => {
888+ // Setup initial summary stream with enough tokens
889+ const initialStream = ( async function * ( ) {
890+ yield { type : "text" as const , text : "This is a longer summary with more content" }
891+ yield { type : "usage" as const , totalCost : 0.05 , outputTokens : 300 }
892+ } ) ( )
893+
894+ mockApiHandler . createMessage = vi . fn ( ) . mockReturnValueOnce ( initialStream ) as any
895+ mockApiHandler . countTokens = vi . fn ( ) . mockResolvedValue ( 250 ) as any
896+
897+ const result = await summarizeConversation (
898+ sampleMessages ,
899+ mockApiHandler ,
900+ defaultSystemPrompt ,
901+ taskId ,
902+ 1000 , // prevContextTokens
903+ false ,
904+ undefined ,
905+ undefined ,
906+ 500 , // minimumCondenseTokens - already met by 550 total
907+ )
908+
909+ // Should only call createMessage once (no expansion needed)
910+ expect ( mockApiHandler . createMessage ) . toHaveBeenCalledTimes ( 1 )
911+ expect ( result . summary ) . toBe ( "This is a longer summary with more content" )
912+ expect ( result . newContextTokens ) . toBe ( 550 ) // 300 output + 250 counted
913+ expect ( result . error ) . toBeUndefined ( )
914+ } )
915+
916+ it ( "should expand summary when below minimum token requirement" , async ( ) => {
917+ // Setup initial summary stream with too few tokens
918+ const initialStream = ( async function * ( ) {
919+ yield { type : "text" as const , text : "Short summary" }
920+ yield { type : "usage" as const , totalCost : 0.02 , outputTokens : 50 }
921+ } ) ( )
922+
923+ // Setup expansion stream
924+ const expansionStream = ( async function * ( ) {
925+ yield {
926+ type : "text" as const ,
927+ text : "This is a much more detailed and expanded summary with lots of additional context and information" ,
928+ }
929+ yield { type : "usage" as const , totalCost : 0.08 , outputTokens : 400 }
930+ } ) ( )
931+
932+ mockApiHandler . createMessage = vi
933+ . fn ( )
934+ . mockReturnValueOnce ( initialStream )
935+ . mockReturnValueOnce ( expansionStream ) as any
936+
937+ mockApiHandler . countTokens = vi
938+ . fn ( )
939+ . mockResolvedValueOnce ( 100 ) // First count after initial summary
940+ . mockResolvedValueOnce ( 150 ) // Count after first expansion attempt
941+ . mockResolvedValueOnce ( 150 ) as any // Count after second expansion attempt (final)
942+
943+ const result = await summarizeConversation (
944+ sampleMessages ,
945+ mockApiHandler ,
946+ defaultSystemPrompt ,
947+ taskId ,
948+ 1000 , // prevContextTokens
949+ false ,
950+ undefined ,
951+ undefined ,
952+ 500 , // minimumCondenseTokens - requires expansion
953+ )
954+
955+ // Should call createMessage three times (initial + 2 expansions due to mock setup)
956+ expect ( mockApiHandler . createMessage ) . toHaveBeenCalledTimes ( 3 )
957+
958+ // Check the expansion request includes the expansion prompt
959+ const secondCall = ( mockApiHandler . createMessage as Mock ) . mock . calls [ 1 ]
960+ const expansionMessages = secondCall [ 1 ]
961+ const lastMessage = expansionMessages [ expansionMessages . length - 1 ]
962+ expect ( lastMessage . content ) . toContain ( "The current summary has" )
963+ expect ( lastMessage . content ) . toContain ( "tokens, but we need at least" )
964+
965+ expect ( result . summary ) . toBe (
966+ "This is a much more detailed and expanded summary with lots of additional context and information" ,
967+ )
968+ expect ( result . newContextTokens ) . toBe ( 150 ) // Final count from mock
969+ expect ( result . cost ) . toBe ( 0.1 ) // 0.02 + 0.08
970+ expect ( result . error ) . toBeUndefined ( )
971+ } )
972+
973+ it ( "should use condensing API handler for expansion when provided" , async ( ) => {
974+ // Setup initial summary stream with too few tokens
975+ const initialStream = ( async function * ( ) {
976+ yield { type : "text" as const , text : "Short summary" }
977+ yield { type : "usage" as const , totalCost : 0.02 , outputTokens : 50 }
978+ } ) ( )
979+
980+ // Setup expansion stream from condensing handler
981+ const expansionStream = ( async function * ( ) {
982+ yield { type : "text" as const , text : "Expanded summary from condensing handler" }
983+ yield { type : "usage" as const , totalCost : 0.06 , outputTokens : 350 }
984+ } ) ( )
985+
986+ mockCondensingApiHandler . createMessage = vi
987+ . fn ( )
988+ . mockReturnValueOnce ( initialStream )
989+ . mockReturnValueOnce ( expansionStream ) as any
990+
991+ mockApiHandler . countTokens = vi
992+ . fn ( )
993+ . mockResolvedValueOnce ( 100 ) // First count
994+ . mockResolvedValueOnce ( 200 ) // After first expansion
995+ . mockResolvedValueOnce ( 200 ) as any // After second expansion (final)
996+
997+ const result = await summarizeConversation (
998+ sampleMessages ,
999+ mockApiHandler ,
1000+ defaultSystemPrompt ,
1001+ taskId ,
1002+ 1000 , // prevContextTokens
1003+ false ,
1004+ "Custom prompt" ,
1005+ mockCondensingApiHandler ,
1006+ 500 , // minimumCondenseTokens
1007+ )
1008+
1009+ // Should use condensing handler for all calls (initial + expansions)
1010+ expect ( mockCondensingApiHandler . createMessage ) . toHaveBeenCalledTimes ( 3 )
1011+ expect ( mockApiHandler . createMessage ) . not . toHaveBeenCalled ( )
1012+
1013+ expect ( result . summary ) . toBe ( "Expanded summary from condensing handler" )
1014+ expect ( result . newContextTokens ) . toBe ( 200 ) // Final count from mock
1015+ expect ( result . cost ) . toBe ( 0.08 ) // 0.02 + 0.06
1016+ expect ( result . error ) . toBeUndefined ( )
1017+ } )
1018+
1019+ it ( "should stop expansion after MAX_ITERATIONS to prevent infinite loops" , async ( ) => {
1020+ // Setup streams that always return insufficient tokens
1021+ const createSmallStream = ( ) =>
1022+ ( async function * ( ) {
1023+ yield { type : "text" as const , text : "Still too short" }
1024+ yield { type : "usage" as const , totalCost : 0.01 , outputTokens : 30 }
1025+ } ) ( )
1026+
1027+ let callCount = 0
1028+ mockApiHandler . createMessage = vi . fn ( ) . mockImplementation ( ( ) => {
1029+ callCount ++
1030+ return createSmallStream ( )
1031+ } ) as any
1032+
1033+ // Always return low token count
1034+ mockApiHandler . countTokens = vi . fn ( ) . mockResolvedValue ( 50 ) as any
1035+
1036+ const result = await summarizeConversation (
1037+ sampleMessages ,
1038+ mockApiHandler ,
1039+ defaultSystemPrompt ,
1040+ taskId ,
1041+ 2000 , // prevContextTokens
1042+ false ,
1043+ undefined ,
1044+ undefined ,
1045+ 1000 , // minimumCondenseTokens - impossible to reach
1046+ )
1047+
1048+ // Should stop after MAX_ITERATIONS (5) + 1 initial call = 6 total
1049+ expect ( mockApiHandler . createMessage ) . toHaveBeenCalledTimes ( 6 )
1050+ expect ( result . summary ) . toBe ( "Still too short" )
1051+ expect ( result . error ) . toBeUndefined ( )
1052+ } )
1053+
1054+ it ( "should revert to previous summary if expansion exceeds context limit" , async ( ) => {
1055+ // Setup initial summary stream
1056+ const initialStream = ( async function * ( ) {
1057+ yield { type : "text" as const , text : "Initial summary" }
1058+ yield { type : "usage" as const , totalCost : 0.02 , outputTokens : 100 }
1059+ } ) ( )
1060+
1061+ // Setup expansion stream that's too large
1062+ const expansionStream = ( async function * ( ) {
1063+ yield { type : "text" as const , text : "Extremely long expanded summary that exceeds context" }
1064+ yield { type : "usage" as const , totalCost : 0.1 , outputTokens : 800 }
1065+ } ) ( )
1066+
1067+ mockApiHandler . createMessage = vi
1068+ . fn ( )
1069+ . mockReturnValueOnce ( initialStream )
1070+ . mockReturnValueOnce ( expansionStream ) as any
1071+
1072+ mockApiHandler . countTokens = vi
1073+ . fn ( )
1074+ . mockResolvedValueOnce ( 150 ) // First count
1075+ . mockResolvedValueOnce ( 500 ) as any // After expansion - too large!
1076+
1077+ const result = await summarizeConversation (
1078+ sampleMessages ,
1079+ mockApiHandler ,
1080+ defaultSystemPrompt ,
1081+ taskId ,
1082+ 400 , // prevContextTokens - will be exceeded
1083+ false ,
1084+ undefined ,
1085+ undefined ,
1086+ 300 , // minimumCondenseTokens
1087+ )
1088+
1089+ // Should revert to initial summary
1090+ expect ( result . summary ) . toBe ( "Initial summary" )
1091+ expect ( result . newContextTokens ) . toBe ( 250 ) // 100 output + 150 counted (initial)
1092+ expect ( result . cost ) . toBeCloseTo ( 0.02 , 5 ) // Only initial cost, expansion cost excluded
1093+ expect ( result . error ) . toBeUndefined ( )
1094+ } )
1095+
1096+ it ( "should handle empty expansion response gracefully" , async ( ) => {
1097+ // Setup initial summary stream
1098+ const initialStream = ( async function * ( ) {
1099+ yield { type : "text" as const , text : "Initial summary" }
1100+ yield { type : "usage" as const , totalCost : 0.02 , outputTokens : 100 }
1101+ } ) ( )
1102+
1103+ // Setup empty expansion stream
1104+ const emptyExpansionStream = ( async function * ( ) {
1105+ yield { type : "text" as const , text : "" }
1106+ yield { type : "usage" as const , totalCost : 0.01 , outputTokens : 0 }
1107+ } ) ( )
1108+
1109+ mockApiHandler . createMessage = vi
1110+ . fn ( )
1111+ . mockReturnValueOnce ( initialStream )
1112+ . mockReturnValueOnce ( emptyExpansionStream ) as any
1113+
1114+ mockApiHandler . countTokens = vi . fn ( ) . mockResolvedValue ( 150 ) as any
1115+
1116+ const result = await summarizeConversation (
1117+ sampleMessages ,
1118+ mockApiHandler ,
1119+ defaultSystemPrompt ,
1120+ taskId ,
1121+ 1000 , // prevContextTokens
1122+ false ,
1123+ undefined ,
1124+ undefined ,
1125+ 500 , // minimumCondenseTokens - requires expansion but gets empty response
1126+ )
1127+
1128+ // Should keep initial summary when expansion fails
1129+ expect ( result . summary ) . toBe ( "Initial summary" )
1130+ expect ( result . newContextTokens ) . toBe ( 250 ) // 100 output + 150 counted
1131+ expect ( result . cost ) . toBe ( 0.02 ) // Only initial cost since expansion produced empty result
1132+ expect ( result . error ) . toBeUndefined ( )
1133+ } )
1134+
1135+ it ( "should use custom prompt for expansion when provided" , async ( ) => {
1136+ const customPrompt = "Custom summarization instructions"
1137+
1138+ // Setup initial summary stream with too few tokens
1139+ const initialStream = ( async function * ( ) {
1140+ yield { type : "text" as const , text : "Short summary" }
1141+ yield { type : "usage" as const , totalCost : 0.02 , outputTokens : 50 }
1142+ } ) ( )
1143+
1144+ // Setup expansion stream
1145+ const expansionStream = ( async function * ( ) {
1146+ yield { type : "text" as const , text : "Expanded with custom prompt" }
1147+ yield { type : "usage" as const , totalCost : 0.05 , outputTokens : 300 }
1148+ } ) ( )
1149+
1150+ mockApiHandler . createMessage = vi
1151+ . fn ( )
1152+ . mockReturnValueOnce ( initialStream )
1153+ . mockReturnValueOnce ( expansionStream ) as any
1154+
1155+ mockApiHandler . countTokens = vi . fn ( ) . mockResolvedValueOnce ( 100 ) . mockResolvedValueOnce ( 200 ) as any
1156+
1157+ const result = await summarizeConversation (
1158+ sampleMessages ,
1159+ mockApiHandler ,
1160+ defaultSystemPrompt ,
1161+ taskId ,
1162+ 1000 ,
1163+ false ,
1164+ customPrompt , // Custom prompt provided
1165+ undefined ,
1166+ 400 , // minimumCondenseTokens
1167+ )
1168+
1169+ // Check that custom prompt was used in expansion
1170+ const expansionCall = ( mockApiHandler . createMessage as Mock ) . mock . calls [ 1 ]
1171+ expect ( expansionCall [ 0 ] ) . toBe ( customPrompt )
1172+
1173+ expect ( result . summary ) . toBe ( "Expanded with custom prompt" )
1174+ expect ( result . error ) . toBeUndefined ( )
1175+ } )
1176+ } )
0 commit comments