From e845f95fe4426f6589f62fd2b90ef24feb8f8272 Mon Sep 17 00:00:00 2001 From: Luyang Wang Date: Sat, 27 Jun 2026 22:19:52 -0400 Subject: [PATCH] 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. --- src/google/adk/flows/llm_flows/functions.py | 22 +++++++--- .../flows/llm_flows/test_functions_simple.py | 41 +++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/google/adk/flows/llm_flows/functions.py b/src/google/adk/flows/llm_flows/functions.py index 795b706559f..f28a6638da1 100644 --- a/src/google/adk/flows/llm_flows/functions.py +++ b/src/google/adk/flows/llm_flows/functions.py @@ -1202,16 +1202,26 @@ def __build_response_event( function_response_parts, ) - # When summarization is skipped, ensure a displayable text part is added so - # the tool's output is not lost in UIs that don't render function responses. - # Control-flow tools (e.g. exit_loop) set skip_summarization but return no - # meaningful output; their None result is normalized to {'result': None}, so - # skip those to avoid emitting a noisy "null" text part. + # When summarization is skipped for an AgentTool, ensure a displayable text + # part is added so the sub-agent's output is not lost in UIs that don't render + # function responses (see #3881). This is scoped to AgentTool deliberately: + # other tools (e.g. UI/widget-rendering tools) set skip_summarization + # precisely because their function response is an internal acknowledgement + # that must NOT be surfaced as visible text. Control-flow tools (e.g. + # exit_loop) set skip_summarization but return no meaningful output; their + # None result is normalized to {'result': None}, so skip those to avoid + # emitting a noisy "null" text part. + # + # Local import to avoid a circular import (agents -> flows -> tools -> agents); + # AgentTool pulls in the agents package, so it cannot be imported at module top. + from ...tools.agent_tool import AgentTool + has_displayable_result = display_result is not None and display_result != { 'result': None } if ( - tool_context.actions.skip_summarization + isinstance(tool, AgentTool) + and tool_context.actions.skip_summarization and 'error' not in function_result and has_displayable_result ): diff --git a/tests/unittests/flows/llm_flows/test_functions_simple.py b/tests/unittests/flows/llm_flows/test_functions_simple.py index 2720967d293..a56c2d4aaee 100644 --- a/tests/unittests/flows/llm_flows/test_functions_simple.py +++ b/tests/unittests/flows/llm_flows/test_functions_simple.py @@ -1623,6 +1623,47 @@ def simple_fn(**kwargs) -> dict: assert function_response.scheduling is None +@pytest.mark.asyncio +async def test_skip_summarization_non_agent_tool_appends_no_text_part(): + """A non-AgentTool with skip_summarization must not emit a visible text part. + + The skip_summarization text-append in __build_response_event exists to keep + AgentTool output visible in UIs that don't render function responses (#3881). + It must not fire for other tools: UI/widget tools set skip_summarization + because their function response is an internal acknowledgement, not + user-facing text. A FunctionTool result must therefore produce only a + function_response part (no Part.from_text), otherwise the ack payload would be + surfaced to the UI as visible text. + """ + + def render_widget(tool_context: ToolContext) -> dict: + tool_context.actions.skip_summarization = True + return {'status': 'ok', 'widget': 'rendered'} + + tool = FunctionTool(render_widget) + model = testing_utils.MockModel.create(responses=[]) + agent = Agent(name='test_agent', model=model, tools=[tool]) + invocation_context = await testing_utils.create_invocation_context( + agent=agent, user_content='' + ) + function_call = types.FunctionCall(name=tool.name, args={}, id='fc_test') + event = Event( + invocation_id=invocation_context.invocation_id, + author=agent.name, + content=types.Content(parts=[types.Part(function_call=function_call)]), + ) + + result_event = await handle_function_calls_async( + invocation_context, event, {tool.name: tool} + ) + + assert result_event is not None + assert result_event.actions.skip_summarization is True + # The ack is carried only as a function_response part — never as text. + assert any(p.function_response is not None for p in result_event.content.parts) + assert all(p.text is None for p in result_event.content.parts) + + async def _drain_live_function_responses( live_request_queue: LiveRequestQueue, count: int,