Skip to content

fix(tools): pass the caller's temp: state to an AgentTool's wrapped agent - #7596

Open
Somay-kousis wants to merge 1 commit into
google:mainfrom
Somay-kousis:fix/agent-tool-temp-state
Open

Somay-kousis wants to merge 1 commit into
google:mainfrom
Somay-kousis:fix/agent-tool-temp-state

Conversation

@Somay-kousis

Copy link
Copy Markdown

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:

AgentTool.run_async copies the caller's state, temp: keys included, into create_session(state=...) for the nested runner. create_session drops temp: keys because they are never stored, so the wrapped agent can't see them. {temp:key?} in its instruction renders empty, {temp:key} raises KeyError, and its callbacks and tools read None.

Solution:

Split the temp: keys out of the copied state and pass them to the nested runner.run_async as state_delta. That's the public way to set temp: state for one invocation: append_event applies it to the in-memory session, then trims it from the stored event. Other keys still go through create_session as before, and nothing is persisted that wasn't before. Values that aren't JSON-serializable reach the wrapped agent unchanged, since they only live in memory.

Out of scope: temp: writes made inside the wrapped agent going back to the caller. Happy to look at that separately.

Testing Plan

Unit Tests:

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

test_agent_tool_reads_caller_temp_state in tests/unittests/tools/test_agent_tool.py, parametrized over each way the wrapped agent reads state: a required {temp:ticket} in its instruction, an optional {temp:ticket?}, its before_agent_callback, a tool it calls, a SequentialAgent wrapping an LlmAgent (the nested runner's legacy path), and an agent with input_schema. The caller sets a string temp: key, a temp: key holding a non-JSON-serializable object (checked by identity) and a plain key.

Before the fix:

message=f'Dynamic node {executed_name} failed',
E         google.adk.workflow._errors.DynamicNodeFailError: Dynamic node root_agent failed
ERROR    google_adk.google.adk.workflow._node_runner:_node_runner.py:163 Node execution failed with exception
ERROR    google_adk.google.adk.runners:runners.py:903 Root node tool_agent failed.
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node tool_agent failed
ERROR    google_adk.google.adk.workflow._node_runner:_node_runner.py:163 Node execution failed with exception
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node tool_agent failed
ERROR    google_adk.google.adk.runners:runners.py:903 Root node root_agent failed.
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node root_agent failed
              message=f'Dynamic node {executed_name} failed',
E         google.adk.workflow._errors.DynamicNodeFailError: Dynamic node root_agent failed
ERROR    google_adk.google.adk.workflow._node_runner:_node_runner.py:163 Node execution failed with exception
ERROR    google_adk.google.adk.runners:runners.py:903 Root node root_agent failed.
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node root_agent failed
              message=f'Dynamic node {executed_name} failed',
E         google.adk.workflow._errors.DynamicNodeFailError: Dynamic node root_agent failed
ERROR    google_adk.google.adk.workflow._node_runner:_node_runner.py:163 Node execution failed with exception
ERROR    google_adk.google.adk.runners:runners.py:903 Root node tool_agent failed.
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node tool_agent failed
ERROR    google_adk.google.adk.workflow._node_runner:_node_runner.py:163 Node execution failed with exception
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node tool_agent failed
ERROR    google_adk.google.adk.runners:runners.py:903 Root node root_agent failed.
google.adk.workflow._errors.DynamicNodeFailError: Dynamic node root_agent failed
FAILED tests/unittests/tools/test_agent_tool.py::test_agent_tool_reads_caller_temp_state[instruction]
FAILED tests/unittests/tools/test_agent_tool.py::test_agent_tool_reads_caller_temp_state[optional_instruction]
FAILED tests/unittests/tools/test_agent_tool.py::test_agent_tool_reads_caller_temp_state[callback]
FAILED tests/unittests/tools/test_agent_tool.py::test_agent_tool_reads_caller_temp_state[tool]
FAILED tests/unittests/tools/test_agent_tool.py::test_agent_tool_reads_caller_temp_state[sequential_agent]
FAILED tests/unittests/tools/test_agent_tool.py::test_agent_tool_reads_caller_temp_state[input_schema]
6 failed, 61 deselected in 10.40s

After the fix:

$ pytest tests/unittests/tools/test_agent_tool.py
67 passed in 6.86s

$ pytest tests/unittests/tools
3136 passed in 141.24s (0:02:21)

pyink --check and isort --check-only are clean on the changed files. mypy src/google/adk/tools/agent_tool.py reports the same one error as on main (AgentTool(agent=agent) in from_config, an existing BaseNode vs BaseAgent mismatch).

Manual End-to-End (E2E) Tests:

The script from the issue runs a coordinator LlmAgent with a tool that sets temp:ticket and an AgentTool specialist, on InMemoryRunner with fake models.

Before:

specialist instruction 'Ticket: {temp:ticket?}'
  coordinator saw: ['Ticket: ', 'Ticket: T-42', 'Ticket: T-42']
  specialist saw:  ['Ticket: ']
specialist instruction 'Ticket: {temp:ticket}'
  coordinator saw: ['Ticket: ', 'Ticket: T-42']
  specialist saw:  []
  run raised:      KeyError: "Context variable not found: `temp:ticket` in agent 'specialist'."

After:

specialist instruction 'Ticket: {temp:ticket?}'
  coordinator saw: ['Ticket: ', 'Ticket: T-42', 'Ticket: T-42']
  specialist saw:  ['Ticket: T-42']
specialist instruction 'Ticket: {temp:ticket}'
  coordinator saw: ['Ticket: ', 'Ticket: T-42', 'Ticket: T-42']
  specialist saw:  ['Ticket: T-42']

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

Open PRs touching agent_tool.py: #6020, #7045, #7249 and #7401. None changes how the caller's state reaches the nested session. #7045 re-indents the event loop, so one of the two may need a small rebase.

…gent

AgentTool copied the caller's state into create_session for the nested
runner, and create_session drops temp: keys because they are never stored.
The wrapped agent then saw no temp: state: {temp:key} in its instruction
raised KeyError, {temp:key?} rendered empty, and its callbacks and tools
read None. Pass the temp: keys to the nested run_async as state_delta,
which applies them for that invocation without storing them.
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's wrapped agent can't read the caller's temp: state

2 participants