Skip to content

A2UI spec convergence

Status: Draft | Last reviewed: 2026-09-01

This doc compares the shipped A2UI dialect — A2UI_SCHEMA_VERSION = "0.9" in a2ui_tool.py — against the a2ui.org specification: v0.9.1 (stable) and the v1.0 release candidate. It recommends where wire-format compliance pays off and where the internal dialect should stay proprietary, and lists the concrete v1.0-readiness steps. Read it before changing the A2UI message shapes, the catalog pipeline, or the ui-action return path. The runtime rendering flow itself is documented in A2UI rendering; the pipecat-side architecture analysis is the companion doc, Pipecat-native alignment.

Dialect inventory: where the implementation diverges from spec

The dialect keeps the spec's v0.9 message vocabulary — createSurface, updateComponents, updateDataModel, deleteSurface, flat adjacency-list components, JSON Pointer data bindings — but everything around it is proprietary. The orchestrator's send_a2ui_json_to_client tool emits it (a2ui_tool.py).

Divergence Spec says Implementation does
Version field Every envelope carries "version": "v0.9.1" No version field anywhere on the wire; "v0.9" exists only as a default catalogId string and an internal constant
Component props Nested props: {} object per component Flat props on the component object; A2UIRenderer.tsx normalizes spec shape away in normalizeComponent
Message set Four envelopes Adds enable_multimodal (permission handshake / SMS link), batch (atomic multi-op, used by reframe), plus envelope fields treatment, layout, advancedFrom, clicked, proxyCapabilities
Transport A2A, AG-UI, MCP, SSE+JSON-RPC, WS, REST bindings Protobuf UIRenderEntry (double-encoded payload_json) → Redis entries:{id} stream → named SSE event UI_RENDER; the payload also rides the tool_call channel redundantly (clients parse both)
Action return path action message with resolved context; optional data model in transport metadata POST /conversations/{id}/ui-action → gRPC TYPE_UI_ACTION + Redis pub/sub → injected as a synthetic user text turn (label smuggled as context.__label)
Catalogs JSON catalog documents, negotiated via a2uiClientCapabilities transport metadata Tenant-scoped versioned ESM bundles built by catalog-builder, served via a per-conversation manifest with sha384 integrity — richer than spec catalogs; no capability negotiation
Capability negotiation Transport metadata exchange None — catalog set comes from agent config

None of this breaks anything today, because both ends of the wire are TrueServe code. It matters the moment a non-TrueServe renderer or agent enters the picture.

v0.9.1 convergence: cheap wins

Three changes are near-free and make the dialect a documented superset of v0.9.1 instead of a silent fork:

  1. Emit "version" on every message in a2ui_tool.py's canonical-ordering pass. The renderer ignores unknown keys, so this is additive. Effort S.
  2. Emit spec-shaped props: {} — the client already normalizes both shapes, so producers can switch to spec shape with no renderer change, then the flat-shape normalization becomes legacy tolerance. Effort S. (Note: LLM-generated payloads will emit whatever the prompt teaches; this is a prompt + normalization-direction change, not a validator.)
  3. Document the custom fields as namespaced dialect extensions (treatment, layout, advancedFrom, clicked, batch, enable_multimodal, proxyCapabilities) in A2UI rendering, so the delta from spec is explicit and reviewable rather than archaeological. Effort S.

v1.0 readiness

The v1.0 candidate's changes, mapped to the implementation:

v1.0 concept What it is Implementation status / step
Single-message instantiation createSurface carries optional components + dataModel Renderer already supports it — A2UIRenderer.tsx#L235-L244 seeds csInner.components/dataModel. The remaining work is the orchestrator prompt: a2ui_tool.py still teaches the two-message pattern. Effort S
surfaceId unique per renderer lifetime Re-creating an existing surface id is an error Tension: the dialect deliberately allows re-issuing createSurface to replace a surface, and the tool prompt warns against it for a different reason (state loss). Decide: keep replace-semantics as a documented extension, or move replacement onto deleteSurface + create. Effort S–M
actionResponse Agent replies synchronously to a renderer action The synthetic-user-turn injection stays (it is what makes actions drive the LLM), but add a correlation id to the ui-action path so an actionResponse envelope can be synthesized for spec clients. Effort M
callFunction / functionResponse, callableFrom boundaries Agent invokes renderer-side functions, with runtime-enforced execution boundaries Closest analog is proxyCapabilities + the server proxy endpoint (capability-scoped API calls declared at createSurface). Map proxy capabilities onto callFunction semantics with callableFrom: "agentOnly"; the permission model already matches the spirit. Effort M
surfaceProperties (theme removed) Extensible per-surface properties; framework-native theming Low impact — the dialect never used spec theme. treatment and layout are natural candidates to carry as surfaceProperties entries instead of bare envelope fields. Effort S
Strict catalog schema Functions as object maps, UAX #31 identifiers, no custom $defs Do not replace internal catalogs — versioned tenant ESM bundles with integrity pinning are strictly richer than spec JSON catalogs. Instead, generate a spec-compliant catalog JSON from a2ui_catalogs/a2ui_components (props_schema is already JSON Schema per component) as an interop artifact. Effort M
Standard validation errors {code, surfaceId, path, message} error messages Nothing emits these; relevant only at an interop boundary. Defer

Recommendation

Full wire compliance across the internal stack is not worth it: the protobuf→Redis→SSE transport, the tool_call redundancy, the synthetic-turn action path, and the ESM catalog pipeline all exist for product reasons (recording, replay, optimistic locking, tenant scoping) that the spec's transport bindings do not cover, and no external renderer consumes the internal stream.

Compliance is worth it at exactly one boundary: A2A. a2a_server.py already exposes agents to external A2A clients, and A2UI's primary transport binding is A2A message parts — a spec-compliant remote client that calls a TrueServe agent should be able to receive its surfaces. That makes conversation-api the natural home for an A2UI-over-A2A adapter: egress translates internal UI_RENDER entries into spec v1.0 envelopes (version field, props:{}, surfaceProperties, generated catalog manifest), ingress translates spec action messages into the ui-action path. The internal wire stays the dialect; the adapter is the compliance surface.

Sequenced plan:

Step What Effort Risk
1 v0.9.1 cheap wins: version field, spec-shaped props, documented extensions S Low — additive; renderer tolerates both
2 v1.0 prep that pays for itself internally: single-message createSurface prompt, surfaceProperties carrying treatment/layout, action correlation ids M Low–M
3 Spec catalog JSON generated from a2ui_catalogs/a2ui_components M Low — additive artifact
4 A2UI-over-A2A adapter in conversation-api — build when a partner needs it, not speculatively L M — first genuinely external protocol surface

See also