Skip to content

fix(google): allow generate_reply without a mutable chat context - #7193

Open
Darshak03 wants to merge 1 commit into
livekit:mainfrom
Darshak03:fix/gemini-generate-reply-immutable-ctx
Open

fix(google): allow generate_reply without a mutable chat context#7193
Darshak03 wants to merge 1 commit into
livekit:mainfrom
Darshak03:fix/gemini-generate-reply-immutable-ctx

Conversation

@Darshak03

Copy link
Copy Markdown

generate_reply() raised RealtimeError whenever mutable_chat_context was False. That capability is computed as "3.1" not in model, so every Gemini 3.1 Live model hit it β€” leaving those models with no way to open an agent-initiated turn at all. Greetings, replies after a handoff, and prompts issued after a tool result all go through generate_reply(), and each raised instead.

The capability describes whether the client may append to the conversation mid-session, not whether the model can generate. The plugin already acts on exactly that when it connects:

history_config=types.HistoryConfig(initial_history_in_client_content=True)
if not self._realtime_model.capabilities.mutable_chat_context
else None,

A session configured that way keeps its own history and rejects the placeholder user turn generate_reply() sends as a trigger. That is a reason to send a different trigger, not to refuse β€” the same session accepts realtime text input, which starts a generation without touching the context.

Fix: drop the refusal and branch on the capability to pick the trigger. instructions is frequently NOT_GIVEN (agent_activity passes instructions or NOT_GIVEN), so the realtime-input path falls back to the same "." nudge the other path already uses. _send_task already routes LiveClientRealtimeInput.text to send_realtime_input, so the send path is unchanged.

Nothing in agent_activity gates generate_reply() on mutable_chat_context β€” its uses there are handoff reuse, chat context reset, and the interrupted-tool commit β€” so this is contained to the plugin, and no capability is added or changed.

Tests: three, covering the mutable path (placeholder turn, unchanged), the immutable path (realtime text, no client content, no longer refused up front), and the missing-instructions fallback. The two immutable-path tests fail on main with RealtimeError: generate_reply is not compatible with 'gemini-3.1-flash-live-preview'.

Fixes #7192

generate_reply raised RealtimeError whenever mutable_chat_context was False, which
is every Gemini 3.1 Live model, leaving them with no way to open an agent-initiated
turn: greetings, replies after a handoff and prompts after a tool result all go
through it.

The capability describes whether the client may append to the conversation, not
whether the model can generate. A session connected with
history_config=initial_history_in_client_content keeps its own history and rejects
the placeholder user turn used as the trigger, but it does accept realtime text
input. Pick the trigger the session accepts instead of refusing.

Fixes livekit#7192
@Darshak03
Darshak03 requested a review from a team as a code owner September 9, 2026 17:47

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

Devin Review

Comment on lines +851 to +853
self._send_client_event(
types.LiveClientRealtimeInput(text=instructions if is_given(instructions) else ".")
)

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.

🟑 Text input disappears before generation

With Gemini 3.1, generate_reply(user_input=...) sends "." because instructions is absent. The immutable context path never forwards the user's text, so the model answers without it.

Prompt for agents
Gemini 3.1 sessions cannot receive user_input through AgentActivity's existing update_chat_ctx flow. AgentActivity adds user_input to a copied context, but RealtimeSession._sync_chat_ctx intentionally skips non-tool context additions when mutable_chat_context is false. generate_reply then receives no user_input parameter and sends the fallback dot. Preserve the provider-neutral generate_reply(user_input=...) contract by adding an immutable-session input path that forwards the actual user text exactly once before generation. Keep per-turn instructions distinct from user content, and add an end-to-end unit test through AgentSession or AgentActivity rather than testing RealtimeSession.generate_reply alone.
Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The gap is real, but it is pre-existing and not in the code this PR touches, and this change strictly improves it.

Two corrections on the mechanism first. generate_reply has no user_input parameter β€” the abstract signature in llm/realtime.py is instructions, tool_choice, tools, and no plugin extends it. user_input is a private parameter of AgentActivity._realtime_reply_task, and it is never forwarded to generate_reply. The text is delivered a step earlier:

if user_input is not None:
    chat_ctx = self._rt_session.chat_ctx.copy()
    msg = chat_ctx.add_message(role="user", content=user_input)
    await self._rt_session.update_chat_ctx(chat_ctx)   # carries the text
...
generate_reply_fut = self._rt_session.generate_reply(...)  # only triggers

So the text is dropped by _sync_chat_ctx, which builds turns only when mutable_chat_context is true. That is where the gap lives, and it predates this PR.

On main today generate_reply raises RealtimeError for these models, so this path produces no turn at all. After this change the turn happens, with the user text still missing β€” a broken path becomes a partial one. Audio input is unaffected either way, since _realtime_generation_task sends audio straight to the model.

Worth flagging for whoever picks the remaining gap up: send_realtime_input(text=...) is itself a generation trigger β€” that is exactly why this PR uses it. Forwarding user text through it inside update_chat_ctx would start a turn, and generate_reply's "." would then start a second one. Closing the gap needs either the plugin holding the unsent user text so generate_reply can send it as the trigger, or letting generate_reply carry user content β€” neither of which belongs in this change.

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.

Gemini realtime: generate_reply is refused on models without a mutable chat context

1 participant