a2ui

package
v1.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

A2UI plugin for Genkit Go

Adds A2UI ("Agent to UI") support to Genkit Go agents. A2UI is a transport-agnostic, JSON-based streaming UI protocol. An A2UI-enabled agent can stream not just prose, but rich, interactive UI surfaces that a client renders incrementally.

Status: experimental.

Design principle: 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 the same envelopes are byte-compatible with the JS plugin and the @a2ui/* renderers.

Usage

The whole server-side integration is the A2UI middleware. Add it to a Generate call via ai.WithUse:

package main

import (
	"context"

	"github.com/firebase/genkit/go/ai"
	"github.com/firebase/genkit/go/genkit"
	"github.com/firebase/genkit/go/plugins/a2ui"
	"github.com/firebase/genkit/go/plugins/googlegenai"
)

func main() {
	ctx := context.Background()
	g := genkit.Init(ctx, genkit.WithPlugins(&googlegenai.GoogleAI{}))

	resp, _ := genkit.Generate(ctx, g,
		ai.WithModel(googlegenai.GoogleAIModel(g, "gemini-flash-latest")),
		ai.WithSystem("You help users. Render UI when it is clearer than prose."),
		ai.WithPrompt("show me the weather in Tokyo"),
		ai.WithUse(&a2ui.Surfaces{}), // defaults to the bundled 'basic' catalog
	)

	// A2UI envelopes ride as data parts on the response message.
	envelopes := a2ui.EnvelopesFromParts(resp.Message.Content)
	_ = envelopes
}

The middleware injects the catalog's capabilities into the system prompt, then intercepts model output (streamed chunks and the final message), extracts a2ui fenced blocks, validates them against the catalog, and rewrites them into a2ui data parts.

Options

a2ui.Surfaces fields:

Field Default Description
Catalog nil An inline catalog (code-defined use only; not serialized). Overrides CatalogID when set.
CatalogID "basic" Id of a catalog registered with LoadCatalog. Resolved from the registry at call time.
Instructions "system" Where to inject catalog capabilities. "none" injects nothing.
Validate "warn" Validate emitted envelopes. "warn" logs and drops bad blocks; "strict" returns an error; "off" skips checking. This is a well-formedness check, not sanitization (see Security and the trust boundary). Invalid values are rejected by New.
SurfaceID fresh UUID Surface id policy. Provide a fixed string to reuse one id for every surface.
Version "v0.9" Protocol version stamped on envelopes. Must be one of the SupportedVersions; a typo is rejected by New.
Custom catalogs

The bundled BasicCatalog() mirrors @a2ui/web_core's basic catalog. To use your own components, register a Catalog with LoadCatalog (or LoadCatalogFile) and reference it by id. Registered catalogs live in the Genkit registry under the value type a2ui-catalog, so they are discoverable by tooling (the Dev UI's GET /api/values?type=a2ui-catalog). The wire representation of a catalog and its envelopes is identical across the JS, Go, and Dart plugins, so a surface rendered by one is byte-compatible with the renderers of another. The registry key differs by runtime, though: Go keys strictly by the catalog's own ID and its config field is CatalogID, whereas JS's loadCatalog keys by a caller-chosen lookup id and its middleware config field is catalog. Tooling or shared config that matches catalogs by registry key across runtimes will not line up; match on the catalog's id value instead.

catalog, err := a2ui.LoadCatalogFile(g, "./my-catalog.json")
if err != nil { /* ... */ }

resp, _ := genkit.Generate(ctx, g,
	ai.WithModel(m),
	ai.WithPrompt("..."),
	ai.WithUse(&a2ui.Surfaces{CatalogID: catalog.ID}),
)

Or construct and register one in memory:

myCatalog := &a2ui.Catalog{
	ID: "https://my-app.org/catalogs/custom.json",
	Components: []a2ui.CatalogComponent{
		{Name: "Banner", Description: "A prominent alert banner.", Props: "title: string (required)."},
	},
}
a2ui.LoadCatalog(g, myCatalog)
// ... ai.WithUse(&a2ui.Surfaces{CatalogID: myCatalog.ID})

Register the bundled basic catalog too (so it shows up in the Dev UI) with a2ui.RegisterBasicCatalog(g).

The middleware resolves the catalog for each turn in this order: an inline Surfaces.Catalog (code-defined only), then Surfaces.CatalogID looked up from the registry, then the bundled basic catalog. Prefer CatalogID: unlike an inline Catalog, it survives JSON/Dev-UI dispatch and appears in tooling.

The catalog JSON file follows the Catalog shape:

{
  "id": "https://my-app.org/catalogs/custom.json",
  "components": [
    { "name": "Banner", "description": "A prominent alert banner.", "props": "title: string (required); severity?: info|warning|error." },
    { "name": "Text", "description": "A plain or inline-markdown text run.", "props": "text: string (required); variant?: body|caption." }
  ]
}
Sending user actions back to the agent

