Skip to content

fix(voice): answer every tool call - #6785

Open
longcw wants to merge 9 commits into
mainfrom
longc/answer-every-tool-call
Open

fix(voice): answer every tool call#6785
longcw wants to merge 9 commits into
mainfrom
longc/answer-every-tool-call

Conversation

@longcw

@longcw longcw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Problem: A tool call could end with no output at all — dropped when the user interrupted, or never produced when a tool raised StopResponse or returned an invalid value. A realtime model holds each call it emitted open until it is answered, so Gemini Live stopped responding and later generate_reply() calls produced no generation (#6569). A pipeline LLM re-issues the call instead and runs its side effects a second time.

Fix: Every call now gets exactly one output. An interrupted turn commits the results of tools that finished and delivers them to the realtime session; a handoff answers as an error, since the interruption left it unapplied.

Behavior: FunctionCallOutput.reply_required says when no reply is wanted, and each realtime plugin honours it as its provider allows — Gemini with SILENT scheduling, Ultravox with agent_reaction="listens", Phonic by not opening a turn — or warns that it cannot. That replaces the blanket warning about models which generate tool replies on their own.

Breaking: FunctionToolsExecutedEvent.function_call_outputs is list[FunctionCallOutput], no longer optional. A tool raising StopResponse now leaves a call and an empty output in history where it previously left nothing.

Replaces #6600. Fixes #6569

longcw added 9 commits July 29, 2026 17:18
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.
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
A realtime model keeps each tool call it emitted open until the result
arrives, so a result dropped because the user interrupted leaves the
session waiting: Gemini stops responding and later generate_reply()
calls produce no generation.

The interrupted turn now answers every call that produced a result,
handoffs included. A handoff answers as an error, since an interruption
leaves it unapplied and an empty success would claim a transfer that
never happened.

Each output carries reply_required, so a session that can record a
result without speaking does, and one that cannot says so once rather
than failing quietly. That also replaces the blanket warning about
models which generate tool replies on their own.
A call with no output is not a neutral omission. An LLM re-issues a call
it never got back and runs its side effects again, and a realtime model
holds the turn open waiting for it, which is how a StopResponse could
strand a Gemini session with no interruption involved.

Tools that produced nothing now answer anyway: StopResponse with an
empty output that wants no reply, an invalid output or more than one
returned agent with an error the model can act on. FunctionCallOutput
carries reply_required, so ToolExecutionOutput no longer needs its own
copy, and function_call_outputs on FunctionToolsExecutedEvent is no
longer optional.
The tool ran before the interruption, so dropping its call lets the next
inference run it again. Both paths now record the call with an error
saying the handoff did not happen, which keeps the transfer askable
without hiding that it was attempted.

The shared construction moves to generation.py, next to make_tool_output
and ToolExecutionOutput.
Warning once per session hid later ones, and neither warning said which
tool it was about. Both now warn per update with the names in `extra`.

`get_tool_results_for_realtime` takes `supports_silent_scheduling`,
naming the capability the caller reports rather than an instruction.

@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 potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread livekit-agents/livekit/agents/voice/events.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant