Skip to content

fix(runners): close shared toolsets only from the last runner - #7045

Open
claxman wants to merge 1 commit into
google:mainfrom
claxman:fix/4045-shared-toolset-refcount
Open

claxman wants to merge 1 commit into
google:mainfrom
claxman:fix/4045-shared-toolset-refcount

Conversation

@claxman

@claxman claxman commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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 calls toolset.close() for every toolset on the agent. Two Runners sharing one LlmAgent with a CountingToolset printed after_first_close 1 then after_second_close 2. The first close already released the toolset while the second runner was still open. Parallel AgentTool runs on agents that share a toolset hit the same path (await runner.close() after each nested run).

Solution:
Each Runner is a weak holder of the toolsets it collected. close() removes that holder and only calls toolset.close() when no live runner remains. A Runner dropped without close() 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_async closes its nested runner in a finally so a raising nested run still unwinds on the same task.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.
PYTHONPATH=src .venv/bin/python -m pytest -q tests/unittests/test_runners.py::TestRunnerWithPlugins::test_shared_toolset_stays_open_until_last_runner_closes tests/unittests/test_runners.py::TestRunnerWithPlugins::test_single_runner_still_closes_its_toolset tests/unittests/test_runners.py::TestRunnerWithPlugins::test_dropped_runner_does_not_pin_shared_toolset tests/unittests/test_runners.py::TestRunnerWithPlugins::test_late_added_toolset_is_closed tests/unittests/tools/test_agent_tool.py::test_parallel_agent_tools_do_not_close_shared_toolset_early tests/unittests/tools/test_agent_tool.py::test_failed_agent_tool_still_closes_shared_toolset

6 passed.

With runners.py and agent_tool.py checked out from b0180620, 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 (every Runner.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 AgentToolManager singleton and was closed on 2026-05-12 after requested changes stayed open.

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.

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 BlueRaddish left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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.

[BUG] When executing Runners(of which agent contains stateful toolset) in parallel, toolset is being released when only one of them has finished

3 participants