When the user interacts with a surface (e.g. presses a Button), the client renderer emits an action. Send it back as the next turn: put the action's name as the user message text, and attach the full action as an a2ui data part. The middleware sanitizes inbound a2ui parts into a short text summary so the model can reason about them, and a2ui.EnvelopesFromParts reads envelopes back off any message/chunk content.

Registering as a plugin (optional)

Passing &a2ui.Surfaces{} to ai.WithUse is all you need. If you also want the middleware to appear in the Dev UI and be referenceable by name, register the plugin during init:

g := genkit.Init(ctx, genkit.WithPlugins(&a2ui.A2UI{}, &googlegenai.GoogleAI{}))

Try it with the web UI

go/samples/basic-middleware/a2ui serves an A2UI agent at POST /api/uiAgent, the exact endpoint the browser frontend in js/testapps/a2ui/web talks to via remoteAgent. Because the Go agent speaks the same wire protocol as the JS backend, the existing web UI works unchanged against it:

# 1. Start the Go backend (needs a Gemini API key in the environment):
cd go/samples/basic-middleware/a2ui
go run .

# 2. In another terminal, build and preview the web UI:
cd js/testapps/a2ui/web
pnpm install && pnpm build && pnpm preview

Open the printed preview URL; Vite proxies /api to the Go backend on :8080. Ask for "the weather in Tokyo" to see a streamed, interactive surface rendered by @a2ui/lit, including a Refresh button whose action round-trips back to the Go agent.

Security and the trust boundary

Validate checks structure and component type names against the catalog. It is a well-formedness check, not a sanitizer, even in "strict" mode:

  • Model-controlled values pass through untouched. An Image's url, a Text's content (which "may use inline Markdown", so a renderer may turn it into HTML), and any other prop value are never inspected or escaped.
  • Validation confirms an envelope is well-formed and its components exist in the catalog. It does not confirm the values are safe to render.

Treat rendered surfaces as untrusted output driven by the model:

  • Prop sanitization is the renderer/catalog's responsibility. A catalog component should escape or constrain the props it accepts.
  • Hosts should CSP-restrict image and other remote sources so a model-controlled Image.url cannot exfiltrate or load hostile content.

This matches the JS plugin's trust boundary exactly.

Note on the upstream A2UI SDKs

The A2UI team is standardizing prompt formatting, catalog management, and inference inside official SDKs (a2ui-core / a2ui-agent). Those are the eventual home for the prompt-rendering, parsing, and validation this plugin does today, so treat those internals as thin and replaceable. The stable surface is the a2ui.Surfaces entrypoint and the spec-defined wire part (application/a2ui+json), both of which are unaffected by an SDK swap.

License

Apache-2.0

Documentation

Overview

Package a2ui adds A2UI ("Agent to UI") support to Genkit agents.

