Skip to content

Commit 7e61b51

Browse files
GWealecopybara-github
authored andcommitted
fix(tools): preserve code_execution_result and executable_code in AgentTool
AgentTool.run_async only extracted text parts from the inner agent's response, silently dropping code_execution_result.output and executable_code.code. Outer agents using an inner agent with a code executor saw nothing. Close #5481 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 916196604
1 parent 6ca6a14 commit 7e61b51

2 files changed

Lines changed: 118 additions & 3 deletions

File tree

src/google/adk/tools/agent_tool.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@
4141
from ..agents.base_agent import BaseAgent
4242

4343

44+
def _part_to_text(part: types.Part) -> str:
45+
"""Returns user-visible text from a Part, including code execution output."""
46+
if part.text:
47+
return part.text
48+
if part.code_execution_result and part.code_execution_result.output:
49+
return part.code_execution_result.output.rstrip('\n')
50+
if part.executable_code and part.executable_code.code:
51+
return part.executable_code.code
52+
return ''
53+
54+
4455
def _get_input_schema(agent: BaseAgent) -> Optional[type[BaseModel]]:
4556
"""Extracts the input_schema from an agent.
4657
@@ -269,9 +280,8 @@ async def run_async(
269280

270281
if last_content is None or last_content.parts is None:
271282
return ''
272-
merged_text = '\n'.join(
273-
p.text for p in last_content.parts if p.text and not p.thought
274-
)
283+
parts_text = (_part_to_text(p) for p in last_content.parts if not p.thought)
284+
merged_text = '\n'.join(t for t in parts_text if t)
275285
output_schema = _get_output_schema(self.agent)
276286
if output_schema:
277287
tool_result = validate_schema(output_schema, merged_text)

tests/unittests/tools/test_agent_tool.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
from typing import Any
1616
from typing import Optional
1717

18+
from google.adk.agents.base_agent import BaseAgent
1819
from google.adk.agents.callback_context import CallbackContext
1920
from google.adk.agents.invocation_context import InvocationContext
2021
from google.adk.agents.llm_agent import Agent
2122
from google.adk.agents.run_config import RunConfig
2223
from google.adk.agents.sequential_agent import SequentialAgent
2324
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
25+
from google.adk.events.event import Event
2426
from google.adk.features import FeatureName
2527
from google.adk.features._feature_registry import temporary_feature_override
2628
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
@@ -985,6 +987,109 @@ async def test_run_async_handles_none_parts_in_response():
985987
assert tool_result == ''
986988

987989

990+
async def _run_agent_tool_with_parts(parts: list[types.Part]) -> Any:
991+
"""Drives AgentTool with an inner agent whose final event content is `parts`."""
992+
993+
class _StaticAgent(BaseAgent):
994+
995+
async def _run_async_impl(self, ctx):
996+
yield Event(
997+
invocation_id=ctx.invocation_id,
998+
author=self.name,
999+
content=types.Content(role='model', parts=parts),
1000+
)
1001+
1002+
inner = _StaticAgent(name='inner_agent', description='static')
1003+
agent_tool = AgentTool(agent=inner)
1004+
1005+
session_service = InMemorySessionService()
1006+
session = await session_service.create_session(
1007+
app_name='test_app', user_id='test_user'
1008+
)
1009+
invocation_context = InvocationContext(
1010+
invocation_id='invocation_id',
1011+
agent=inner,
1012+
session=session,
1013+
session_service=session_service,
1014+
)
1015+
tool_context = ToolContext(invocation_context=invocation_context)
1016+
1017+
return await agent_tool.run_async(
1018+
args={'request': 'test request'}, tool_context=tool_context
1019+
)
1020+
1021+
1022+
@mark.asyncio
1023+
async def test_run_async_extracts_text_only():
1024+
"""Plain text parts pass through unchanged."""
1025+
result = await _run_agent_tool_with_parts([types.Part(text='hello world')])
1026+
assert result == 'hello world'
1027+
1028+
1029+
@mark.asyncio
1030+
async def test_run_async_extracts_code_execution_result_only():
1031+
"""code_execution_result.output and executable_code.code are returned."""
1032+
result = await _run_agent_tool_with_parts([
1033+
types.Part(
1034+
executable_code=types.ExecutableCode(
1035+
language=types.Language.PYTHON, code='print(2 ** 10)'
1036+
)
1037+
),
1038+
types.Part(
1039+
code_execution_result=types.CodeExecutionResult(
1040+
outcome=types.Outcome.OUTCOME_OK, output='1024\n'
1041+
)
1042+
),
1043+
])
1044+
assert result == 'print(2 ** 10)\n1024'
1045+
1046+
1047+
@mark.asyncio
1048+
async def test_run_async_extracts_text_and_code_execution_result():
1049+
"""Mixed text + code parts are concatenated in order."""
1050+
result = await _run_agent_tool_with_parts([
1051+
types.Part(text='Here is the answer:'),
1052+
types.Part(
1053+
executable_code=types.ExecutableCode(
1054+
language=types.Language.PYTHON, code='print(2 ** 10)'
1055+
)
1056+
),
1057+
types.Part(
1058+
code_execution_result=types.CodeExecutionResult(
1059+
outcome=types.Outcome.OUTCOME_OK, output='1024\n'
1060+
)
1061+
),
1062+
])
1063+
assert result == 'Here is the answer:\nprint(2 ** 10)\n1024'
1064+
1065+
1066+
@mark.asyncio
1067+
async def test_run_async_extracts_executable_code_only():
1068+
"""executable_code.code alone is returned when no result part follows."""
1069+
result = await _run_agent_tool_with_parts([
1070+
types.Part(
1071+
executable_code=types.ExecutableCode(
1072+
language=types.Language.PYTHON, code='print("hi")'
1073+
)
1074+
),
1075+
])
1076+
assert result == 'print("hi")'
1077+
1078+
1079+
@mark.asyncio
1080+
async def test_run_async_skips_thought_parts():
1081+
"""Parts marked thought=True are dropped regardless of kind."""
1082+
result = await _run_agent_tool_with_parts([
1083+
types.Part(text='thinking out loud', thought=True),
1084+
types.Part(
1085+
code_execution_result=types.CodeExecutionResult(
1086+
outcome=types.Outcome.OUTCOME_OK, output='42\n'
1087+
)
1088+
),
1089+
])
1090+
assert result == '42'
1091+
1092+
9881093
class TestAgentToolWithCompositeAgents:
9891094
"""Tests for AgentTool wrapping composite agents (SequentialAgent, etc.)."""
9901095

0 commit comments

Comments
 (0)