Skip to content

[BUG] CodeExecution emits a stray usage-only event that ends the CodeAct loop (already reported upstream) #1276

@SuperCorleone

Description

@SuperCorleone

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions