Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
41 changes: 41 additions & 0 deletions tests/unittests/flows/llm_flows/test_functions_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down