Skip to content

fix(tools): prevent agent tools from overflowing the context window - #7401

Open
mahir-m01 wants to merge 1 commit into
google:mainfrom
mahir-m01:fix/agent-tool-nested-compaction
Open

mahir-m01 wants to merge 1 commit into
google:mainfrom
mahir-m01:fix/agent-tool-nested-compaction

Conversation

@mahir-m01

Copy link
Copy Markdown

Link to Issue or Description of Change

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

Problem:

AgentTool runs the wrapped agent through a nested Runner created from a bare agent, so the nested run gets no events_compaction_config. A wrapped agent that makes many tool calls keeps growing its own history until the model rejects the request (prompt + max_output_tokens over the context window). The resulting ContextWindowExceededError aborts the caller's invocation, even though the caller's own history is being compacted.

Solution:

AgentTool.run_async now builds the nested App itself (the construction Runner recommends) and gives it the caller's token-threshold compaction settings (token_threshold, event_retention_size). The sliding-window trigger is not inherited: it runs after an invocation finishes, and the nested session is discarded when the tool returns, so it would only add a summarizer call. Building the App also replaces the deprecated Runner(plugins=...) argument, which this code path used to pass.

Testing Plan

Unit Tests:

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

  • All unit tests pass locally.

  • New test_agent_tool_compacts_nested_history_at_caller_token_threshold: the caller's app compacts at 100 tokens and the wrapped agent reads a large file three times. The wrapped agent's last request carries the compaction summary. On main the same test fails (assert 'NESTED SUMMARY' in 'test1'), because the nested history is never compacted.

  • New test_agent_tool_compacts_each_call_and_keeps_caller_config: the root agent calls the agent tool twice. Each nested run compacts its own history, the second run starts from a fresh history, and the caller's EventsCompactionConfig is left unchanged. Fails on main for the same reason.

  • New test_agent_tool_keeps_nested_history_without_token_threshold, parametrized over no compaction config and a sliding-window-only config: the wrapped agent keeps its full history and no summary is added. Passes on main and with the fix; it guards against inheriting more than the token-threshold trigger.

  • The existing Runner stubs in test_agent_tool.py now take app=.

  • pytest tests/unittests/tools/test_agent_tool.py: 58 passed on Python 3.10, 3.11, 3.12, 3.13 and 3.14.

  • pytest tests/unittests -n auto (Python 3.12, rebased on 86a47f6): 17412 passed, 89 skipped, 25 xfailed, 2 xpassed.

  • tox on Python 3.10, 3.11, 3.12, 3.13 and 3.14 passed in every environment on the previous revision, which had the same source change before the added tests and the rebase.

  • pre-commit run --files src/google/adk/tools/agent_tool.py tests/unittests/tools/test_agent_tool.py: all hooks pass. mypy src/google/adk/tools/agent_tool.py reports no new errors compared with main.

Manual End-to-End (E2E) Tests:

A root agent calls an AgentTool-wrapped reader agent, which reads four long chapters with a function tool before answering. Both agents use gemini-3.1-flash-lite; the app sets EventsCompactionConfig(token_threshold=2000, event_retention_size=1). A before_model_callback on reader logs each request it sends:

main (reader history keeps growing):
nested request 1: contents=1 tool_results_in_prompt=0
nested request 2: contents=3 tool_results_in_prompt=1
nested request 3: contents=5 tool_results_in_prompt=2
nested request 4: contents=7 tool_results_in_prompt=3
nested request 5: contents=9 tool_results_in_prompt=4

this branch (reader history is compacted at the caller's threshold):
nested request 1: contents=1 tool_results_in_prompt=0
nested request 2: contents=3 tool_results_in_prompt=1
nested request 3: contents=5 tool_results_in_prompt=2
nested request 4: contents=3 tool_results_in_prompt=1   <- compaction summary added
nested request 5: contents=5 tool_results_in_prompt=2

The same failure was observed in practice with google-adk 1.36.1 and LiteLlm -> vLLM 0.19.1 serving Gemma 4 31B (max_model_len=32768): across about 130 sessions of a coding agent with an AgentTool-wrapped navigation agent, all 8 context-window failures came from the wrapped agent and none from the root agent, which compacts at 14,336 tokens.

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

Related: #3230 (context caching for agent tools and sub agents). Thanks to @loyce-cheng for reviewing the fix on #7397 and suggesting the additional regression cases.

This targets main; I can retarget it to v1 if that branch is preferred.

AgentTool runs the wrapped agent through a nested Runner built without an
App, so the caller's events_compaction_config never reaches the nested
invocation. A wrapped agent that makes many tool calls keeps growing its own
history until the model rejects the request (prompt + max_output_tokens over
the context window), and the error aborts the caller's invocation even though
the caller's own history is being compacted.

The nested run now inherits the caller's token-threshold trigger
(token_threshold and event_retention_size). The sliding-window trigger is not
inherited: it runs after an invocation finishes, and the nested session is
discarded when the tool returns.

Fixes google#7397
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.

AgentTool runs the wrapped agent without the parent App's events_compaction_config, so agent-as-tool history grows unbounded

1 participant