@@ -246,6 +246,94 @@ describe("getKeepMessagesWithToolBlocks", () => {
246246 expect ( result . keepMessages ) . toEqual ( messages )
247247 expect ( result . toolUseBlocksToPreserve ) . toHaveLength ( 0 )
248248 } )
249+
250+ it ( "should preserve reasoning blocks alongside tool_use blocks for DeepSeek/Z.ai interleaved thinking" , ( ) => {
251+ const reasoningBlock = {
252+ type : "reasoning" as const ,
253+ text : "Let me think about this step by step..." ,
254+ }
255+ const toolUseBlock = {
256+ type : "tool_use" as const ,
257+ id : "toolu_deepseek_123" ,
258+ name : "read_file" ,
259+ input : { path : "test.txt" } ,
260+ }
261+ const toolResultBlock = {
262+ type : "tool_result" as const ,
263+ tool_use_id : "toolu_deepseek_123" ,
264+ content : "file contents" ,
265+ }
266+
267+ const messages : ApiMessage [ ] = [
268+ { role : "user" , content : "Hello" , ts : 1 } ,
269+ { role : "assistant" , content : "Let me help" , ts : 2 } ,
270+ { role : "user" , content : "Please read the file" , ts : 3 } ,
271+ {
272+ role : "assistant" ,
273+ // DeepSeek stores reasoning as content blocks alongside tool_use
274+ content : [ reasoningBlock as any , { type : "text" as const , text : "Reading file..." } , toolUseBlock ] ,
275+ ts : 4 ,
276+ } ,
277+ {
278+ role : "user" ,
279+ content : [ toolResultBlock , { type : "text" as const , text : "Continue" } ] ,
280+ ts : 5 ,
281+ } ,
282+ { role : "assistant" , content : "Got it, the file says..." , ts : 6 } ,
283+ { role : "user" , content : "Thanks" , ts : 7 } ,
284+ ]
285+
286+ const result = getKeepMessagesWithToolBlocks ( messages , 3 )
287+
288+ // keepMessages should be the last 3 messages
289+ expect ( result . keepMessages ) . toHaveLength ( 3 )
290+ expect ( result . keepMessages [ 0 ] . ts ) . toBe ( 5 )
291+
292+ // Should preserve the tool_use block
293+ expect ( result . toolUseBlocksToPreserve ) . toHaveLength ( 1 )
294+ expect ( result . toolUseBlocksToPreserve [ 0 ] ) . toEqual ( toolUseBlock )
295+
296+ // Should preserve the reasoning block for DeepSeek/Z.ai interleaved thinking
297+ expect ( result . reasoningBlocksToPreserve ) . toHaveLength ( 1 )
298+ expect ( ( result . reasoningBlocksToPreserve [ 0 ] as any ) . type ) . toBe ( "reasoning" )
299+ expect ( ( result . reasoningBlocksToPreserve [ 0 ] as any ) . text ) . toBe ( "Let me think about this step by step..." )
300+ } )
301+
302+ it ( "should return empty reasoningBlocksToPreserve when no reasoning blocks present" , ( ) => {
303+ const toolUseBlock = {
304+ type : "tool_use" as const ,
305+ id : "toolu_123" ,
306+ name : "read_file" ,
307+ input : { path : "test.txt" } ,
308+ }
309+ const toolResultBlock = {
310+ type : "tool_result" as const ,
311+ tool_use_id : "toolu_123" ,
312+ content : "file contents" ,
313+ }
314+
315+ const messages : ApiMessage [ ] = [
316+ { role : "user" , content : "Hello" , ts : 1 } ,
317+ {
318+ role : "assistant" ,
319+ // No reasoning block, just text and tool_use
320+ content : [ { type : "text" as const , text : "Reading file..." } , toolUseBlock ] ,
321+ ts : 2 ,
322+ } ,
323+ {
324+ role : "user" ,
325+ content : [ toolResultBlock ] ,
326+ ts : 3 ,
327+ } ,
328+ { role : "assistant" , content : "Done" , ts : 4 } ,
329+ { role : "user" , content : "Thanks" , ts : 5 } ,
330+ ]
331+
332+ const result = getKeepMessagesWithToolBlocks ( messages , 3 )
333+
334+ expect ( result . toolUseBlocksToPreserve ) . toHaveLength ( 1 )
335+ expect ( result . reasoningBlocksToPreserve ) . toHaveLength ( 0 )
336+ } )
249337} )
250338
251339describe ( "getMessagesSinceLastSummary" , ( ) => {
@@ -422,7 +510,14 @@ describe("summarizeConversation", () => {
422510 const summaryMessage = result . messages . find ( ( m ) => m . isSummary )
423511 expect ( summaryMessage ) . toBeDefined ( )
424512 expect ( summaryMessage ! . role ) . toBe ( "assistant" )
425- expect ( summaryMessage ! . content ) . toBe ( "This is a summary" )
513+ // Summary content is now always an array with [synthetic reasoning, text]
514+ // for DeepSeek-reasoner compatibility (requires reasoning_content on all assistant messages)
515+ expect ( Array . isArray ( summaryMessage ! . content ) ) . toBe ( true )
516+ const content = summaryMessage ! . content as any [ ]
517+ expect ( content ) . toHaveLength ( 2 )
518+ expect ( content [ 0 ] . type ) . toBe ( "reasoning" )
519+ expect ( content [ 1 ] . type ) . toBe ( "text" )
520+ expect ( content [ 1 ] . text ) . toBe ( "This is a summary" )
426521 expect ( summaryMessage ! . isSummary ) . toBe ( true )
427522
428523 // Verify that the effective API history matches expected: first + summary + last N messages
@@ -827,14 +922,16 @@ describe("summarizeConversation", () => {
827922 expect ( summaryMessage ! . isSummary ) . toBe ( true )
828923 expect ( Array . isArray ( summaryMessage ! . content ) ) . toBe ( true )
829924
830- // Content should be [text block, tool_use block]
925+ // Content should be [synthetic reasoning, text block, tool_use block]
926+ // The synthetic reasoning is always added for DeepSeek-reasoner compatibility
831927 const content = summaryMessage ! . content as Anthropic . Messages . ContentBlockParam [ ]
832- expect ( content ) . toHaveLength ( 2 )
833- expect ( content [ 0 ] . type ) . toBe ( "text" )
834- expect ( ( content [ 0 ] as Anthropic . Messages . TextBlockParam ) . text ) . toBe ( "Summary of conversation" )
835- expect ( content [ 1 ] . type ) . toBe ( "tool_use" )
836- expect ( ( content [ 1 ] as Anthropic . Messages . ToolUseBlockParam ) . id ) . toBe ( "toolu_123" )
837- expect ( ( content [ 1 ] as Anthropic . Messages . ToolUseBlockParam ) . name ) . toBe ( "read_file" )
928+ expect ( content ) . toHaveLength ( 3 )
929+ expect ( ( content [ 0 ] as any ) . type ) . toBe ( "reasoning" ) // Synthetic reasoning for DeepSeek
930+ expect ( content [ 1 ] . type ) . toBe ( "text" )
931+ expect ( ( content [ 1 ] as Anthropic . Messages . TextBlockParam ) . text ) . toBe ( "Summary of conversation" )
932+ expect ( content [ 2 ] . type ) . toBe ( "tool_use" )
933+ expect ( ( content [ 2 ] as Anthropic . Messages . ToolUseBlockParam ) . id ) . toBe ( "toolu_123" )
934+ expect ( ( content [ 2 ] as Anthropic . Messages . ToolUseBlockParam ) . name ) . toBe ( "read_file" )
838935
839936 // With non-destructive condensing, all messages are retained plus the summary
840937 expect ( result . messages . length ) . toBe ( messages . length + 1 ) // all original + summary
@@ -981,14 +1078,164 @@ describe("summarizeConversation", () => {
9811078 expect ( summaryMessage ) . toBeDefined ( )
9821079 expect ( Array . isArray ( summaryMessage ! . content ) ) . toBe ( true )
9831080 const summaryContent = summaryMessage ! . content as Anthropic . Messages . ContentBlockParam [ ]
984- expect ( summaryContent [ 0 ] ) . toEqual ( { type : "text" , text : "This is a summary" } )
1081+ // First block is synthetic reasoning for DeepSeek-reasoner compatibility
1082+ expect ( ( summaryContent [ 0 ] as any ) . type ) . toBe ( "reasoning" )
1083+ // Second block is the text summary
1084+ expect ( summaryContent [ 1 ] ) . toEqual ( { type : "text" , text : "This is a summary" } )
9851085
9861086 const preservedToolUses = summaryContent . filter (
9871087 ( block ) : block is Anthropic . Messages . ToolUseBlockParam => block . type === "tool_use" ,
9881088 )
9891089 expect ( preservedToolUses ) . toHaveLength ( 2 )
9901090 expect ( preservedToolUses . map ( ( block ) => block . id ) ) . toEqual ( [ "toolu_parallel_1" , "toolu_parallel_2" ] )
9911091 } )
1092+
1093+ it ( "should preserve reasoning blocks in summary message for DeepSeek/Z.ai interleaved thinking" , async ( ) => {
1094+ const reasoningBlock = {
1095+ type : "reasoning" as const ,
1096+ text : "Let me think about this step by step..." ,
1097+ }
1098+ const toolUseBlock = {
1099+ type : "tool_use" as const ,
1100+ id : "toolu_deepseek_reason" ,
1101+ name : "read_file" ,
1102+ input : { path : "test.txt" } ,
1103+ }
1104+ const toolResultBlock = {
1105+ type : "tool_result" as const ,
1106+ tool_use_id : "toolu_deepseek_reason" ,
1107+ content : "file contents" ,
1108+ }
1109+
1110+ const messages : ApiMessage [ ] = [
1111+ { role : "user" , content : "Hello" , ts : 1 } ,
1112+ { role : "assistant" , content : "Let me help" , ts : 2 } ,
1113+ { role : "user" , content : "Please read the file" , ts : 3 } ,
1114+ {
1115+ role : "assistant" ,
1116+ // DeepSeek stores reasoning as content blocks alongside tool_use
1117+ content : [ reasoningBlock as any , { type : "text" as const , text : "Reading file..." } , toolUseBlock ] ,
1118+ ts : 4 ,
1119+ } ,
1120+ {
1121+ role : "user" ,
1122+ content : [ toolResultBlock , { type : "text" as const , text : "Continue" } ] ,
1123+ ts : 5 ,
1124+ } ,
1125+ { role : "assistant" , content : "Got it, the file says..." , ts : 6 } ,
1126+ { role : "user" , content : "Thanks" , ts : 7 } ,
1127+ ]
1128+
1129+ // Create a stream with usage information
1130+ const streamWithUsage = ( async function * ( ) {
1131+ yield { type : "text" as const , text : "Summary of conversation" }
1132+ yield { type : "usage" as const , totalCost : 0.05 , outputTokens : 100 }
1133+ } ) ( )
1134+
1135+ mockApiHandler . createMessage = vi . fn ( ) . mockReturnValue ( streamWithUsage ) as any
1136+ mockApiHandler . countTokens = vi . fn ( ) . mockImplementation ( ( ) => Promise . resolve ( 50 ) ) as any
1137+
1138+ const result = await summarizeConversation (
1139+ messages ,
1140+ mockApiHandler ,
1141+ defaultSystemPrompt ,
1142+ taskId ,
1143+ DEFAULT_PREV_CONTEXT_TOKENS ,
1144+ false , // isAutomaticTrigger
1145+ undefined , // customCondensingPrompt
1146+ undefined , // condensingApiHandler
1147+ true , // useNativeTools - required for tool_use block preservation
1148+ )
1149+
1150+ // Find the summary message
1151+ const summaryMessage = result . messages . find ( ( m ) => m . isSummary )
1152+ expect ( summaryMessage ) . toBeDefined ( )
1153+ expect ( summaryMessage ! . role ) . toBe ( "assistant" )
1154+ expect ( summaryMessage ! . isSummary ) . toBe ( true )
1155+ expect ( Array . isArray ( summaryMessage ! . content ) ) . toBe ( true )
1156+
1157+ // Content should be [synthetic reasoning, preserved reasoning, text block, tool_use block]
1158+ // - Synthetic reasoning is always added for DeepSeek-reasoner compatibility
1159+ // - Preserved reasoning from the condensed assistant message
1160+ // This order ensures reasoning_content is always present for DeepSeek/Z.ai
1161+ const content = summaryMessage ! . content as Anthropic . Messages . ContentBlockParam [ ]
1162+ expect ( content ) . toHaveLength ( 4 )
1163+
1164+ // First block should be synthetic reasoning
1165+ expect ( ( content [ 0 ] as any ) . type ) . toBe ( "reasoning" )
1166+ expect ( ( content [ 0 ] as any ) . text ) . toContain ( "Condensing conversation context" )
1167+
1168+ // Second block should be preserved reasoning from the condensed message
1169+ expect ( ( content [ 1 ] as any ) . type ) . toBe ( "reasoning" )
1170+ expect ( ( content [ 1 ] as any ) . text ) . toBe ( "Let me think about this step by step..." )
1171+
1172+ // Third block should be text (the summary)
1173+ expect ( content [ 2 ] . type ) . toBe ( "text" )
1174+ expect ( ( content [ 2 ] as Anthropic . Messages . TextBlockParam ) . text ) . toBe ( "Summary of conversation" )
1175+
1176+ // Fourth block should be tool_use
1177+ expect ( content [ 3 ] . type ) . toBe ( "tool_use" )
1178+ expect ( ( content [ 3 ] as Anthropic . Messages . ToolUseBlockParam ) . id ) . toBe ( "toolu_deepseek_reason" )
1179+
1180+ expect ( result . error ) . toBeUndefined ( )
1181+ } )
1182+
1183+ it ( "should include synthetic reasoning block in summary for DeepSeek-reasoner compatibility even without tool_use blocks" , async ( ) => {
1184+ // This test verifies the fix for the DeepSeek-reasoner 400 error:
1185+ // "Missing `reasoning_content` field in the assistant message at message index 1"
1186+ // DeepSeek-reasoner requires reasoning_content on ALL assistant messages, not just those with tool_calls.
1187+ // After condensation, the summary becomes an assistant message that needs reasoning_content.
1188+ const messages : ApiMessage [ ] = [
1189+ { role : "user" , content : "Tell me a joke" , ts : 1 } ,
1190+ { role : "assistant" , content : "Why did the programmer quit?" , ts : 2 } ,
1191+ { role : "user" , content : "I don't know, why?" , ts : 3 } ,
1192+ { role : "assistant" , content : "He didn't get arrays!" , ts : 4 } ,
1193+ { role : "user" , content : "Another one please" , ts : 5 } ,
1194+ { role : "assistant" , content : "Why do programmers prefer dark mode?" , ts : 6 } ,
1195+ { role : "user" , content : "Why?" , ts : 7 } ,
1196+ ]
1197+
1198+ // Create a stream with usage information (no tool calls in this conversation)
1199+ const streamWithUsage = ( async function * ( ) {
1200+ yield { type : "text" as const , text : "Summary: User requested jokes." }
1201+ yield { type : "usage" as const , totalCost : 0.05 , outputTokens : 100 }
1202+ } ) ( )
1203+
1204+ mockApiHandler . createMessage = vi . fn ( ) . mockReturnValue ( streamWithUsage ) as any
1205+ mockApiHandler . countTokens = vi . fn ( ) . mockImplementation ( ( ) => Promise . resolve ( 50 ) ) as any
1206+
1207+ const result = await summarizeConversation (
1208+ messages ,
1209+ mockApiHandler ,
1210+ defaultSystemPrompt ,
1211+ taskId ,
1212+ DEFAULT_PREV_CONTEXT_TOKENS ,
1213+ false , // isAutomaticTrigger
1214+ undefined , // customCondensingPrompt
1215+ undefined , // condensingApiHandler
1216+ false , // useNativeTools - not using tools in this test
1217+ )
1218+
1219+ // Find the summary message
1220+ const summaryMessage = result . messages . find ( ( m ) => m . isSummary )
1221+ expect ( summaryMessage ) . toBeDefined ( )
1222+ expect ( summaryMessage ! . role ) . toBe ( "assistant" )
1223+ expect ( summaryMessage ! . isSummary ) . toBe ( true )
1224+
1225+ // CRITICAL: Content must be an array with a synthetic reasoning block
1226+ // This is required for DeepSeek-reasoner which needs reasoning_content on all assistant messages
1227+ expect ( Array . isArray ( summaryMessage ! . content ) ) . toBe ( true )
1228+ const content = summaryMessage ! . content as any [ ]
1229+
1230+ // Should have [synthetic reasoning, text]
1231+ expect ( content ) . toHaveLength ( 2 )
1232+ expect ( content [ 0 ] . type ) . toBe ( "reasoning" )
1233+ expect ( content [ 0 ] . text ) . toContain ( "Condensing conversation context" )
1234+ expect ( content [ 1 ] . type ) . toBe ( "text" )
1235+ expect ( content [ 1 ] . text ) . toBe ( "Summary: User requested jokes." )
1236+
1237+ expect ( result . error ) . toBeUndefined ( )
1238+ } )
9921239} )
9931240
9941241describe ( "summarizeConversation with custom settings" , ( ) => {
0 commit comments