Server
A2UIServer wraps a plain autogen.beta.Agent and is a ready-to-serve Starlette ASGI app that speaks canonical A2UI over HTTP. It mirrors autogen.beta.a2a.A2AServer: hold the agent, configure A2UI with flat kwargs, pick a transport= for the wire encoding, and bring your own uvicorn (or any ASGI server) to run the A2UIServer instance directly.
A2UI's wire is transport-agnostic, so one deployment serves one frontend over one transport:
RestTransport(encoding="sse")— Server-Sent Events (text/event-stream), suited to browserEventSourceclients and reconnection.RestTransport(encoding="jsonl")— canonical A2UI NDJSON (application/x-ndjson), suited to generic A2UI clients that consume the native JSON Lines wire.AgUiTransport()— AG-UI events for CopilotKit's renderer (see Serving over AG-UI below).
The transports live in autogen.beta.a2ui.transports.
Note
A2UIServer is re-exported from the top-level autogen.beta.a2ui. It depends on Starlette (declared as an additional dependency, not a core extra); a missing install surfaces as a clear hint rather than an opaque ImportError.
Minimal Server#
The A2UIServer instance is itself the ASGI app — hand it straight to uvicorn:
The turn is served at POST /a2ui by default; pass RestTransport(encoding="jsonl", path="/chat") to change it. Swap encoding="jsonl" for encoding="sse" to stream Server-Sent Events instead.
The Request Contract#
The adapter speaks a minimal, dependency-free JSON body (no ag-ui / a2a-sdk types) so any client that can POST JSON can drive the agent:
{
"messages": [{"role": "user", "content": "show a booking form"}],
"variables": {"locale": "en"},
"a2ui": [{"version": "v0.9", "action": {"name": "confirm", "...": "..."}}],
"a2uiClientCapabilities": {"v0.9": {"supportedCatalogIds": ["..."]}}
}
| Field | Meaning |
|---|---|
messages | The conversation. A trailing run of user messages is the current turn; earlier messages become history. system / developer roles become prompt; assistant becomes a prior response. |
variables | Context variables merged into the turn (e.g. locale, user id). |
a2ui | Client→server envelopes — button clicks (action), v1.0 functionResponse, and client errors — rewritten into corrective prompts for the turn. See Actions. |
a2uiClientCapabilities | Optional catalog negotiation, nested under the protocol version. Folded into the prompt so the LLM only targets components the client can render. |
A body that is not valid JSON, not a JSON object, or has a mistyped field returns 400 with an {"error": ...} payload. A failure during the turn (after the 200 has been sent) surfaces as a final event: error (SSE) or {"error": ...} (NDJSON) frame and is logged — the connection is not torn down silently.
The server is stateless
There is no session store: the client resends the full conversation on every request. Each turn runs on a fresh stream, so horizontal scaling needs no sticky sessions — but the client owns the history.
Response Frames#
Each turn streams the conversational prose first (when present), then one frame per validated A2UI message.
{"text": "Here is your booking form."}
{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"https://a2ui.org/..."}}
{"version":"v0.9","updateComponents":{"surfaceId":"s1","components":[...]}}
{"text": ...} line is AG2 framing (the prose), not itself an A2UI message. The remaining lines are canonical A2UI messages, one per line, ending at EOF. event: text
data: {"text": "Here is your booking form."}
data: {"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"https://a2ui.org/..."}}
event: done
data: {}
event: text frame, each A2UI message as a default (unnamed) data: frame, and the turn closes with event: done. When the agent answers in plain prose (or validation degrades after exhausting retries), only the prose frame is emitted — there are no A2UI message frames.
Driving It From a Client#
Any HTTP client works. Here is a full round-trip with httpx, reading the NDJSON stream line by line:
Because the server is stateless, a follow-up turn just resends the prior messages plus the new one:
Tip
For an end-to-end, runnable example that exercises both the NDJSON and SSE encodings over a real HTTP transport — including a button-click round-trip — see test/beta/a2ui/transports/test_e2e_rest.py in the repository.
Serving over AG-UI (CopilotKit)#
Swap RestTransport for AgUiTransport to serve the same agent as AG-UI events — the protocol CopilotKit's @copilotkit/a2ui-renderer consumes. Nothing about the agent changes; only the wire encoding does:
The turn is served at POST / by default (pass AgUiTransport(path="/agent") to change it) and the request body is an AG-UI RunAgentInput. Each validated A2UI message reaches the renderer as an ACTIVITY_SNAPSHOT event with activityType="a2ui-surface" and the operations under content["a2ui_operations"]; the prose arrives as TEXT_MESSAGE_CHUNK. A CopilotKit button click rides forwardedProps.a2uiAction and is rewritten into the next turn — the same click round-trip as REST (see Actions).
Tip
For a runnable AG-UI round-trip — text + ACTIVITY_SNAPSHOT emission and a forwardedProps button click — see test/beta/a2ui/transports/test_e2e_ag_ui.py in the repository.