Skip to content

fix(live): stop background tools before the transfer delay - #7011

Open
ehtesham-zahoor wants to merge 1 commit into
google:mainfrom
ehtesham-zahoor:fix/7006-stop-live-tools-before-transfer-delay
Open

fix(live): stop background tools before the transfer delay#7011
ehtesham-zahoor wants to merge 1 commit into
google:mainfrom
ehtesham-zahoor:fix/7006-stop-live-tools-before-transfer-delay

Conversation

@ehtesham-zahoor

Copy link
Copy Markdown

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added to tests/unittests/streaming/test_live_tool_shutdown.py:

  • test_handoff_stops_tools_before_the_transfer_delay, parametrized over
    transfer delays and tool completion times.
  • test_handoff_stops_streaming_tools_before_the_transfer_delay, covering the
    other kind of background tool that _stop_background_tool_tasks stops.

Both assert that no function response from the handing-off agent's tools
reaches the model after the transfer event is yielded. A streaming tool
legitimately streams to its own agent before the handoff, so only what follows
the handoff is asserted on. Each case also asserts the tool actually started,
so none can pass vacuously.

Timings covered, and which ones discriminate (result on unmodified main):

delay tool completes on main why it is here
0.5 0.05 fails early in the window
0.5 0.4 fails just before the window closes
1.0 0.5 fails the shipped delay
0.5 0.05 (streaming) fails streaming tool, not just non-blocking
0.5 2.0 passes tool outside the window — guards the guard
0.0 0.05 passes no window at all

Without the fix (source reverted, tests unchanged):

FAILED ...::test_handoff_stops_tools_before_the_transfer_delay[0.5-0.05]
FAILED ...::test_handoff_stops_tools_before_the_transfer_delay[0.5-0.4]
FAILED ...::test_handoff_stops_tools_before_the_transfer_delay[1.0-0.5]
FAILED ...::test_handoff_stops_streaming_tools_before_the_transfer_delay
4 failed, 8 passed, 2 warnings in 14.77s

With the fix:

$ pytest tests/unittests/streaming/test_live_tool_shutdown.py -q
12 passed, 2 warnings in 14.62s

$ pytest tests/unittests/streaming/ -q
79 passed, 74 warnings in 55.67s

$ pytest tests/unittests/streaming/ tests/unittests/flows/llm_flows/ -q
786 passed, 156 warnings in 60.66s

pyink==25.12 and isort==8.0.1 both report clean on the two changed files.

Multi-version run with tox (py310–py314):

  py310: OK (820.28 seconds)
  py311: OK (356.34 seconds)
  py312: OK (357.47 seconds)
  py313: FAIL code 1 (370.52 seconds)
  py314: FAIL code 1 (379.13 seconds)

py313 and py314 each report the same two failures, and they are not from this
change:

FAILED tests/unittests/test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[agent]
FAILED tests/unittests/test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[runner]
2 failed, 13896 passed, 87 skipped, 27 xfailed, 2 xpassed

Both fail identically with this branch's two files reverted to main, so they
reproduce on unmodified upstream. The cause is local: the assertion trips on
sitecustomize, which ships with Homebrew's Python
(/opt/homebrew/Cellar/python@3.13/.../lib/python3.13/sitecustomize.py) and is
not present in the interpreters uv downloads — which is why only the two
Homebrew versions here are affected. The tests added by this PR pass on both:
py313: 12 passed, py314: 12 passed.

The existing test_streaming_tool_stops_when_its_agent_hands_off does not
cover this: it observes tasks[0].done() and the tick count from inside the
sub agent, which only runs once the handoff has already completed, so it passes
either way.

Manual End-to-End (E2E) Tests:

adk web is not involved — this is a timing window inside run_live, so the
E2E evidence is a standalone script that drives the real flow, live request
queue, send task and background tool task against a recording connection. The
full script is in #7006 and runs against released google-adk==2.8.0 with
pip install google-adk==2.8.0 && python repro.py.

Before (a tool completing 0.25s into the 1.0s delay):

DEFAULT_TRANSFER_AGENT_DELAY = 1.0   tool completes at 0.25s
  [ 0.01s] transfer event YIELDED to caller
  [ 0.25s] background tool COMPLETES, enqueues response
  [ 0.26s] connection RECEIVED content  closed=False
  [ 1.01s] connection CLOSED

late output reached the parent connection: True

After:

DEFAULT_TRANSFER_AGENT_DELAY = 1.0   tool completes at 0.25s
  [ 0.01s] transfer event YIELDED to caller
  [ 0.01s] background tool CANCELLED by the guard
  [ 1.01s] connection CLOSED

late output reached the parent connection: False

Control — the same script with the tool completing at 2.0s, outside the window,
on unmodified main. Nothing is delivered, confirming the harness distinguishes
the two cases rather than always reporting a hit:

DEFAULT_TRANSFER_AGENT_DELAY = 1.0   tool completes at 2.0s
  [ 0.01s] transfer event YIELDED to caller
  [ 1.01s] connection CLOSED
  [ 1.02s] background tool CANCELLED by the guard

late output reached the parent connection: False

Note the close still happens at 1.01s after the change: the delay, the send
task cancellation and the connection close are untouched, so the transfer's own
function response — queued just above the branch with
live_request_queue.send_content(event.content) — still reaches the model
before the connection goes away. Only the parent's tools are fenced earlier.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

The guard and the delay both entered in 0088abbe, with the guard already
positioned after the delay; 65b382d5 later moved the block into
_live_llm_flow.py unchanged. This keeps the boundary set in #6541, where the
connection-close and delay logic was explicitly left alone — that logic is not
touched here, only the position of _stop_background_tool_tasks.

Disclosure: I used an AI assistant for repository navigation, reproduction
support, and drafting. I reviewed the change, ran the tests and the E2E script,
and verified the failing-without-the-fix result myself.

The handoff waits DEFAULT_TRANSFER_AGENT_DELAY before it cancels the send
task and closes the connection. The connection is open for the whole of that
wait and the send task is still draining the live request queue, so a tool of
the handing-off agent that completed inside the window had its function
response forwarded to a model that never called it -- the outcome
_stop_background_tool_tasks exists to prevent.

Stop the tools before the delay rather than after it. The delay, the send
task cancellation and the connection close are unchanged, so the transfer's
own function response still reaches the model before the connection closes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Python][Live] Background tool output can reach the parent model during the transfer delay

2 participants