PydanticAI v2's AGUIAdapter.load_messages converts AG-UI attachments to
native content types before the model boundary; v1 delivered the raw AG-UI
part dicts. _NATIVE_CONTENT listed BinaryContent as a flatten fixpoint, so
under v2 inline attachments were waved straight through and the whole
provider gate was skipped:
- inline PDFs were no longer text-extracted, so raw bytes went to OpenAI
- unsupported image subtypes (HEIC/SVG/TIFF) were no longer degraded and
reached the provider as images, which fails the turn
- missing-mime magic-byte sniffing never ran
- AudioUrl/VideoUrl were neither fixpoints nor classifiable, so they hit
the fail-loud raise
BinaryContent is no longer treated as a fixpoint. _classify_native_content
maps native content onto the same (kind, scheme, mime, value) tuple the
AG-UI classifier produces, so every existing gate — PDF extraction, image
sniffing, the supported-subtype allow list, the binary choke point —
applies unchanged. audio/* and video/* mimes are named explicitly because
_kind_for routes them to "other", and a missing mime defaults to "image"
so the sniffer runs.
Five test assertions checked that state-backing content was still AG-UI
InputContent, which encoded v1's bridging. They now assert the flatten's
output (ImageUrl) never appears in state, which is the guarantee they were
written to protect. Behaviour is otherwise unchanged: a supported inline
image still flattens to an ImageUrl data URI exactly as on v1.
52/52 python tests pass, up from 42/52. Verified against pydantic-ai
2.22.0.
Server, agents and pins moved to v2. The /multimodal cell is NOT yet
ported — see below.
- requirements.txt: pydantic-ai-slim 2.22.0, ag-ui-protocol 0.1.19. Drops
the opentelemetry-api<1.44 ceiling; v2 works with otel 1.44.0.
- StateDeps imports move from pydantic_ai.ag_ui to pydantic_ai.ui across
the 8 agent modules and one test.
- agent_server.py: to_ag_ui() was removed in v2, so a mount_agent() helper
builds the equivalent Starlette sub-app (single POST / route, matching
what AGUIApp produced) and mounts it. All 19 paths are byte-identical,
trailing slashes included, so no TS route changes. deps are constructed
per request because v2's adapter assigns client state onto deps.state
instead of replacing the deps object as v1 did.
- test_multimodal_content_mapping.py: importorskip pointed at the removed
pydantic_ai.ag_ui, which would have skipped all 43 tests green under v2.
Now targets pydantic_ai.ui.ag_ui and uses the public
AGUIAdapter.load_messages in place of the v1 private helper.
Verified against pydantic-ai 2.22.0 by driving the real app with
TestClient: 16/19 mounts return 200 SSE with RUN_STARTED..RUN_FINISHED and
no RUN_ERROR. The other 3 (/a2ui_dynamic, /beautiful_chat, /) reach tool
execution and then fail on a raw OpenAI() client constructed inside a tool,
which the harness cannot intercept and aimock handles in CI. Per-request
deps isolation confirmed on /shared_state_read_write: state sent by one
request does not appear in the next.
Known incomplete: multimodal_agent.py does not handle v2's content types.
v2's load_messages emits BinaryContent where v1 emitted ImageUrl, so
_flatten_messages_for_model no longer flattens it, VideoUrl reaches the
unrecognised-part raise, and one binary path raises RuntimeError. 10 of
the 43 multimodal tests fail as a result.