Skip to content

Python: preserve multimodal messages in AG-UI chat client requests - #8116

Open
CoralGarden52 wants to merge 6 commits into
microsoft:mainfrom
CoralGarden52:fix/ag-ui-multimodal-input-8114
Open

Python: preserve multimodal messages in AG-UI chat client requests#8116
CoralGarden52 wants to merge 6 commits into
microsoft:mainfrom
CoralGarden52:fix/ag-ui-multimodal-input-8114

Conversation

@CoralGarden52

Copy link
Copy Markdown

Summary

  • Preserve ordered text, URI, and inline data content when Python AGUIChatClient serializes user messages for AG-UI.
  • Emit current AG-UI InputContent source fields (source.type, source.value, and mimeType) with MIME-aware image/audio/video/document part types.
  • Keep text-only messages, tool calls/results, and assistant message string content backward compatible.
  • Add adapter regressions plus a real httpx.MockTransport request JSON regression test.

Validation

  • PYTHONPATH="$PWD/python/packages/core:$PWD/python/packages/ag-ui:$PWD/python/packages/orchestrations" /home/nyc/agent-framework/python/.venv/bin/pytest python/packages/ag-ui/tests/ag_ui -q — 1100 passed, 1 skipped.
  • uvx --from ruff==0.16.4 ruff check ... — passed.
  • uvx --from ruff==0.16.4 ruff format --check ... — 3 files already formatted.
  • Generated user payload was validated with ag_ui.core.UserMessage; the captured HTTP JSON contains the ordered URL and data parts.

Related issue

Fixes #8114

This is the outbound Agent Framework-to-AG-UI conversion path. It is distinct from #8083, which addresses multimodal content during orchestrator handoff.

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@CoralGarden52

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@CoralGarden52
CoralGarden52 deployed to github-app-auth September 7, 2026 11:25 — with GitHub Actions Active
@moonbox3 Evan Mattson (moonbox3) added the ag-ui Usage: [Issues, PRs], Target: AG-UI protocol integration label Sep 8, 2026
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py Outdated
@CoralGarden52
CoralGarden52 deployed to github-app-auth September 8, 2026 07:11 — with GitHub Actions Active
Comment thread python/packages/ag-ui/agent_framework_ag_ui/_client.py Outdated
@moonbox3

Copy link
Copy Markdown
Contributor

One file conflict, too.

@CoralGarden52
CoralGarden52 force-pushed the fix/ag-ui-multimodal-input-8114 branch from 884cfd2 to e562c95 Compare September 8, 2026 08:14
@CoralGarden52
CoralGarden52 deployed to github-app-auth September 8, 2026 08:14 — with GitHub Actions Active
@agent-framework-automation agent-framework-automation Bot added the documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs label Sep 8, 2026
@CoralGarden52

Copy link
Copy Markdown
Author

Thanks for the heads-up, Evan Mattson (@moonbox3). I rebased the branch onto the current upstream main and resolved the conflict. The PR should now be conflict-free.

@@ -293,22 +294,34 @@ def _extract_state_from_messages(self, messages: Sequence[Message]) -> tuple[lis

last_message = messages[-1]

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.

Following up on the state-marker change: could we strip every marked carrier from messages_to_send and use the most recent carrier as state, instead of inspecting only messages[-1]? Once a carrier is retained in client-controlled history, the next turn makes it non-final, so _extract_state_from_messages() returns it and _encode_agui_segment() serializes the state as a model-visible document. I reproduced {"secret":"SENSITIVE"} in the next request's messages while request state was None.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I’ve addressed this in a follow-up. State extraction now scans the full client-controlled history, removes every dedicated explicitly marked carrier, and uses the most recent successfully decoded carrier as the request state. Historical carriers therefore cannot reach the AG-UI converter as model-visible documents. I also added a regression test for a carrier retained from an earlier turn.

Comment on lines +306 to +307
if (content.additional_properties or {}).get(STATE_CARRIER_KEY) is not True:
return list(messages), None

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.

Have we considered a compatibility path for existing callers that send a final base64 application/json content as request state? The new STATE_CARRIER_KEY guard makes every unmarked legacy carrier bypass extraction, and the multimodal path sends its payload as a model-visible document instead, silently removing request state in a Production/Stable package. Could we preserve legacy extraction behind an option or deprecation window before requiring state_carrier()?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Evan Mattson (@moonbox3) I’ve added a compatibility path in a follow-up. Callers can set allow_legacy_state_carrier=True to retain the existing final base64 application/json extraction behavior; it emits a DeprecationWarning and remains client-only. The option defaults to False, so unmarked JSON documents are treated as model-visible documents by default. I also added a request-level regression test. If you prefer preserving legacy behavior by default during a deprecation window, I can adjust that policy.

@CoralGarden52
CoralGarden52 deployed to github-app-auth September 8, 2026 09:50 — with GitHub Actions Active
Comment on lines +355 to +358
if allow_legacy_state_carrier and messages and not _is_state_carrier_message(messages[-1]):
legacy_state = _extract_legacy_json_state(messages[-1])
if legacy_state is not None:
messages_to_send.pop()

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.

Following up on the earlier mixed-JSON fix: could legacy extraction keep the dedicated state-only boundary? With allow_legacy_state_carrier=True, _extract_legacy_json_state() accepts JSON inside a final text-plus-document message, then messages_to_send.pop() drops the entire prompt and sends the document as state. Could the fallback require exactly one unmarked JSON content so opting into migration does not reintroduce the message loss fixed earlier?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Evan Mattson (@moonbox3) I’ve addressed this in a follow-up. The legacy fallback now requires the final message to contain exactly one unmarked application/json content, so mixed text-plus-document messages remain intact and are sent through the AG-UI converter. The single-content legacy convention remains available with allow_legacy_state_carrier=True, including the deprecation warning. A request-level regression test covers the mixed-message case.

@CoralGarden52
CoralGarden52 deployed to github-app-auth September 8, 2026 11:11 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ag-ui Usage: [Issues, PRs], Target: AG-UI protocol integration documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: AG-UI chat client drops multimodal message content

3 participants