3636import io .agentscope .core .message .Msg ;
3737import io .agentscope .core .message .MsgRole ;
3838import io .agentscope .core .message .TextBlock ;
39+ import io .agentscope .core .message .ThinkingBlock ;
3940import io .agentscope .core .message .ToolResultBlock ;
4041import io .agentscope .core .message .ToolUseBlock ;
4142import io .agentscope .core .model .ChatResponse ;
@@ -807,6 +808,188 @@ public <T extends HookEvent> Mono<T> onEvent(T event) {
807808 "call_stream_1" , accumulatedTub .getId (), "Accumulated tool call ID should match" );
808809 }
809810
811+ @ Test
812+ @ DisplayName ("Should keep cumulative reasoning chunks across tool calls" )
813+ void testCumulativeReasoningStreamKeepsHistoryAfterToolCall () {
814+ MockModel toolModel = createTwoRoundStreamingModel ();
815+
816+ agent =
817+ ReActAgent .builder ()
818+ .name (TestConstants .TEST_REACT_AGENT_NAME )
819+ .sysPrompt (TestConstants .DEFAULT_SYS_PROMPT )
820+ .model (toolModel )
821+ .toolkit (mockToolkit )
822+ .memory (memory )
823+ .build ();
824+
825+ StreamOptions options =
826+ StreamOptions .builder ()
827+ .eventTypes (EventType .REASONING )
828+ .incremental (false )
829+ .includeReasoningResult (false )
830+ .build ();
831+
832+ List <Event > events =
833+ agent .stream (
834+ TestUtils .createUserMessage ("User" , "Use a tool, then continue." ),
835+ options )
836+ .collectList ()
837+ .block (Duration .ofMillis (TestConstants .DEFAULT_TEST_TIMEOUT_MS ));
838+
839+ assertNotNull (events , "Streaming events should not be null" );
840+
841+ List <Msg > reasoningChunks =
842+ events .stream ()
843+ .filter (event -> event .getType () == EventType .REASONING )
844+ .filter (event -> !event .isLast ())
845+ .map (Event ::getMessage )
846+ .toList ();
847+
848+ assertEquals (5 , reasoningChunks .size (), "Should emit every streamed reasoning chunk" );
849+
850+ Msg finalCumulativeChunk = reasoningChunks .get (reasoningChunks .size () - 1 );
851+ List <ContentBlock > cumulativeContent = finalCumulativeChunk .getContent ();
852+
853+ assertTrue (
854+ cumulativeContent .stream ()
855+ .filter (ThinkingBlock .class ::isInstance )
856+ .map (ThinkingBlock .class ::cast )
857+ .anyMatch (block -> block .getThinking ().contains ("think before tool." )),
858+ "Cumulative mode should keep pre-tool thinking content" );
859+ assertTrue (
860+ cumulativeContent .stream ()
861+ .filter (TextBlock .class ::isInstance )
862+ .map (TextBlock .class ::cast )
863+ .anyMatch (block -> block .getText ().contains ("text before tool." )),
864+ "Cumulative mode should keep pre-tool text content" );
865+ assertTrue (
866+ cumulativeContent .stream ()
867+ .filter (ToolUseBlock .class ::isInstance )
868+ .map (ToolUseBlock .class ::cast )
869+ .anyMatch (block -> "call_stream_reset_1" .equals (block .getId ())),
870+ "Cumulative mode should keep the tool call that split reasoning rounds" );
871+ assertTrue (
872+ cumulativeContent .stream ()
873+ .filter (ThinkingBlock .class ::isInstance )
874+ .map (ThinkingBlock .class ::cast )
875+ .anyMatch (block -> block .getThinking ().contains ("think after tool." )),
876+ "Cumulative mode should include post-tool thinking content" );
877+ assertTrue (
878+ cumulativeContent .stream ()
879+ .filter (TextBlock .class ::isInstance )
880+ .map (TextBlock .class ::cast )
881+ .anyMatch (block -> block .getText ().contains ("text after tool." )),
882+ "Cumulative mode should include post-tool text content" );
883+ }
884+
885+ @ Test
886+ @ DisplayName ("Should keep incremental reasoning chunks as deltas after tool calls" )
887+ void testIncrementalReasoningStreamStillEmitsDeltasAfterToolCall () {
888+ MockModel toolModel = createTwoRoundStreamingModel ();
889+
890+ agent =
891+ ReActAgent .builder ()
892+ .name (TestConstants .TEST_REACT_AGENT_NAME )
893+ .sysPrompt (TestConstants .DEFAULT_SYS_PROMPT )
894+ .model (toolModel )
895+ .toolkit (mockToolkit )
896+ .memory (memory )
897+ .build ();
898+
899+ StreamOptions options =
900+ StreamOptions .builder ()
901+ .eventTypes (EventType .REASONING )
902+ .incremental (true )
903+ .includeReasoningResult (false )
904+ .build ();
905+
906+ List <Event > events =
907+ agent .stream (
908+ TestUtils .createUserMessage ("User" , "Use a tool, then continue." ),
909+ options )
910+ .collectList ()
911+ .block (Duration .ofMillis (TestConstants .DEFAULT_TEST_TIMEOUT_MS ));
912+
913+ assertNotNull (events , "Streaming events should not be null" );
914+
915+ List <Msg > reasoningChunks =
916+ events .stream ()
917+ .filter (event -> event .getType () == EventType .REASONING )
918+ .filter (event -> !event .isLast ())
919+ .map (Event ::getMessage )
920+ .toList ();
921+
922+ assertEquals (5 , reasoningChunks .size (), "Should emit every streamed reasoning chunk" );
923+
924+ Msg finalIncrementalChunk = reasoningChunks .get (reasoningChunks .size () - 1 );
925+ List <ContentBlock > incrementalContent = finalIncrementalChunk .getContent ();
926+
927+ assertEquals (1 , incrementalContent .size (), "Incremental mode should emit only the delta" );
928+ TextBlock textBlock = assertInstanceOf (TextBlock .class , incrementalContent .get (0 ));
929+ assertEquals ("text after tool." , textBlock .getText ());
930+ }
931+
932+ private static MockModel createTwoRoundStreamingModel () {
933+ final int [] callCount = {0 };
934+ return new MockModel (
935+ messages -> {
936+ int currentCall = callCount [0 ]++;
937+ if (currentCall == 0 ) {
938+ return List .of (
939+ ChatResponse .builder ()
940+ .id ("reasoning-round-1" )
941+ .content (
942+ List .of (
943+ ThinkingBlock .builder ()
944+ .thinking ("think before tool. " )
945+ .build ()))
946+ .usage (new ChatUsage (10 , 20 , 30 ))
947+ .build (),
948+ ChatResponse .builder ()
949+ .id ("reasoning-round-1" )
950+ .content (
951+ List .of (
952+ TextBlock .builder ()
953+ .text ("text before tool. " )
954+ .build ()))
955+ .usage (new ChatUsage (10 , 20 , 30 ))
956+ .build (),
957+ ChatResponse .builder ()
958+ .id ("reasoning-round-1" )
959+ .content (
960+ List .of (
961+ ToolUseBlock .builder ()
962+ .id ("call_stream_reset_1" )
963+ .name (TestConstants .TEST_TOOL_NAME )
964+ .input (Map .of ())
965+ .content ("{}" )
966+ .build ()))
967+ .usage (new ChatUsage (10 , 20 , 30 ))
968+ .build ());
969+ }
970+
971+ return List .of (
972+ ChatResponse .builder ()
973+ .id ("reasoning-round-2" )
974+ .content (
975+ List .of (
976+ ThinkingBlock .builder ()
977+ .thinking ("think after tool. " )
978+ .build ()))
979+ .usage (new ChatUsage (10 , 20 , 30 ))
980+ .build (),
981+ ChatResponse .builder ()
982+ .id ("reasoning-round-2" )
983+ .content (
984+ List .of (
985+ TextBlock .builder ()
986+ .text ("text after tool." )
987+ .build ()))
988+ .usage (new ChatUsage (10 , 20 , 30 ))
989+ .build ());
990+ });
991+ }
992+
810993 @ Test
811994 @ DisplayName ("Should emit ReasoningChunkEvent for multiple parallel tool calls" )
812995 void testStreamingMultipleToolCallsChunkEvents () {
@@ -1051,4 +1234,4 @@ private static ChatResponse createToolCallResponseHelper(
10511234 .usage (new ChatUsage (8 , 15 , 23 ))
10521235 .build ();
10531236 }
1054- }
1237+ }
0 commit comments