genkit_a2ui 0.2.2
genkit_a2ui: ^0.2.2 copied to clipboard
A2UI (Agent-to-UI) streaming UI protocol support for Genkit Dart, provided as model middleware.
genkit_a2ui #
A Genkit Dart plugin that brings A2UI ("Agent to UI"), a transport-agnostic, JSON-based streaming UI protocol, to Genkit agents.
An A2UI-enabled agent can stream more than prose. It streams rich, interactive UI
surfaces (cards, lists, forms, buttons) that a client renders incrementally
as the model responds. The whole server-side integration is a single model
middleware: add a2ui() to an agent's use list and nothing else changes.
Documentation • API Reference • A2UI Specification

Status: experimental.
Installation #
Add the plugin to your project:
dart pub add genkit_a2ui
To render surfaces you also need a renderer. The Dart examples below use
genui, the Flutter renderer for A2UI
surfaces. Add it to your client (Flutter) app:
flutter pub add genui
Quickstart #
1. Add the middleware on the server #
Add a2ui() to your agent's use list. That is the entire server-side setup.
Unlike the JS plugin, Dart middleware is resolved by name from the registry, so
you must register A2uiPlugin() in Genkit(plugins: [...]) before referencing
it via a2ui().
import 'package:genkit/genkit.dart';
import 'package:genkit_a2ui/a2ui.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
final ai = Genkit(
plugins: [googleAI(), A2uiPlugin()],
);
final uiAgent = ai.defineAgent(
name: 'uiAgent',
model: googleAI.gemini('gemini-flash-latest'),
system: 'You help users. Render UI when it is clearer than prose.',
use: [a2ui()], // <- A2UI support (defaults to the bundled 'basic' catalog)
);
It works the same on a one-shot generate:
final res = await ai.generate(
model: googleAI.gemini('gemini-flash-latest'),
prompt: 'Show me the weather in Tokyo',
use: [a2ui()],
);
2. Render surfaces on the client #
package:genkit_a2ui/client.dart is browser/Flutter-safe (no dart:io). Consume
the agent with remoteAgent from package:genkit/client.dart, pull A2UI
envelopes off each chunk's content with a2uiEnvelopesFromParts, and feed them
to a renderer such as genui:
import 'package:a2ui_core/a2ui_core.dart' as core;
import 'package:genkit/client.dart';
import 'package:genkit_a2ui/client.dart';
import 'package:genui/genui.dart' hide basicCatalogId;
final agent = remoteAgent(url: '/api/uiAgent');
final chat = agent.chat();
// genui otherwise registers an empty stub for an unknown catalog id, so re-tag
// its basic catalog with the id the plugin's bundled basic catalog advertises.
final catalog = BasicCatalogItems.asCatalog().copyWith(
catalogId: basicCatalogId,
);
final surfaceController = SurfaceController(catalogs: [catalog]);
final turn = chat.sendStream(text: 'weather in Tokyo');
await for (final chunk in turn.stream) {
for (final envelope in a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content)) {
surfaceController.handleMessage(core.A2uiMessage.fromJson(envelope));
}
}
See
testapps/a2uifor a complete, runnable sample: a shelf server hosting the agent, plus a Flutter client that renders surfaces withgenui.
Options #
Pass options to a2ui() to control the catalog, prompt injection, and
validation:
| Option | Default | Description |
|---|---|---|
catalog |
'basic' |
The id of the catalog describing what the agent may render. |
instructions |
'system' |
Where to inject catalog capabilities. 'none' injects nothing. |
validate |
'warn' |
Validate emitted envelopes against the catalog. 'warn' logs and drops bad blocks; 'strict' throws; 'off' skips checking. |
surfaceId |
fresh UUID | Surface id policy. Defaults to a new UUID per surface; pass a fixed string to reuse one id for every surface. |
version |
'v0.9' |
Protocol version stamped on envelopes. |
Handling user actions #
When a user interacts with a surface (for example, presses a Button), the
renderer emits an action. Turn it into an agent input with actionToMessage and
send it as the next turn:
import 'package:genkit_a2ui/client.dart';
final message = actionToMessage(
A2uiClientAction(
name: 'refresh',
surfaceId: surfaceId,
sourceComponentId: 'refreshBtn',
timestamp: DateTime.now().toIso8601String(),
context: {'city': 'Tokyo'},
),
);
final turn = chat.sendStream(message: message);
The action's name is sent as the user message; the full action (including its
context) is attached as an a2ui data part so the agent can react to it.
Forms #
Input components (TextField, CheckBox, Slider) do not send their values
automatically. To capture what the user entered, the model must:
- Bind each input's
valueto a data-model path ({ "path": "/email" }). - Echo those same paths in the submit
Button'saction.event.context.
The catalog capabilities injected into the system prompt already instruct the
model to do this. Without both steps, the action arrives with an empty context.
Renderer requirements #
genui registers an empty stub for a catalog id it does not recognize, so surfaces
created by the agent would render as blanks. Re-tag genui's basic catalog with the
plugin's basicCatalogId so surfaces resolve to real widgets:
final catalog = BasicCatalogItems.asCatalog().copyWith(catalogId: basicCatalogId);
Note that both genkit_a2ui and genui export a basicCatalogId symbol with
different values. You want the plugin's, so hide genui's with
import 'package:genui/genui.dart' hide basicCatalogId;. See
testapps/a2ui
for the full wiring.
Custom catalogs #
The catalog option is a catalog id resolved from the Genkit registry. The
bundled 'basic' catalog is the default and needs no registration.
To match your own layout elements and design system, define a custom catalog,
register it with loadCatalog, and reference it by id.
Catalog format #
An A2UI catalog describes the components the model is allowed to emit:
id: A globally unique URI identifying the catalog (used ascatalogIdoncreateSurface).components: An array of components, where each has:name: The component type name, matching the renderer type (for exampleCustomCard,Text).description: A clear, one-line summary of what the component is and when to use it.props: A compact, model-facing text description of its properties (kept as a simple, human-readable string to minimize system prompt token usage).
Option A: load from a JSON file #
Create a JSON file (for example ./my-catalog.json) following this format:
{
"id": "https://my-app.org/catalogs/custom.json",
"components": [
{
"name": "Banner",
"description": "Displays a prominent alert banner at the top of a section.",
"props": "title: string (required); severity?: info|warning|error."
},
{
"name": "Text",
"description": "Displays a plain or inline-markdown text run.",
"props": "text: string (required); variant?: body|caption."
}
]
}
Then register it under a lookup identifier (for example 'my-catalog') on the
server:
import 'package:genkit_a2ui/a2ui.dart';
await loadCatalog(
ai,
id: 'my-catalog',
file: './my-catalog.json',
);
Option B: in-memory definition #
You can construct and register an A2uiCatalog directly in pure Dart:
import 'package:genkit_a2ui/a2ui.dart';
final myCatalog = A2uiCatalog(
id: 'https://my-app.org/catalogs/custom.json',
components: [
const A2uiCatalogComponent(
name: 'Banner',
description: 'Displays a prominent alert banner at the top of a section.',
props: 'title: string (required); severity?: info|warning|error.',
),
const A2uiCatalogComponent(
name: 'Text',
description: 'Displays a plain or inline-markdown text run.',
props: 'text: string (required); variant?: body|caption.',
),
],
);
await loadCatalog(
ai,
id: 'my-catalog',
catalog: myCatalog,
);
Using a registered catalog #
Once registered, reference the lookup id in your a2ui() options:
final uiAgent = ai.defineAgent(
name: 'uiAgent',
model: googleAI.gemini('gemini-flash-latest'),
use: [a2ui(catalog: 'my-catalog')],
);
Catalogs live in the registry (value type a2ui-catalog) so the middleware can
resolve them by id.
Security and the trust boundary #
Generative UI moves model output into the UI, so treat every surface an agent
emits as untrusted input. The a2ui() middleware's validate option
(including 'strict') checks envelope structure and component type names
against the catalog only. It does not validate component props or data-model
values: model-controlled values such as Image.url and Text (inline Markdown,
which a renderer may turn into rich content) pass through untouched. 'strict' is
a well-formedness check, not a security boundary.
A prompt-injected or simply mistaken model can therefore emit an arbitrary remote image URL, or Markdown that a renderer turns into formatted content. To keep that safe:
- The renderer/catalog owns prop sanitization. Whatever renders a surface
(for example
genuiplus your Markdown renderer) is responsible for escaping and sanitizing prop values before they reach the UI. If you ship a custom catalog, its renderer must sanitize its own components' props. - Restrict remote sources at the host. On the web, serve the app with a
Content Security Policy that limits
img-src(and other fetch directives) to origins you trust, so a model-supplied image or link URL cannot exfiltrate data or load unexpected content. - Do not put secrets in the data model. Anything bound into a surface's data
model can be echoed back through an action's
context.
If you need server-side control over props (for example, allow-listing image
hosts), add your own model middleware after a2ui() to inspect and rewrite the
emitted a2ui parts.
How it works #
One representation #
A2UI rides on its own part channel: a Genkit data part carrying the mime type
application/a2ui+json whose data is an object { "envelopes": [...] }
wrapping an array of A2UI envelope messages. This maps 1:1 onto the A2A binding of
the A2UI spec, so an A2A or MCP binding can drop in later for free.
- A mixed turn is a message whose content is
[textPart, a2uiPart, ...]. - A pure-surface turn is the special case with no text parts.
- Downstream consumers (the client transport,
genui) only ever see a2ui parts. "Pure vs mixed" is a prompting choice, not a separate code path.
The middleware pipeline #
On each model call inside the agent's tool loop, a2ui():
- Injects the catalog's capabilities into the system prompt so the model knows
what UI it may render (unless
instructions: 'none'). - Intercepts the model's output, both the streamed chunks and the final aggregated message.
- Extracts
a2uifenced code blocks from the model's text. - Validates them against the catalog (per the
validateoption). - Rewrites them into canonical a2ui data parts.
Inbound a2ui parts (for example, a surface action sent back as the next turn, or replayed history) are summarized into plain text before the underlying model sees them, so a model that does not understand the a2ui mime type can still reason about prior surfaces and user actions.
License #
Apache-2.0
