Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,14 @@ private static List<Event> rearrangeEventsForAsyncFunctionResponsesInHistory(
resultEvents.add(mergeFunctionResponseEvents(responseEventsToAdd));
}
}

// Skip events between the function call and the last function response.
if (!sortedIndices.isEmpty()) {
int maxIndex = sortedIndices.get(sortedIndices.size() - 1);
if (maxIndex > i) {
i = maxIndex;
}
}
Comment on lines +540 to +546
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This change correctly skips intermediate events. However, I've noticed a potential logic issue in the surrounding method. The functionCallIdToResponseEventIndex map only stores the index of the last function response for a given call ID. This prevents merging of multiple responses for a single function call, as only the last response is ever processed. mergeFunctionResponseEvents is not called in this case.

For example, in the rearrangeHistory_multipleFRsForSameFC_returnsMergedFR test, fr1, fr2, and fr3 should be merged, but with the current logic, only fr3 is picked up and added to the results.

To properly support merging, you might consider changing functionCallIdToResponseEventIndex to a Map<String, List<Integer>> to track all responses for a function call. This seems like a significant issue, so I've marked it as high severity.

}
} else {
// gemini-3 specific part: buffer response events
Expand Down
66 changes: 60 additions & 6 deletions core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -203,27 +203,81 @@ public void rearrangeHistory_asyncFR_returnsRearrangedList() {
public void rearrangeHistory_multipleFRsForSameFC_returnsMergedFR() {
Event fcEvent = createFunctionCallEvent("fc1", "tool1", "call1");
Event frEvent1 =
createFunctionResponseEvent("fr1", "tool1", "call1", ImmutableMap.of("status", "running"));
createFunctionResponseEvent("fr1", "tool1", "call1", ImmutableMap.of("status", "pending"));
Event frEvent2 =
createFunctionResponseEvent("fr2", "tool1", "call1", ImmutableMap.of("status", "done"));
createFunctionResponseEvent("fr2", "tool1", "call1", ImmutableMap.of("status", "running"));
Event frEvent3 =
createFunctionResponseEvent("fr3", "tool1", "call1", ImmutableMap.of("status", "done"));
ImmutableList<Event> inputEvents =
ImmutableList.of(
createUserEvent("u1", "Query"),
fcEvent,
createUserEvent("u2", "Wait"),
frEvent1,
createUserEvent("u3", "Done?"),
frEvent2);
frEvent2,
frEvent3,
createUserEvent("u4", "Follow up query"));

List<Content> result = runContentsProcessor(inputEvents);

assertThat(result).hasSize(3); // u1, fc1, merged_fr
assertThat(result).hasSize(4); // u1, fc1, merged_fr, u4
assertThat(result.get(0)).isEqualTo(inputEvents.get(0).content().get());
assertThat(result.get(1)).isEqualTo(inputEvents.get(1).content().get()); // Check merged event
assertThat(result.get(1)).isEqualTo(inputEvents.get(1).content().get()); // Check fcEvent
Content mergedContent = result.get(2);
assertThat(mergedContent.parts().get()).hasSize(1);
assertThat(mergedContent.parts().get().get(0).functionResponse().get().response().get())
.containsExactly("status", "done"); // Last FR wins
.containsExactly("status", "done"); // Last FR wins (frEvent3)
assertThat(result.get(3)).isEqualTo(inputEvents.get(7).content().get()); // u4
}

@Test
public void rearrangeHistory_multipleFRsForMultipleFC_returnsMergedFR() {
Event fcEvent1 = createFunctionCallEvent("fc1", "tool1", "call1");
Event fcEvent2 = createFunctionCallEvent("fc2", "tool1", "call2");

Event frEvent1 =
createFunctionResponseEvent("fr1", "tool1", "call1", ImmutableMap.of("status", "pending"));
Event frEvent2 =
createFunctionResponseEvent("fr2", "tool1", "call1", ImmutableMap.of("status", "done"));

Event frEvent3 =
createFunctionResponseEvent("fr3", "tool1", "call2", ImmutableMap.of("status", "pending"));
Event frEvent4 =
createFunctionResponseEvent("fr4", "tool1", "call2", ImmutableMap.of("status", "done"));

ImmutableList<Event> inputEvents =
ImmutableList.of(
createUserEvent("u1", "I"),
fcEvent1,
createUserEvent("u2", "am"),
frEvent1,
createUserEvent("u3", "waiting"),
frEvent2,
createUserEvent("u4", "for"),
fcEvent2,
createUserEvent("u5", "you"),
frEvent3,
createUserEvent("u6", "to"),
frEvent4,
createUserEvent("u7", "Follow up query"));

List<Content> result = runContentsProcessor(inputEvents);

assertThat(result).hasSize(7); // u1, fc1, merged_fr, u4, fc2, merged_fr2, u7
assertThat(result.get(0)).isEqualTo(inputEvents.get(0).content().get()); // u1
assertThat(result.get(1)).isEqualTo(inputEvents.get(1).content().get()); // fc1
Content mergedContent = result.get(2);
assertThat(mergedContent.parts().get()).hasSize(1);
assertThat(mergedContent.parts().get().get(0).functionResponse().get().response().get())
.containsExactly("status", "done"); // Last FR wins (frEvent2)
assertThat(result.get(3)).isEqualTo(inputEvents.get(6).content().get()); // u4
assertThat(result.get(4)).isEqualTo(inputEvents.get(7).content().get()); // fc2
Content mergedContent2 = result.get(5);
assertThat(mergedContent2.parts().get()).hasSize(1);
assertThat(mergedContent2.parts().get().get(0).functionResponse().get().response().get())
.containsExactly("status", "done"); // Last FR wins (merged_fr2)
assertThat(result.get(6)).isEqualTo(inputEvents.get(12).content().get()); // u7
}

@Test
Expand Down