Skip to content

fix(google): preserve non-blocking realtime generation - #6741

Open
usluio wants to merge 2 commits into
livekit:mainfrom
usluio:fix/google-realtime-nonblocking-generation-1.6.8
Open

fix(google): preserve non-blocking realtime generation#6741
usluio wants to merge 2 commits into
livekit:mainfrom
usluio:fix/google-realtime-nonblocking-generation-1.6.8

Conversation

@usluio

@usluio usluio commented Aug 7, 2026

Copy link
Copy Markdown

Summary

  • keep Gemini realtime generations open after NON_BLOCKING tool calls
  • let Gemini generation_complete / turn_complete events close the output streams and generation
  • preserve the existing immediate finalization behavior for blocking tool calls

This replaces #6526 with a clean change based on the current LiveKit Agents 1.6.8 codebase.

Tests

  • uv run pytest tests/test_plugin_google_realtime.py -q (5 passed)
  • uv run ruff check livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py tests/test_plugin_google_realtime.py
  • uv run ruff format --check livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py tests/test_plugin_google_realtime.py

@usluio
usluio requested a review from a team as a code owner August 7, 2026 11:56
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1509 to +1526
while not gen._done:
elapsed = time.monotonic() - started_at
if elapsed >= NON_BLOCKING_TOOL_DRAIN_TIMEOUT_SECONDS:
break

await asyncio.sleep(
min(
NON_BLOCKING_TOOL_DRAIN_QUIESCENCE_SECONDS,
NON_BLOCKING_TOOL_DRAIN_TIMEOUT_SECONDS - elapsed,
)
)

current_activity_at = gen._last_output_activity_at
if current_activity_at == last_activity_at:
break
last_activity_at = current_activity_at

self._mark_generation_done(gen)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Agent speech is cut off five seconds after a non-blocking tool call

The spoken reply is force-ended (_mark_generation_done(gen) at livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py:1526) five seconds after a non-blocking tool call even while the model is still actively talking, so the reply is chopped off mid-sentence.
Impact: Users hear the agent's answer truncated and then abruptly restarted whenever it speaks for more than five seconds following a non-blocking tool call.

Hard drain deadline ignores ongoing output activity

_finalize_non_blocking_generation_after_output_drain computes elapsed from started_at (the moment the tool call arrived) and breaks out of the loop as soon as elapsed >= NON_BLOCKING_TOOL_DRAIN_TIMEOUT_SECONDS (realtime_api.py:1510-1512), regardless of whether gen._last_output_activity_at is still being refreshed by incoming audio/transcription (realtime_api.py:1285-1288). The constant is documented as a fallback "for models that wait for the tool response instead of completing the turn" (realtime_api.py:50-54), i.e. an idle timeout, but it is implemented as an absolute cap on the whole generation.

When the model keeps streaming audio past 5s, _mark_generation_done closes audio_ch/text_ch via _close_output_streams and closes message_ch/function_ch (realtime_api.py:1400-1408), ending the audio segment mid-speech. Subsequent audio then hits the _current_generation._done branch in the receive loop (realtime_api.py:1106), which starts a brand-new generation; _start_new_generation emits input_speech_started (realtime_api.py:1259), interrupting whatever playout was left.

Making the timeout relative to the last observed output activity (or only arming it while the stream is quiet) would preserve the intended fallback without truncating active speech.

Prompt for agents
In _finalize_non_blocking_generation_after_output_drain (livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/realtime_api.py), the NON_BLOCKING_TOOL_DRAIN_TIMEOUT_SECONDS budget is measured from the moment the tool call was received (started_at) and is checked unconditionally, so the generation is finalized 5 seconds after the tool call even when the model is still streaming audio/transcription (which refreshes gen._last_output_activity_at in _handle_server_content). This truncates the agent's speech and causes a new generation (and an input_speech_started interrupt) to be started for the remaining audio. The constant is intended as a safety net for models that never emit generation_complete/turn_complete while waiting for a tool response, i.e. an idle bound. Rework the loop so the timeout is measured against the last observed output activity (or only applies while no output has been seen), letting an actively streaming turn continue until generation_complete/turn_complete finalizes it.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant