@@ -806,6 +806,135 @@ void testCurrentRoundCompressionPreservesToolPairStructure() {
806806 "Compressed tool message should preserve both tool results" );
807807 }
808808
809+ @ Test
810+ @ DisplayName (
811+ "Should keep plain current round compression as a single summary message when no"
812+ + " tool interaction exists" )
813+ void testMergeAndCompressCurrentRoundMessagesWithoutToolInteraction ()
814+ throws ReflectiveOperationException {
815+ TestModel model = new TestModel ("Plain current round summary" );
816+ AutoContextMemory mem = new AutoContextMemory (config , model );
817+
818+ List <Msg > currentRoundMessages =
819+ new ArrayList <>(
820+ List .of (
821+ createTextMessage ("User asks a plain question" , MsgRole .USER ),
822+ createTextMessage (
823+ "Assistant answers without tools" , MsgRole .ASSISTANT )));
824+
825+ Method method =
826+ AutoContextMemory .class .getDeclaredMethod (
827+ "mergeAndCompressCurrentRoundMessages" , List .class );
828+ method .setAccessible (true );
829+
830+ @ SuppressWarnings ("unchecked" )
831+ List <Msg > compressedMessages = (List <Msg >) method .invoke (mem , currentRoundMessages );
832+
833+ assertNotNull (compressedMessages , "Compression should return a summary message list" );
834+ assertEquals (
835+ 1 ,
836+ compressedMessages .size (),
837+ "Plain current round compression should stay as a single summary message" );
838+ assertEquals (
839+ MsgRole .ASSISTANT ,
840+ compressedMessages .get (0 ).getRole (),
841+ "Summary message should remain an assistant message" );
842+ assertTrue (
843+ compressedMessages .get (0 ).getTextContent ().contains ("Plain current round summary" ),
844+ "Summary text should come from the compression model" );
845+
846+ assertEquals (
847+ 1 , mem .getOffloadContext ().size (), "Original messages should still be offloaded" );
848+ assertEquals (
849+ 2 ,
850+ mem .getOffloadContext ().values ().iterator ().next ().size (),
851+ "Offload context should retain the original current-round messages" );
852+ }
853+
854+ @ Test
855+ @ DisplayName (
856+ "Should fall back to summary text when tool-aware compression only sees tool results" )
857+ void testBuildToolAwareCurrentRoundCompressionWithToolResultsOnly ()
858+ throws ReflectiveOperationException {
859+ AutoContextMemory mem = new AutoContextMemory (config , testModel );
860+ List <Msg > toolOnlyMessages =
861+ List .of (
862+ Msg .builder ()
863+ .role (MsgRole .TOOL )
864+ .name ("tool" )
865+ .content (
866+ ToolResultBlock .of (
867+ "call_only" ,
868+ null ,
869+ TextBlock .builder ().text ("raw tool output" ).build (),
870+ Map .of ("source" , "tool-only-test" )))
871+ .build ());
872+ Msg summaryMsg =
873+ Msg .builder ()
874+ .role (MsgRole .ASSISTANT )
875+ .name ("assistant" )
876+ .content (TextBlock .builder ().text ("Tool-only summary" ).build ())
877+ .build ();
878+
879+ Method method =
880+ AutoContextMemory .class .getDeclaredMethod (
881+ "buildToolAwareCurrentRoundCompression" ,
882+ List .class ,
883+ Msg .class ,
884+ String .class );
885+ method .setAccessible (true );
886+
887+ @ SuppressWarnings ("unchecked" )
888+ List <Msg > compressedMessages =
889+ (List <Msg >) method .invoke (mem , toolOnlyMessages , summaryMsg , null );
890+
891+ assertEquals (
892+ 2 ,
893+ compressedMessages .size (),
894+ "Tool-only current round should still emit assistant and tool messages" );
895+
896+ Msg assistantMsg = compressedMessages .get (0 );
897+ assertEquals (
898+ MsgRole .ASSISTANT ,
899+ assistantMsg .getRole (),
900+ "First compressed message should be assistant" );
901+ assertEquals (
902+ "Tool-only summary" ,
903+ assistantMsg .getTextContent (),
904+ "Assistant fallback should keep the summary text when no tool-use blocks exist" );
905+ assertFalse (
906+ assistantMsg .hasContentBlocks (ToolUseBlock .class ),
907+ "Tool-only fallback should not fabricate ToolUseBlock content" );
908+ assertTrue (
909+ assistantMsg .getMetadata ().isEmpty (),
910+ "Assistant metadata should stay empty when summary metadata is absent" );
911+
912+ Msg toolMsg = compressedMessages .get (1 );
913+ assertEquals (
914+ MsgRole .TOOL , toolMsg .getRole (), "Second compressed message should be tool role" );
915+ ToolResultBlock resultBlock = toolMsg .getFirstContentBlock (ToolResultBlock .class );
916+ assertNotNull (
917+ resultBlock , "Compressed tool message should preserve ToolResultBlock structure" );
918+ assertEquals ("call_only" , resultBlock .getId (), "Tool result id should be preserved" );
919+ assertNull (
920+ resultBlock .getName (), "Null tool names should stay null on the preserved block" );
921+ assertEquals (
922+ "Tool result for tool is summarized in the paired assistant compression message." ,
923+ resultBlock .getOutput ().stream ()
924+ .filter (TextBlock .class ::isInstance )
925+ .map (TextBlock .class ::cast )
926+ .map (TextBlock ::getText )
927+ .findFirst ()
928+ .orElse ("" ),
929+ "Placeholder should fall back to the generic tool label without an offload tag" );
930+ assertTrue (
931+ toolMsg .getMetadata ().containsKey ("_compress_meta" ),
932+ "Compressed tool message should still include compression metadata wrapper" );
933+ assertTrue (
934+ ((Map <?, ?>) toolMsg .getMetadata ().get ("_compress_meta" )).isEmpty (),
935+ "Compression metadata should stay empty when no offload uuid is provided" );
936+ }
937+
809938 @ Test
810939 @ DisplayName (
811940 "Should skip tool message compression when token count is below"
0 commit comments