Describe the bug
In ADK 2.7.0, 2.8.0, and current main, a valid non-built-in code-execution response that ends with finish_reason=STOP executes successfully but the LLM loop terminates with MODEL_RETURNED_NO_CONTENT before the model can consume the execution result and produce a final answer.
This differs from #5631. In that issue the provider's post-tool model call genuinely returns an empty response. Here the provider returned executable code; ADK itself clears that response's content as its internal signal to continue after executing the code.
Minimal reproduction
Use a deterministic BaseLlm that returns these two responses:
responses = [
LlmResponse(
content=types.Content(
role="model",
parts=[types.Part(text="```python\nprint(6 * 7)\n```")],
),
finish_reason=types.FinishReason.STOP,
),
LlmResponse(
content=types.Content(
role="model",
parts=[types.Part(text="The answer is 42.")],
),
finish_reason=types.FinishReason.STOP,
),
]
agent = Agent(
name="code_agent",
model=sequence_model,
code_executor=recording_code_executor,
)
events = testing_utils.InMemoryRunner(agent).run("What is 6 * 7?")
The recording executor can simply return:
CodeExecutionResult(stdout="42\n")
Observed behavior
- the model is called once
- the code executor runs successfully
- ADK emits the code and execution-result events
- ADK then emits
MODEL_RETURNED_NO_CONTENT
- the second configured model response is never requested
Expected behavior
- the model is called twice
- the second request includes the execution result
- the final answer is emitted
- a genuinely provider-empty non-streaming
STOP response still emits MODEL_RETURNED_NO_CONTENT
Root cause
BaseLlmFlow._postprocess_async currently runs response processors before validating a genuinely empty non-streaming STOP response.
The code-execution response processor intentionally sets llm_response.content = None after execution so the outer LLM loop continues. The subsequent empty-STOP guard mistakes that internal mutation for a provider-empty response and assigns MODEL_RETURNED_NO_CONTENT, preventing the continuation.
Moving the genuine-empty check before response processors preserves both contracts:
- provider-empty
STOP responses are still surfaced as errors
- processor-cleared code-execution responses remain the continuation sentinel
Environment
google-adk: reproduced with 2.7.0 and 2.8.0; code path is present on main
- Python: 3.14
- executor: non-built-in
BaseCodeExecutor
I have a small regression-tested patch prepared and can open it once this issue is filed.
Describe the bug
In ADK 2.7.0, 2.8.0, and current
main, a valid non-built-in code-execution response that ends withfinish_reason=STOPexecutes successfully but the LLM loop terminates withMODEL_RETURNED_NO_CONTENTbefore the model can consume the execution result and produce a final answer.This differs from #5631. In that issue the provider's post-tool model call genuinely returns an empty response. Here the provider returned executable code; ADK itself clears that response's content as its internal signal to continue after executing the code.
Minimal reproduction
Use a deterministic
BaseLlmthat returns these two responses:The recording executor can simply return:
Observed behavior
MODEL_RETURNED_NO_CONTENTExpected behavior
STOPresponse still emitsMODEL_RETURNED_NO_CONTENTRoot cause
BaseLlmFlow._postprocess_asynccurrently runs response processors before validating a genuinely empty non-streamingSTOPresponse.The code-execution response processor intentionally sets
llm_response.content = Noneafter execution so the outer LLM loop continues. The subsequent empty-STOPguard mistakes that internal mutation for a provider-empty response and assignsMODEL_RETURNED_NO_CONTENT, preventing the continuation.Moving the genuine-empty check before response processors preserves both contracts:
STOPresponses are still surfaced as errorsEnvironment
google-adk: reproduced with 2.7.0 and 2.8.0; code path is present onmainBaseCodeExecutorI have a small regression-tested patch prepared and can open it once this issue is filed.