Overview and architecture
This page explains what @lynx-js/genui/a2ui is, the mental model behind
it, and how a server message becomes a rendered UI on the client. It
opens with a runnable quick start, then works through
the architecture so you understand which part of the stack owns each
responsibility — and why the package is shaped the way it is.
What this package is
@lynx-js/genui/a2ui is the ReactLynx client runtime for the A2UI
v0.9 protocol. It consumes validated server-to-client JSON messages and
renders trusted ReactLynx components inside your app.
It is deliberately a renderer and nothing more. The package does not:
- host an Agent or call an LLM,
- own a backend route or chat shell,
- decide what to render — that is the Agent's job.
Your app owns the transport layer and pushes messages into the renderer. Use this package when you already have, or plan to build, an Agent service that returns A2UI messages.
Quick start
Install the package in a ReactLynx app, then render a MessageStore with
<A2UI>. Your transport writes the Agent's messages into the store; the
renderer turns them into UI and hands user actions back through onAction.
That is the entire client loop: push messages in, render, send actions
out. The transport can be REST, SSE, WebSocket, or an in-process mock —
the renderer does not care. The rest of this page explains what happens
between store.push(...) and the rendered surface. For install details and
optional theme tokens, see the README.
The mental model
If you have written React, the shift is small but important:
- In React, your code chooses components and passes props.
- In A2UI, an Agent chooses from a component catalog that your app publishes, and sends data describing which approved component to render and with what props.
The model never ships executable code. It selects a component name and a
prop bag from a contract you defined up front. The client looks that name up
in the catalog and renders the real ReactLynx component you registered.
The result is not arbitrary generated markup. It is a ReactLynx UI tree assembled from a trusted catalog — which is what makes generated UI safe to mount in a production app. An Agent can only reach the components and functions you put in the catalog; anything else it emits renders nothing.
The end-to-end picture
A2UI is a round trip between a server that decides and a client that renders. This package is everything inside the Client box below.
- The user prompts, or taps something in already-rendered UI.
- Your transport adapter sends that to your Agent service.
- The Agent calls a model with the A2UI system prompt and your catalog contract, validates the output, and returns A2UI messages.
- Your adapter writes those messages into a
MessageStore. <A2UI>consumes them, renders the active surface, and forwards any user actions throughonAction— which loops back to step 2.
Because the loop is just "push messages in, get actions out," the transport can be REST, SSE, WebSocket, or an in-process mock. The renderer does not care how messages arrive.
Who owns what
The package draws a hard line between what it provides and what your application provides. Keeping that line crisp is the reason the runtime stays transport-agnostic and the catalog stays explicit.
A useful way to remember it: the server decides, the client renders, and the catalog is the contract they agree on. The catalog is the one piece that lives on both sides of the wire — your client registers implementations; your Agent receives the serialized schema during the handshake.
Inside the client: how a message becomes UI
<A2UI> is an all-in-one front door, but underneath it the package is three
independently composable layers. Understanding the path a message takes
through them makes the renderer's behavior — and its lifecycle gotchas —
predictable.
- Store layer (
@lynx-js/genui/a2ui/store) — pure data logic, no React. TheMessageStoreis an append-only buffer with auseSyncExternalStore-friendlysubscribe/getSnapshotAPI. Your transport callsstore.push(msg); the store stays intentionally dumb about protocol semantics. MessageProcessor— the protocol brain. It owns everySurface, appliescreateSurface/updateComponents/updateDataModel/deleteSurfaceinto surface state, and emits typed events (beginRendering,surfaceUpdate,deleteSurface) for the React layer to consume.dispatch({ userAction })fans actions out to listeners.Resource— apending → success → errorstate machine, one per surface root and per component instance. Its snapshot reference changes on every transition souseSyncExternalStorenever bails out of apending → errorupdate.SignalStore— a@preact/signalswrapper used as the per-surface data model, addressed with JSON-pointer-style paths.- React layer (
@lynx-js/genui/a2ui/react) —<A2UI>plusNodeRendererand the hooks (useAction,useDataBinding,useResolvedProps,useChecks) that turn surface state into a ReactLynx tree.
A few runtime behaviors worth knowing because they explain things you will see while building:
- Children by reference. A component instance references children by id
(
child: "text-1"orchildren: ["a", "b"]). Catalog components render their child ids by delegating to<NodeRenderer>for the same surface. - Data binding. A bound prop is
{ path: string }resolved against the surface'sSignalStore. Relative paths resolve against the component'sdataContextPath, which is what makes templates and repeated lists work. - Template expansion. When
updateComponentscarries a "templated children" placeholder, the processor stores__templatemetadata. When a laterupdateDataModelfills the bound path, it clones the template subtree per item, rewrites child ids, and scopes each clone'sdataContextPath. This is why components can appear or disappear when only the data model changes. - Actions loop back as messages. A tap calls
sendAction;useActionresolves any dynamic values, builds aUserActionPayload, and dispatches it.<A2UI>forwards it to youronAction. Responses, if any, return as new protocol messages that you push into the sameMessageStore. - Unknown components fail soft. A
componentname not in the catalog logs a warning once per tag and rendersnull, rather than throwing.
Package contents
The building blocks you compose against:
<A2UI>— the all-in-one component. It owns aMessageProcessor, subscribes to a developer-suppliedMessageStore, and renders the most recent surface.MessageStore— an append-only buffer of raw protocol messages you push into from any transport: fetch, SSE, WebSocket, or an in-process mock.- Catalog API —
defineCatalog,mergeCatalogs,serializeCatalog,resolveCatalog, anddefineFunction. There is no global component registry; every consumer composes the component and function entries it wants. - Built-in components — 20 A2UI v0.9 basic-catalog renderers (
Text,Image,Button,Row,Column,List,Loading,Card,Modal,Divider,Icon,CheckBox,ChoicePicker,DateTimeInput,LineChart,PieChart,RadioGroup,Slider,TextField, andTabs). See the catalog guide for what each one does. - Per-component manifests —
catalog/<Name>/catalog.json, the JSON-Schema descriptions used during Agent handshakes. basicFunctions— A2UI v0.9 basic-catalog client function entries, ready to spread into yourcatalogsarray.
Exports
The package is split into subpaths so you import only what you use.
Most apps only ever import from @lynx-js/genui/a2ui. Reach for /store and
/react when you build custom catalog components or your own renderer.
<A2UI> props and lifecycle
<A2UI> takes two required props and a set of optional render hooks.
Lifecycle notes that save debugging time:
- One processor per mount.
<A2UI>creates itsMessageProcessor(surfaces, signals, resources) once per mount. Passing a differentmessageStoreinstance later does not reset internal state. To start a fresh session or turn, mount with a differentkeyderived from your turn/session id:<A2UI key={turnId} messageStore={turnStore} … />. onActionis fire-and-forget. The renderer never awaits it. Your Agent pushes follow-up messages back into the sameMessageStoreto update the UI.classNamevswrapSurface. Both can drive theme switching;classNamestyles the surface root,wrapSurfaceadds an outer wrapper. Choose the layer that matches your styling strategy.
Where to go next
- Catalogs, built-ins, and custom components — compose the contract, add manifests, and register your own components.
- System prompts — generate the model instructions that pair an Agent with your catalog.
- Open the A2UI playground — try it live.

