From 6291a53c4a6a03e03645af0d931de2bb50903f11 Mon Sep 17 00:00:00 2001 From: Simon Su Date: Wed, 10 Jun 2026 00:05:14 +1000 Subject: [PATCH] fix: ignore usage-only responses outside bidi --- .../adk/flows/llmflows/BaseLlmFlow.java | 3 +- .../adk/flows/llmflows/BaseLlmFlowTest.java | 35 +++++++++++++++++-- .../adk/flows/llmflows/CodeExecutionTest.java | 33 +++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java index 3b28761a1..4cb2c009b 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java @@ -720,7 +720,8 @@ private Flowable buildPostprocessingEvents( && updatedResponse.errorCode().isEmpty() && !updatedResponse.interrupted().orElse(false) && !updatedResponse.turnComplete().orElse(false) - && updatedResponse.usageMetadata().isEmpty() + && (context.runConfig().streamingMode() != StreamingMode.BIDI + || updatedResponse.usageMetadata().isEmpty()) && updatedResponse.inputTranscription().isEmpty() && updatedResponse.outputTranscription().isEmpty()) { return processorEvents; diff --git a/core/src/test/java/com/google/adk/flows/llmflows/BaseLlmFlowTest.java b/core/src/test/java/com/google/adk/flows/llmflows/BaseLlmFlowTest.java index a58a206d9..5c101e2df 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/BaseLlmFlowTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/BaseLlmFlowTest.java @@ -31,6 +31,7 @@ import com.google.adk.agents.InvocationContext; import com.google.adk.agents.LlmAgent; import com.google.adk.agents.ReadonlyContext; +import com.google.adk.agents.RunConfig; import com.google.adk.events.Event; import com.google.adk.flows.llmflows.RequestProcessor.RequestProcessingResult; import com.google.adk.flows.llmflows.ResponseProcessor.ResponseProcessingResult; @@ -736,7 +737,7 @@ public void run_responseWithTranscriptions_propagatesTranscriptionsToEvent() { } @Test - public void postprocess_noResponseProcessors_onlyUsageMetadata_returnsEvent() { + public void postprocess_noResponseProcessors_onlyUsageMetadata_returnsNoEvent() { GenerateContentResponseUsageMetadata usageMetadata = createGenerateContentResponseUsageMetadata().build(); LlmResponse llmResponse = LlmResponse.builder().usageMetadata(usageMetadata).build(); @@ -760,12 +761,40 @@ public void postprocess_noResponseProcessors_onlyUsageMetadata_returnsEvent() { .toList() .blockingGet(); + assertThat(events).isEmpty(); + } + + @Test + public void postprocess_bidiUsageMetadataOnlyResponse_returnsEvent() { + GenerateContentResponseUsageMetadata usageMetadata = + createGenerateContentResponseUsageMetadata().build(); + LlmResponse llmResponse = LlmResponse.builder().usageMetadata(usageMetadata).build(); + InvocationContext invocationContext = + createInvocationContext( + createTestAgent(createTestLlm(llmResponse)), + RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.BIDI).build()); + BaseLlmFlow baseLlmFlow = createBaseLlmFlowWithoutProcessors(); + Event baseEvent = + Event.builder() + .invocationId(invocationContext.invocationId()) + .author(invocationContext.agent().name()) + .build(); + + List events = + baseLlmFlow + .postprocess( + invocationContext, + baseEvent, + LlmRequest.builder().build(), + llmResponse, + Context.current()) + .toList() + .blockingGet(); + assertThat(events).hasSize(1); Event event = getOnlyElement(events); assertThat(event.content()).isEmpty(); assertThat(event.usageMetadata()).hasValue(usageMetadata); - assertThat(event.author()).isEqualTo(invocationContext.agent().name()); - assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId()); } @Test diff --git a/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java b/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java index 1485ca2c4..7e8882939 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/CodeExecutionTest.java @@ -16,6 +16,8 @@ package com.google.adk.flows.llmflows; +import static com.google.adk.testing.TestUtils.createGenerateContentResponseUsageMetadata; +import static com.google.adk.testing.TestUtils.createInvocationContext; import static com.google.adk.testing.TestUtils.createLlmResponse; import static com.google.adk.testing.TestUtils.createTestAgentBuilder; import static com.google.adk.testing.TestUtils.createTestLlm; @@ -122,6 +124,37 @@ public void testResponseProcessor_withCode_executesCode() { .hasValue("Code execution result:\nhello\n\n"); } + @Test + public void run_withCodeExecutionResponseAndUsageMetadata_continuesToFinalAnswer() { + String code = "print('hello')"; + Content codeResponseContent = + Content.builder() + .role("model") + .parts(Part.fromText("```tool_code\n" + code + "\n```")) + .build(); + var usageMetadata = createGenerateContentResponseUsageMetadata().build(); + LlmResponse codeResponse = + createLlmResponse(codeResponseContent).toBuilder().usageMetadata(usageMetadata).build(); + Content finalResponseContent = Content.fromParts(Part.fromText("Done.")); + testLlm = createTestLlm(codeResponse, createLlmResponse(finalResponseContent)); + agent = createTestAgentBuilder(testLlm).codeExecutor(mockCodeExecutor).build(); + InvocationContext realInvocationContext = createInvocationContext(agent); + CodeExecutionResult executionResult = CodeExecutionResult.builder().stdout("hello\n").build(); + when(mockCodeExecutor.errorRetryAttempts()).thenReturn(2); + when(mockCodeExecutor.executeCode(any(), any())).thenReturn(executionResult); + when(mockCodeExecutor.codeBlockDelimiters()) + .thenReturn(ImmutableList.of(ImmutableList.of("```tool_code\n", "\n```"))); + + ImmutableList events = + ImmutableList.copyOf(new SingleFlow().run(realInvocationContext).toList().blockingGet()); + + assertThat(testLlm.getRequests()).hasSize(2); + assertThat(events).hasSize(3); + assertThat(events.get(0).usageMetadata()).isEmpty(); + assertThat(events.get(1).hasTrailingCodeExecutionResult()).isTrue(); + assertThat(events.get(2).content()).hasValue(finalResponseContent); + } + @Test public void testRequestProcessor_withCode_hasNoErrors() throws Exception { // arrange