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

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions