Repository navigation
Conversation
Runner.close always released every toolset on the agent, so the first of two parallel runners or AgentTools tore down a still-used toolset. Holders are a WeakSet of Runners so a dropped runner cannot pin the toolset. AgentTool closes the nested runner in a finally. Fixes google#4045
BlueRaddish
left a comment
There was a problem hiding this comment.
I independently checked the nested AgentTool cleanup path at 93b70886c373f4a6f4718596b4a7f38aacd464bd on Ubuntu/Python 3.13. A test-only probe uses the real AgentTool and Runner, an opened BaseToolset, and a deterministic fake model that either raises a specific RuntimeError or returns done.
With both changed production modules restored to comparison base b0180620f4c2f4f4467a89c37a30f75bf849700b, the error case preserves that same exception but fails cleanup (close_calls == 0); normal success passes. At this PR head both cases pass: the opened toolset closes exactly once and the parent-owned plugin stays open. The full test_agent_tool.py module also passes: 56 tests. Normal scoped repository hooks pass; four hooks have no applicable files. Production source and tracked configuration were checked unchanged.
Actual run and output · Exact test-only probe
This supports ordinary-error cleanup and the normal-success control. I did not run the full tox matrix or establish cancellation behavior or every shared-toolset ownership case. Earlier probe attempts had invalid fixture assumptions and are excluded from these results. OpenAI Codex assisted with this independent validation and comment.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
Problem:
Runner.close()always callstoolset.close()for every toolset on the agent. TwoRunners sharing oneLlmAgentwith aCountingToolsetprintedafter_first_close 1thenafter_second_close 2. The first close already released the toolset while the second runner was still open. ParallelAgentToolruns on agents that share a toolset hit the same path (await runner.close()after each nested run).Solution:
Each
Runneris a weak holder of the toolsets it collected.close()removes that holder and only callstoolset.close()when no live runner remains. ARunnerdropped withoutclose()falls out of the set, so it cannot pin the toolset open.close()also re-collects toolsets so one appended after construct is still released.AgentTool.run_asynccloses its nested runner in afinallyso a raising nested run still unwinds on the same task.Testing Plan
Unit Tests:
6 passed.
With
runners.pyandagent_tool.pychecked out fromb0180620, four of those six fail (test_shared_toolset_stays_open_until_last_runner_closes,test_single_runner_still_closes_its_toolset,test_parallel_agent_tools_do_not_close_shared_toolset_early,test_failed_agent_tool_still_closes_shared_toolset). The dropped-runner and late-added tests pass on main (everyRunner.close()already closed the toolset) and guard the WeakSet / re-collect paths..venv/bin/python -m mypy src/google/adk/runners.py: Success, no issues.Manual End-to-End (E2E) Tests:
Not run. The CountingToolset runners and the parallel AgentTool test cover the two cases in the issue.
Additional context
Claim: #4045 (comment)
#4046 added an
AgentToolManagersingleton and was closed on 2026-05-12 after requested changes stayed open.Checklist