When a Gemini live session returns multiple function calls in a single toolCall
message, ADK executes only the last one; the earlier parallel calls are silently
dropped, produce no tool response, and leave the session in a mismatched state.
Please make sure you read the contribution guide and file the issues in the right place.
Contribution guide.
🔴 Required Information
Describe the Bug:
On the live/BIDI path (Runner.runLive + LiveRequestQueue), when the Gemini
Live API emits a single BidiGenerateContentToolCall message containing multiple
function calls (parallel tool calling), only the last call is executed. In
GeminiLlmConnection.createToolCallResponse, the response Content is rebuilt
inside the loop over the calls, so each iteration overwrites the previous one
and only the final FunctionCall survives:
// GeminiLlmConnection.createToolCallResponse (before)
toolCall
.functionCalls()
.ifPresent(
calls -> {
for (FunctionCall call : calls) {
builder.content( // overwrites every iteration
Content.builder()
.parts(ImmutableList.of(Part.builder().functionCall(call).build()))
.build()); // and no role is set
}
});
Two problems follow:
- All parallel calls except the last are discarded before the
Event is even
built, so downstream code (including handleFunctionCallsLive, which itself
loops correctly) never sees them. No functionResponse is generated for the
dropped calls.
- The built
Content has no role, so it can later be treated as empty/invalid
in session history.
The trigger is any single message carrying ≥2 calls — including several calls to
the same tool (e.g. getWeather for two different cities), not only calls to
distinct tools. A single call per message is unaffected (one loop iteration has
nothing to overwrite).
Because the drop happens at the connection layer, it is invisible from the ADK
Event stream (the resulting Event only ever shows the surviving last call),
which makes it easy to mistake for the model simply calling one tool.
This is specific to the Gemini live connection — the non-live runAsync path
(Gemini.generateContent → handleFunctionCalls) is unaffected and handles
parallel calls correctly.
Steps to Reproduce:
- Build an
LlmAgent with two independent tools (e.g. getWeather(city) and
getTime(timezone)) and a live-capable Gemini model.
- Start a live session (
runner.runLive(userId, sessionId, liveRequestQueue, runConfig)).
- Send one user turn that requires both tools (e.g. "What's the weather in Paris
and the time in Europe/London? Call both tools in the same turn.").
- Enable
DEBUG logging for com.google.adk.models.GeminiLlmConnection to see the
raw server toolCall message, and record which tools actually executed.
Expected Behavior:
All function calls contained in a single toolCall message are preserved and
executed. The built content carries role = "model" and remains in session
history, so every call gets a matching functionResponse and the turn completes
normally.
Observed Behavior:
Only the last function call is kept. The raw server message contains 2 calls, but
the Event ADK builds from it contains only the last one — so only that tool runs;
the earlier call has no functionResponse. The model then waits for a tool result
that will never be produced, so the turn never completes and the live session hangs
(surfaced as a TimeoutException once the client stops waiting). The dropped call in
the Event is the direct evidence; the hang is a downstream symptom of the missing
response. Single-call turns are unaffected — with one call the loop's single
iteration has nothing to overwrite, which is why the bug stayed latent.
Environment Details:
- ADK Library Version (see maven dependency):
1.6.1-SNAPSHOT (reproduced on 1.6.0)
- OS: Windows 11 (transport-independent; reproducible on macOS/Linux)
- TS Version (tsc --version): N/A (Java) — JDK 17
Model Information:
- Which model is being used:
gemini-3.1-flash-live-preview (any Gemini
Live/bidiGenerateContent model that emits parallel calls; response modality
AUDIO)
🟡 Optional Information
Regression:
No — the loop-overwrite has been present in createToolCallResponse since the live
connection was introduced.
Logs:
With DEBUG logging on com.google.adk.models.GeminiLlmConnection, the raw Live
server message carries both calls, but the Event ADK builds from it contains
only the last one (and no role):
# Raw server toolCall message (DEBUG: "Received server message") - TWO calls
{"toolCall":{"functionCalls":[
{"args":{"city":"Paris"},"name":"getWeather"},
{"args":{"timezone":"Europe/London"},"name":"getTime"}]}}
# The Event ADK constructed - only ONE call survived, and no "role"
Event: {"content":{"parts":[{"functionCall":{"args":{"timezone":"Europe/London"},"name":"getTime"}}]}, ...}
# getWeather never gets a functionResponse. The model waits for it, so the turn
# never completes (no turnComplete) and the session hangs until the client gives up:
(runLive ended: java.util.concurrent.TimeoutException)
The direct evidence of the drop is the Event above (getWeather is gone before any
tool execution is attempted); the timeout is a downstream symptom of the missing
response.
Additional Context:
Multiple calls per toolCall message is by design, not an edge case:
LiveServerToolCall.functionCalls() is a List<FunctionCall>, the live endpoint does
emit several calls in one message (parallel function calling), and the downstream
handleFunctionCallsLive already fans out over all calls and merges the results
(mergeParallelFunctionResponseEvents). Only createToolCallResponse fails to keep
them all.
Minimal Reproduction Code:
The defect can be exercised directly via the package-private
GeminiLlmConnection.convertToServerResponse with a LiveServerMessage whose
toolCall carries two FunctionCalls — before the fix, the resulting LlmResponse
content has only 1 part instead of 2:
LiveServerToolCall toolCall =
LiveServerToolCall.builder()
.functionCalls(ImmutableList.of(
FunctionCall.builder().name("getWeather").build(),
FunctionCall.builder().name("getTime").build()))
.build();
LiveServerMessage message = LiveServerMessage.builder().toolCall(toolCall).build();
List<LlmResponse> out =
GeminiLlmConnection.convertToServerResponse(message).toList().blockingGet();
// before fix: out.get(0).content().get().parts().get().size() == 1 (getTime only)
// after fix : == 2 (getWeather + getTime), role == "model"
How often has this issue occurred?:
- Always (100%) — deterministic whenever a single
toolCall message carries ≥2
function calls (i.e. whenever the model parallel-calls in a live turn).
When a Gemini live session returns multiple function calls in a single
toolCallmessage, ADK executes only the last one; the earlier parallel calls are silently
dropped, produce no tool response, and leave the session in a mismatched state.
Please make sure you read the contribution guide and file the issues in the right place.
Contribution guide.
🔴 Required Information
Describe the Bug:
On the live/BIDI path (
Runner.runLive+LiveRequestQueue), when the GeminiLive API emits a single
BidiGenerateContentToolCallmessage containing multiplefunction calls (parallel tool calling), only the last call is executed. In
GeminiLlmConnection.createToolCallResponse, the responseContentis rebuiltinside the loop over the calls, so each iteration overwrites the previous one
and only the final
FunctionCallsurvives:Two problems follow:
Eventis evenbuilt, so downstream code (including
handleFunctionCallsLive, which itselfloops correctly) never sees them. No
functionResponseis generated for thedropped calls.
Contenthas norole, so it can later be treated as empty/invalidin session history.
The trigger is any single message carrying ≥2 calls — including several calls to
the same tool (e.g.
getWeatherfor two different cities), not only calls todistinct tools. A single call per message is unaffected (one loop iteration has
nothing to overwrite).
Because the drop happens at the connection layer, it is invisible from the ADK
Eventstream (the resultingEventonly ever shows the surviving last call),which makes it easy to mistake for the model simply calling one tool.
This is specific to the Gemini live connection — the non-live
runAsyncpath(
Gemini.generateContent→handleFunctionCalls) is unaffected and handlesparallel calls correctly.
Steps to Reproduce:
LlmAgentwith two independent tools (e.g.getWeather(city)andgetTime(timezone)) and a live-capable Gemini model.runner.runLive(userId, sessionId, liveRequestQueue, runConfig)).and the time in Europe/London? Call both tools in the same turn.").
DEBUGlogging forcom.google.adk.models.GeminiLlmConnectionto see theraw server
toolCallmessage, and record which tools actually executed.Expected Behavior:
All function calls contained in a single
toolCallmessage are preserved andexecuted. The built content carries
role = "model"and remains in sessionhistory, so every call gets a matching
functionResponseand the turn completesnormally.
Observed Behavior:
Only the last function call is kept. The raw server message contains 2 calls, but
the
EventADK builds from it contains only the last one — so only that tool runs;the earlier call has no
functionResponse. The model then waits for a tool resultthat will never be produced, so the turn never completes and the live session hangs
(surfaced as a
TimeoutExceptiononce the client stops waiting). The dropped call inthe
Eventis the direct evidence; the hang is a downstream symptom of the missingresponse. Single-call turns are unaffected — with one call the loop's single
iteration has nothing to overwrite, which is why the bug stayed latent.
Environment Details:
1.6.1-SNAPSHOT(reproduced on1.6.0)Model Information:
gemini-3.1-flash-live-preview(any GeminiLive/
bidiGenerateContentmodel that emits parallel calls; response modalityAUDIO)🟡 Optional Information
Regression:
No — the loop-overwrite has been present in
createToolCallResponsesince the liveconnection was introduced.
Logs:
With
DEBUGlogging oncom.google.adk.models.GeminiLlmConnection, the raw Liveserver message carries both calls, but the
EventADK builds from it containsonly the last one (and no
role):The direct evidence of the drop is the
Eventabove (getWeatheris gone before anytool execution is attempted); the timeout is a downstream symptom of the missing
response.
Additional Context:
Multiple calls per
toolCallmessage is by design, not an edge case:LiveServerToolCall.functionCalls()is aList<FunctionCall>, the live endpoint doesemit several calls in one message (parallel function calling), and the downstream
handleFunctionCallsLivealready fans out over all calls and merges the results(
mergeParallelFunctionResponseEvents). OnlycreateToolCallResponsefails to keepthem all.
Minimal Reproduction Code:
The defect can be exercised directly via the package-private
GeminiLlmConnection.convertToServerResponsewith aLiveServerMessagewhosetoolCallcarries twoFunctionCalls — before the fix, the resultingLlmResponsecontent has only 1 part instead of 2:
How often has this issue occurred?:
toolCallmessage carries ≥2function calls (i.e. whenever the model parallel-calls in a live turn).