fix(models): fix multipart text data loss and co-delivered tool_call drop in live receive loop - #6618
Conversation
…drop in live receive loop Two independent bugs in GeminiLlmConnection.receive() caused silent data loss in live streaming mode: 1. **Multipart text data loss** (fixes google#6616) The receive loop checked `content.parts[0].text` and accumulated only the first part's text: ```python # Before (buggy): if content.parts[0].text: text += content.parts[0].text ``` Any text in `parts[1]`, `parts[2]`, etc. was silently discarded. This affects multimodal streaming responses where a single server message contains multiple text parts. Fix: collect all text parts in the chunk and accumulate each one: ```python # After: _text_parts = [p for p in content.parts if p.text] _has_inline_data = any(p.inline_data for p in content.parts) if _text_parts: for _tp in _text_parts: text += _tp.text ``` The `inline_data` guard in the `elif` branch is also updated to scan all parts rather than only `parts[0]`. 2. **Tool call silently dropped when co-delivered with turn_complete** (fixes google#6615) The receive loop contains: ```python async for message in agen: if message.server_content: ... if message.server_content.turn_complete: ... break # exits the async for loop if message.tool_call: # NEVER reached on the same message ... ``` When the Gemini API delivers a `tool_call` in the **same** `LiveServerMessage` as `server_content.turn_complete=True`, the `break` exits the loop before the `if message.tool_call:` block is reached. The tool call was silently dropped, causing the agent to stall waiting for a function result that was never requested. Fix: inspect `message.tool_call` inside the `turn_complete` branch, before the `break`, and append any function calls to `tool_call_parts` so they are yielded by the existing aggregation logic: ```python if message.server_content.turn_complete: if message.tool_call: # handle co-delivered tool call tool_call_parts.extend([...]) ... # existing text flush + tool_call_parts yield break ``` Both fixes are surgical and do not change the behaviour of any path that was already working correctly.
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
|
@googlebot I signed the CLA! |
|
Hi team! 👋 Just a quick heads up on this PR — it fixes two silent data-loss bugs in the live streaming receive loop that affect real-world multimodal and tool-calling use cases:
Both fixes are surgical (25 lines changed, one file), backward-compatible, and traced directly against the live API message format. Happy to add unit tests or address any review feedback. Looking forward to getting this in! 🙏 |
Summary
Fixes #6616 and #6615.
Two independent bugs in
GeminiLlmConnection.receive()caused silent data loss in live streaming mode. Both are insrc/google/adk/models/gemini_llm_connection.py.Bug 1 — Multipart text data loss (
parts[0]indexing) — fixes #6616Root cause
The receive loop examined only
content.parts[0].textwhen accumulating streaming text:If a single
LiveServerMessagecarried more than one text part — as can happen in multimodal streaming responses — only the first part's text was accumulated. All text inparts[1],parts[2], etc. was silently discarded.Fix
Collect all text parts from the chunk and accumulate each one, and likewise scan all parts when checking for
inline_data:Bug 2 —
tool_callsilently dropped when co-delivered withturn_complete— fixes #6615Root cause
The receive loop is structured as:
When the Gemini API delivers a
tool_callin the sameLiveServerMessageasserver_content.turn_complete = True, thebreakexits the loop before theif message.tool_call:block is ever evaluated. The tool call is silently discarded, leaving the agent in a deadlocked state — it waits for a function result that was never requested.Fix
Inspect
message.tool_callinside theturn_completebranch, before thebreak, and append any function calls totool_call_partsso they are yielded by the existing aggregation logic:Impact
tool_call+turn_completeChanges
src/google/adk/models/gemini_llm_connection.py— two surgical fixes toreceive(), no other files modifiedTesting
google-genaiSDK.tests/unittests/models/test_gemini_llm_connection.py