2929import com .google .adk .models .LlmRequest ;
3030import com .google .common .base .Strings ;
3131import com .google .common .collect .ImmutableList ;
32+ import com .google .common .collect .ImmutableMap ;
33+ import com .google .common .collect .ImmutableSet ;
3234import com .google .common .collect .Iterables ;
3335import com .google .common .collect .Lists ;
3436import com .google .genai .types .Content ;
4143import java .util .Collections ;
4244import java .util .HashMap ;
4345import java .util .HashSet ;
46+ import java .util .IdentityHashMap ;
4447import java .util .List ;
4548import java .util .Map ;
4649import java .util .Optional ;
@@ -176,7 +179,8 @@ private ImmutableList<Content> getContents(
176179 }
177180
178181 if (hasCompactEvent ) {
179- filteredEvents = processCompactionEvent (filteredEvents , agentName );
182+ List <Event > compactedEvents = processCompactionEvent (filteredEvents , agentName );
183+ filteredEvents = recoverCompactedFunctionCalls (compactedEvents , filteredEvents );
180184 }
181185
182186 // Merge after compaction, which may end mid-run, and before other agents are narrated.
@@ -529,6 +533,83 @@ private static Event createCompactionEvent(Event event, String agentName) {
529533 .build ();
530534 }
531535
536+ /**
537+ * Re-injects function-call events from {@code sourceEvents} when compaction summarized away a
538+ * call whose response arrived later. Restores the full call event to preserve first-part thought
539+ * signatures on parallel calls, along with the latest compacted response for each sibling call so
540+ * siblings do not appear pending. Resolves call ownership in {@code sourceEvents} order by event
541+ * identity so reused call IDs bind to the latest preceding call.
542+ */
543+ private static List <Event > recoverCompactedFunctionCalls (
544+ List <Event > events , List <Event > sourceEvents ) {
545+ Set <Event > presentEvents = Collections .newSetFromMap (new IdentityHashMap <>());
546+ presentEvents .addAll (events );
547+
548+ Map <String , Event > latestCallById = new HashMap <>();
549+ IdentityHashMap <Event , List <Event >> callsByResponse = new IdentityHashMap <>();
550+ IdentityHashMap <Event , Map <String , Event >> responseByCall = new IdentityHashMap <>();
551+ IdentityHashMap <Event , Set <String >> survivingResponses = new IdentityHashMap <>();
552+
553+ for (Event event : sourceEvents ) {
554+ for (FunctionCall functionCall : event .functionCalls ()) {
555+ functionCall
556+ .id ()
557+ .filter (id -> !id .isEmpty ())
558+ .ifPresent (id -> latestCallById .put (id , event ));
559+ }
560+ for (FunctionResponse functionResponse : event .functionResponses ()) {
561+ String responseId = functionResponse .id ().orElse ("" );
562+ Event callEvent = latestCallById .get (responseId );
563+ if (callEvent == null ) {
564+ continue ;
565+ }
566+ callsByResponse .computeIfAbsent (event , k -> new ArrayList <>()).add (callEvent );
567+ if (presentEvents .contains (event )) {
568+ survivingResponses .computeIfAbsent (callEvent , k -> new HashSet <>()).add (responseId );
569+ }
570+ // Keep the latest update for this call; equal timestamps retain source order.
571+ Map <String , Event > responsesForCall =
572+ responseByCall .computeIfAbsent (callEvent , k -> new HashMap <>());
573+ Event existing = responsesForCall .get (responseId );
574+ if (existing == null || event .timestamp () >= existing .timestamp ()) {
575+ responsesForCall .put (responseId , event );
576+ }
577+ }
578+ }
579+
580+ List <Event > result = new ArrayList <>();
581+ Set <Event > reinjectedCalls = Collections .newSetFromMap (new IdentityHashMap <>());
582+ for (Event event : events ) {
583+ for (Event callEvent : callsByResponse .getOrDefault (event , ImmutableList .of ())) {
584+ if (presentEvents .contains (callEvent ) || !reinjectedCalls .add (callEvent )) {
585+ continue ;
586+ }
587+ result .add (callEvent );
588+ Set <String > survivingForCall =
589+ survivingResponses .getOrDefault (callEvent , ImmutableSet .of ());
590+ Map <String , Event > responsesForCall =
591+ responseByCall .getOrDefault (callEvent , ImmutableMap .of ());
592+ Set <Event > siblingResponsesToAdd = Collections .newSetFromMap (new IdentityHashMap <>());
593+ for (FunctionCall siblingCall : callEvent .functionCalls ()) {
594+ String siblingId = siblingCall .id ().orElse ("" );
595+ if (!siblingId .isEmpty () && !survivingForCall .contains (siblingId )) {
596+ Event siblingResponse = responsesForCall .get (siblingId );
597+ if (siblingResponse != null ) {
598+ siblingResponsesToAdd .add (siblingResponse );
599+ }
600+ }
601+ }
602+ for (Event sourceEvent : sourceEvents ) {
603+ if (siblingResponsesToAdd .contains (sourceEvent )) {
604+ result .add (sourceEvent );
605+ }
606+ }
607+ }
608+ result .add (event );
609+ }
610+ return result ;
611+ }
612+
532613 /** Whether the event is a reply from another agent. */
533614 private static boolean isOtherAgentReply (String agentName , Event event ) {
534615 return !agentName .isEmpty ()
@@ -770,27 +851,25 @@ private static List<Event> rearrangeEventsForLatestFunctionResponse(List<Event>
770851
771852 private static List <Event > rearrangeEventsForAsyncFunctionResponsesInHistory (
772853 List <Event > events , boolean groupFunctionResponses ) {
773- Map <String , Integer > functionCallIdToResponseEventIndex = new HashMap <>();
854+ // Pair each response with the latest preceding call of that ID, keeping the last update.
855+ Map <String , Integer > latestCallIndexById = new HashMap <>();
856+ Map <Integer , Map <String , Integer >> responseEventIndexByCall = new HashMap <>();
774857 for (int i = 0 ; i < events .size (); i ++) {
775- final int index = i ;
776- Event event = events .get (index );
777- event
778- .content ()
779- .flatMap (Content ::parts )
780- .ifPresent (
781- parts -> {
782- for (Part part : parts ) {
783- part .functionResponse ()
784- .ifPresent (
785- response ->
786- response
787- .id ()
788- .ifPresent (
789- functionCallId ->
790- functionCallIdToResponseEventIndex .put (
791- functionCallId , index )));
792- }
793- });
858+ final int eventIndex = i ;
859+ Event event = events .get (eventIndex );
860+ for (FunctionCall call : event .functionCalls ()) {
861+ call .id ().ifPresent (id -> latestCallIndexById .put (id , eventIndex ));
862+ }
863+ for (FunctionResponse response : event .functionResponses ()) {
864+ response
865+ .id ()
866+ .map (latestCallIndexById ::get )
867+ .ifPresent (
868+ owningCallIndex ->
869+ responseEventIndexByCall
870+ .computeIfAbsent (owningCallIndex , k -> new HashMap <>())
871+ .put (response .id ().get (), eventIndex ));
872+ }
794873 }
795874
796875 List <Event > resultEvents = new ArrayList <>();
@@ -818,6 +897,8 @@ private static List<Event> rearrangeEventsForAsyncFunctionResponsesInHistory(
818897 .orElse (false );
819898
820899 if (hasFunctionCalls ) {
900+ Map <String , Integer > responsesForCall =
901+ responseEventIndexByCall .getOrDefault (i , ImmutableMap .of ());
821902 Set <Integer > responseEventIndices = new HashSet <>();
822903 // Iterate through parts again to get function call IDs
823904 partsOptional
@@ -830,11 +911,9 @@ private static List<Event> rearrangeEventsForAsyncFunctionResponsesInHistory(
830911 call .id ()
831912 .ifPresent (
832913 functionCallId -> {
833- if (functionCallIdToResponseEventIndex .containsKey (
834- functionCallId )) {
835- responseEventIndices .add (
836- functionCallIdToResponseEventIndex .get (
837- functionCallId ));
914+ Integer respIdx = responsesForCall .get (functionCallId );
915+ if (respIdx != null ) {
916+ responseEventIndices .add (respIdx );
838917 }
839918 })));
840919
0 commit comments