A2UI Surface
Lets an agent answer with a layout instead of plain text: metrics, charts, tables, tabs and buttons. The agent writes A2UI messages that name components from a catalog. Your app checks them and draws them with real Sprõut components. The agent never sends code, styles or class names.
In chat
checkSurface: valid
Two entry points
@tulipfarm/sprout/a2ui/cataloghas no React. Use it on the server to describe the catalog to a model, check its reply and turn the reply into text. It needszod4.@tulipfarm/sprout/a2uiaddsA2uiSurfaceand the adapters that draw each component. It also re-exports everything in the catalog entry.
On the server
import {
checkSurface,
promptSnippet,
sproutCatalog,
surfaceText,
} from "@tulipfarm/sprout/a2ui/catalog";
// 1. Tell the model what it can use.
const system = `${instructions}\n\n${promptSnippet(sproutCatalog)}`;
// 2. Check what it sent.
const result = checkSurface(messages, { catalog: sproutCatalog });
if (!result.ok) {
// A2UI errors: { code: "VALIDATION_FAILED", surfaceId, path, message }.
return retryWith(result.errors);
}
// 3. Keep a text version for copy, search and the model's history.
const text = surfaceText(result.surface, sproutCatalog);checkSurfacerefuses unknown components and props, missing or repeated IDs, children that do not exist, cycles, parts not reachable fromroot, and links other than HTTP, HTTPS and mailto.- Default limits: 300 components, 256 KB, 1,000 table rows and 2,000 chart points. Pass
limitsto change them andallowUrlto change which links and images are allowed. Images are refused by default. catalogDefinition(catalog)returns the catalog as an A2UI JSON Schema, for tools that take one.
In the browser
import { A2uiSurface } from "@tulipfarm/sprout/a2ui";
<A2uiSurface
messages={messages}
size="compact" // "full" in a side panel
streaming={isStreaming}
onAction={action => {
if (action.type === "send") sendMessage(action.text);
if (action.type === "expand") openInCanvas(action.componentId);
}}
/>size="compact"fits a chat message: charts are 240px tall and tables show 10 rows with a “Show all” button. That button callsonActionwithexpand, or expands in place without a handler.- While
streaming, parts that have not arrived show a placeholder. Afterwards, a part that is missing or fails to draw shows “This part couldn’t be displayed” and the rest of the reply still shows. - Every chart has a “Show data” button that swaps it for a table of the same numbers.
ButtoncallsonActionwithsendand its message. Without a handler it is disabled.
Add your own components
import { z } from "zod";
import { defineCatalog, defineComponent, sproutCatalog } from "@tulipfarm/sprout/a2ui/catalog";
import { sproutAdapters, type A2uiAdapter } from "@tulipfarm/sprout/a2ui";
export const appCatalog = defineCatalog({
id: "acme/chat@1",
extends: sproutCatalog,
components: {
Weather: defineComponent({
description: "Today's weather for one city.",
props: z.strictObject({ city: z.string().max(80) }),
text: props => `Weather in ${props.city}`,
}),
},
});
const Weather: A2uiAdapter<{ city: string }> = ({ props }) => <WeatherCard city={props.city} />;
export const appAdapters = { ...sproutAdapters, Weather };Give each catalog a versioned ID. Within one ID, only add components and optional props, so stored replies still draw. Change the ID for anything else.
Catalog
sprout/basic@1 has these components:
- Text:
Text(Markdown),Heading,Code,Badge,Callout. - Layout:
Column,Row,Section,Card,Divider,Tabs,Details,List. - Data:
Metric,Table,Chart(line, area, bar, pie, donut),Progress. - Other:
Image(only where the app allows it) andButton.
Values such as table rows can be literal or { path } into the surface’s data model. A child list can be a template, { componentId, path }, that repeats once per item of a list.
Accessibility
Headings start at level 2, so they sit under the page’s own heading. Charts name themselves with their title and can switch to a table. Badges, callouts and metric changes always say their meaning in words; tone only tints them. Placeholders are hidden from screen readers, and the surface is aria-busy while it streams.