Skip to content

Commit e432360

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: recover compacted function calls and pair reused call IDs in LLM content assembly
PiperOrigin-RevId: 995900828
1 parent 461f7d6 commit e432360

2 files changed

Lines changed: 595 additions & 34 deletions

File tree

‎core/src/main/java/com/google/adk/flows/llmflows/Contents.java‎

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import com.google.adk.models.LlmRequest;
3030
import com.google.common.base.Strings;
3131
import com.google.common.collect.ImmutableList;
32+
import com.google.common.collect.ImmutableMap;
33+
import com.google.common.collect.ImmutableSet;
3234
import com.google.common.collect.Iterables;
3335
import com.google.common.collect.Lists;
3436
import com.google.genai.types.Content;
@@ -41,6 +43,7 @@
4143
import java.util.Collections;
4244
import java.util.HashMap;
4345
import java.util.HashSet;
46+
import java.util.IdentityHashMap;
4447
import java.util.List;
4548
import java.util.Map;
4649
import 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

Comments
 (0)