Quick Start
Parse an A2UI stream, build its data model, and resolve a dynamic value — end to end, in a few minutes.
@threadplane/a2ui is the protocol layer. It parses the JSONL message stream, gives you typed envelopes, and resolves dynamic values against a data model. It does not render anything. Rendering is @threadplane/chat's <a2ui-surface>. This library is what sits underneath it.
Goals
By the end of this page you'll be able to:
- Install
@threadplane/a2ui. - Parse a newline-delimited A2UI stream into typed messages.
- Build a plain data-model object with
setByPointer. - Resolve a dynamic value with
resolveDynamic.
Install
npm install @threadplane/a2uiThe package has no peer dependencies.
Parse a stream
Let's start with a real stream. An agent emits A2UI as newline-delimited JSON — one envelope per line, each stamped with "version": "v0.9". Here's a booking form, in emission order: the surface is created first, then its data, then the component tree (whose first component is root).
---a2ui_JSON---
{"version":"v0.9","createSurface":{"surfaceId":"booking","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}}
{"version":"v0.9","updateDataModel":{"surfaceId":"booking","value":{"origin":["LAX"],"dest":["JFK"],"passengers":1}}}
{"version":"v0.9","updateComponents":{"surfaceId":"booking","components":[{"id":"root","component":"Column","children":["title","origin","submit"]},{"id":"title","component":"Text","text":"Book a flight","variant":"h2"},{"id":"origin","component":"ChoicePicker","label":"Origin","options":[{"label":"LAX","value":"LAX"},{"label":"JFK","value":"JFK"}],"value":{"path":"/origin"},"variant":"mutuallyExclusive"},{"id":"submit_label","component":"Text","text":"Search flights"},{"id":"submit","component":"Button","child":"submit_label","variant":"primary","action":{"event":{"name":"bookingSubmit","context":{"origin":{"path":"/origin"},"dest":{"path":"/dest"}}}}}]}}Feed each chunk to a parser. push returns the A2uiMessage[] it could complete from everything buffered so far.
import { createA2uiMessageParser } from '@threadplane/a2ui';
const parser = createA2uiMessageParser();
const messages = parser.push(
'{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}}\n',
);
// messages -> 1 message: { version: 'v0.9', createSurface: { surfaceId: 's1', catalogId: '...' } }The parser is line-oriented. A line is only parsed once a newline arrives, so partial JSON buffers until it's complete:
parser.push('{"version":"v0.9","deleteSurface":'); // -> [] (incomplete, buffered)
parser.push('{"surfaceId":"s1"}}\n'); // -> 1 messageThat buffering is deliberate. Agent output streams in fragments, and a half-finished line shouldn't throw mid-render. A missing version field defaults to v0.9, and unknown envelope keys — such as future v1.0 messages — are skipped rather than treated as errors.
Build the data model
An updateDataModel envelope carries an optional path (a JSON pointer; omitted or "/" targets the whole model) and an optional value — plain JSON, no typed wrappers. If value is present, it replaces (or creates) the data at path. If value is omitted, the key at path is deleted.
Applying an update is just the pointer helpers:
import { setByPointer, deleteByPointer } from '@threadplane/a2ui';
import type { A2uiUpdateDataModel } from '@threadplane/a2ui';
function applyUpdateDataModel(
model: Record<string, unknown>,
update: A2uiUpdateDataModel,
): Record<string, unknown> {
const path = update.path && update.path !== '/' ? update.path : undefined;
if (!('value' in update) || update.value === undefined) {
// Omitted value = delete at path (whole-model reset when path is root).
return path ? deleteByPointer(model, path) : {};
}
if (!path) return update.value as Record<string, unknown>;
return setByPointer(model, path, update.value);
}Run it against the booking stream's updateDataModel and you get the model back:
let model: Record<string, unknown> = {};
model = applyUpdateDataModel(model, {
surfaceId: 'booking',
value: { origin: ['LAX'], dest: ['JFK'], passengers: 1 },
});
// model -> { origin: ['LAX'], dest: ['JFK'], passengers: 1 }setByPointer builds the object immutably — each call returns a new object, the input is untouched. The data model guide covers path scoping, deletes, and the pointer helpers in depth.
Resolve a value
A component's props can be bare literals or path references. resolveDynamic collapses both against the model.
import { resolveDynamic } from '@threadplane/a2ui';
resolveDynamic({ path: '/passengers' }, model); // 1
resolveDynamic('Search flights', model); // "Search flights"
resolveDynamic({ path: '/missing' }, model); // undefinedA bare literal (string, number, boolean) passes through unchanged. A { path } reads from the model by JSON pointer. A missing path resolves to undefined rather than throwing — same conservative posture as the parser. A { call } function-call value executes through a function registry (createA2uiFunctionRegistry()) when one is supplied; without one it resolves to undefined.
Conclusion
That's the full loop: stream in, model built, value resolved. From here, the three guides go deeper:
- The A2UI message protocol — surfaces, components, dynamic values, and the four envelopes.
- Working with the data model — the pointer helpers, immutability, and scopes.
- Validating and adapting an A2UI stream — guards, test payloads, and custom renderers.