Skip to content

skip_summarization text-append in __build_response_event is not scoped to AgentTool #6230

Description

@lwangverizon

Description

The skip_summarization text-append in __build_response_event
(src/google/adk/flows/llm_flows/functions.py) is not scoped to AgentTool,
so it fires for every tool that sets skip_summarization. For non-AgentTool
tools whose function response is an internal acknowledgement, this surfaces the
raw response payload to the user as visible text.

Background

PR #5974 (closing #3881) added this block so an AgentTool's output is not lost
in UIs that don't render function responses:

# src/google/adk/flows/llm_flows/functions.py  (current main)
has_displayable_result = display_result is not None and display_result != {
    'result': None
}
if (
    tool_context.actions.skip_summarization
    and 'error' not in function_result
    and has_displayable_result
):
    if isinstance(display_result, str):
        result_text = display_result
    else:
        result_text = json.dumps(display_result, ensure_ascii=False, default=str)
    content.parts.append(types.Part.from_text(text=result_text))

Issue #3881 is specifically about AgentTool (its title: "The agent doesn't
work as expected when skip_summarization is set to True in AgentTool"
), and the
PR title is "ensure AgentTool text output when skip_summarization is True". But
the condition only checks tool_context.actions.skip_summarization — there is no
isinstance(tool, AgentTool) guard, so the behavior applies to all tools.

Why this is a problem

skip_summarization is also set by tools for the opposite reason: their
function response is an internal acknowledgement that should not be summarized
or shown. UI / widget-rendering tools are the common case — they return
something like {"status": "ok"} and set skip_summarization=True because the
user-facing result is a rendered widget, not text.

With the unguarded append, that ack is force-converted into a text Part. A
text part then bypasses the typical UI/SSE filters that strip
functionResponse / functionCall / thought parts, so the raw payload
({"status": "ok", ...}) is surfaced to the user as visible text. We observed
this as a consistent, user-visible regression after upgrading to a build that
contains #5974: render-style tools began leaking their ack JSON into the chat
transcript.

Steps to reproduce

  1. Define a non-AgentTool (e.g. a FunctionTool) that sets
    tool_context.actions.skip_summarization = True and returns a dict such as
    {"status": "ok"}.
  2. Invoke it through the normal function-calling flow.
  3. The resulting function-response event contains an extra Part.from_text(...)
    with the serialized dict, in addition to the functionResponse part.

Expected: only the functionResponse part is present (the ack is not shown as
text). Actual: a text part carrying the serialized response is appended.

Proposed fix

Guard the append with isinstance(tool, AgentTool) so it matches the original
intent — AgentTool keeps its text output; other tools that opt out of
summarization do not have their function response duplicated as text.

A PR implementing this (with a regression test) is attached.

Environment

Metadata

Metadata

Assignees

Labels

core[Component] This issue is related to the core interface and implementationrequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions