What happens
In livekit-plugins-google, when a single LiveServerMessage carries both a terminal server_content (one with turn_complete) and a tool_call, the receive task raises and the session goes down.
The order is what does it. In the receive loop (realtime_api.py, v1.7.1):
if response.server_content:
self._handle_server_content(response.server_content) # L1159
if response.tool_call:
self._handle_tool_calls(response.tool_call) # L1161
_handle_server_content sees turn_complete and calls _mark_current_generation_done() (L1382-1383).
- That closes the function channel:
gen.function_ch.close() (L1424).
_handle_tool_calls then does an unguarded gen.function_ch.send_nowait(...) (L1491) on the closed channel.
The exception propagates out of the receive task, so the whole realtime session is torn down rather than a single turn being lost.
Why the obvious fix doesn't work
Swapping the two dispatch calls moves the problem instead of solving it. _mark_current_generation_done sets _done = True but does not clear self._current_generation, so _handle_server_content would still find a generation and write into the streams _handle_tool_calls had just closed through its own trailing _mark_current_generation_done() (L1498).
Suggested direction
Hold the finalization back for the message that carries both, so the tool call is delivered on an open channel and the generation is finalized once afterwards. This also keeps the invariant #6785 introduced — that every tool call gets answered — which a message like this currently breaks by dropping the call on the floor (or, without a guard, by killing the session).
Notes
We currently carry a local guard that returns early when function_ch.closed, which converts the crash into a dropped tool call plus a warning. We would rather drop the guard than maintain it — happy to send a PR if the direction above looks right to you.
What happens
In
livekit-plugins-google, when a singleLiveServerMessagecarries both a terminalserver_content(one withturn_complete) and atool_call, the receive task raises and the session goes down.The order is what does it. In the receive loop (
realtime_api.py, v1.7.1):_handle_server_contentseesturn_completeand calls_mark_current_generation_done()(L1382-1383).gen.function_ch.close()(L1424)._handle_tool_callsthen does an unguardedgen.function_ch.send_nowait(...)(L1491) on the closed channel.The exception propagates out of the receive task, so the whole realtime session is torn down rather than a single turn being lost.
Why the obvious fix doesn't work
Swapping the two dispatch calls moves the problem instead of solving it.
_mark_current_generation_donesets_done = Truebut does not clearself._current_generation, so_handle_server_contentwould still find a generation and write into the streams_handle_tool_callshad just closed through its own trailing_mark_current_generation_done()(L1498).Suggested direction
Hold the finalization back for the message that carries both, so the tool call is delivered on an open channel and the generation is finalized once afterwards. This also keeps the invariant #6785 introduced — that every tool call gets answered — which a message like this currently breaks by dropping the call on the floor (or, without a guard, by killing the session).
Notes
We currently carry a local guard that returns early when
function_ch.closed, which converts the crash into a dropped tool call plus a warning. We would rather drop the guard than maintain it — happy to send a PR if the direction above looks right to you.