@@ -498,7 +498,9 @@ public function __construct(
498498 new UserMessage ('What is the weather? ' ),
499499 new AssistantMessage (
500500 toolCalls: new ToolCallCollection ([
501- new ToolCall ('tool_123 ' , new FunctionCall ('get_weather ' , ['location ' => 'London ' ])),
501+ new ToolCall ('tool_123 ' , new FunctionCall ('get_weather ' , [
502+ 'location ' => 'London ' ,
503+ ])),
502504 ]),
503505 ),
504506 new ToolMessage ('The weather in London is sunny and 22°C ' , 'tool_123 ' ),
@@ -527,7 +529,9 @@ public function __construct(
527529 new AssistantMessage (
528530 content: 'Let me check the weather for you. ' ,
529531 toolCalls: new ToolCallCollection ([
530- new ToolCall ('tool_abc ' , new FunctionCall ('get_weather ' , ['location ' => 'Paris ' ])),
532+ new ToolCall ('tool_abc ' , new FunctionCall ('get_weather ' , [
533+ 'location ' => 'Paris ' ,
534+ ])),
531535 ]),
532536 ),
533537 new ToolMessage ('Rainy, 15°C ' , 'tool_abc ' ),
@@ -551,7 +555,9 @@ public function __construct(
551555 if ($ block ['type ' ] === 'tool_use '
552556 && $ block ['id ' ] === 'tool_abc '
553557 && $ block ['name ' ] === 'get_weather '
554- && $ block ['input ' ] === ['location ' => 'Paris ' ]) {
558+ && $ block ['input ' ] === [
559+ 'location ' => 'Paris ' ,
560+ ]) {
555561 $ hasToolUse = true ;
556562 }
557563 }
@@ -569,8 +575,12 @@ public function __construct(
569575 new UserMessage ('Compare weather in London and Paris ' ),
570576 new AssistantMessage (
571577 toolCalls: new ToolCallCollection ([
572- new ToolCall ('tool_1 ' , new FunctionCall ('get_weather ' , ['location ' => 'London ' ])),
573- new ToolCall ('tool_2 ' , new FunctionCall ('get_weather ' , ['location ' => 'Paris ' ])),
578+ new ToolCall ('tool_1 ' , new FunctionCall ('get_weather ' , [
579+ 'location ' => 'London ' ,
580+ ])),
581+ new ToolCall ('tool_2 ' , new FunctionCall ('get_weather ' , [
582+ 'location ' => 'Paris ' ,
583+ ])),
574584 ]),
575585 ),
576586 new ToolMessage ('Sunny, 20°C ' , 'tool_1 ' ),
@@ -585,7 +595,7 @@ public function __construct(
585595 // Should have two tool_use blocks
586596 $ toolUseBlocks = array_filter (
587597 $ assistantMessage ['content ' ],
588- fn ( $ block ) => $ block ['type ' ] === 'tool_use ' ,
598+ fn ( array $ block ): bool => $ block ['type ' ] === 'tool_use ' ,
589599 );
590600
591601 return count ($ toolUseBlocks ) === 2 ;
@@ -658,7 +668,7 @@ public function __construct(
658668 $ llm ->addFeature (ModelFeature::Vision);
659669
660670 $ base64Data = base64_encode ('fake-image-data ' );
661- $ dataUrl = " data:image/png;base64, { $ base64Data}" ;
671+ $ dataUrl = ' data:image/png;base64, ' . $ base64Data ;
662672
663673 $ llm ->invoke ([
664674 new UserMessage ([
@@ -725,3 +735,105 @@ public function __construct(
725735 && $ assistantMessage ['content ' ][0 ]['text ' ] === 'Hi there! ' ;
726736 });
727737});
738+
739+ test ('it merges consecutive tool messages into a single user message ' , function (): void {
740+ $ llm = AnthropicChat::fake ([
741+ CreateMessage::class => MockResponse::fixture ('anthropic/messages/simple ' ),
742+ ]);
743+
744+ $ llm ->invoke ([
745+ new UserMessage ('Compare weather in London and Paris ' ),
746+ new AssistantMessage (
747+ toolCalls: new ToolCallCollection ([
748+ new ToolCall ('tool_1 ' , new FunctionCall ('get_weather ' , [
749+ 'location ' => 'London ' ,
750+ ])),
751+ new ToolCall ('tool_2 ' , new FunctionCall ('get_weather ' , [
752+ 'location ' => 'Paris ' ,
753+ ])),
754+ ]),
755+ ),
756+ new ToolMessage ('Sunny, 20°C ' , 'tool_1 ' ),
757+ new ToolMessage ('Rainy, 15°C ' , 'tool_2 ' ),
758+ ]);
759+
760+ MockClient::getGlobal ()->assertSent (function (CreateMessage $ request ): bool {
761+ $ messages = $ request ->body ()->get ('messages ' );
762+
763+ // Should have exactly 3 messages: user, assistant, user (merged tool results)
764+ if (count ($ messages ) !== 3 ) {
765+ return false ;
766+ }
767+
768+ // Third message should be a single user message with both tool results
769+ $ toolResultsMessage = $ messages [2 ];
770+
771+ if ($ toolResultsMessage ['role ' ] !== 'user ' ) {
772+ return false ;
773+ }
774+
775+ // Should have 2 tool_result content blocks
776+ if (count ($ toolResultsMessage ['content ' ]) !== 2 ) {
777+ return false ;
778+ }
779+
780+ $ firstResult = $ toolResultsMessage ['content ' ][0 ];
781+ $ secondResult = $ toolResultsMessage ['content ' ][1 ];
782+
783+ return $ firstResult ['type ' ] === 'tool_result '
784+ && $ firstResult ['tool_use_id ' ] === 'tool_1 '
785+ && $ firstResult ['content ' ] === 'Sunny, 20°C '
786+ && $ secondResult ['type ' ] === 'tool_result '
787+ && $ secondResult ['tool_use_id ' ] === 'tool_2 '
788+ && $ secondResult ['content ' ] === 'Rainy, 15°C ' ;
789+ });
790+ });
791+
792+ test ('it does not merge non-consecutive tool messages ' , function (): void {
793+ $ llm = AnthropicChat::fake ([
794+ CreateMessage::class => MockResponse::fixture ('anthropic/messages/simple ' ),
795+ ]);
796+
797+ $ llm ->invoke ([
798+ new UserMessage ('What is the weather? ' ),
799+ new AssistantMessage (
800+ toolCalls: new ToolCallCollection ([
801+ new ToolCall ('tool_1 ' , new FunctionCall ('get_weather ' , [
802+ 'location ' => 'London ' ,
803+ ])),
804+ ]),
805+ ),
806+ new ToolMessage ('Sunny ' , 'tool_1 ' ),
807+ new AssistantMessage ('The weather in London is sunny. Want to check another city? ' ),
808+ new UserMessage ('Yes, check Paris ' ),
809+ new AssistantMessage (
810+ toolCalls: new ToolCallCollection ([
811+ new ToolCall ('tool_2 ' , new FunctionCall ('get_weather ' , [
812+ 'location ' => 'Paris ' ,
813+ ])),
814+ ]),
815+ ),
816+ new ToolMessage ('Rainy ' , 'tool_2 ' ),
817+ ]);
818+
819+ MockClient::getGlobal ()->assertSent (function (CreateMessage $ request ): bool {
820+ $ messages = $ request ->body ()->get ('messages ' );
821+
822+ // Should have 7 messages (tool messages not merged because they're not consecutive)
823+ if (count ($ messages ) !== 7 ) {
824+ return false ;
825+ }
826+
827+ // First tool result at index 2
828+ $ firstToolResult = $ messages [2 ];
829+ // Second tool result at index 6
830+ $ secondToolResult = $ messages [6 ];
831+
832+ return $ firstToolResult ['role ' ] === 'user '
833+ && count ($ firstToolResult ['content ' ]) === 1
834+ && $ firstToolResult ['content ' ][0 ]['tool_use_id ' ] === 'tool_1 '
835+ && $ secondToolResult ['role ' ] === 'user '
836+ && count ($ secondToolResult ['content ' ]) === 1
837+ && $ secondToolResult ['content ' ][0 ]['tool_use_id ' ] === 'tool_2 ' ;
838+ });
839+ });
0 commit comments