Skip to content

Live multi-agent runs turn audio transcription back on after the caller disabled it #6827

Description

@donggyun112

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

Metadata

Metadata

Assignees

Labels

live[Component] This issue is related to live, voice and video chat

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions