diff --git a/src/google/adk/runners.py b/src/google/adk/runners.py index 626b6ee5d1..39a8529b95 100644 --- a/src/google/adk/runners.py +++ b/src/google/adk/runners.py @@ -2379,18 +2379,24 @@ def _new_invocation_context_for_live( run_config = run_config or RunConfig() # For live multi-agents system, we need model's text transcription as - # context for the transferred agent. + # context for the transferred agent. Only fill fields the caller left + # unset: an explicit None is an opt-out and stays None. Both fields + # default to an AudioTranscriptionConfig() via default_factory, so + # `model_fields_set` is what separates "unset" from "explicitly off". + # Writes go on a copy, never the caller's own RunConfig (see the same + # treatment for response_modalities in run_live). if hasattr(self.agent, 'sub_agents') and self.agent.sub_agents: + updates: dict[str, Any] = {} if ( run_config.response_modalities and types.Modality.AUDIO in run_config.response_modalities + and 'output_audio_transcription' not in run_config.model_fields_set ): - if not run_config.output_audio_transcription: - run_config.output_audio_transcription = ( - types.AudioTranscriptionConfig() - ) - if not run_config.input_audio_transcription: - run_config.input_audio_transcription = types.AudioTranscriptionConfig() + updates['output_audio_transcription'] = types.AudioTranscriptionConfig() + if 'input_audio_transcription' not in run_config.model_fields_set: + updates['input_audio_transcription'] = types.AudioTranscriptionConfig() + if updates: + run_config = run_config.model_copy(update=updates) return self._new_invocation_context( session, live_request_queue=live_request_queue, diff --git a/tests/unittests/test_runners.py b/tests/unittests/test_runners.py index 05460f50f2..ef6417f7f8 100644 --- a/tests/unittests/test_runners.py +++ b/tests/unittests/test_runners.py @@ -2983,5 +2983,115 @@ async def test_base_agent_run_live_does_not_leak_context(): otel_context.detach(token) +def _live_runner(agent) -> Runner: + return Runner( + app_name=TEST_APP_ID, + agent=agent, + session_service=InMemorySessionService(), + artifact_service=InMemoryArtifactService(), + ) + + +def _multi_agent() -> LlmAgent: + return LlmAgent( + name="live_root", + model="gemini-2.0-flash-live-001", + sub_agents=[ + LlmAgent(name="live_child", model="gemini-2.0-flash-live-001") + ], + ) + + +def _live_session() -> Session: + return Session(id=TEST_SESSION_ID, app_name=TEST_APP_ID, user_id=TEST_USER_ID) + + +def test_live_context_respects_explicit_transcription_opt_out(): + """An explicit None for transcription stays None for the run.""" + from google.adk.agents.live_request_queue import LiveRequestQueue + + runner = _live_runner(_multi_agent()) + config = RunConfig( + response_modalities=[types.Modality.AUDIO], + input_audio_transcription=None, + output_audio_transcription=None, + ) + + ic = runner._new_invocation_context_for_live( + _live_session(), + live_request_queue=LiveRequestQueue(), + run_config=config, + ) + + assert ic.run_config.input_audio_transcription is None + assert ic.run_config.output_audio_transcription is None + # The caller's own RunConfig is never written to. + assert config.input_audio_transcription is None + assert config.output_audio_transcription is None + + +def test_live_context_keeps_default_transcription_enabled(): + """A default RunConfig keeps its default-on transcription configs.""" + from google.adk.agents.live_request_queue import LiveRequestQueue + + runner = _live_runner(_multi_agent()) + config = RunConfig(response_modalities=[types.Modality.AUDIO]) + + ic = runner._new_invocation_context_for_live( + _live_session(), + live_request_queue=LiveRequestQueue(), + run_config=config, + ) + + assert ic.run_config.input_audio_transcription is not None + assert ic.run_config.output_audio_transcription is not None + + +def test_live_context_without_sub_agents_passes_config_through(): + """Without sub_agents the config is passed through untouched.""" + from google.adk.agents.live_request_queue import LiveRequestQueue + + runner = _live_runner( + LlmAgent(name="live_solo", model="gemini-2.0-flash-live-001") + ) + config = RunConfig( + response_modalities=[types.Modality.AUDIO], + input_audio_transcription=None, + output_audio_transcription=None, + ) + + ic = runner._new_invocation_context_for_live( + _live_session(), + live_request_queue=LiveRequestQueue(), + run_config=config, + ) + + assert ic.run_config is config + assert config.input_audio_transcription is None + assert config.output_audio_transcription is None + + +def test_live_context_opt_out_survives_unset_modalities(): + """Explicit None is respected even when response_modalities is unset.""" + from google.adk.agents.live_request_queue import LiveRequestQueue + + runner = _live_runner(_multi_agent()) + config = RunConfig( + input_audio_transcription=None, + output_audio_transcription=None, + ) + + ic = runner._new_invocation_context_for_live( + _live_session(), + live_request_queue=LiveRequestQueue(), + run_config=config, + ) + + assert ic.run_config.input_audio_transcription is None + assert ic.run_config.output_audio_transcription is None + assert config.input_audio_transcription is None + assert config.output_audio_transcription is None + + if __name__ == "__main__": pytest.main([__file__])