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 implementation

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions