@@ -110,4 +110,163 @@ describe("ConversationCompressor", () => {
110110 expect ( result . compressedItems . length ) . toBe ( 2 ) ;
111111 } ) ;
112112 } ) ;
113+
114+ describe ( "token watermark compression" , ( ) => {
115+ it ( "should trigger compression when lastPromptTokens exceeds watermark" , ( ) => {
116+ const watermarkCompressor = new ConversationCompressor ( mockModel , {
117+ tokenWatermark : 150000 ,
118+ } ) ;
119+
120+ expect ( watermarkCompressor . shouldCompress ( 10 , 200000 ) ) . toBe ( true ) ;
121+ expect ( watermarkCompressor . shouldCompress ( 10 , 100000 ) ) . toBe ( false ) ;
122+ } ) ;
123+
124+ it ( "should fallback to item count when tokenWatermark not set" , ( ) => {
125+ const compressor = new ConversationCompressor ( mockModel , {
126+ summarizeAfterItems : 20 ,
127+ } ) ;
128+
129+ expect ( compressor . shouldCompress ( 25 , 100000 ) ) . toBe ( true ) ;
130+ expect ( compressor . shouldCompress ( 15 , 200000 ) ) . toBe ( false ) ;
131+ } ) ;
132+
133+ it ( "should fallback to item count when lastPromptTokens not provided" , ( ) => {
134+ const watermarkCompressor = new ConversationCompressor ( mockModel , {
135+ tokenWatermark : 150000 ,
136+ summarizeAfterItems : 20 ,
137+ } ) ;
138+
139+ expect ( watermarkCompressor . shouldCompress ( 25 ) ) . toBe ( true ) ;
140+ expect ( watermarkCompressor . shouldCompress ( 15 ) ) . toBe ( false ) ;
141+ } ) ;
142+ } ) ;
143+
144+ describe ( "protectRecentMessages" , ( ) => {
145+ it ( "should protect recent N message items" , async ( ) => {
146+ const protectCompressor = new ConversationCompressor ( mockModel , {
147+ summarizeAfterItems : 5 ,
148+ protectRecentMessages : 2 ,
149+ } ) ;
150+
151+ const items : AgentInputItem [ ] = [
152+ createUserMessage ( "Message 1" ) ,
153+ createAssistantMessage ( "Response 1" ) ,
154+ createUserMessage ( "Message 2" ) ,
155+ createAssistantMessage ( "Response 2" ) ,
156+ createUserMessage ( "Message 3" ) ,
157+ createAssistantMessage ( "Response 3" ) ,
158+ ] ;
159+
160+ const result = await protectCompressor . compressItems ( items ) ;
161+
162+ // Should protect the last 2 messages (assistant 2 and assistant 3)
163+ // and summarize everything before that
164+ expect ( result . compressedItems . length ) . toBe ( 2 ) ;
165+ expect ( result . summary ) . toBe ( "Compressed summary" ) ;
166+ } ) ;
167+
168+ it ( "should protect tool call/result pairs with recent messages" , async ( ) => {
169+ const protectCompressor = new ConversationCompressor ( mockModel , {
170+ summarizeAfterItems : 5 ,
171+ protectRecentMessages : 1 ,
172+ } ) ;
173+
174+ const items : AgentInputItem [ ] = [
175+ createUserMessage ( "Message 1" ) ,
176+ createAssistantMessage ( "Response 1" ) ,
177+ createUserMessage ( "Use a tool" ) ,
178+ createAssistantMessage ( "I'll use the tool" ) ,
179+ {
180+ type : "function_call" ,
181+ callId : "call_123" ,
182+ name : "testTool" ,
183+ arguments : "{}" ,
184+ } as AgentInputItem ,
185+ {
186+ type : "function_call_result" ,
187+ callId : "call_123" ,
188+ name : "testTool" ,
189+ output : "result" ,
190+ } as AgentInputItem ,
191+ createAssistantMessage ( "Tool result received" ) ,
192+ ] ;
193+
194+ const result = await protectCompressor . compressItems ( items ) ;
195+
196+ // Should protect the last assistant message AND the tool call/result pair
197+ // and the assistant message before the tool calls
198+ expect ( result . compressedItems . length ) . toBeGreaterThanOrEqual ( 4 ) ;
199+ expect ( result . summary ) . toBe ( "Compressed summary" ) ;
200+ } ) ;
201+
202+ it ( "should include assistant message preceding tool calls" , async ( ) => {
203+ const protectCompressor = new ConversationCompressor ( mockModel , {
204+ summarizeAfterItems : 5 ,
205+ protectRecentMessages : 1 ,
206+ } ) ;
207+
208+ const items : AgentInputItem [ ] = [
209+ createUserMessage ( "Message 1" ) ,
210+ createAssistantMessage ( "Response 1" ) ,
211+ createUserMessage ( "Message 2" ) ,
212+ createAssistantMessage ( "I'll call a tool" ) ,
213+ {
214+ type : "function_call" ,
215+ callId : "call_456" ,
216+ name : "testTool" ,
217+ arguments : "{}" ,
218+ } as AgentInputItem ,
219+ {
220+ type : "function_call_result" ,
221+ callId : "call_456" ,
222+ name : "testTool" ,
223+ output : "result" ,
224+ } as AgentInputItem ,
225+ createAssistantMessage ( "Final response" ) ,
226+ ] ;
227+
228+ const result = await protectCompressor . compressItems ( items ) ;
229+
230+ // Should protect the final assistant message, tool items,
231+ // and the assistant message that initiated the tool call
232+ const finalAssistant = result . compressedItems . find (
233+ ( item ) =>
234+ item . type === "message" &&
235+ item . role === "assistant" &&
236+ ( item . content as any ) ?. [ 0 ] ?. text === "Final response" ,
237+ ) ;
238+ const toolCall = result . compressedItems . find (
239+ ( item ) => item . type === "function_call" ,
240+ ) ;
241+ const precedingAssistant = result . compressedItems . find (
242+ ( item ) =>
243+ item . type === "message" &&
244+ item . role === "assistant" &&
245+ ( item . content as any ) ?. [ 0 ] ?. text === "I'll call a tool" ,
246+ ) ;
247+
248+ expect ( finalAssistant ) . toBeDefined ( ) ;
249+ expect ( toolCall ) . toBeDefined ( ) ;
250+ expect ( precedingAssistant ) . toBeDefined ( ) ;
251+ } ) ;
252+
253+ it ( "should handle case with no messages to protect" , async ( ) => {
254+ const protectCompressor = new ConversationCompressor ( mockModel , {
255+ summarizeAfterItems : 5 ,
256+ protectRecentMessages : 10 ,
257+ } ) ;
258+
259+ const items : AgentInputItem [ ] = [
260+ createUserMessage ( "Message 1" ) ,
261+ createAssistantMessage ( "Response 1" ) ,
262+ createUserMessage ( "Message 2" ) ,
263+ ] ;
264+
265+ const result = await protectCompressor . compressItems ( items ) ;
266+
267+ // Should protect all items since protectRecentMessages > total messages
268+ expect ( result . compressedItems . length ) . toBe ( 3 ) ;
269+ expect ( result . summary ) . toBe ( "" ) ;
270+ } ) ;
271+ } ) ;
113272} ) ;
0 commit comments