Skip to content

Python: Allow separate tool_result (UI) and tool_result_for_llm (LLM context) #5760

@moonbox3

Description

@moonbox3

Discussed in #5391

Originally posted by tschokokuki April 21, 2026
Situation
Since the latest version of agent-framework-ag-ui tools can return text and alter state deterministically. Which is already great! 👌

@tool
            async def get_weather(city: str) -> Content:
                data = await _fetch_weather(city)
                return state_update(
                    text=f"Weather in {city}: {data['temp']}°C {data['conditions']}",
                    state={"weather": {"city": city, **data}},
                )

Problem
The get_weather example works fine for simple text. But suppose data is a large structured object (e.g. a list of work items, a chart dataset) that the UI should render as a component — not as text. My hypothesis is, that there is currently no way to send rich JSON to the UI as function_result without also sending it to the LLM, where it tends to be repeated or re-summarised verbatim. Correct?

Today _emit_tool_result_common uses the same value for both:

  • ToolCallResultEvent.content — what the UI renders
    -flow.tool_results[].content— what is fed back to the LLM

additional_properties already travels on the Content object but is never read by the emitter to override either result slot.

Proposed solution
Since additional_properties is already on Content, the emitter could honour two reserved keys — no signature change required, full backward compat:

TOOL_RESULT_KEY     = "__ag_ui_tool_result__"          # overrides ToolCallResultEvent.content
TOOL_RESULT_LLM_KEY = "__ag_ui_tool_result_for_llm__"  # overrides what LLM receives

And expose them via state_update:

@tool
async def get_weather(city: str) -> Content:
    data = await _fetch_weather(city)
    return state_update(
        tool_result=data,                                               # UI gets full JSON
        tool_result_for_llm=f"{data['temp']}°C, {data['conditions']}", # LLM gets short text
        state={"weather": data},
    )

Why not use state as a workaround?
state is global and persistent. Tool results are ephemeral per-turn. Mixing them forces the frontend to clean up after every tool call and breaks in parallel multi-tool flows.

Metadata

Metadata

Assignees

No fields configured for Feature.

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions