_new_invocation_context_for_live() (runners.py:2371) forces input_audio_transcription / output_audio_transcription on for live multi-agent runs, overwriting an explicit None. It writes into the RunConfig it was given, so the caller's object is modified too.
None is the only way to say "off": both fields are Field(default_factory=types.AudioTranscriptionConfig), so the default is already truthy. if not run_config.input_audio_transcription: can't tell "off" from "unset". The block is a no-op for a default RunConfig — it only fires when someone deliberately turned transcription off.
Expected: None stays None for the run, and the caller's RunConfig is not mutated. (0b39e728 already copies before filling the AUDIO default at runners.py:1899 for that reason.)
Observed:
response_modalities=[<Modality.AUDIO: 'AUDIO'>]
caller's cfg.input_audio_transcription = AudioTranscriptionConfig()
run's ic.run_config.input_audio_transcription = AudioTranscriptionConfig()
response_modalities=None
caller's cfg.input_audio_transcription = AudioTranscriptionConfig()
run's ic.run_config.input_audio_transcription = AudioTranscriptionConfig()
Going through run_live() instead, the caller is shielded when response_modalities is unset because runners.py:1899 copies first. The config the run actually uses has transcription on either way, and a reused RunConfig carries it forward.
from google.adk.agents.live_request_queue import LiveRequestQueue
from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents.run_config import RunConfig
from google.adk.runners import InMemoryRunner
from google.adk.sessions.session import Session
from google.genai import types
MODEL = "gemini-2.0-flash-live-001"
root = LlmAgent(name="root", model=MODEL,
sub_agents=[LlmAgent(name="child", model=MODEL)])
runner = InMemoryRunner(agent=root, app_name="app")
session = Session(id="s", app_name="app", user_id="u")
for modalities in ([types.Modality.AUDIO], None):
cfg = RunConfig(response_modalities=modalities,
input_audio_transcription=None,
output_audio_transcription=None)
ic = runner._new_invocation_context_for_live(
session, live_request_queue=LiveRequestQueue(), run_config=cfg)
print(f"response_modalities={modalities}")
print(f" caller's cfg.input_audio_transcription = "
f"{cfg.input_audio_transcription!r}")
print(f" run's ic.run_config.input_audio_transcription = "
f"{ic.run_config.input_audio_transcription!r}")
39f78dc2 added the force-on when both fields still defaulted to None. ab69ef8d later switched them to default_factory, which is why the block can only fire on an explicit opt-out. 0b39e728 fixed the same class of write for response_modalities; this site was not included.
Fix: don't write when the field is in model_fields_set, and model_copy before any update so the caller is left alone. Deleting the block also works — the defaults already provide what it's trying to force. If transfer actually requires transcription, raise or warn instead of silently flipping it.
ADK v2.7.1 / v1.39.0 / main (4599a526). macOS, Python 3.13.1. Model never reached — this happens while building the invocation context.
_new_invocation_context_for_live()(runners.py:2371) forcesinput_audio_transcription/output_audio_transcriptionon for live multi-agent runs, overwriting an explicitNone. It writes into theRunConfigit was given, so the caller's object is modified too.Noneis the only way to say "off": both fields areField(default_factory=types.AudioTranscriptionConfig), so the default is already truthy.if not run_config.input_audio_transcription:can't tell "off" from "unset". The block is a no-op for a defaultRunConfig— it only fires when someone deliberately turned transcription off.Expected:
NonestaysNonefor the run, and the caller'sRunConfigis not mutated. (0b39e728already copies before filling the AUDIO default atrunners.py:1899for that reason.)Observed:
Going through
run_live()instead, the caller is shielded whenresponse_modalitiesis unset becauserunners.py:1899copies first. The config the run actually uses has transcription on either way, and a reusedRunConfigcarries it forward.39f78dc2added the force-on when both fields still defaulted toNone.ab69ef8dlater switched them todefault_factory, which is why the block can only fire on an explicit opt-out.0b39e728fixed the same class of write forresponse_modalities; this site was not included.Fix: don't write when the field is in
model_fields_set, andmodel_copybefore any update so the caller is left alone. Deleting the block also works — the defaults already provide what it's trying to force. If transfer actually requires transcription, raise or warn instead of silently flipping it.ADK
v2.7.1/v1.39.0/main(4599a526). macOS, Python 3.13.1. Model never reached — this happens while building the invocation context.