Render A2UI surfaces from AG-UI activity snapshots or A2A messages, task snapshots, and artifacts as generative UI.
A2UI is a declarative generative UI protocol: the agent streams surface operations (create a surface, upsert components, update a data model) and the host renders them from a pre-approved component catalog, with no code over the wire. The AG-UI ecosystem carries A2UI as ACTIVITY_SNAPSHOT events with activityType: "a2ui-surface" and the operations under content.a2ui_operations; this is the convention emitted by @ag-ui/a2ui-middleware.
useAgUiRuntime consumes these snapshots natively. The A2A runtime produces the same surfaces from agent messages, task snapshots, and artifacts. Each surface becomes one tool-call part with toolCallId a2ui:<surfaceId> and toolName "present", whose args are the converted generative UI spec, so surfaces render through the same path as the present frontend tool. Snapshots with any other activityType are ignored, and the MCP Apps activity path is unaffected.
Wire contract
A backend paints or updates a surface by emitting an activity snapshot on the AG-UI stream:
{
"type": "ACTIVITY_SNAPSHOT",
"messageId": "a2ui-surface-call_1",
"activityType": "a2ui-surface",
"replace": true,
"content": {
"a2ui_operations": [
{ "version": "v0.9", "createSurface": { "surfaceId": "s1" } },
{
"version": "v0.9",
"updateComponents": {
"surfaceId": "s1",
"components": [
{ "id": "root", "component": "Card", "child": "total" },
{ "id": "total", "component": "Text", "text": { "path": "/total" } }
]
}
},
{
"version": "v0.9",
"updateDataModel": { "surfaceId": "s1", "path": "/", "contents": { "total": "$42" } }
}
]
}
}- A snapshot with
replace: true(the schema default) rebuilds thatmessageId's surface state from the operations it carries.replace: falsemeans the snapshot is ignored when thatmessageIdhas already been seen, matching the AG-UI event spec. The middleware always emitsreplace: true. - Surfaces are keyed by the
surfaceIdinside the operations, not bymessageId. Multiple snapshots that share amessageIdupdate their surfaces in place, and the synthesized part keeps itstoolCallId, so the rendered surface updates without duplicating parts. - Each synthesized surface stores its replayable operations in
artifact.a2ui, so it replays from history and in the Assistant Cloud dashboard. - Component trees use the A2UI adjacency-list model: nodes reference their children by id (
childrenfor a list,childfor a single component), the root node has idroot, and props of shape{ "path": "/x/y" }are JSON Pointer bindings resolved against the surface data model. Operation payloads from v0.9, v0.9.1 (read as v0.9), and v1.0 are accepted, including the v1.0 inlinecomponentsanddataModeloncreateSurface. - Lifecycle snapshots that carry a
status(such as"building") but noa2ui_operationsare tolerated and produce no part until operations arrive. AdeleteSurfaceoperation removes the surface's part. - The
a2ui:tool-call id prefix is reserved for synthesized surface parts: they are client-side render artifacts and are excluded from the history sent back to the agent, so genuine agent tool calls must not use ids starting witha2ui:.
Quick start
Register the present frontend tool from @assistant-ui/generative-ui/react; incoming surfaces then render with the default vocabulary:
import {
JSONGenerativeUI,
defaultGenerativeUILibrary,
} from "@assistant-ui/generative-ui/react";
const generative = new JSONGenerativeUI({
library: defaultGenerativeUILibrary,
});
const toolkit = {
present: generative.present({ display: "standalone" }),
};Pass the toolkit to your assistant as on the Generative UI page; no other configuration is needed on useAgUiRuntime.
Actions
Button nodes dispatch $action objects with type "a2ui:action" through the action registry. Wire the registry to useAgUiSendA2uiAction inside a component; the hook returns a stable function, so the toolkit can be memoized on it:
import { useMemo } from "react";
import { useAgUiSendA2uiAction } from "@assistant-ui/react-ag-ui";
import {
JSONGenerativeUI,
createActionRegistry,
defaultGenerativeUILibrary,
} from "@assistant-ui/generative-ui/react";
function useA2uiToolkit() {
const sendA2uiAction = useAgUiSendA2uiAction();
return useMemo(() => {
const generative = new JSONGenerativeUI({
library: defaultGenerativeUILibrary,
actions: createActionRegistry({
"a2ui:action": ({ payload }) => sendA2uiAction(payload),
}),
});
return { present: generative.present({ display: "standalone" }) };
}, [sendA2uiAction]);
}Sending an action triggers a run with no new user message, and the agent receives it as forwardedProps.a2uiAction.userAction, the convention @ag-ui/a2ui-middleware consumes. The action rides exactly one run; the type field is stripped and a timestamp is added when absent.
A Button whose action is a functionCall runs on the client and never reaches the agent. It dispatches type "a2ui:functionCall" with the function's call name, its resolved args, surfaceId, and sourceComponentId, so register a handler for each function you support. The basic catalog's only function with a side effect is openUrl; the agent chose its URL, so check the protocol before opening it:
createActionRegistry({
"a2ui:action": ({ payload }) => sendA2uiAction(payload),
"a2ui:functionCall": ({ payload }) => {
const args = payload["args"] as { url?: unknown } | undefined;
if (payload["call"] !== "openUrl" || typeof args?.url !== "string") return;
if (!URL.canParse(args.url, window.location.href)) return;
const url = new URL(args.url, window.location.href);
if (url.protocol === "https:" || url.protocol === "http:") {
window.open(url, "_blank", "noopener,noreferrer");
}
},
});An action reads what the user entered: a context entry or functionCall argument bound to an input's path carries the input's value when the button is clicked, not the value the agent sent, and one bound to an object that contains input paths carries that object with those fields replaced. A userMessage carries the entered text the same way only when bound to a TextField or DateTimeInput that no other input shares, because the spec types it as a string. The converter writes each such value as a $field reference that the renderer resolves when the action fires, with the value the agent sent as its fallback, so a custom component that dispatches the action itself sends that value instead. A single choice ChoicePicker bound to a list sends an empty list when nothing is selected and a one item list after selection, matching the string list the spec binds it to.
The middleware turns the action into a synthetic log_a2ui_event tool-call pair that is never declared in input.tools; a backend that validates tool calls against the declared tool list will reject it.
Component mapping
The converter maps the A2UI basic catalog onto the default generative UI vocabulary: Text becomes Markdown (h1 to h6 variants become Header, caption becomes Caption), Column becomes Col, List becomes ListView with ListViewItem children (or Row when horizontal), TextField becomes Input (a longText variant makes it multiline, obscured sets inputType to "password", and number sets it to "number" with arbitrary decimal steps; all values remain strings), CheckBox becomes Checkbox, ChoicePicker becomes RadioGroup regardless of its display hint, including chips (CheckboxGroup for its multipleSelection variant), DateTimeInput becomes DatePicker with inputType set to "date", "time", or "datetime" from its enabled flags, or from the agent value when neither flag is provided; an incompatible or unparseable value uses a text Input to preserve it, and Slider becomes Slider (its steps divide the range evenly; without them it steps by the largest power of ten within one percent of its range, or finer when the bound value needs it). Icon maps when its name is in the default vocabulary. Tabs becomes Tabs and keeps each tab's { title, child } entry, with each child converted as its panel in order; Tabs labels each panel with its entry's title. Image, Row, Card, Divider, and Button map one to one. A child reference converts like a one element children. Template children, written as { "componentId", "path" }, repeat that component for each item of the bound list inside the container a Row, Column, or List converts to, so a Row template stays a Row; any other node with template children expands into a ListView by default. Bound values resolve against the data model, and a relative path resolves against the current template item. An input shows its bound value, including a later update from the agent, and is named by the full JSON Pointer of its binding, such as /form/email, or /items/0/qty inside a template item. Function call values (formatString with its ${...} expressions, formatNumber, formatCurrency, formatDate, pluralize, and, or, and not) are evaluated during conversion in the browser's locale and time zone, including inside an action's context and a functionCall's args; any other function in a value is skipped with a warning. A Button takes its label from a child Text, and any other child, such as an Icon, renders inside the button; its primary and borderless variants become the primary and ghost button styles. Button actions, written as { "event": { "name", "context" } } or as a bare { "name", "context" }, become $action objects with type "a2ui:action" carrying the action name, surfaceId, sourceComponentId, the resolved context, and the event's userMessage when it resolves to a string, which is how your action registry receives them. A { "functionCall": { "call", "args" } } action becomes type "a2ui:functionCall" instead, as described under Actions, and an action the converter cannot read is dropped with a warning. Unknown components and operations are skipped with a debug warning by default. Pass { keepUnknownComponents: true } as the second argument to convertSurfaceToUISpec to retain unmapped components as resolved $type nodes for a viewer that provides them; a retained Modal keeps its trigger and content as its children in that order. A reference the converter cannot follow is dropped with a warning, so those children line up with the references only when the conversion reports no warnings. surfaceToOperations, exported from @assistant-ui/generative-ui/a2ui, produces JSON-serializable v0.9 operations for storing and replaying a reduced surface. Pass surfaceId when the surface was not created by the reducer; otherwise it uses the reducer's stored id. Conversion is bounded (depth 32, 100 template items, 5000 nodes) so a malformed stream cannot hang the client.
When JSONGenerativeUI renders an A2UI tool call with artifact.a2ui operations, bound inputs update a local data model for that surface as the user types. Sibling bindings and function values update immediately, and inputs bound to the same path share one value. Incoming snapshots preserve local edits unless the agent changes the value at the same path. Button actions keep their $field references and read the current controls at dispatch, including edits made by custom inputs or browser autofill. These edits stay local to the renderer. Calling convertSurfaceToUISpec directly produces a snapshot tree.
Limitations
- The upstream middleware currently emits v0.9 operation payloads; the renderer accepts v0.9, v0.9.1, and v1.0 shapes.
sendDataModelis not implemented, so an action never carries the whole data model; bind the fields it needs in itscontext.DateTimeInputusesenableDateandenableTimewhen either flag is provided: both true select"datetime",enableTimetrue alone selects"time", and otherwise the input uses"date". With neither flag, a date, time, or datetime value selects its matching type; an empty value selects"date". A value that does not fit the selected type uses a textInput, preserving the original string and edits exactly as entered, including""when cleared. On a datetimeDatePicker, a date onlyminexpands to the start of that day and a date onlymaxexpands to its end. Other bounds that do not match the picker mode are dropped. The text fallback enforces no native temporal bounds.- A single choice
ChoicePickerrenders as a radio group even when its display hint ischips; the default vocabulary does not render chips.