Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 133 additions & 26 deletions core/src/main/java/com/google/adk/flows/llmflows/Contents.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
import com.google.adk.models.LlmRequest;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.genai.types.Content;
import com.google.genai.types.FunctionCall;
import com.google.genai.types.FunctionResponse;
Expand All @@ -41,6 +44,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -176,7 +180,8 @@ private ImmutableList<Content> getContents(
}

if (hasCompactEvent) {
filteredEvents = processCompactionEvent(filteredEvents, agentName);
List<Event> compactedEvents = processCompactionEvent(filteredEvents, agentName);
filteredEvents = recoverCompactedFunctionCalls(compactedEvents, filteredEvents);
}

// Merge after compaction, which may end mid-run, and before other agents are narrated.
Expand Down Expand Up @@ -529,6 +534,84 @@ private static Event createCompactionEvent(Event event, String agentName) {
.build();
}

/**
* Re-injects function-call events from {@code sourceEvents} when compaction summarized away a
* call whose response arrived later. Restores the full call event to preserve first-part thought
* signatures on parallel calls, along with the latest compacted response for each sibling call so
* siblings do not appear pending. Resolves call ownership in {@code sourceEvents} order by event
* identity so reused call IDs bind to the latest preceding call.
*/
private static List<Event> recoverCompactedFunctionCalls(
List<Event> events, List<Event> sourceEvents) {
Set<Event> presentEvents = Sets.newIdentityHashSet();
presentEvents.addAll(events);

Map<String, Event> latestCallById = new HashMap<>();
IdentityHashMap<Event, List<Event>> callsByResponse = new IdentityHashMap<>();
IdentityHashMap<Event, Map<String, Event>> responseByCall = new IdentityHashMap<>();
IdentityHashMap<Event, Set<String>> survivingResponses = new IdentityHashMap<>();

for (Event event : sourceEvents) {
for (FunctionCall functionCall : event.functionCalls()) {
functionCall
.id()
.filter(id -> !id.isEmpty())
.ifPresent(id -> latestCallById.put(id, event));
}
for (FunctionResponse functionResponse : event.functionResponses()) {
String responseId = functionResponse.id().orElse("");
Event callEvent = latestCallById.get(responseId);
if (callEvent == null) {
continue;
}
callsByResponse.computeIfAbsent(event, k -> new ArrayList<>()).add(callEvent);
if (presentEvents.contains(event)) {
survivingResponses.computeIfAbsent(callEvent, k -> new HashSet<>()).add(responseId);
}
// Keep the latest update for this call; equal timestamps retain source order.
Map<String, Event> responsesForCall =
responseByCall.computeIfAbsent(callEvent, k -> new HashMap<>());
Event existing = responsesForCall.get(responseId);
if (existing == null || event.timestamp() >= existing.timestamp()) {
responsesForCall.put(responseId, event);
}
}
}

List<Event> result = new ArrayList<>();
Set<Event> reinjectedCalls = Sets.newIdentityHashSet();
Set<Event> reinjectedResponses = Sets.newIdentityHashSet();
for (Event event : events) {
for (Event callEvent : callsByResponse.getOrDefault(event, ImmutableList.of())) {
if (presentEvents.contains(callEvent) || !reinjectedCalls.add(callEvent)) {
continue;
}
result.add(callEvent);
Set<String> survivingForCall =
survivingResponses.getOrDefault(callEvent, ImmutableSet.of());
Map<String, Event> responsesForCall =
responseByCall.getOrDefault(callEvent, ImmutableMap.of());
Set<Event> siblingResponsesToAdd = Sets.newIdentityHashSet();
for (FunctionCall siblingCall : callEvent.functionCalls()) {
String siblingId = siblingCall.id().orElse("");
if (!siblingId.isEmpty() && !survivingForCall.contains(siblingId)) {
Event siblingResponse = responsesForCall.get(siblingId);
if (siblingResponse != null && reinjectedResponses.add(siblingResponse)) {
siblingResponsesToAdd.add(siblingResponse);
}
}
}
for (Event sourceEvent : sourceEvents) {
if (siblingResponsesToAdd.contains(sourceEvent)) {
result.add(sourceEvent);
}
}
}
result.add(event);
}
return result;
}

/** Whether the event is a reply from another agent. */
private static boolean isOtherAgentReply(String agentName, Event event) {
return !agentName.isEmpty()
Expand Down Expand Up @@ -770,27 +853,50 @@ private static List<Event> rearrangeEventsForLatestFunctionResponse(List<Event>

private static List<Event> rearrangeEventsForAsyncFunctionResponsesInHistory(
List<Event> events, boolean groupFunctionResponses) {
Map<String, Integer> functionCallIdToResponseEventIndex = new HashMap<>();
// Pair each response with the latest preceding call of that ID, keeping the last update.
Map<String, List<Integer>> callEventIndicesById = new HashMap<>();
for (int i = 0; i < events.size(); i++) {
final int index = i;
Event event = events.get(index);
event
.content()
.flatMap(Content::parts)
.ifPresent(
parts -> {
for (Part part : parts) {
part.functionResponse()
.ifPresent(
response ->
response
.id()
.ifPresent(
functionCallId ->
functionCallIdToResponseEventIndex.put(
functionCallId, index)));
}
});
Event event = events.get(i);
if (!event.functionResponses().isEmpty()) {
continue;
}
for (FunctionCall call : event.functionCalls()) {
final int callIndex = i;
call.id()
.ifPresent(
id ->
callEventIndicesById
.computeIfAbsent(id, k -> new ArrayList<>())
.add(callIndex));
}
}

Map<Integer, Map<String, Integer>> responseEventIndexByCall = new HashMap<>();
for (int i = 0; i < events.size(); i++) {
final int responseIndex = i;
Event event = events.get(responseIndex);
for (FunctionResponse response : event.functionResponses()) {
response
.id()
.ifPresent(
functionCallId -> {
List<Integer> callIndices = callEventIndicesById.get(functionCallId);
if (callIndices == null || callIndices.isEmpty()) {
return;
}
int owningCallIndex = callIndices.get(0);
for (int callIdx : callIndices) {
if (callIdx < responseIndex) {
owningCallIndex = callIdx;
} else {
break;
}
}
responseEventIndexByCall
.computeIfAbsent(owningCallIndex, k -> new HashMap<>())
.put(functionCallId, responseIndex);
});
}
}

List<Event> resultEvents = new ArrayList<>();
Expand Down Expand Up @@ -818,6 +924,8 @@ private static List<Event> rearrangeEventsForAsyncFunctionResponsesInHistory(
.orElse(false);

if (hasFunctionCalls) {
Map<String, Integer> responsesForCall =
responseEventIndexByCall.getOrDefault(i, ImmutableMap.of());
Set<Integer> responseEventIndices = new HashSet<>();
// Iterate through parts again to get function call IDs
partsOptional
Expand All @@ -830,11 +938,10 @@ private static List<Event> rearrangeEventsForAsyncFunctionResponsesInHistory(
call.id()
.ifPresent(
functionCallId -> {
if (functionCallIdToResponseEventIndex.containsKey(
functionCallId)) {
responseEventIndices.add(
functionCallIdToResponseEventIndex.get(
functionCallId));
Integer responseEventIndex =
responsesForCall.get(functionCallId);
if (responseEventIndex != null) {
responseEventIndices.add(responseEventIndex);
}
})));

Expand Down
Loading
Loading