Skip to content

Python: Preserve MCP Host payloads in AG-UI snapshots - #8129

Draft
Eduard van Valkenburg (eavanvalkenburg) wants to merge 1 commit into
mcp-host-payload-corefrom
ag-ui-mcp-live-snapshots
Draft

Python: Preserve MCP Host payloads in AG-UI snapshots#8129
Eduard van Valkenburg (eavanvalkenburg) wants to merge 1 commit into
mcp-host-payload-corefrom
ag-ui-mcp-live-snapshots

Conversation

@eavanvalkenburg

Copy link
Copy Markdown
Member

Motivation & Context

AG-UI live tool events and complete message snapshots currently expose only the model-facing MCP result, so Host applications lose structured content and other complete MCP result fields. Snapshot replay also needs to preserve the original model-facing Content items without allowing Host/UI JSON to enter provider history.

This is layer 2 of the #7971 replacement stack. It depends on #8128, which introduces the bounded private core marker carrying the complete MCP Host payload. This layer does not change the model-content selection behavior established by #7897.

Description & Review Guide

  • What are the major changes? Detect the layer-1 marker with an older-core fallback; project complete MCP Host payloads through live TOOL_CALL_RESULT, approval results, hosted-MCP compatibility results, and MESSAGES_SNAPSHOT; attach private, lossless model-content replay metadata; restore that metadata only on marked inbound snapshot messages; and enforce a fixed aggregate history budget covering both Host JSON and replay sidecars while retaining newest projections.
  • What is the impact of these changes? AG-UI Hosts receive structured MCP result data consistently across live and replayed flows, while model history remains unchanged and safe. Explicit TOOL_RESULT_DISPLAY_KEY payloads remain authoritative, ordinary tool results and MCP-shaped JSON are unchanged, server _meta is not replicated into replay items, and existing parallel/mixed function-result conversion remains intact.
  • What do you want reviewers to focus on? The provenance gate for inbound replay, lossless rich-content restoration, approval/live/snapshot parity, and newest-first aggregate retention after persisted-history seeding.

Related Issue

Part of #7959

This draft is stacked on #8128 and replaces only the AG-UI slice of the existing #7971 implementation; #8128 owns the core payload marker, while this PR owns AG-UI projection and replay safety.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (1 commit(s)): 9289ea07a267
Model: gpt-5.6-sol-fast

Overview

The change consistently projects complete MCP Host payloads across live, approval, hosted-MCP, and snapshot paths while preserving model-facing content in a private replay sidecar. Exact marker checks, metadata stripping, safe fallback behavior, and newest-first aggregate retention provide strong guardrails. Two compatibility gaps remain: persisted snapshots are unsafe for older readers in a rolling deployment, and valid custom MCP Content types are silently degraded on replay.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (2 medium) across 2 files. Details are attached to the affected lines below.

Affected areas: python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py, python/packages/ag-ui/agent_framework_ag_ui/_run_common.py

"id": message_id,
"role": "tool",
"toolCallId": call_id,
"content": snapshot_result_content,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This persists the Host projection in canonical content and depends on two new private fields to recover the model-facing result. During a rolling deployment, an older worker reading a snapshot written by this version ignores those fields and treats the Host JSON as model input; if an explicit display payload contains accepted, the old adapter can even reinterpret it as an approval and re-execute the matching tool. Keep the canonical persisted representation safe for older readers, or version/gate the snapshot format so incompatible workers cannot consume it.

isinstance(serialized_items, list)
and serialized_items
and all(
isinstance(item, dict) and item.get("type") in {"text", "data", "uri", "error"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sidecar writer serializes every Content returned by the public MCPTool.parse_tool_results contract, but this allowlist rejects every valid type outside these four. A custom parser returning another model-facing Content type works on the live turn, then the next snapshot replay replaces the entire result with the text fallback and changes provider history. Ensure the writer and reader support the same valid content set, or reject/normalize unsupported items before the first model turn so replay remains equivalent.

Comment on lines +194 to +197
model_text = _model_text_from_replay_items(message)
if not model_text:
model_text = _model_content_from_mcp_host_payload(safe_json_parse(message.get("content")))
bounded_message["content"] = model_text

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when the model replay consists only of data, uri, or error items? _model_text_from_replay_items returns an empty string, so eviction substitutes text from the complete Host payload; a custom parser that deliberately excluded sensitive MCP text will then send that Host-only text to the provider on replay. Could the omitted form retain a safe serialization of the actual model items, or use a generic result instead of consulting the Host payload?

Comment on lines +197 to +200
bounded_message["content"] = model_text
bounded_message.pop(_AGUI_MCP_TOOL_RESULT_KEY, None)
bounded_message.pop(_AGUI_TOOL_RESULT_MODEL_CONTENT_KEY, None)
bounded_message[_AGUI_HOST_PAYLOAD_OMITTED_KEY] = True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the bounded form retain provenance for model text such as {"accepted": true}? Removing _agentFrameworkMcpResult makes agui_messages_to_agent_framework reinterpret that legitimate terminal result as a new approval response, so it drops the function result and leaves an orphan tool call that provider APIs reject. _agentFrameworkHostPayloadOmitted is never read on the inbound path, so it does not prevent the reclassification.

Comment on lines +126 to +128
serialized_items: list[dict[str, Any]] = []
for item in items:
serialized = item.to_dict()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could replay serialization fail safely before the result is emitted? item.to_dict() recursively traverses arbitrary additional_properties, so a cyclic provider value raises RecursionError after the MCP tool has already run but before AG-UI emits or persists its result; retrying the turn can execute the side effect again. A serialization fallback to the already-bounded model_result would preserve the terminal result without traversing that metadata.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we preserve the model replay metadata on the event path used by AgentFrameworkWorkflow? _emit_tool_result_common puts the complete Host payload in ToolCallResultEvent.content, but adds _agentFrameworkMcpResult and _agentFrameworkModelContent only to flow.tool_results; _WorkflowSnapshotBuilder therefore persists unmarked Host JSON, and the next turn sends structuredContent to the provider. That workflow save path also bypasses _bound_host_payload_history, so these snapshots can grow without the fixed aggregate limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants