fix: handle Gemini Live 2.5 quirks when re-seeding context on reconnect

Extends the reconnect re-seeding fix to work cleanly on Gemini Live 2.5,
which has stricter seed requirements than 3.x and a documented audio-input /
history-recall limitation. Both initial connection and reconnect now share a
single code path (`_create_initial_response(for_reconnect=...)`), with four
well-documented cases.

On Gemini 2.5 reconnect, `turn_complete=True` is now forced on the seed so
the model produces a recap-style response immediately instead of briefly
acting "forgetful" on the user's next utterance — the latter being
especially jarring mid-conversation. When a 2.5 seed doesn't already end
with a user turn (e.g. the bot had finished speaking before the disconnect),
a blank user turn is appended to satisfy the server's seed-shape
requirement. Gemini 3.x needs neither workaround.
This commit is contained in:
Paul Kompfner
2026-04-22 15:58:54 -04:00
parent 7b3141ba19
commit 1421c4ba22
2 changed files with 84 additions and 26 deletions

View File

@@ -1,3 +1 @@
Fixed Gemini Live losing conversation history on WebSocket reconnect. When the session
reconnects (e.g. on system instruction change), conversation history is now re-seeded
into the new session before it is marked ready for input.
Fixed Gemini Live losing conversation history in the (rare) case of a WebSocket reconnect before any session resumption handle is received. When the session reconnects (e.g. on system instruction change), conversation history is now re-seeded into the new session before it is marked ready for input.

View File

@@ -78,6 +78,7 @@ try:
AudioTranscriptionConfig,
AutomaticActivityDetection,
Blob,
Content,
ContextWindowCompressionConfig,
EndSensitivity,
FunctionResponse,
@@ -89,6 +90,7 @@ try:
LiveServerMessage,
MediaResolution,
Modality,
Part,
ProactivityConfig,
RealtimeInputConfig,
SessionResumptionConfig,
@@ -1262,8 +1264,60 @@ class GeminiLiveLLMService(LLMService):
except Exception as e:
await self._handle_send_error(e)
async def _create_initial_response(self):
"""Create initial response based on context history."""
async def _create_initial_response(self, for_reconnect: bool = False):
"""Seed conversation history and optionally trigger an initial model response.
Behavior by case:
Initial connection, ``_inference_on_context_initialization=True``:
Seed with ``turn_complete=True`` so the model generates a first
response to the trailing user turn. On Gemini 3.x we also send a
realtime-input nudge (the model won't actually run inference
without one).
Initial connection, ``_inference_on_context_initialization=False``:
Seed with ``turn_complete=False`` (no response yet). On Gemini 2.5
we set ``_needs_initial_turn_complete_message`` so the next
``user_stopped_speaking`` sends an explicit ``turn_complete=True``;
otherwise 2.5 ignores the seeded history when the user's first
input is audio. Gemini 3.x merges seeded history and audio
cleanly and needs no workaround.
Note: on Gemini 2.5 the first user utterance after this kind of
seed still produces one "naive" response (context-ignoring) that
is immediately followed by a context-aware response. This is
Gemini 2.5's documented audio-input / history-recall limitation
(https://discuss.ai.google.dev/t/audio-input-cannot-trigger-history-recall-in-gemini-live-api-only-text-input-works/111617).
It's tolerable at the start of a conversation because the first
utterance is usually a greeting.
Reconnect, Gemini 3.x:
Seed with ``turn_complete=False`` — no workaround needed, the
next user utterance gets a single history-aware response.
Reconnect, Gemini 2.5:
Force ``turn_complete=True`` on the seed so the model generates
an inference over the seeded history immediately (typically a
recap/continuation of the conversation). This works around the
same Gemini 2.5 audio-input / history-recall limitation noted
above
(https://discuss.ai.google.dev/t/audio-input-cannot-trigger-history-recall-in-gemini-live-api-only-text-input-works/111617):
if we seeded without triggering inference, the user's next
utterance — mid-conversation — would briefly hear the bot act
like it had forgotten everything before recovering on the
following turn. Forcing a recap-style response up front avoids
that jarring UX.
Seed shape:
Gemini 2.5's ``send_client_content`` requires the seed's final
turn to be a user turn. When it isn't (e.g. on reconnect where
the bot had finished speaking before the disconnect), we append
a blank user turn to satisfy the server. Gemini 3.x has no such
requirement.
Args:
for_reconnect: When True, we're re-seeding after a reconnect.
"""
if self._disconnecting:
return
@@ -1278,25 +1332,41 @@ class GeminiLiveLLMService(LLMService):
self._ready_for_realtime_input = True
return
# On reconnect, Gemini 2.5 needs us to force an inference so the user
# doesn't momentarily experience a "forgotten" assistant (see
# docstring). Gemini 3.x doesn't have that limitation. In the
# non-reconnect case we honor the configured setting.
if for_reconnect:
trigger_inference = not self._is_gemini_3
else:
trigger_inference = self._inference_on_context_initialization
logger.debug(f"Creating initial response: {messages}")
# Enforce Gemini 2.5's "seed must end with user turn" requirement.
seed_messages = messages
if not self._is_gemini_3:
last_role = getattr(messages[-1], "role", None)
if last_role != "user":
seed_messages = messages + [Content(role="user", parts=[Part(text=" ")])]
await self.start_ttfb_metrics()
try:
await self._session.send_client_content(
turns=messages, turn_complete=self._inference_on_context_initialization
turns=seed_messages,
turn_complete=trigger_inference,
)
# Gemini 3.x wants turn_complete=True, but also won't run inference without a realtime input
if self._is_gemini_3 and self._inference_on_context_initialization:
if self._is_gemini_3 and trigger_inference:
await self._session.send_realtime_input(text=" ")
except Exception as e:
await self._handle_send_error(e)
# If we're generating a response right away upon initializing
# conversation history, set a flag saying that we'll need a turn
# complete message when the user stops speaking.
# This is a quirky workaround, and not one that Gemini 3 needs.
if not self._inference_on_context_initialization and not self._is_gemini_3:
# Gemini 2.5-only workaround: when we've seeded without triggering
# inference, flag that the next user_stopped_speaking should send
# turn_complete=True so 2.5 picks up the seeded history.
if not trigger_inference and not self._is_gemini_3:
self._needs_initial_turn_complete_message = True
self._ready_for_realtime_input = True
@@ -1365,20 +1435,10 @@ class GeminiLiveLLMService(LLMService):
# Reconnect without session resumption (e.g. error occurred
# before server sent a resumption handle): re-seed conversation
# history so the new session retains full context before
# accepting input.
try:
adapter = self.get_llm_adapter()
messages = adapter.get_llm_invocation_params(self._context).get("messages", [])
if messages:
logger.info(
f"Re-seeding {len(messages)} conversation turns after reconnect"
)
await self._session.send_client_content(
turns=messages, turn_complete=False
)
except Exception as e:
logger.error(f"Failed to re-seed conversation on reconnect: {e}")
self._ready_for_realtime_input = True
# accepting input. We route through _create_initial_response so
# that the seed/commit dance stays in one place. See that
# method's docstring for the reconnect-specific behavior.
await self._create_initial_response(for_reconnect=True)
else:
# Initial connection: session is ready before context has
# arrived. Nothing to do — _handle_context will call