A2UI is a transport-agnostic, JSON-based streaming UI protocol (https://a2ui.org/). An A2UI-enabled agent can stream not just prose, but rich, interactive UI "surfaces" that a client renders incrementally.

The whole server-side integration is the ai.Middleware, added to a github.com/firebase/genkit/go/ai.Generate call via github.com/firebase/genkit/go/ai.WithUse. It injects the catalog's capabilities into the system prompt, then intercepts model output (streamed chunks and the final message), extracts a2ui fenced blocks, validates them against the catalog, and rewrites them into a2ui data parts.

This is an experimental package.

Index

Constants

View Source
const (
	// InstructionsSystem appends A2UI capabilities to the system prompt
	// (default).
	InstructionsSystem = "system"
	// InstructionsNone injects nothing (useful if you supply your own
	// instructions).
	InstructionsNone = "none"
)

Instruction placement options for Surfaces.Instructions.

View Source
const A2UIMimeType = "application/a2ui+json"

A2UIMimeType identifies an A2UI payload. It is stamped onto the metadata.mimeType of the Genkit data part that carries A2UI envelopes, matching the A2A binding of the A2UI spec exactly.

View Source
const BasicCatalogID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"

BasicCatalogID is the catalog id of the A2UI "Basic Catalog" (v0.9). Surfaces created with the basic catalog reference this id, and the client renderer registers a catalog under the same id.

View Source
const CatalogValueType = "a2ui-catalog"

CatalogValueType is the registry value type under which A2UI catalogs are stored (key `/a2ui-catalog/<id>`), so the middleware can look them up by id and tooling (e.g. the Dev UI's GET /api/values?type=a2ui-catalog) can list them. Matches the JS plugin's value type.

View Source
const DefaultCatalogID = "basic"

DefaultCatalogID is the id used when Surfaces specifies neither Catalog nor CatalogID. It resolves to the bundled basic catalog.

View Source
const DefaultVersion = "v0.9"

DefaultVersion is the default A2UI protocol version stamped on emitted envelopes.

Variables

View Source
var SupportedVersions = []string{"v0.9", "v0.9.1"}

SupportedVersions is the set of A2UI protocol versions the plugin can stamp on emitted envelopes. Surfaces.Version is validated against it so a typo cannot stamp a version the renderer will reject at runtime. Matches the JS plugin's SUPPORTED_VERSIONS.

Functions

func IsPart

func IsPart(p *ai.Part) bool

IsPart reports whether p is an a2ui data part (mime application/a2ui+json carrying an "envelopes" array).

func LoadCatalog

func LoadCatalog(g *genkit.Genkit, catalog *Catalog) error

LoadCatalog registers an A2UI catalog in the Genkit registry under the key `/a2ui-catalog/<id>` (using the catalog's own ID), so the ai.Middleware can resolve it by id via Surfaces.CatalogID, and tooling such as the Dev UI can enumerate catalogs (GET /api/values?type=a2ui-catalog). This mirrors the JS and Dart plugins, keeping the catalog representation identical across runtimes.

Re-registering the same id is idempotent (the existing registration is kept), so calling it more than once, or registering the basic catalog twice, is safe. Registering a different catalog under an existing id keeps the original and logs a warning, so an edited catalog re-loaded under the same id is not silently ignored. It uses a register-if-absent primitive, so concurrent callers racing on the same id cannot panic.

Example:

a2ui.LoadCatalog(g, myCatalog)
// ... then reference it by id:
ai.WithUse(&a2ui.Surfaces{CatalogID: myCatalog.ID})

func RegisterBasicCatalog

func RegisterBasicCatalog(g *genkit.Genkit) error

RegisterBasicCatalog registers the bundled BasicCatalog in the registry so it appears alongside custom catalogs in tooling. The middleware falls back to the basic catalog even without this call; register it explicitly to surface it in the Dev UI. Idempotent.

func RenderCatalogInstructions

func RenderCatalogInstructions(catalog *Catalog) string

RenderCatalogInstructions renders a catalog into model-facing instructions describing the A2UI protocol and the available components. It is injected into the system prompt by the middleware when Instructions is InstructionsSystem.

Types

type A2UI

type A2UI struct{}

A2UI provides the Surfaces middleware as a Genkit plugin, so it appears in the Dev UI and can be referenced by name (for example from a prompt file's `use:` list). Registering the plugin is optional: the middleware works when passed directly to ai.WithUse. Register it with github.com/firebase/genkit/go/genkit.WithPlugins during Init:

g := genkit.Init(ctx, genkit.WithPlugins(&a2ui.A2UI{}))

The plugin carries no settings; every option lives on the per-call Surfaces.

func (*A2UI) Init

func (p *A2UI) Init(ctx context.Context) []api.Action

Init implements api.Plugin. A2UI registers no actions.

func (*A2UI) Middlewares

func (p *A2UI) Middlewares(ctx context.Context) ([]*ai.MiddlewareDesc, error)

Middlewares implements ai.MiddlewarePlugin, exposing the Surfaces middleware descriptor to Genkit.

func (*A2UI) Name

func (p *A2UI) Name() string

Name returns the plugin's unique identifier, which is also the registered name of the Surfaces middleware.

type Catalog

type Catalog struct {
	// ID is a globally-unique catalog id (also used as catalogId on
	// createSurface).
	ID string `json:"id"`
	// Components are the components available in this catalog.
	Components []CatalogComponent `json:"components"`
}

Catalog pins the set of components a surface may render. The Surfaces middleware uses it to tell the model what it may render (prompt injection) and to validate that emitted envelopes only reference known components. The renderer on the client registers a matching @a2ui/* catalog under the same ID.

func BasicCatalog

func BasicCatalog() *Catalog

BasicCatalog returns the A2UI "Basic Catalog" (v0.9), mirroring the components published by @a2ui/web_core's basic catalog. Use it to render standard UI without defining your own design system.

func LoadCatalogFile

func LoadCatalogFile(g *genkit.Genkit, path string) (*Catalog, error)

LoadCatalogFile reads an A2UI catalog from a JSON file and registers it with LoadCatalog. The file must contain an object with an "id" string and a "components" array (see Catalog).

type CatalogComponent

type CatalogComponent struct {
	// Name is the component type name, e.g. "Text". It must match the renderer
	// type.
	Name string `json:"name"`
	// Description is a one-line summary of what the component renders and when
	// to use it.
	Description string `json:"description"`
	// Props is a short, model-facing description of the component's props. Kept
	// as plain text (rather than a JSON Schema) to keep the injected prompt
	// compact.
	Props string `json:"props"`
}

CatalogComponent is a component the model may use, plus a short description of its props.

type Envelope

type Envelope = map[string]any

Envelope is a single A2UI envelope message (e.g. createSurface, updateComponents, updateDataModel, deleteSurface). It is represented as a generic JSON object because the protocol is open-ended and versioned; the middleware only inspects a few well-known keys.

A component within an updateComponents envelope is a single entry in an A2UI adjacency list: UI is a flat list of components, and the tree is reconstructed via id references, with exactly one component having id "root". Beyond component/id, every component carries catalog-specific props, so components are handled as generic map[string]any objects rather than a dedicated type.

func EnvelopesFromParts

func EnvelopesFromParts(parts []*ai.Part) []Envelope

EnvelopesFromParts extracts all A2UI envelopes carried by the given parts. Pass a message's, chunk's, or response's content. Returns nil for content that carries no a2ui parts (e.g. plain prose).

type Surfaces

type Surfaces struct {
	// Catalog describes what the agent may render, provided inline. When set it
	// takes precedence over CatalogID. Not serialized, so it is only honored for
	// code-defined use (not JSON/Dev-UI dispatch); prefer CatalogID with
	// [LoadCatalog] for a registry-backed catalog that also survives dispatch
	// and appears in the Dev UI.
	Catalog *Catalog `json:"-"`

	// CatalogID references a catalog registered with [LoadCatalog] by its id.
	// The middleware resolves it from the registry at call time. Defaults to
	// [DefaultCatalogID] (the bundled basic catalog) when neither Catalog nor
	// CatalogID is set.
	CatalogID string `` /* 187-byte string literal not displayed */

	// Instructions controls where the catalog's capabilities are injected.
	// InstructionsSystem (default) appends A2UI instructions to the system
	// prompt; InstructionsNone injects nothing.
	Instructions string `` /* 245-byte string literal not displayed */

	// Validate controls validation of emitted envelopes against the catalog.
	// ValidateWarn (default) logs and drops bad blocks; ValidateStrict returns
	// an error; ValidateOff skips checking. An unrecognized value is rejected by
	// New rather than silently downgraded.
	//
	// This validates envelope structure and component type names against the
	// catalog only. It is a well-formedness check, not sanitization: even under
	// ValidateStrict, model-controlled values (an Image's url, a Text's inline
	// Markdown, any other prop) pass through untouched. Prop sanitization is the
	// renderer/catalog's responsibility, and hosts should CSP-restrict remote
	// sources. See the "Security and the trust boundary" section of the README.
	Validate ValidateMode `` /* 325-byte string literal not displayed */

	// SurfaceID sets the surface-id policy. Provide a fixed id to reuse for
	// every surface; leave empty for a fresh UUID per surface.
	SurfaceID string `` /* 126-byte string literal not displayed */

	// Version is the protocol version stamped on emitted envelopes. Defaults to
	// [DefaultVersion].
	Version string `json:"version,omitempty" jsonschema_description:"Protocol version stamped on emitted envelopes. Defaults to \"v0.9\"."`
}

Surfaces is the A2UI ai.Middleware: it lets the model stream UI surfaces drawn from a catalog. Add it to a generate call with github.com/firebase/genkit/go/ai.WithUse.

Example:

resp, err := genkit.Generate(ctx, g,
    ai.WithModel(m),
    ai.WithPrompt("show me the weather in Tokyo"),
    ai.WithUse(&a2ui.Surfaces{}), // defaults to the bundled basic catalog
)

Middleware ordering: A2UI keeps per-turn streaming state (a stream parser and its minted surface ids) for the model call it wraps. Place any retrying or fallback middleware (which re-invokes the model) OUTSIDE A2UI so each attempt gets a fresh A2UI turn, i.e. WithUse(retry, &a2ui.Surfaces{}) rather than WithUse(&a2ui.Surfaces{}, retry). WithUse(A, B) means A wraps B.

Every field is per-call configuration; the A2UI plugin only registers the middleware by name and carries no settings of its own.

func (*Surfaces) Name

func (c *Surfaces) Name() string

Name returns the middleware's stable identifier.

func (*Surfaces) New

func (c *Surfaces) New(ctx context.Context) (*ai.Hooks, error)

New produces the per-call ai.Hooks bundle that implements the A2UI integration.

type ValidateMode

type ValidateMode string

ValidateMode controls how the parser handles malformed or invalid envelopes.

const (
	// ValidateStrict throws (returns an error) on malformed JSON or unknown
	// components.
	ValidateStrict ValidateMode = "strict"
	// ValidateWarn logs a warning and drops the offending block/envelope,
	// keeping the rest of the turn alive. This is the default.
	ValidateWarn ValidateMode = "warn"
	// ValidateOff passes envelopes through unchecked.
	ValidateOff ValidateMode = "off"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL