Environment
- adk-java: 1.5.0 (latest)
- Model: gemini-3-flash-preview / gemini-3.1-flash-lite (Vertex AI)
- StreamingMode: SSE
Summary
With a Gemini 3.x model + SSE streaming + function calling, after a tool
executes, the agent produces an empty final response (no summary text ever
reaches the client). The exact same agent works correctly with Claude models
and also works when StreamingMode is NONE.
Root cause (traced through source, adk-java 1.5.0)
In SSE streaming, Gemini 3.x returns the functionCall and then a trailing
empty chunk: content is present with a single part whose text is "",
finishReason=STOP, partial=false, and no functionCall/functionResponse.
BaseLlmFlow.buildPostprocessingEvents only drops responses whose
content() is fully empty. This chunk has content present (a part with an
empty-string text), so it is NOT dropped and becomes an Event.
Event.finalResponse() returns true for it:
functionCalls().isEmpty() && functionResponses().isEmpty() && !partial().orElse(false) && !hasTrailingCodeExecutionResult() — all true.
BaseLlmFlow.run() checks Iterables.getLast(eventList).finalResponse().
Since this empty chunk is the last event produced by the step, the loop
terminates and the 2nd LLM call to summarize the tool result is never
issued.
Claude is unaffected because it returns text + functionCall in a single
response and does not emit a standalone empty chunk.
Which version introduced this (best-effort, from release notes)
- The empty stop-chunk first became reachable as a surviving
finalResponse
event in v0.8.0 (commit a0cba25, "Fixed issue where events were marked
empty if the first part had an empty text; now checks all parts for meaningful
content"). That change stopped treating an event as empty based on the first
part alone, so a "content-present-but-textless" Gemini 3.x chunk is no longer
filtered out. The same release (acffdb9) added Gemini 3.1 event buffering.
- A targeted fix, "Suppress empty-text-only chunks from streaming responses
while preserving carried metadata" (commit b4791ef), was added in
v1.4.0 but reverted in the same release (commit 69638df), and was
not restored in v1.5.0 — so the bug remains reachable through 1.5.0.
(Note: I could not verify the exact introduction commit by source diff; the
above is inferred from release notes and matches the observed behavior.)
Steps to reproduce
LlmAgent with a Gemini 3.x model + one FunctionTool, SSE streaming.
- Ask a question that triggers the tool.
- Observe: functionCall → tool executes → functionResponse → empty STOP chunk
→ loop ends. No summary text is produced.
Observed event sequence (from our tracing)
AFTER_MODEL #1: hasFunctionCall=true hasText=false finishReason=- (functionCall)
STREAM_EVENT: functionCalls=1 finalResponse=false
[tool executes]
STREAM_EVENT: functionResponses=1 finalResponse=false
AFTER_MODEL #2: hasFunctionCall=false hasText=false finishReason=STOP (EMPTY chunk)
STREAM_EVENT: finalResponse=true textLen=0 <-- loop terminates here
STREAM_COMPLETE: accumulatedTextLen=0
Expected
The empty trailing chunk should not terminate the multi-step loop; the flow
should issue the follow-up LLM call to summarize the tool result (as it does
with Claude, and as SSE + Gemini does once the empty chunk is removed).
Workaround we are using
In afterModelCallback, detect this throwaway empty chunk — content present,
not partial, and EVERY part has blank text AND none of the payload-bearing
fields (functionCall / functionResponse / thoughtSignature / inlineData /
fileData / executableCode / codeExecutionResult / videoMetadata) — and return a
content-less LlmResponse, so it hits ADK's own drop path in
buildPostprocessingEvents. This keeps SSE streaming and lets the loop
continue.
Important: chunks carrying a thoughtSignature (even with empty text) must NOT
be dropped, since they carry reasoning-chain context for multi-turn function
calling.
Notes
- Setting
StreamingMode.NONE also avoids the issue (Gemini returns a single
complete functionCall response, with no separate empty chunk).
- Related history:
b4791ef (v1.4.0) added the suppression fix, reverted by
69638df in the same release.
Environment
Summary
With a Gemini 3.x model + SSE streaming + function calling, after a tool
executes, the agent produces an empty final response (no summary text ever
reaches the client). The exact same agent works correctly with Claude models
and also works when StreamingMode is NONE.
Root cause (traced through source, adk-java 1.5.0)
In SSE streaming, Gemini 3.x returns the
functionCalland then a trailingempty chunk:
contentis present with a single part whosetextis"",finishReason=STOP,partial=false, and no functionCall/functionResponse.BaseLlmFlow.buildPostprocessingEventsonly drops responses whosecontent()is fully empty. This chunk hascontentpresent (a part with anempty-string text), so it is NOT dropped and becomes an
Event.Event.finalResponse()returnstruefor it:functionCalls().isEmpty() && functionResponses().isEmpty() && !partial().orElse(false) && !hasTrailingCodeExecutionResult()— all true.BaseLlmFlow.run()checksIterables.getLast(eventList).finalResponse().Since this empty chunk is the last event produced by the step, the loop
terminates and the 2nd LLM call to summarize the tool result is never
issued.
Claude is unaffected because it returns text + functionCall in a single
response and does not emit a standalone empty chunk.
Which version introduced this (best-effort, from release notes)
finalResponseevent in v0.8.0 (commit
a0cba25, "Fixed issue where events were markedempty if the first part had an empty text; now checks all parts for meaningful
content"). That change stopped treating an event as empty based on the first
part alone, so a "content-present-but-textless" Gemini 3.x chunk is no longer
filtered out. The same release (
acffdb9) added Gemini 3.1 event buffering.while preserving carried metadata" (commit
b4791ef), was added inv1.4.0 but reverted in the same release (commit
69638df), and wasnot restored in v1.5.0 — so the bug remains reachable through 1.5.0.
(Note: I could not verify the exact introduction commit by source diff; the
above is inferred from release notes and matches the observed behavior.)
Steps to reproduce
LlmAgentwith a Gemini 3.x model + oneFunctionTool, SSE streaming.→ loop ends. No summary text is produced.
Observed event sequence (from our tracing)
Expected
The empty trailing chunk should not terminate the multi-step loop; the flow
should issue the follow-up LLM call to summarize the tool result (as it does
with Claude, and as SSE + Gemini does once the empty chunk is removed).
Workaround we are using
In
afterModelCallback, detect this throwaway empty chunk — content present,not partial, and EVERY part has blank text AND none of the payload-bearing
fields (functionCall / functionResponse / thoughtSignature / inlineData /
fileData / executableCode / codeExecutionResult / videoMetadata) — and return a
content-less
LlmResponse, so it hits ADK's own drop path inbuildPostprocessingEvents. This keeps SSE streaming and lets the loopcontinue.
Important: chunks carrying a
thoughtSignature(even with empty text) must NOTbe dropped, since they carry reasoning-chain context for multi-turn function
calling.
Notes
StreamingMode.NONEalso avoids the issue (Gemini returns a singlecomplete functionCall response, with no separate empty chunk).
b4791ef(v1.4.0) added the suppression fix, reverted by69638dfin the same release.