Live multi-agent runs turn audio transcription back on after the caller disabled it
- Dominant language
- Python
- Stars
- 21.5k
- Forks
- 4k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 37
Description
`_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=[]
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.
```python
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.
Contributor guide
Assessment
This issue has not been assessed yet.