Core protocol layer, data binding, 18 built-in components, LiveView integration, Local transport, and demo app are complete. See the codebase and README for current functionality.
This document is the implementation plan for remaining work — organized as independent phases that can be built and shipped incrementally.
The A2UI v0.9 spec is transport-agnostic. It defines the JSON envelope format and message semantics but does not prescribe specific transport bindings. The spec lists these as viable options:
| Transport | Type | Status |
|---|---|---|
| Local | Erlang messages | Done (A2UI.Transport.Local) |
| SSE + JSON-RPC | HTTP transport | Done (A2UI.Plug, A2UI.Plug.SSE, A2UI.Plug.JSONRPC) |
| A2A | Protocol adapter | Done (A2UI.A2A, A2UI.Transport.A2A) |
| WebSocket | HTTP transport | Phase 3 |
| REST | HTTP transport | Phase 4 |
| AG-UI | Protocol adapter | Phase 5 |
| MCP | Protocol adapter | Phase 6 (future — spec needs more detail) |
A2UI.Connection is an opaque handle representing a connected client. The
agent receives it in handle_connect/2 and passes it to send_message/2 —
the agent never knows which transport is in use.
External Client ←──HTTP/WS──→ Handler Process ←──Erlang msgs──→ A2UI Agent
(A2UI.Connection)
A2UI.Connectionstruct:id,transport(module),ref,pidA2UI.Agent.send_message/2dispatches throughtransport.deliver_message(ref, msg)A2UI.Transportbehaviour includesdeliver_message/2(agent→client direction)- Agent tracks connections as
%{id => Connection.t()}withProcess.monitor - Dispatch helpers:
A2UI.Transport.send_action/3,.send_error/3,.disconnect/1
All transports must fulfill:
- Reliable, ordered delivery — messages arrive in generation order
- Message framing — clear boundaries between JSON envelopes (newline-delimited JSON, SSE events, or WebSocket frames)
- Metadata support — data model sync (
a2uiClientDataModel) and capabilities exchange (a2uiClientCapabilities) - Bidirectional capability (optional) — return channel for
actionmessages
Primary network transport for web clients. Server→client via SSE stream, client→server via JSON-RPC 2.0 POST.
-
A2UI.Plug— Plug router, guarded withCode.ensure_loaded?(Plug)GET /sse→ SSE connection endpointPOST /rpc→ JSON-RPC endpoint- Options:
:agent(required),:sse_path,:rpc_path
-
A2UI.Plug.SSE— SSE connection handler- On GET: generate unique connection ID, create
A2UI.Connection - Connects to agent via
{:a2ui_connect, %Connection{}} - Receives
{:a2ui_deliver, msg}→Message.to_json/1→ SSEdata:event - SSE event format:
id: <seq>\ndata: <json>\n\n - Keep-alive pings every 30s
- Initial SSE event includes
connectionIdfor client to use in JSON-RPC
- On GET: generate unique connection ID, create
-
A2UI.Plug.JSONRPC— JSON-RPC request handler- Method
a2ui.action: params{connectionId, action}→ parse Action → forward to agent via handler pid - Method
a2ui.error: params{connectionId, error}→ parse Error → forward to agent via handler pid - Returns JSON-RPC 2.0 success/error responses
- Looks up handler pid from connection registry
- Method
-
A2UI.Plug.ConnectionRegistry— ETS-based registry- Maps
connection_id → handler_pid - Created lazily in
A2UI.Plug.init/1 - Auto-cleanup when handler process exits (via monitor)
- Maps
Server→client (SSE events):
id: 1
data: {"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"basic"}}
id: 2
data: {"version":"v0.9","updateComponents":{"surfaceId":"main","components":[...]}}
Client→server (JSON-RPC POST):
{"jsonrpc":"2.0","method":"a2ui.action","params":{"connectionId":"abc123","action":{"name":"submit","surfaceId":"main","sourceComponentId":"btn1","timestamp":"2025-01-01T00:00:00Z","context":{}}},"id":1}No new required deps. Plug is already transitive via phoenix_live_view.
test/a2ui/plug/sse_test.exs— start agent, open SSE, verify events streamtest/a2ui/plug/json_rpc_test.exs— POST actions, verify JSON-RPC responsestest/a2ui/plug_test.exs— routing, 404s, method not allowed
Uses the A2A (Agent-to-Agent) protocol as transport between a LiveView renderer and a remote A2UI agent. Enables the core agentic use case: AI agents rendering UI across process or network boundaries.
The v0.9 spec defines A2A as a first-class transport binding:
- Each A2UI envelope maps to a single
A2A.Part.Datain an A2A message a2uiClientDataModelplaced in A2A messagemetadatawhensendDataModelis activea2uiClientCapabilitiesplaced inmetadataof every client→server message- A2UI sessions map to A2A
contextId
Implements the A2UI.Transport behaviour. Allows a LiveView to connect
to a remote A2UI agent over A2A protocol (HTTP JSON-RPC + SSE streaming).
-
A2UI.Transport.A2A— A2A client transportconnect/1— accepts:urlor:client(pre-builtA2A.Client), sends initial A2A message, spawns handler process- Extracts A2UI envelopes from
A2A.Part.Dataparts in the response - Delivers each as
{:a2ui_message, parsed_struct}to the LiveView send_action/3— sends A2A message with Action asPart.Datasend_error/3— sends A2A message with Error asPart.Datadisconnect/1— cancels the A2A task if still running- Manages A2A task lifecycle: continues tasks via
task_idoninput_requiredstate
Wraps an A2UI.Agent so it can be served over A2A protocol. An A2A client
(including A2UI.Transport.A2A above) can connect to it.
-
A2UI.A2A— adapter macrouse A2UI.A2A, agent: MyApp.UIAgentgenerates anA2A.Agentthat wraps the givenA2UI.Agent- On
handle_message/2: extracts A2UI actions/errors from incomingPart.Dataparts → createsA2UI.Connection→ forwards to the A2UI agent - Collects responses via sync fence → wraps as
A2A.Part.Data - Returns
{:input_required, parts}to keep the task open for more turns - First message in a task triggers
handle_connect; subsequent messages with the sametask_idroute tohandle_action
A2A Message (client → server) A2UI Message
───────────────────────────── ─────────────
Part.Data with action JSON → Action struct
Part.Data with error JSON → Error struct
metadata.a2uiClientDataModel → data model sync
metadata.a2uiClientCapabilities → capabilities exchange
contextId → session/connection ID
A2A Message (server → client) A2UI Message
───────────────────────────── ─────────────
Part.Data with createSurface ← CreateSurface
Part.Data with updateComponents ← UpdateComponents
Part.Data with updateDataModel ← UpdateDataModel
Part.Data with deleteSurface ← DeleteSurface
A2UI envelope embedded as A2A Data part:
{
"role": "ROLE_AGENT",
"parts": [
{"kind": "data", "data": {"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"basic"}}}
],
"metadata": {}
}Client action sent as A2A message:
{
"role": "ROLE_USER",
"parts": [
{"kind": "data", "data": {"name":"submit","surfaceId":"main","sourceComponentId":"btn1","timestamp":"...","context":{}}}
],
"metadata": {
"a2uiClientCapabilities": {"supportedCatalogIds": ["https://a2ui.org/specification/v0_9/basic_catalog.json"]},
"a2uiClientDataModel": {"surfaces": {"main": {"reservation": {"name": "Alice"}}}}
}
}{:a2a, "~> 0.2", optional: true}— guarded withCode.ensure_loaded?(A2A.Agent)/Code.ensure_loaded?(A2A.Client)
-
test/a2ui/transport/a2a_test.exs— client transport: connect, receive messages, send actions, multi-turn -
test/a2ui/a2a_test.exs— server adapter: connect, action forwarding, error forwarding, cancel, multi-turn
Full-duplex communication over a single WebSocket connection. Simpler than SSE + JSON-RPC since both directions use the same connection.
-
A2UI.Plug.WebSocket—WebSockbehaviour handler- On upgrade: generate connection ID, connect to agent via
{:a2ui_connect, self()} - Receives WS text frame →
Message.from_json/1→ forward to agent as{:a2ui_action, action, metadata}or{:a2ui_error, error, metadata} - Receives
{:a2ui_message, msg}→Message.to_json/1→ send as WS text frame - On WS close:
{:a2ui_disconnect, self()}
- On upgrade: generate connection ID, connect to agent via
Both directions use the same JSON envelope format as the protocol spec:
{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[...]}}{"name":"submit","surfaceId":"main","sourceComponentId":"btn1","timestamp":"...","context":{}}{:websock, "~> 0.5", optional: true}— or rely on it being available via Bandit/Phoenix
test/a2ui/plug/websocket_test.exs— start Bandit, connect WS client, verify bidirectional messages
Stateless HTTP for simple integrations, webhooks, and non-streaming clients.
-
A2UI.Plug.REST— stateless HTTP handlerPOST /action— receive Action JSON, return response messages as JSON array- Implementation: spawn temporary proxy process → connect to agent → send action → collect response messages (with configurable timeout) → disconnect → return
POST /connect— optional: create surface, return initial messagesPOST /disconnect— optional: clean up
- No streaming — client gets a batch of response messages, not a live stream
- Timing-dependent: proxy process waits for a configurable window (e.g. 100ms) to collect agent responses
- Not suitable for long-running agent interactions
- Good for: webhooks, simple form submissions, testing, one-shot queries
test/a2ui/plug/rest_test.exs— POST action, verify response contains expected messages
Maps A2UI protocol to CopilotKit's AG-UI event-based protocol. Enables A2UI agents to serve AG-UI compatible frontends.
AG-UI uses a single POST / endpoint. The client sends RunAgentInput JSON,
the server responds with an SSE stream of typed events. AG-UI defines 16 event
types organized into lifecycle, text, tool, state, and custom categories.
-
A2UI.AGUI.Plug— AG-UI endpointPOST /— receivesRunAgentInput, starts SSE event stream- Spawns handler process (proxy pid) → connects to agent
- Wraps A2UI message flow in AG-UI lifecycle:
RUN_STARTED→ events →RUN_FINISHED
-
A2UI.AGUI.EventMapper— A2UI → AG-UI event translationCreateSurface→STATE_SNAPSHOT(surface as initial state)UpdateComponents→STATE_DELTA(RFC 6902 JSON Patch for component changes)UpdateDataModel→STATE_DELTA(JSON Patch for data model changes)DeleteSurface→STATE_DELTA(remove surface key)- Custom events for A2UI-specific semantics that don't map cleanly
-
A2UI.AGUI.InputMapper— AG-UI → A2UI input translationRunAgentInput.messages→ A2UIActionmessagesRunAgentInput.state→ A2UI data model (surface state sync)RunAgentInput.tools→ A2UI capabilities metadata
-
A2UI.AGUI.Encoder— SSE event encoding- Encodes AG-UI events as SSE:
event: <type>\ndata: <json>\n\n - Event types:
RUN_STARTED,RUN_FINISHED,RUN_ERROR,STATE_SNAPSHOT,STATE_DELTA,CUSTOM, etc.
- Encodes AG-UI events as SSE:
A2UI Agent Flow AG-UI Event Stream
────────────── ──────────────────
connect → RUN_STARTED {threadId, runId}
CreateSurface{surfaceId, ...} → STATE_SNAPSHOT {snapshot: {surfaces: {<id>: ...}}}
UpdateComponents{surfaceId, ..} → STATE_DELTA {delta: [{op:"replace", path:"/surfaces/<id>/components", value:...}]}
UpdateDataModel{surfaceId, ..} → STATE_DELTA {delta: [{op:"replace", path:"/surfaces/<id>/dataModel/<path>", value:...}]}
DeleteSurface{surfaceId} → STATE_DELTA {delta: [{op:"remove", path:"/surfaces/<id>"}]}
disconnect → RUN_FINISHED {threadId, runId}
error → RUN_ERROR {message, code}
Request:
POST /
Content-Type: application/json
Accept: text/event-stream
{"threadId":"t1","runId":"r1","state":{},"messages":[{"id":"m1","role":"user","content":"..."}],"tools":[],"context":[],"forwardedProps":{}}Response (SSE):
event: RUN_STARTED
data: {"type":"RUN_STARTED","threadId":"t1","runId":"r1"}
event: STATE_SNAPSHOT
data: {"type":"STATE_SNAPSHOT","snapshot":{"surfaces":{"main":{"catalogId":"basic","components":{},"dataModel":{}}}}}
event: STATE_DELTA
data: {"type":"STATE_DELTA","delta":[{"op":"replace","path":"/surfaces/main/components","value":{...}}]}
event: RUN_FINISHED
data: {"type":"RUN_FINISHED","threadId":"t1","runId":"r1"}
No external deps — AG-UI is SSE-based, uses the same Plug chunked response pattern as Phase 1. No Elixir AG-UI SDK exists, so we implement from spec.
test/a2ui/agui/event_mapper_test.exs— unit tests for A2UI → AG-UI mappingtest/a2ui/agui/input_mapper_test.exs— unit tests for AG-UI → A2UI mappingtest/a2ui/agui/plug_test.exs— integration: POST RunAgentInput, verify SSE events
Map A2UI to Model Context Protocol for AI agents that use MCP tool calls.
The v0.9 spec mentions MCP as "delivered as tool outputs or resource subscriptions" but provides no detailed binding. This phase is deferred until the spec provides more guidance.
- Separate package:
a2ui_mcp - A2UI messages as MCP tool results with structured content
- MCP tool calls → A2UI actions
- MCP resource subscriptions → A2UI surface streams
- Depends on an MCP Elixir library (e.g.
mcp)
These are not planned for this library:
- Agent logic —
A2UI.Agentprovides connection/lifecycle scaffolding; business logic uses GenServer,langchain, etc. - Custom component catalogs — renderers can be extended via
A2UI.ComponentRendererbehaviour, but catalog hosting is not this library's concern