Skip to content

fix(agents): route the turn after a paused single_turn sub-agent back to its delegator - #7605

Closed
Somay-kousis wants to merge 2 commits into
google:mainfrom
Somay-kousis:fix/single-turn-sticky-routing
Closed

Somay-kousis wants to merge 2 commits into
google:mainfrom
Somay-kousis:fix/single-turn-sticky-routing

Conversation

@Somay-kousis

Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

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

2. Or, if no issue exists, describe the change:

Problem:

A mode='single_turn' sub-agent that pauses on a tool confirmation takes every later turn. find_agent_to_run maps an agent inside a delegation back to its delegator through _resolve_task_delegator, but that helper only checks mode == "task". A paused single_turn agent is the last author, so it is picked as the agent to run: a new user message goes to it instead of the root, and after an approval it replies to the user directly while the root's call to it never gets a response.

Solution:

Check mode in DELEGATED_TASK_MODES (task and single_turn, from utils/_agent_mode.py) instead of == "task". The paused single_turn agent now resolves to its delegator on both paths that call the helper: the resume path (the user answers the confirmation; the delegator resumes the sub-agent and gets its result) and the event scan (the user sends a new message; the delegator answers it). Chat sub-agents keep their sticky routing.

Testing Plan

Unit Tests:

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

  • All unit tests pass locally.

  • test_find_agent_to_run_routes_single_turn_agent_to_its_delegator in tests/unittests/agents/test_agent_router.py, next to the existing task-mode test. Parametrized over the mode as a string and as AgentMode.SINGLE_TURN, a delegator that is the root or a nested chat agent, and whether the user answered the pending call.

  • test_run_async_paused_single_turn_agent_returns_turn_to_delegator in tests/unittests/test_runners.py, end to end through Runner: a root with a single_turn sub-agent whose tool needs confirmation. Parametrized over the next user turn (a new message, approve, reject) and ResumabilityConfig(is_resumable=False/True). It checks the tool ran only on approve, the root replies on turn 2, the sub-agent never gets a chat turn, and turn 3 goes to the root.

Before the fix (main at 4dbb55d):

FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-True]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-True]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-True]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-False]
FAILED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-True]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-False]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-True]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-False]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-True]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-False]
FAILED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-True]
14 failed in 6.10s

After the fix:

PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-root_agent-True]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[str-helper-True]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-root_agent-True]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-False]
PASSED tests/unittests/agents/test_agent_router.py::test_find_agent_to_run_routes_single_turn_agent_to_its_delegator[enum-helper-True]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-False]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[new_message-True]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-False]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[approve-True]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-False]
PASSED tests/unittests/test_runners.py::test_run_async_paused_single_turn_agent_returns_turn_to_delegator[reject-True]
14 passed in 9.95s

pytest tests/unittests/agents tests/unittests/test_runners.py tests/unittests/runners tests/unittests/flows tests/unittests/workflow: this branch 3540 passed, 1 skipped, 5 xfailed, 1 xpassed; with main's _agent_router.py, 14 failed, 3526 passed, 1 skipped, 6 xfailed (the 14 are the new tests). The xpass is test_single_turn_child_agent_interrupt_and_resume in test_llm_agent_interruptions.py (xfail "Single-turn agent as subagent not supported yet"), which now passes. I left its marker alone; happy to drop it here if you want.

pyink --check and isort --check-only are clean on the changed files. mypy src/google/adk/agents/_agent_router.py: no issues, same as main.

Manual End-to-End (E2E) Tests:

The script from the issue: a chat root with a single_turn helper whose pay tool needs confirmation, on InMemoryRunner with fake models.

User types something else after the pause. Before:

=== scenario=new_message resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'unrelated message 2'
    helper ['HELPER: second reply']
--- turn 3: user -> 'unrelated message 3'
    helper ['HELPER: third reply']
root LLM calls: 1  helper LLM calls: 3

After:

=== scenario=new_message resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'unrelated message 2'
    root ['ROOT: second reply']
--- turn 3: user -> 'unrelated message 3'
    root ['ROOT: third reply']
root LLM calls: 3  helper LLM calls: 1

User approves. Before:

=== scenario=approve resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'approve confirmation'
    helper ["response pay {'paid': 5}"]
    helper ['HELPER: second reply']
--- turn 3: user -> 'unrelated message 3'
    helper ['HELPER: third reply']
root LLM calls: 1  helper LLM calls: 3

After:

=== scenario=approve resumable=False
--- turn 1: user -> 'pay 5 please'
    root ['call helper']
    helper ['call pay']
    helper ['call adk_request_confirmation']
    helper ["response pay {'error': 'This tool call requires confirmation, please approve or reject.'}"]
--- turn 2: user -> 'approve confirmation'
    helper ["response pay {'paid': 5}"]
    helper ['HELPER: second reply']
    root ["response helper {'result': 'HELPER: second reply'}"]
    root ['ROOT: second reply']
--- turn 3: user -> 'unrelated message 3'
    root ['ROOT: third reply']
root LLM calls: 3  helper LLM calls: 2

Approving after an unrelated message also works on this branch: the root answers the message, then the approval resumes helper and the root gets its result.

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

No open PR touches _agent_router.py. The task-mode mapping came in with c5ee35e.

copybara-service Bot pushed a commit that referenced this pull request Oct 20, 2026
…ator

Merge #7605

A single_turn sub-agent paused on a tool confirmation no longer takes later
turns: a new user message goes to the agent that called it, and an approval
resumes the sub-agent and returns its result to that caller.

Fixes #7602

PiperOrigin-RevId: 1003189869
@adk-bot

adk-bot commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Thank you @Somay-kousis for your contribution! 🎉

Your changes have been successfully imported and merged via Copybara in commit 6727f8f.

Closing this PR as the changes are now in the main branch.

@adk-bot adk-bot added the merged [Status] This PR is merged label Oct 20, 2026
@adk-bot adk-bot closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merged [Status] This PR is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A single_turn sub-agent paused on a tool confirmation takes over every later turn

3 participants