Skip to content

Commit b983fcf

Browse files
ItsMactoharanrk
authored andcommitted
fix: ensure AgentTool text output when skip_summarization is True
When skip_summarization=True, AgentTool previously terminated the flow with a FunctionResponse event that contained no text, leaving UIs stuck. Most UIs do not render function responses, so the agent appeared to hang or return an empty result. This change appends a Text part to the response event when summarization is skipped, so the tool's output is displayed. If the result is a string it is used directly; otherwise it is safely serialized to JSON. Error responses are left unchanged, preserving existing behavior for normal flows. Added regression tests verifying text output is present for string, JSON-string, and structured (output_schema) tool results. Closes #3881 Co-authored-by: Haran Rajkumar <haranrk@google.com> PiperOrigin-RevId: 938286418
1 parent e506fa6 commit b983fcf

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/google/adk/flows/llm_flows/functions.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import contextvars
2424
import copy
2525
import inspect
26+
import json
2627
import logging
2728
import threading
2829
from typing import Any
@@ -1181,6 +1182,9 @@ def __build_response_event(
11811182
tool_context: ToolContext,
11821183
invocation_context: InvocationContext,
11831184
) -> Event:
1185+
# Capture the raw result for display purposes before any normalization.
1186+
display_result = function_result
1187+
11841188
# Specs requires the result to be a dict.
11851189
if not isinstance(function_result, dict):
11861190
function_result = {'result': function_result}
@@ -1198,6 +1202,25 @@ def __build_response_event(
11981202
function_response_parts,
11991203
)
12001204

1205+
# When summarization is skipped, ensure a displayable text part is added so
1206+
# the tool's output is not lost in UIs that don't render function responses.
1207+
# Control-flow tools (e.g. exit_loop) set skip_summarization but return no
1208+
# meaningful output; their None result is normalized to {'result': None}, so
1209+
# skip those to avoid emitting a noisy "null" text part.
1210+
has_displayable_result = display_result is not None and display_result != {
1211+
'result': None
1212+
}
1213+
if (
1214+
tool_context.actions.skip_summarization
1215+
and 'error' not in function_result
1216+
and has_displayable_result
1217+
):
1218+
if isinstance(display_result, str):
1219+
result_text = display_result
1220+
else:
1221+
result_text = json.dumps(display_result, ensure_ascii=False, default=str)
1222+
content.parts.append(types.Part.from_text(text=result_text))
1223+
12011224
function_response_event = Event(
12021225
invocation_id=invocation_context.invocation_id,
12031226
author=invocation_context.agent.name,

tests/unittests/tools/test_agent_tool.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,3 +1531,98 @@ async def close(self):
15311531
assert captured['new_message'] is not None
15321532
text = captured['new_message'].parts[0].text
15331533
assert text == expected_text
1534+
1535+
1536+
@pytest.fixture
1537+
def setup_skip_summarization_runner():
1538+
def _setup_runner(tool_agent_model_responses, tool_agent_output_schema=None):
1539+
tool_agent_model = testing_utils.MockModel.create(
1540+
responses=tool_agent_model_responses
1541+
)
1542+
tool_agent = Agent(
1543+
name='tool_agent',
1544+
model=tool_agent_model,
1545+
output_schema=tool_agent_output_schema,
1546+
)
1547+
1548+
agent_tool = AgentTool(agent=tool_agent, skip_summarization=True)
1549+
1550+
root_agent_model = testing_utils.MockModel.create(
1551+
responses=[
1552+
function_call_no_schema,
1553+
'final_summary_text_that_should_not_be_reached',
1554+
]
1555+
)
1556+
1557+
root_agent = Agent(
1558+
name='root_agent',
1559+
model=root_agent_model,
1560+
tools=[agent_tool],
1561+
)
1562+
return testing_utils.InMemoryRunner(root_agent)
1563+
1564+
return _setup_runner
1565+
1566+
1567+
def test_agent_tool_skip_summarization_has_text_output(
1568+
setup_skip_summarization_runner,
1569+
):
1570+
"""Tests that when skip_summarization is True, the final event contains text content."""
1571+
runner = setup_skip_summarization_runner(
1572+
tool_agent_model_responses=['tool_response_text']
1573+
)
1574+
events = runner.run('start')
1575+
1576+
final_events = [e for e in events if e.is_final_response()]
1577+
assert final_events
1578+
last_event = final_events[-1]
1579+
assert last_event.is_final_response()
1580+
1581+
assert any(p.function_response for p in last_event.content.parts)
1582+
1583+
assert [p.text for p in last_event.content.parts if p.text] == [
1584+
'tool_response_text'
1585+
]
1586+
1587+
1588+
def test_agent_tool_skip_summarization_preserves_json_string_output(
1589+
setup_skip_summarization_runner,
1590+
):
1591+
"""Tests that structured output string is preserved as text when skipping summarization."""
1592+
runner = setup_skip_summarization_runner(
1593+
tool_agent_model_responses=['{"field": "value"}']
1594+
)
1595+
events = runner.run('start')
1596+
1597+
final_events = [e for e in events if e.is_final_response()]
1598+
assert final_events
1599+
last_event = final_events[-1]
1600+
assert last_event.is_final_response()
1601+
1602+
text_parts = [p.text for p in last_event.content.parts if p.text]
1603+
1604+
# Check that the JSON string content is preserved exactly
1605+
assert text_parts == ['{"field": "value"}']
1606+
1607+
1608+
def test_agent_tool_skip_summarization_handles_non_string_result(
1609+
setup_skip_summarization_runner,
1610+
):
1611+
"""Tests that non-string (dict) output is correctly serialized as JSON text."""
1612+
1613+
class CustomOutput(BaseModel):
1614+
value: int
1615+
1616+
runner = setup_skip_summarization_runner(
1617+
tool_agent_model_responses=['{"value": 123}'],
1618+
tool_agent_output_schema=CustomOutput,
1619+
)
1620+
events = runner.run('start')
1621+
1622+
final_events = [e for e in events if e.is_final_response()]
1623+
assert final_events
1624+
last_event = final_events[-1]
1625+
1626+
text_parts = [p.text for p in last_event.content.parts if p.text]
1627+
1628+
assert text_parts == ['{"value": 123}']

0 commit comments

Comments
 (0)