Skip to content

fix(models): fix multipart text data loss and co-delivered tool_call drop in live receive loop - #6618

Open
patrickswedish wants to merge 1 commit into
google:mainfrom
patrickswedish:fix/live-multipart-text-and-codelivered-tool-call
Open

fix(models): fix multipart text data loss and co-delivered tool_call drop in live receive loop#6618
patrickswedish wants to merge 1 commit into
google:mainfrom
patrickswedish:fix/live-multipart-text-and-codelivered-tool-call

Conversation

@patrickswedish

Copy link
Copy Markdown

Summary

Fixes #6616 and #6615.

Two independent bugs in GeminiLlmConnection.receive() caused silent data loss in live streaming mode. Both are in src/google/adk/models/gemini_llm_connection.py.


Bug 1 — Multipart text data loss (parts[0] indexing) — fixes #6616

Root cause

The receive loop examined only content.parts[0].text when accumulating streaming text:

# Before (buggy):
if content.parts[0].text:
    current_is_thought = getattr(content.parts[0], 'thought', False)
    ...
    text += content.parts[0].text
    ...
elif text and not content.parts[0].inline_data:
    ...

If a single LiveServerMessage carried more than one text part — as can happen in multimodal streaming responses — only the first part's text was accumulated. All text in parts[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:

# 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:
    current_is_thought = getattr(_text_parts[0], 'thought', False)
    ...
    for _tp in _text_parts:
        text += _tp.text
    ...
elif text and not _has_inline_data:
    ...

Bug 2 — tool_call silently dropped when co-delivered with turn_completefixes #6615

Root cause

The receive loop is structured as:

async for message in agen:
    if message.server_content:
        ...
        if message.server_content.turn_complete:
            ...
            break          # exits the entire async for loop
    if message.tool_call:  # UNREACHABLE 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 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_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:

if message.server_content.turn_complete:
    # Process any tool_call co-delivered in the same server message as
    # turn_complete. Without this, the `break` below exits the receive
    # loop before the `if message.tool_call:` block lower in the loop
    # body is reached, silently discarding the tool call.
    if message.tool_call:
        logger.debug('Processing tool_call co-delivered with turn_complete')
        if text:
            yield self.__build_full_text_response(...)
            ...
        tool_call_parts.extend([
            types.Part(function_call=fc)
            for fc in message.tool_call.function_calls or []
        ])
    ...
    if tool_call_parts:        # already yielded by existing logic
        yield LlmResponse(...)
    ...
    break

Impact

Bug Affected users Symptom
#6616 (multipart text loss) Any live-mode user with multimodal or multi-segment streaming responses Text content silently truncated to first segment only
#6615 (tool_call drop) Any live-mode user calling tools with models that co-deliver tool_call + turn_complete Agent permanently stalls — function call never arrives, no error thrown

Changes

  • src/google/adk/models/gemini_llm_connection.py — two surgical fixes to receive(), no other files modified

Testing

  • Both code paths traced manually against the live API message format documented in google-genai SDK.
  • The fixes are strictly additive: they handle cases the original code never reached, so all previously-working paths remain unchanged.
  • Relevant existing tests: tests/unittests/models/test_gemini_llm_connection.py

…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.
@google-cla

google-cla Bot commented Aug 6, 2026

Copy link
Copy Markdown

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.

@patrickswedish

Copy link
Copy Markdown
Author

@googlebot I signed the CLA!

@patrickswedish

Copy link
Copy Markdown
Author

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:

  1. Bug: Systemic parts[0] indexing drops multimodal streaming data and bypasses content validation #6616 — multi-part text chunks lose all text beyond parts[0]
  2. Live mode: tool_call can be silently dropped when co-framed with turn_complete in gemini_llm_connection.py #6615tool_call co-delivered with turn_complete is silently dropped, deadlocking the agent

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! 🙏

@adk-bot adk-bot added the live [Component] This issue is related to live, voice and video chat label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

live [Component] This issue is related to live, voice and video chat

Projects

None yet

3 participants