A2UI Overview
A2UI is the structured UI path for agent-built surfaces in chat.
The important boundary is simple: the agent streams declarative messages, the client owns rendering, and Angular handlers stay inside your application. The model does not ship code. It emits a constrained surface description that @threadplane/chat, @threadplane/a2ui, and @threadplane/render turn into Angular UI.
When an assistant message starts with ---a2ui_JSON---, the chat streaming pipeline treats the rest of the content as newline-delimited A2UI JSON.
#Runtime Flow
This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. A2uiSurfaceComponent turns the accumulated surface state into a render spec and delegates to @threadplane/render, so handlers, render events, and json-render state bindings use the same path for both the preferred state input and the legacy surface input.
#Message Envelopes
The parser recognizes four envelope keys:
| Envelope | Purpose |
|---|---|
surfaceUpdate | Adds or replaces components on a surface. |
dataModelUpdate | Updates the surface data model. |
beginRendering | Marks the surface root and optional style hints. |
deleteSurface | Removes a surface. |
Unknown envelopes are ignored. Malformed JSONL lines are skipped. Incomplete JSON waits in the parser buffer until a newline arrives.
That behavior is deliberate. Agent streams are partial. The parser should not crash the UI because one line is unfinished mid-token.
#Minimal Protocol Stream
A surface usually needs three messages: components, data, and a root. On the wire, each envelope is one newline-delimited JSON object after the sentinel:
The same surfaceUpdate envelope, expanded for readability, looks like this:
The data and root envelopes are smaller:
Components use keyed union definitions:
This is different from a flat component: "Text" shape. The current source expects the keyed union form.
This stream demonstrates the protocol boundary. The chat path accumulates the surface state as JSONL arrives, then A2uiSurfaceComponent converts the current surface into a render spec: it unwraps keyed component definitions, maps children, creates render state bindings, and turns actions into handlers.
#Data Model
A2UI component props can point at the surface data model.
surfaceToSpec() converts path references into json-render state bindings. Catalog input components can use emitBinding() to write back through the render event pipeline.
The write-back protocol is client-side state. The current action-message builder does not re-read component state at click time. It serializes the action params it receives. If a caller provides an internal A2uiSurface with sendDataModel: true, the builder also includes the current surface data model snapshot under metadata.a2uiClientDataModel; the JSONL wire envelopes do not expose a sendDataModel field.
#Actions
Buttons carry an A2uiAction:
surfaceToSpec() turns this into a render click binding that calls the built-in a2ui:event handler. A2uiSurfaceComponent then emits an A2uiActionMessage.
If the internal surface object has sendDataModel: true, the emitted message also includes metadata.a2uiClientDataModel with the current surface data model snapshot. Streamed protocol surfaces created by the current surface store do not set that flag.
Catalog components receive resolved props as Angular inputs from the render engine. Bind (action) when you want agent-bound events, and bind (events) when you want the lower-level render stream.
#Local Handlers
A2uiSurfaceComponent also registers an a2ui:localAction handler. Consumer handlers take priority, and the built-in fallback currently supports openUrl.
Use local handlers for client-owned behavior. Use A2UI actions for agent-bound events.
#A2UI vs json-render
Both paths render structured UI, but they optimize for different jobs.
| Dimension | A2UI | json-render |
|---|---|---|
| Wire shape | JSONL message stream | Single JSON spec |
| State | Surface data model | Spec state |
| Best fit | Incremental agent-owned surfaces; protocol-level A2UI streams | One-shot rendered content |
| Detection | ---a2ui_JSON--- prefix | JSON object content |
| Rendering | Surface state converted to a render spec | json-render spec directly |
Use A2UI when the agent needs to keep updating a surface and you are working at the protocol boundary. Use json-render when the agent needs a stable, directly rendered structured result. For production interaction that depends on component handlers, state bindings, and child projection, verify the exact A2UI rendering path you are using.
#Setup
Pass the built-in A2UI catalog to chat:
For custom component sets, build a catalog with the same view registry tools used by @threadplane/render.
#Gotchas
The A2UI parser is not a full schema validator. It recognizes envelope keys and leaves deeper validation to typed code, tests, and your runtime boundary.
Schema-valid messages are not enough to make UI executable. Your catalog must contain components for the emitted types, and your handlers must exist for the actions you expect users to take.
Do not use old envelope names such as createSurface, updateComponents, or updateDataModel with the current parser. They are ignored because the source recognizes surfaceUpdate, dataModelUpdate, beginRendering, and deleteSurface.
Do not assume the progressive chat renderer and the render-spec compatibility path have identical capabilities. The compatibility path projects a surface through surfaceToSpec(). The progressive path mounts catalog components from A2uiComponentView state and pushes Angular inputs directly.
#What's Next
Render a surface outside the full ChatComponent composition.
Understand how messages update surfaces and data models.
See the built-in catalog components and their props.
Read the protocol package docs for parser, schema, and pointer helpers.