Skip to content

[Python][Live] transfer_to_agent is ignored when its function response is not content.parts[0] #6541

Description

@haydeem2

Bug Description:

In Live mode (BaseLlmFlow.run_live()), agent transfer is gated on whether
transfer_to_agent is the first function response in event.content.parts[0].

When the model issues parallel function calls and a non-transfer tool's
response is merged first, ADK:

  1. Executes both tools successfully
  2. Merges both function responses into one event
  3. Correctly sets event.actions.transfer_to_agent on the merged event
  4. Does not start the child agent, because the transfer gate only inspects
    parts[0]

Reversing the merged response order (transfer first) makes transfer work. A lone
transfer_to_agent call also works. This is an order-dependent Live transfer
bug, not a failure to execute or merge parallel tools.

The non-Live async path uses function_response_event.actions.transfer_to_agent
directly and does not exhibit this parts[0] dependency.

Steps to Reproduce:

  1. Install ADK 1.34.0 and download the reproducer (see Minimal Reproduction Code
    below).
  2. Run python adk_live_transfer_order_repro.py.
  3. Observe Layer 1: merged event has parts[0]=set_state,
    parts[1]=transfer_to_agent, and actions.transfer_to_agent set — but the
    stock predicate misses the transfer.
  4. Observe Layer 2: with [set_state, transfer_to_agent] parallel order, the
    child agent does not start (XFAIL on stock ADK 1.34.0).
  5. Re-run mentally / via script with reversed order [transfer_to_agent, set_state] — child starts (PASS).

Expected Behavior:

Transfer should be order-independent: if the merged event contains a
transfer_to_agent function response and event.actions.transfer_to_agent
names a target, the child agent should start exactly once.

Merged response order Expected
[set_state, transfer_to_agent] Child starts once
[transfer_to_agent, set_state] Child starts once
lone transfer_to_agent Child starts once
parallel non-transfer tools No child starts

Observed Behavior:

Merged response order Stock ADK 1.34.0
[set_state, transfer_to_agent] Child does not start
[transfer_to_agent, set_state] Child starts
lone transfer_to_agent Child starts

Example output from the reproducer on stock google-adk==1.34.0:

Layer 1: 15 passed
Layer 2: 7 passed, 1 xfail, 1 skipped
  XFAIL [set_state, transfer] child starts in run_live  (known stock ADK bug)
RESULT: OK on stock ADK — bug reproduced

Root cause in BaseLlmFlow.run_live() (v1.34.0,
base_llm_flow.py#L578-L614):

if (
    event.content
    and event.content.parts
    and event.content.parts[0].function_response
    and event.content.parts[0].function_response.name == "transfer_to_agent"
):
    transfer_to_agent = event.actions.transfer_to_agent
    if transfer_to_agent:
        ...  # start child run_live()

The same parts[0] predicate remains in v2.6.0
(base_llm_flow.py#L681-L717).

Environment Details:

  • ADK Library Version (pip show google-adk): 1.34.0 (also present in 2.6.0
    and current main as of 2026-07-31)
  • Desktop OS: macOS (reproducer also runs on Linux; not Windows-tested)
  • Python Version (python -V): 3.12.8 (reproducer requires 3.11+)

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-live-2.5-flash-native-audio (Gemini 2.5
    Live native audio) in production via direct ADK Live + Vertex AI Agent Engine.
    The attached reproducer uses a MockModel (no GCP / API key) to force
    parallel response order deterministically.

Regression:

Did this work in a previous version of ADK? If so, which one?

Not confirmed on older ADK versions for this specific parts[0] predicate. The
issue is present on 1.34.0, 2.6.0, and main (checked 2026-07-31).
Related Live transfer fixes (#5238,
#5536) addressed different
mechanisms (history attribution and resumption-handle inheritance) and do not
remove the parts[0] check.

Logs:

Please attach relevant logs. Wrap them in code blocks (```) or attach a
text file.

ADK Live transfer_to_agent response-order reproducer
  google-adk: 1.34.0
  Python:     3.12.8

--- Layer 1: event-level probe (merge + predicates) ---
  PASS  [set_state, transfer] parts[0] is set_state
  PASS  [set_state, transfer] parts[1] is transfer_to_agent
  PASS  [set_state, transfer] action names specialist
  PASS  [set_state, transfer] stock predicate misses transfer
  PASS  [set_state, transfer] patched predicate detects transfer
  → 15 passed, 0 failed, 0 xfail, 0 skipped

--- Layer 2: run_live() regression (MockModel) ---
  XFAIL [set_state, transfer] child starts in run_live  (known stock ADK bug)
  PASS  [transfer, set_state] child starts
  PASS  lone transfer: child starts
  → 7 passed, 0 failed, 1 xfail, 1 skipped

RESULT: OK on stock ADK — bug reproduced

Screenshots / Video:

N/A

Additional Context:

Parallel Live tool execution merges responses correctly in
merge_parallel_function_response_events()
— all parts are appended and actions (including transfer_to_agent) are merged.
The bug is solely in the Live transfer gate inside run_live().

Proposed fix (scan all function responses and require the transfer action):

has_transfer_response = any(
    response.name == "transfer_to_agent"
    for response in event.get_function_responses()
)
if has_transfer_response and event.actions.transfer_to_agent:
    ...  # existing child run_live() logic unchanged

Useful references:

Full repo (optional): https://github.com/haydeem2/adk-live-transfer-order-repro

Minimal Reproduction Code:

Gist: https://gist.github.com/haydeem2/4608a4a4e306a20ae9b5dfaa54338a34

python3 -m venv .venv
source .venv/bin/activate
pip install "google-adk==1.34.0"
curl -fsSL -o adk_live_transfer_order_repro.py \
  "https://gist.githubusercontent.com/haydeem2/4608a4a4e306a20ae9b5dfaa54338a34/raw/adk_live_transfer_order_repro.py"
python adk_live_transfer_order_repro.py

Minimal merged-event proof (Layer 1 only — no run_live()):

from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.flows.llm_flows.functions import merge_parallel_function_response_events
from google.genai import types

def response_event(name, *, transfer_target=None):
    actions = EventActions()
    if transfer_target:
        actions.transfer_to_agent = transfer_target
    return Event(
        author="coordinator_agent",
        content=types.Content(
            role="user",
            parts=[types.Part(function_response=types.FunctionResponse(
                id=f"{name}-id", name=name, response={"result": "ok"},
            ))],
        ),
        actions=actions,
    )

merged = merge_parallel_function_response_events([
    response_event("set_state"),
    response_event("transfer_to_agent", transfer_target="specialist_agent"),
])

assert merged.content.parts[0].function_response.name == "set_state"
assert merged.content.parts[1].function_response.name == "transfer_to_agent"
assert merged.actions.transfer_to_agent == "specialist_agent"
# Stock run_live() gate checks parts[0] only → transfer missed

How often has this issue occurred?:

  • Rare

In production the symptom is rare: the model must batch transfer_to_agent with
another tool and have the non-transfer response land in parts[0]. Prompting
for a lone transfer reduces frequency further. When those conditions align, the
bug is deterministic (100% in the attached reproducer).

Metadata

Metadata

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