Skip to content

fix(voice): deliver tool results to realtime session on interruption - #6600

Closed
longcw wants to merge 3 commits into
mainfrom
longc/gemini-tool-output-on-interrupt
Closed

longcw wants to merge 3 commits into
mainfrom
longc/gemini-tool-output-on-interrupt

Conversation

@longcw

@longcw longcw commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Gemini Live blocks a turn until every tool call it emitted is answered, and offers no way to cancel one. If playout is interrupted from our side while a tool result is still owed, dropping that result leaves the server waiting forever: it stops responding and subsequent generate_reply() calls produce no generation. Reported in #6569.

When it happens

Only when the interrupt is client-initiated — a programmatic session.interrupt(), a handoff or close, false-interruption resume, or local turn detection firing alone. A voice barge-in makes Gemini send interrupted, which releases its wait on its own.

Fix

The realtime path now mirrors what #6349 did for the pipeline path: commit the outputs of tools that finished despite the interruption, handoffs excluded so they stay retryable. On top of that it syncs those outputs to the realtime session, which the pipeline has no need to do.

Delivering a result makes Gemini reply, which is unwanted right after an interruption, so it goes out with SILENT scheduling. Gemini ignores scheduling on BLOCKING declarations (the default), so with blocking tools the reply is unavoidable — unblocking the turn is the better trade.

A realtime model keeps a tool call open server-side. Gemini Live blocks the turn
until every call it emitted is answered and offers no way to cancel one, so
discarding a result the handler already returned strands the session: it stops
responding and subsequent generate_reply() calls produce no generation.

Mirror the pipeline path on the realtime path — commit the outputs of tools that
finished despite the interruption, handoffs excluded so they stay retryable — and
additionally sync them to the realtime session.

Gemini answers a delivered result with speech, which is unwanted right after an
interruption, so the response is sent with SILENT scheduling where the API
honours it (NON_BLOCKING declarations on the Gemini API).

Also stop setting tool_response_scheduling on Vertex AI, which does not support
the field, and warn when it is configured there instead of dropping it silently.
@longcw
longcw requested a review from a team as a code owner July 29, 2026 09:19

@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: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

self._session._tool_items_added(interrupted_fnc_outputs)

# unlike the pipeline, a realtime model holds the call open server-side
chat_ctx = self._rt_session.chat_ctx.copy()

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.

chat_ctx already returns self._chat_ctx.copy(), so this copies twice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd keep copy() here to make it explicit since the rt_session.chat_ctx is implemented per plugin and not documented to must be a copy.

longcw added 2 commits August 5, 2026 17:10
create_function_response already skips scheduling for vertex, so the
call site does not need to repeat the rule.
…put-on-interrupt

# Conflicts:
#	tests/test_plugin_google_realtime.py

@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 3 additional findings in Devin Review.

Open in Devin Review

Comment thread livekit-agents/livekit/agents/voice/agent_activity.py

def interrupt(self) -> None:
# recorded locally since this cannot reach Gemini under automatic activity detection
self._playout_interrupted = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Q: if _start_new_generation clears the flag and but immediately sets it to True with _handle_input_speech_started, we would have silent tools whenever the generation is not triggered by "generate_reply"?

chat_ctx = self._rt_session.chat_ctx.copy()
chat_ctx.items.extend(interrupted_fnc_outputs)
try:
await self._rt_session.update_chat_ctx(chat_ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

seems update might be handled differently, from Sol:

Ultravox converts that output to `agent_reaction="speaks"`, while Phonic starts a new assistant turn. Thus an interrupted tool can make non-Google agents speak again. Silent interrupted-result delivery needs provider-specific semantics or an explicit capability.

@longcw

longcw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #6785, which broadens this to every tool call rather than just Gemini's.

@longcw longcw closed this Aug 11, 2026
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.

3 participants