Describe the Bug
After executing code, runPostProcessor clears the consumed model response's content but leaves its usageMetadata set. The flow then emits an extra content-less, usage-only event after the codeExecutionResult. BaseLlmFlow checks only the last event of the step, treats the usage-only event as final=true, and stops the loop — so the model is never called again with the code-execution result, breaking the CodeAct loop (model emits code → execute → emit result → should call model again for the final answer).
Root Cause
// CodeExecution.runPostProcessor, L299
llmResponseBuilder.content(Optional.empty()); // clears content but NOT usageMetadata
The builder (still carrying usageMetadata) is later built/emitted (around L160-166), producing the stray usage-only event.
Expected vs Observed
Expected: executableCode → codeExecutionResult (final=false) → [next LLM call] → final answer (final=true)
Observed: executableCode → codeExecutionResult (final=false) → usage-only event (final=true) ← loop stops
Suggested Fix
Also clear usageMetadata when clearing content (or do not emit an event when content is empty), so the post-code response does not present as a final event.
Note
This matches a known upstream report (custom/non-built-in BaseCodeExecutor, streaming/SSE, provider that returns usageMetadata). Search github.com/google/adk-java before any action; contribute a Java reproduction to the existing issue rather than opening a duplicate.
Corresponding Test (generated)
The generated tests for this method exercise processResponse (the post-processor) but crash on a mocked LlmResponse.toBuilder() returning null, so they do not themselves assert the usageMetadata defect — which is why our triage initially dismissed them. Included verbatim for completeness; the defect is confirmed from source + the upstream issue, not from this test:
@Test
public void testProcessResponse_WithCodeExecutorAndEvents_ReturnsMappedResultWithEvents() {
// Arrange
Content content = Content.builder().build();
LlmResponse mockResponse = mock(LlmResponse.class);
when(mockResponse.partial()).thenReturn(Optional.of(false));
when(mockResponse.content()).thenReturn(Optional.of(content));
when(invocationContext.agent()).thenReturn(agent);
when(agent.codeExecutor()).thenReturn(codeExecutor);
when(agent.name()).thenReturn("test-agent");
// Mock execution result to return events
Event mockEvent = mock(Event.class);
Flowable<Event> eventFlowable = Flowable.just(mockEvent);
// Mock the code execution result
when(codeExecutor.executeCode(any(), any())).thenReturn(executionResult);
when(executionResult.stdout()).thenReturn("stdout");
when(executionResult.stderr()).thenReturn("");
when(executionResult.outputFiles()).thenReturn(ImmutableList.of());
// Mock session to avoid null pointer
ConcurrentHashMap<String, Object> sessionState = new ConcurrentHashMap<>();
when(session.state()).thenReturn(sessionState);
// Act
Single<ResponseProcessor.ResponseProcessingResult> result =
processor.processResponse(invocationContext, mockResponse);
// Assert
ResponseProcessor.ResponseProcessingResult processedResult = result.blockingGet();
assertNotNull(processedResult);
assertEquals(mockResponse, processedResult.updatedResponse());
assertTrue(processedResult.events().iterator().hasNext());
assertFalse(processedResult.transferToAgent().isPresent());
}
Environment
- Project: adk / google-adk (
com.google.adk) · upstream: github.com/google/adk-java
- Version:
0.5.1-SNAPSHOT (local; upstream issue references 1.3.0) · Commit: <fill SUT commit>
- Java 17 · OS macOS
This input was generated by the test case generator TestFusion developed in our STAR lab.
Describe the Bug
After executing code,
runPostProcessorclears the consumed model response's content but leaves itsusageMetadataset. The flow then emits an extra content-less, usage-only event after thecodeExecutionResult.BaseLlmFlowchecks only the last event of the step, treats the usage-only event asfinal=true, and stops the loop — so the model is never called again with the code-execution result, breaking the CodeAct loop (model emits code → execute → emit result → should call model again for the final answer).Root Cause
The builder (still carrying
usageMetadata) is later built/emitted (around L160-166), producing the stray usage-only event.Expected vs Observed
Suggested Fix
Also clear
usageMetadatawhen clearing content (or do not emit an event when content is empty), so the post-code response does not present as a final event.Note
This matches a known upstream report (custom/non-built-in
BaseCodeExecutor, streaming/SSE, provider that returnsusageMetadata). Search github.com/google/adk-java before any action; contribute a Java reproduction to the existing issue rather than opening a duplicate.Corresponding Test (generated)
The generated tests for this method exercise
processResponse(the post-processor) but crash on a mockedLlmResponse.toBuilder()returningnull, so they do not themselves assert theusageMetadatadefect — which is why our triage initially dismissed them. Included verbatim for completeness; the defect is confirmed from source + the upstream issue, not from this test:Environment
com.google.adk) · upstream: github.com/google/adk-java0.5.1-SNAPSHOT(local; upstream issue references 1.3.0) · Commit:<fill SUT commit>This input was generated by the test case generator
TestFusiondeveloped in our STAR lab.