@@ -211,3 +211,165 @@ describe("openai-chat tool history repair", () => {
211211 expect ( body . messages [ 1 ] ) . toMatchObject ( { role : "tool" , tool_call_id : "call_1" } ) ;
212212 } ) ;
213213} ) ;
214+
215+ describe ( "anthropic tool result history repair" , ( ) => {
216+ test ( "merges adjacent tool results after multiple tool uses into one user message" , ( ) => {
217+ const adapter = createAnthropicAdapter ( { ...provider , adapter : "anthropic" } ) ;
218+ const request = adapter . buildRequest ( {
219+ modelId : "claude-sonnet" ,
220+ context : {
221+ messages : [
222+ { role : "user" , content : "start" , timestamp : 0 } ,
223+ {
224+ role : "assistant" ,
225+ content : [
226+ { type : "toolCall" , id : "call_1" , name : "first_tool" , arguments : { } } ,
227+ { type : "toolCall" , id : "call_2" , name : "second_tool" , arguments : { } } ,
228+ ] ,
229+ model : "claude-sonnet" ,
230+ timestamp : 0 ,
231+ } ,
232+ { role : "toolResult" , toolCallId : "call_1" , toolName : "first_tool" , content : "one" , isError : false , timestamp : 0 } ,
233+ { role : "toolResult" , toolCallId : "call_2" , toolName : "second_tool" , content : "two" , isError : false , timestamp : 0 } ,
234+ { role : "user" , content : "continue" , timestamp : 0 } ,
235+ ] ,
236+ } ,
237+ stream : true ,
238+ options : { } ,
239+ } ) ;
240+ const body = JSON . parse ( request . body ) as { messages : Array < { role : string ; content : any } > } ;
241+
242+ expect ( body . messages ) . toHaveLength ( 4 ) ;
243+ expect ( body . messages [ 2 ] . role ) . toBe ( "user" ) ;
244+ expect ( body . messages [ 2 ] . content ) . toEqual ( [
245+ { type : "tool_result" , tool_use_id : "call_1" , content : "one" } ,
246+ { type : "tool_result" , tool_use_id : "call_2" , content : "two" } ,
247+ ] ) ;
248+ } ) ;
249+
250+ test ( "adds an error tool result when history is missing a tool result" , ( ) => {
251+ const adapter = createAnthropicAdapter ( { ...provider , adapter : "anthropic" } ) ;
252+ const request = adapter . buildRequest ( {
253+ modelId : "claude-sonnet" ,
254+ context : {
255+ messages : [ {
256+ role : "assistant" ,
257+ content : [ { type : "toolCall" , id : "call_1" , name : "read_file" , arguments : { } } ] ,
258+ model : "claude-sonnet" ,
259+ timestamp : 0 ,
260+ } ] ,
261+ } ,
262+ stream : true ,
263+ options : { } ,
264+ } ) ;
265+ const body = JSON . parse ( request . body ) as { messages : Array < { role : string ; content : any } > } ;
266+
267+ expect ( body . messages [ 1 ] ) . toEqual ( {
268+ role : "user" ,
269+ content : [ {
270+ type : "tool_result" ,
271+ tool_use_id : "call_1" ,
272+ content : "[opencodex: missing tool_result for this tool_use in Codex history]" ,
273+ is_error : true ,
274+ } ] ,
275+ } ) ;
276+ } ) ;
277+
278+ test ( "preserves orphan tool results as text instead of invalid Anthropic tool_result blocks" , ( ) => {
279+ const adapter = createAnthropicAdapter ( { ...provider , adapter : "anthropic" } ) ;
280+ const request = adapter . buildRequest ( {
281+ modelId : "claude-sonnet" ,
282+ context : {
283+ messages : [ {
284+ role : "toolResult" ,
285+ toolCallId : "orphan_call" ,
286+ toolName : "lost_tool" ,
287+ content : "orphan output" ,
288+ isError : false ,
289+ timestamp : 0 ,
290+ } ] ,
291+ } ,
292+ stream : true ,
293+ options : { } ,
294+ } ) ;
295+ const body = JSON . parse ( request . body ) as { messages : Array < { role : string ; content : string } > } ;
296+
297+ expect ( body . messages ) . toEqual ( [ {
298+ role : "user" ,
299+ content : "[tool_result without adjacent tool_use: lost_tool (orphan_call)]\norphan output" ,
300+ } ] ) ;
301+ } ) ;
302+
303+ test ( "preserves duplicate adjacent tool results as text after the matching result" , ( ) => {
304+ const adapter = createAnthropicAdapter ( { ...provider , adapter : "anthropic" } ) ;
305+ const request = adapter . buildRequest ( {
306+ modelId : "claude-sonnet" ,
307+ context : {
308+ messages : [
309+ {
310+ role : "assistant" ,
311+ content : [ { type : "toolCall" , id : "call_1" , name : "read_file" , arguments : { } } ] ,
312+ model : "claude-sonnet" ,
313+ timestamp : 0 ,
314+ } ,
315+ { role : "toolResult" , toolCallId : "call_1" , toolName : "read_file" , content : "first" , isError : false , timestamp : 0 } ,
316+ { role : "toolResult" , toolCallId : "call_1" , toolName : "read_file" , content : "duplicate" , isError : false , timestamp : 0 } ,
317+ ] ,
318+ } ,
319+ stream : true ,
320+ options : { } ,
321+ } ) ;
322+ const body = JSON . parse ( request . body ) as { messages : Array < { role : string ; content : any } > } ;
323+
324+ expect ( body . messages [ 1 ] ) . toEqual ( {
325+ role : "user" ,
326+ content : [
327+ { type : "tool_result" , tool_use_id : "call_1" , content : "first" } ,
328+ { type : "text" , text : "[tool_result without adjacent tool_use: read_file (call_1)]\nduplicate" } ,
329+ ] ,
330+ } ) ;
331+ } ) ;
332+
333+ test ( "maps non-string tool result content through Anthropic content blocks" , ( ) => {
334+ const adapter = createAnthropicAdapter ( { ...provider , adapter : "anthropic" } ) ;
335+ const request = adapter . buildRequest ( {
336+ modelId : "claude-sonnet" ,
337+ context : {
338+ messages : [
339+ {
340+ role : "assistant" ,
341+ content : [ { type : "toolCall" , id : "call_1" , name : "view_image" , arguments : { } } ] ,
342+ model : "claude-sonnet" ,
343+ timestamp : 0 ,
344+ } ,
345+ {
346+ role : "toolResult" ,
347+ toolCallId : "call_1" ,
348+ toolName : "view_image" ,
349+ content : [
350+ { type : "text" , text : "image attached" } ,
351+ { type : "image" , imageUrl : "data:image/png;base64,AAAA" , detail : "high" } ,
352+ ] ,
353+ isError : false ,
354+ timestamp : 0 ,
355+ } ,
356+ ] ,
357+ } ,
358+ stream : true ,
359+ options : { } ,
360+ } ) ;
361+ const body = JSON . parse ( request . body ) as { messages : Array < { role : string ; content : any } > } ;
362+
363+ expect ( body . messages [ 1 ] ) . toEqual ( {
364+ role : "user" ,
365+ content : [ {
366+ type : "tool_result" ,
367+ tool_use_id : "call_1" ,
368+ content : [
369+ { type : "text" , text : "image attached" } ,
370+ { type : "image" , source : { type : "base64" , media_type : "image/png" , data : "AAAA" } } ,
371+ ] ,
372+ } ] ,
373+ } ) ;
374+ } ) ;
375+ } ) ;
0 commit comments