diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index 70695256a..91651a92d 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -376,7 +376,7 @@ private static List rearrangeEventsForAsyncFunctionResponsesInHistory( // Gemini 3 requires function calls to be grouped first and only then function responses: // FC1 FC2 FR1 FR2 - boolean shouldBufferResponseEvents = modelName.startsWith("gemini-3"); + boolean shouldBufferResponseEvents = modelName.startsWith("gemini-3-"); for (int i = 0; i < events.size(); i++) { Event event = events.get(i); diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java index 0e4bdc3cb..e1756d09f 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java @@ -26,6 +26,7 @@ import com.google.adk.artifacts.InMemoryArtifactService; import com.google.adk.events.Event; import com.google.adk.models.LlmRequest; +import com.google.adk.models.Model; import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; import com.google.common.collect.ImmutableList; @@ -42,6 +43,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.Mockito; /** Unit tests for {@link Contents}. */ @RunWith(JUnit4.class) @@ -464,6 +466,30 @@ public void processRequest_sequentialFCFR_returnsOriginalList() { assertThat(result).isEqualTo(eventsToContents(inputEvents)); } + @Test + public void rearrangeHistory_gemini3interleavedFCFR_groupsFcAndFr() { + Event u1 = createUserEvent("u1", "Query"); + Event fc1 = createFunctionCallEvent("fc1", "tool1", "call1"); + Event fr1 = createFunctionResponseEvent("fr1", "tool1", "call1"); + Event fc2 = createFunctionCallEvent("fc2", "tool2", "call2"); + Event fr2 = createFunctionResponseEvent("fr2", "tool2", "call2"); + + ImmutableList inputEvents = ImmutableList.of(u1, fc1, fr1, fc2, fr2); + + List result = runContentsProcessorWithModelName(inputEvents, "gemini-3-flash-exp"); + + assertThat(result).hasSize(4); + assertThat(result.get(0)).isEqualTo(u1.content().get()); + assertThat(result.get(1)).isEqualTo(fc1.content().get()); + assertThat(result.get(2)).isEqualTo(fc2.content().get()); + Content mergedContent = result.get(3); + assertThat(mergedContent.parts().get()).hasSize(2); + assertThat(mergedContent.parts().get().get(0).functionResponse().get().name()) + .hasValue("tool1"); + assertThat(mergedContent.parts().get().get(1).functionResponse().get().name()) + .hasValue("tool2"); + } + private static Event createUserEvent(String id, String text) { return Event.builder() .id(id) @@ -628,6 +654,38 @@ private List runContentsProcessorWithIncludeContents( return result.updatedRequest().contents(); } + private List runContentsProcessorWithModelName(List events, String modelName) { + LlmAgent agent = + Mockito.spy( + LlmAgent.builder() + .name(AGENT) + .includeContents(LlmAgent.IncludeContents.DEFAULT) + .build()); + Model model = Model.builder().modelName(modelName).build(); + Mockito.doReturn(model).when(agent).resolvedModel(); + + Session session = + Session.builder("test-session") + .appName("test-app") + .userId("test-user") + .events(new ArrayList<>(events)) + .build(); + InvocationContext context = + InvocationContext.create( + new InMemorySessionService(), + new InMemoryArtifactService(), + "test-invocation", + agent, + session, + /* userContent= */ null, + RunConfig.builder().build()); + + LlmRequest initialRequest = LlmRequest.builder().build(); + RequestProcessor.RequestProcessingResult result = + contentsProcessor.processRequest(context, initialRequest).blockingGet(); + return result.updatedRequest().contents(); + } + private static ImmutableList eventsToContents(List events) { return events.stream() .map(Event::content)