Installation
@flabbergasted-ai/a2ui renders Google's A2UI (Agent to UI) protocol with Flab components. Install the package, wire the Flab theme, connect a transport, and hand a surface to <A2UISurface>.
<A2UISurface> is a client component — it consumes a live message stream — so it renders inside Server Components but runs in the browser.Add the package
@flabbergasted-ai/react and @flabbergasted-ai/tokens come along as dependencies; add them explicitly only if you also use Flab directly. React and react-dom are peer dependencies.
pnpm add @flabbergasted-ai/a2ui# npm i / yarn add / bun add all work tooImport the token layer and expose the classes
In your global stylesheet, after Tailwind. The token import must follow Tailwind so its @theme registration can extend it. Point @source at both Flab and the A2UI renderer's shipped files, so Tailwind generates the utilities the adapters use (the renderer adds a few layout classes of its own).
/* app/globals.css */@import 'tailwindcss';@import '@flabbergasted-ai/tokens/css';
@source '../node_modules/@flabbergasted-ai/react/dist';@source '../node_modules/@flabbergasted-ai/a2ui/dist';Add the theme provider
A2UI renders Flab components, so the theme setup is identical: wrap your app in FlabThemeProvider and run themeInitScript in the document head so the light/dark class is set before first paint. Visual styling is owned by the Flab theme — the agent never sends colours.
// app/layout.tsx (Next.js App Router)import { FlabThemeProvider } from '@flabbergasted-ai/react';import { themeInitScript } from '@flabbergasted-ai/react/theme-script';import './globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} /> </head> <body data-flab-root className="bg-canvas text-primary"> <FlabThemeProvider>{children}</FlabThemeProvider> </body> </html> );}Connect a transport and render a surface
An A2uiClient consumes a transport, parses the agent's message stream, and maintains a store of surfaces. Subscribe to that store, then hand a surface to <A2UISurface>. Pass client.onAction so user interactions travel back to the agent. This example uses the in-memory transport for a local demo.
'use client';
import { FLAB_CATALOG_ID, A2uiClient, A2UISurface, createMemoryTransport, type RendererAction,} from '@flabbergasted-ai/a2ui';import { useEffect, useMemo, useSyncExternalStore } from 'react';
export function AgentView() { const transport = useMemo(() => createMemoryTransport(), []); const client = useMemo(() => new A2uiClient(transport), [transport]);
useEffect(() => { const disconnect = client.connect(); // Demo: push a stream. In production this arrives from the agent. transport.push({ version: 'v1.0', createSurface: { surfaceId: 'main', catalogId: FLAB_CATALOG_ID }, }); transport.push({ version: 'v1.0', updateComponents: { surfaceId: 'main', components: [{ id: 'root', component: 'Text', text: 'Hello from the agent' }], }, }); return disconnect; }, [client, transport]);
const surfaces = useSyncExternalStore( client.surfaces.subscribe, client.surfaces.getSnapshot, client.surfaces.getSnapshot, ); const surface = surfaces.get('main'); const handleAction = (action: RendererAction) => { void client.onAction(action).catch((error) => console.error('A2UI action failed', error)); };
return surface ? <A2UISurface surface={surface} onAction={handleAction} /> : null;}Use a real transport in production
A2UI is transport-agnostic. The AG-UI binding follows the a2ui-surface convention emitted by @ag-ui/a2ui-middleware: surface operations arrive inside ACTIVITY_SNAPSHOT events (activityType: "a2ui-surface", operations under content.a2ui_operations), and user actions travel back as the A2UI userAction. Inject your AG-UI client as a bridge — a subscribe over the event stream plus a sendAction that triggers a run carrying forwardedProps.a2uiAction.userAction.
import { A2uiClient, createAgUiTransport } from '@flabbergasted-ai/a2ui';
const client = new A2uiClient( createAgUiTransport({ // Forward the raw AG-UI event stream (RunStarted, ActivitySnapshot, …). subscribe: (handler) => agUiClient.subscribe(handler), // Send the A2UI user action back to the agent (middleware logs it). sendAction: (userAction) => agUiClient.runAgent({ forwardedProps: { a2uiAction: { userAction } } }), }),);client.connect();Or connect a raw Server-Sent Events endpoint directly. The agent streams A2UI envelopes as SSE data: frames; createSseTransport reads them with fetch (so it can carry auth headers and a request body) and POSTs user actions back to sendUrl. It reconnects if the stream drops.
import { A2uiClient, createSseTransport } from '@flabbergasted-ai/a2ui';
const client = new A2uiClient( createSseTransport('/api/agent/stream', { headers: { Authorization: `Bearer ${token}` }, sendUrl: '/api/agent/action', // where user actions are POSTed }),);client.connect();Register renderer functions (optional)
Local functions the agent (or UI) may call. Each declares who can invoke it via allowedCallers; the renderer enforces that at runtime and never executes arbitrary code.
import { A2uiClient, FunctionRegistry } from '@flabbergasted-ai/a2ui';
const functions = new FunctionRegistry().register({ name: 'getScreenResolution', allowedCallers: 'agentOnly', handler: () => [window.innerWidth, window.innerHeight],});
const client = new A2uiClient(transport, { functions });Production contract
This package accepts strict v1.0 envelopes and pins the upstream candidate reference e9c44fc50cd7a583947ff89f779ddaa71a611bdb. Executable schemas come byte-for-byte from lockfile-pinned @a2ui/web_core@0.12.0 with committed SHA-256 values. Missing or different versions are rejected; protocol upgrades are explicit package changes rather than silent compatibility rewrites.
Trust boundary
Agent messages are untrusted. Official schemas, catalog negotiation, component schemas, graph validation, URL policy and function caller policy run before state is committed or UI is rendered. Your application still owns authentication, action authorization and domain permissions.
Failure channels
Use onParseError for invalid wire messages, onResult for structured runtime failures, onTransportError for failed client sends,<A2UISurface onError> for recoverable rendering failures, and each transport's onError for connection failures.
Default ceilings
32 surfaces, 500 components per surface, 64 graph levels, 1,000 template instances, 1 MiB per message and 512 KiB per data model. Runtime and transport options can lower or raise these limits deliberately.
Upgrade policy
A schema or catalog change requires synced schemas, conformance fixtures, migration notes and a changelog entry. Older envelopes are not normalized implicitly; a new protocol version gets a separate versioned parser and catalog path.
Give the agent the catalog
The agent generates against the Flab A2UI catalog — the JSON Schema that enumerates the components and their props. Include it in the agent's prompt so it only produces UI the renderer can draw. The quickest way:
import { flabCatalog } from '@flabbergasted-ai/a2ui/catalog';# or export as JSON for non-JS agentsflab a2ui catalog --out flab-a2ui-catalog.jsonFor the full guide — system prompt injection, token-optimised variants, Python examples, protocol-level catalogId negotiation, and an end-to-end demo — see Agent Integration.