Skip to content

Commit e845f95

Browse files
committed
fix(flows): scope skip_summarization text-append to AgentTool
PR #5974 (issue #3881) added a text part to the function-response event when skip_summarization is True, so an AgentTool's output stays visible in UIs that do not render function responses. The condition was not guarded by tool type, so it fires for every tool that sets skip_summarization. Tools other than AgentTool set skip_summarization for the opposite reason: their function response is an internal acknowledgement (e.g. a UI/widget tool returning {"status": "ok"}) that should not be summarized or shown to the user. Force-converting that ack into a text Part makes it bypass UI/SSE filters that only strip functionResponse/functionCall/thought parts, so the raw payload is surfaced to the user as visible text. Scope the append to AgentTool via isinstance, matching the original change's stated intent (issue #3881 is specifically about AgentTool). AgentTool keeps its text output; other tools no longer have their function response duplicated as text. AgentTool is imported lazily to avoid the agents -> flows -> tools -> agents circular import. Adds a regression test: a non-AgentTool with skip_summarization=True produces only a function_response part and no text part. The existing AgentTool skip_summarization tests continue to pass.
1 parent 50c81eb commit e845f95

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,16 +1202,26 @@ def __build_response_event(
12021202
function_response_parts,
12031203
)
12041204

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.
1205+
# When summarization is skipped for an AgentTool, ensure a displayable text
1206+
# part is added so the sub-agent's output is not lost in UIs that don't render
1207+
# function responses (see #3881). This is scoped to AgentTool deliberately:
1208+
# other tools (e.g. UI/widget-rendering tools) set skip_summarization
1209+
# precisely because their function response is an internal acknowledgement
1210+
# that must NOT be surfaced as visible text. Control-flow tools (e.g.
1211+
# exit_loop) set skip_summarization but return no meaningful output; their
1212+
# None result is normalized to {'result': None}, so skip those to avoid
1213+
# emitting a noisy "null" text part.
1214+
#
1215+
# Local import to avoid a circular import (agents -> flows -> tools -> agents);
1216+
# AgentTool pulls in the agents package, so it cannot be imported at module top.
1217+
from ...tools.agent_tool import AgentTool
1218+
12101219
has_displayable_result = display_result is not None and display_result != {
12111220
'result': None
12121221
}
12131222
if (
1214-
tool_context.actions.skip_summarization
1223+
isinstance(tool, AgentTool)
1224+
and tool_context.actions.skip_summarization
12151225
and 'error' not in function_result
12161226
and has_displayable_result
12171227
):

tests/unittests/flows/llm_flows/test_functions_simple.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,6 +1623,47 @@ def simple_fn(**kwargs) -> dict:
16231623
assert function_response.scheduling is None
16241624

16251625

1626+
@pytest.mark.asyncio
1627+
async def test_skip_summarization_non_agent_tool_appends_no_text_part():
1628+
"""A non-AgentTool with skip_summarization must not emit a visible text part.
1629+
1630+
The skip_summarization text-append in __build_response_event exists to keep
1631+
AgentTool output visible in UIs that don't render function responses (#3881).
1632+
It must not fire for other tools: UI/widget tools set skip_summarization
1633+
because their function response is an internal acknowledgement, not
1634+
user-facing text. A FunctionTool result must therefore produce only a
1635+
function_response part (no Part.from_text), otherwise the ack payload would be
1636+
surfaced to the UI as visible text.
1637+
"""
1638+
1639+
def render_widget(tool_context: ToolContext) -> dict:
1640+
tool_context.actions.skip_summarization = True
1641+
return {'status': 'ok', 'widget': 'rendered'}
1642+
1643+
tool = FunctionTool(render_widget)
1644+
model = testing_utils.MockModel.create(responses=[])
1645+
agent = Agent(name='test_agent', model=model, tools=[tool])
1646+
invocation_context = await testing_utils.create_invocation_context(
1647+
agent=agent, user_content=''
1648+
)
1649+
function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test')
1650+
event = Event(
1651+
invocation_id=invocation_context.invocation_id,
1652+
author=agent.name,
1653+
content=types.Content(parts=[types.Part(function_call=function_call)]),
1654+
)
1655+
1656+
result_event = await handle_function_calls_async(
1657+
invocation_context, event, {tool.name: tool}
1658+
)
1659+
1660+
assert result_event is not None
1661+
assert result_event.actions.skip_summarization is True
1662+
# The ack is carried only as a function_response part — never as text.
1663+
assert any(p.function_response is not None for p in result_event.content.parts)
1664+
assert all(p.text is None for p in result_event.content.parts)
1665+
1666+
16261667
async def _drain_live_function_responses(
16271668
live_request_queue: LiveRequestQueue,
16281669
count: int,

0 commit comments

Comments
 (0)