Skip to content

How to Integrate A2UI into an Agent

Integrate A2UI into an agent using the Agent SDKs to stream validated interactive surfaces to clients and respond to user actions across turns.

For the client side, see How to Integrate A2UI into a Client. If your agent and renderer live in the same binary or codebase, combine both guides, skip the transport step, and pass messages directly from agent output into the rendering pipeline (the Agent and Renderer SDKs share the same Core SDK data structures). To register custom widgets beyond the built-in Basic catalog, see How to Define a Catalog, and for trade-offs between inference formats see Inference Formats. Agent SDKs are available for TypeScript, Python, and Dart, while Swift and Kotlin target client-side rendering (see SDKs).


Step 1: Install dependencies

Install the Agent SDK and Core SDK packages for your language:

Terminal window
npm install @a2ui/agent @a2ui/web_core

Step 2: Initialize the A2UI generator

Create a shared generator configured with your component catalogs, your chosen inference format, and a session map to track active surface state across turns.

The steps below use an illustrative OrderAgentService class representing your application’s own domain service rather than an A2UI SDK class. Register the built-in v1.0 Basic catalog (and any custom domain catalogs) at startup, and inject a model-streaming callback so the service stays decoupled from any specific LLM client or transport. The generator also configures the inference format: Express (ExpressFormatFactory) is the default, instructing the model to emit compact <a2ui> blocks that compile into validated A2UI messages, and can be swapped for other formats such as Direct JSON (see Inference Formats):

import {
A2uiGenerator,
type AgentToRendererMessage,
CatalogConfig,
ExpressFormatFactory,
RendererState,
type RendererToAgentMessage,
RendererToAgentMessageProcessor,
} from '@a2ui/agent';
import {basicCatalog} from '@a2ui/web_core/v1_0';
export type ModelStreamFn = (
sessionId: string,
systemPrompt: string,
userMessage: string,
) => AsyncIterable<string>;
export class OrderAgentService {
readonly catalogs = [
basicCatalog,
// Add custom domain catalogs here (see /guides/defining-a-catalog/).
];
readonly generator = new A2uiGenerator(
this.catalogs.map(catalog => new CatalogConfig(catalog)),
undefined, // Optional few-shot examples in A2UI JSON (decompiled into the active format).
new ExpressFormatFactory(), // Default; swap to change inference format.
);
private readonly sessions = new Map<string, RendererState>();
constructor(private readonly callModel: ModelStreamFn) {}
}

Step 3: Build the system prompt and stream A2UI messages

On each conversation turn, generate the A2UI system prompt instructions for the session and stream the language model’s token output through a request processor to emit validated A2UI messages.

Look up or initialize the session’s renderer state, create a request processor for your registered catalogs (or negotiate client capabilities per request when serving external clients) bound to that state, and append its generated prompt snippet to your agent’s role instructions. When reusing session state across turns, instruct the model to pass update=true (surface("id", update=true)) when modifying an existing surface so follow-up turns update the active surface in place rather than attempting to recreate it. If each chat turn creates an independent surface instead, omit the session state so every response validates from an empty state.

As tokens arrive from the language model, feed each chunk into the processor’s stream and close the stream when generation finishes. Stream parts carry either conversational prose (text parts, omitted in the UI-only generator below) or A2UI payloads: the stream reader closes partial string properties such as text incrementally as tokens arrive, updates the session state, and yields Core SDK AgentToRendererMessage structures (createSurface, updateComponents, updateDataModel, and deleteSurface) as soon as they are ready:

async *runTurn(
sessionId: string,
userMessage: string,
): AsyncGenerator<AgentToRendererMessage> {
let state = this.sessions.get(sessionId);
if (!state) {
state = new RendererState(this.catalogs);
this.sessions.set(sessionId, state);
}
const processor = this.generator.createProcessor(
// Synthesize rendererCapabilities from registered catalogs (see #negotiate-client-capabilities for external clients):
{'1.0': {supportedCatalogIds: this.catalogs.map(c => c.id)}},
undefined, // Optional per-turn inferenceFormatFactory override.
{state},
);
const systemPrompt = [
'You are an order assistant. Show order details in an interactive card.',
'Give each new surface a unique surfaceId, or call surface("id", update=true) when updating an existing surface.',
processor.promptSnippet,
].join('\n\n');
const stream = processor.openStream();
for await (const chunk of this.callModel(sessionId, systemPrompt, userMessage)) {
for (const part of stream.feed(chunk)) {
if (part.type === 'a2ui') {
yield* part.a2ui;
}
}
}
for (const part of stream.close()) {
if (part.type === 'a2ui') {
yield* part.a2ui;
}
}
}
Wire format: outbound agent-to-renderer messages

The stream compiles the model’s output into standard A2UI wire messages (agent_to_renderer.json in v1.0, server_to_client.json in v0.9.1). In v1.0, createSurface embeds initial components and dataModel directly and binds dynamic values with {"@path": "..."}, whereas v0.9.1 sends separate updateComponents and updateDataModel envelopes and binds paths with {"path": "..."}:

[
{
"version": "1.0",
"createSurface": {
"surfaceId": "order_summary",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json",
"components": [
{"id": "root", "component": "Card", "child": "col"},
{"id": "col", "component": "Column", "children": ["headline", "confirm_btn"]},
{"id": "headline", "component": "Text", "text": {"@path": "/summaryText"}},
{
"id": "confirm_btn",
"component": "Button",
"child": "confirm_label",
"action": {
"event": {"name": "confirm_order", "context": {"orderId": {"@path": "/orderId"}}}
}
},
{"id": "confirm_label", "component": "Text", "text": "Confirm order"}
],
"dataModel": {
"orderId": "ORD-4821",
"summaryText": "2 items ready for pickup"
}
}
}
]

Step 4: Process messages from the renderer

Handle incoming A2UI messages from the client renderer with RendererToAgentMessageProcessor, prompting the model on user actions and handling other messages with deterministic logic.

Separate plain text inputs from A2UI renderer_to_agent messages at your transport boundary: text messages always prompt the model directly through your turn runner, whereas A2UI messages go through your renderer message handler. Register an action callback on RendererToAgentMessageProcessor that forwards action events to the turn runner, while deterministic logic handles callAgentFunction RPCs (see Register agent-hosted functions) and client error reports:

async *handleRendererMessage(
sessionId: string,
message: RendererToAgentMessage | Record<string, unknown>,
): AsyncGenerator<AgentToRendererMessage> {
let modelTurn: AsyncIterable<AgentToRendererMessage> | undefined;
const processor = new RendererToAgentMessageProcessor({
catalogs: this.catalogs,
onAction: actionMessage => {
modelTurn = this.runTurn(sessionId, JSON.stringify(actionMessage));
},
onError: errorMessage => {
console.error('Renderer error:', errorMessage.error);
},
});
const result = await processor.processMessage(message);
yield* result.replies;
if (modelTurn) {
yield* modelTurn;
}
}
Wire format: inbound action message

When the user clicks Confirm order, the client resolves context bindings against its local data model and sends the action message below ("version": "1.0" in v1.0, "version": "v0.9" in v0.9.1). RendererToAgentMessageProcessor processes v1.0 envelopes; agents serving v0.9.1 clients pass the action payload directly to the turn runner:

{
"version": "1.0",
"action": {
"name": "confirm_order",
"surfaceId": "order_summary",
"sourceComponentId": "confirm_btn",
"timestamp": "2026-05-01T18:30:00.000Z",
"context": {
"orderId": "ORD-4821"
}
}
}

Step 5: Connect to a transport

Connect your agent service to the transport channel your application uses to exchange messages with clients, or pass messages directly to a local renderer when running in the same process.

A2UI is transport-agnostic: an agent can exchange JSON envelopes over Server-Sent Events (SSE), WebSockets, HTTP streaming responses, gRPC, or standardized agent protocols such as AG-UI, A2A, and MCP (covered in Connect to agent frameworks and transports). Whichever transport you choose, separate plain text messages from A2UI messages: pass text prompts directly to your turn runner, pass A2UI envelopes to your renderer message handler, and serialize each emitted AgentToRendererMessage onto your outbound channel.

The example below wires OrderAgentService to a streaming LLM chat session per sessionId and exposes it over a minimal HTTP SSE endpoint (POST /api/agent):

Terminal window
npm install @google/genai
import {createServer, type IncomingMessage, type ServerResponse} from 'node:http';
import {type Chat, GoogleGenAI} from '@google/genai';
import {OrderAgentService} from './order-agent.js';
interface AgentRequestBody {
sessionId?: string;
prompt?: string;
message?: Record<string, unknown>;
}
async function readJson(req: IncomingMessage): Promise<AgentRequestBody> {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as AgentRequestBody;
}
// Example using @google/genai; substitute any streaming LLM SDK.
const ai = new GoogleGenAI({apiKey: process.env.GEMINI_API_KEY});
const chats = new Map<string, Chat>();
async function* callModelStream(
sessionId: string,
systemPrompt: string,
userMessage: string,
): AsyncGenerator<string> {
let chat = chats.get(sessionId);
if (!chat) {
chat = ai.chats.create({model: 'gemini-2.5-flash'});
chats.set(sessionId, chat);
}
const response = await chat.sendMessageStream({
message: userMessage,
config: {systemInstruction: systemPrompt},
});
for await (const chunk of response) {
if (chunk.text) {
yield chunk.text;
}
}
}
const agentService = new OrderAgentService(callModelStream);
createServer(async (req: IncomingMessage, res: ServerResponse) => {
if (req.method !== 'POST' || req.url !== '/api/agent') {
res.writeHead(404).end();
return;
}
try {
const body = await readJson(req);
const sessionId = body.sessionId ?? 'default';
const events = body.message
? agentService.handleRendererMessage(sessionId, body.message)
: agentService.runTurn(sessionId, body.prompt ?? 'Show my order summary');
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
for await (const msg of events) {
res.write(`data: ${JSON.stringify(msg)}\n\n`);
}
} catch (err) {
if (!res.headersSent) {
res.writeHead(500, {'Content-Type': 'application/json'});
res.write(JSON.stringify({error: String(err)}));
}
} finally {
res.end();
}
}).listen(8080);

Additional steps

Negotiate client capabilities

Negotiate active catalogs per request when your agent serves external clients or multiple shipped app versions with different widget sets.

If your team owns both the agent and the client app, targeting your known catalogs directly as in Step 3 avoids runtime capability plumbing. Otherwise, register every catalog your agent supports on the generator, read metadata.a2uiRendererCapabilities (or metadata.a2uiClientCapabilities for v0.9.1 clients) on each request, and verify that the negotiated processor accepts the existing session state in case the caller’s catalogs changed. In TypeScript and Python, all catalogs on a single generator must target the same protocol version, so maintain a separate v0.9 generator when serving both v1.0 and v0.9.1 clients (Dart groups mixed-version catalogs automatically):

// Add `type A2uiRequestProcessor` and `type RendererCapabilities` to the Step 2 import.
prepareNegotiatedTurn(
sessionId: string,
capabilitiesPayload?: Record<string, RendererCapabilities>,
): {processor: A2uiRequestProcessor; state: RendererState; systemPrompt: string} {
const capabilities: Record<string, RendererCapabilities> = capabilitiesPayload ?? {
'1.0': {supportedCatalogIds: this.catalogs.map(c => c.id)},
};
const probe = this.generator.createProcessor(capabilities);
let state = this.sessions.get(sessionId);
if (!state || !probe.acceptsState(state)) {
state = probe.createState();
this.sessions.set(sessionId, state);
}
const processor = this.generator.createProcessor(
capabilities,
undefined, // Optional per-turn inferenceFormatFactory override.
{state},
);
const systemPrompt = [
'You are an order assistant. Show order details in an interactive card.',
'Give each new surface a unique surfaceId, or call surface("id", update=true) when updating an existing surface.',
processor.promptSnippet,
].join('\n\n');
return {processor, state, systemPrompt};
}
Wire format: inbound renderer capabilities metadata

Client renderers attach their supported capabilities in transport metadata (a2uiRendererCapabilities under "1.0" in v1.0, or a2uiClientCapabilities under "v0.9" in v0.9.1):

{
"metadata": {
"a2uiRendererCapabilities": {
"1.0": {
"supportedCatalogIds": ["https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"]
}
}
}
}

Register agent-hosted functions

Attach AgentFunction handlers to your catalog to run deterministic server-side operations without an LLM round-trip.

Use AgentFunction for deterministic backend work (such as database updates, pricing rules, or order lookups) rather than branching on action event names. Attach your agent functions to the catalog before passing it to A2uiGenerator and RendererToAgentMessageProcessor in Step 4; incoming callAgentFunction messages (v1.0 only) validate arguments against the function schema and return an agentFunctionResponse automatically:

import {AgentFunction, RendererToAgentMessageProcessor} from '@a2ui/agent';
import {basicCatalog} from '@a2ui/web_core/v1_0';
export const pickupEtaFn = new AgentFunction({
name: 'lookupPickupEta',
description: 'Looks up the pickup window for an order.',
returnType: 'object',
schema: {
type: 'object',
properties: {orderId: {type: 'string'}},
required: ['orderId'],
},
handler: args => ({
orderId: String(args.orderId ?? 'unknown'),
eta: 'Today, 5:30 PM',
}),
});
export const agentCatalog = basicCatalog.copyWith({
agentFunctions: [pickupEtaFn],
});
export const rendererMessages = new RendererToAgentMessageProcessor({
catalogs: [agentCatalog],
});
Wire format: callAgentFunction and agentFunctionResponse

When the client invokes an agent-hosted function in v1.0, it sends callAgentFunction and awaits the correlated agentFunctionResponse:

{
"version": "1.0",
"callAgentFunction": {
"surfaceId": "order_summary",
"functionCallId": "call_01",
"callFunction": {
"@call": "lookupPickupEta",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json",
"args": {
"orderId": "ORD-4821"
}
}
}
}
{
"version": "1.0",
"agentFunctionResponse": {
"functionCallId": "call_01",
"value": {
"orderId": "ORD-4821",
"eta": "Today, 5:30 PM"
}
}
}

Call renderer-side functions

Invoke authorized client-side functions from the agent over a persistent duplex connection using RendererFunctionCalls.

Over full-duplex transports such as WebSockets, RendererFunctionCalls builds callRendererFunction messages (v1.0 only) for catalog functions whose allowedCallers permits agent calls (rendererOrAgent or agentOnly). Given a custom catalog (customCatalog / custom_catalog) that declares readViewportWidth with allowedCallers: "rendererOrAgent", wire resolve into RendererToAgentMessageProcessor so incoming rendererFunctionResponse messages settle the pending promise or future:

import {
type AgentToRendererMessage,
RendererFunctionCalls,
RendererToAgentMessageProcessor,
} from '@a2ui/agent';
const rendererCalls = new RendererFunctionCalls({catalogs: [customCatalog]});
const rendererMessages = new RendererToAgentMessageProcessor({
catalogs: [customCatalog],
onRendererFunctionResponse: msg => rendererCalls.resolve(msg),
});
async function fetchViewportWidth(
sendToClient: (msg: AgentToRendererMessage) => void,
): Promise<unknown> {
const [callMsg, pendingValue] = rendererCalls.call({
catalogId: customCatalog.id,
name: 'readViewportWidth',
});
sendToClient(callMsg);
return await pendingValue;
}

Read synchronized client data models

Read the surface data model attached in transport metadata when a surface is created with sendDataModel: true.

Two-way input components mutate the client’s local data model without sending a wire message on every keystroke. When a surface sets sendDataModel: true, the renderer attaches that surface’s full data model under metadata.a2uiRendererDataModel.surfaces[surfaceId] (metadata.a2uiClientDataModel.surfaces[surfaceId] in v0.9.1) on every message it sends to the agent:

export function readSyncedSurfaceData(
surfaceId: string,
metadata?: Record<string, unknown>,
): Record<string, unknown> {
const dm = (metadata?.a2uiRendererDataModel ?? metadata?.a2uiClientDataModel) as
| {surfaces?: Record<string, Record<string, unknown>>}
| undefined;
return dm?.surfaces?.[surfaceId] ?? {};
}
Wire format: synchronized data-model metadata

Below is an inbound action request accompanied by synchronized data-model metadata (a2uiRendererDataModel in v1.0, a2uiClientDataModel in v0.9.1):

{
"message": {
"version": "1.0",
"action": {
"name": "confirm_order",
"surfaceId": "order_summary",
"sourceComponentId": "confirm_btn",
"timestamp": "2026-05-01T18:30:00.000Z",
"context": {
"orderId": "ORD-4821"
}
}
},
"metadata": {
"a2uiRendererDataModel": {
"version": "1.0",
"surfaces": {
"order_summary": {
"orderId": "ORD-4821",
"summaryText": "2 items ready for pickup"
}
}
}
}
}

Non-streaming generation and validation retry

Buffer complete model responses before sending messages when you want atomic validation and clean-slate retries instead of progressive streaming.

In Step 3, openStream() (open_stream() in Python) validates component schemas and surface topology incrementally as tokens arrive. Because progressive messages have already reached the client when a streamed block fails, recovering mid-stream means flushing err.partialResults (err.partial_results in Python), opening a new openStream() on the same RendererState, and either repairing existing surfaces in place (surface("id", update=true)) or sending deleteSurface before regenerating.

When your agent uses buffered (non-streaming) LLM calls instead, pass the full model text to parseResponse (parse_response in Python). If a block invents an unknown component, omits root, or leaves a dangling child ID, parseResponse rolls back RendererState and raises an A2uiError so you can pass the failed output and error message back to the model on a retry turn:

import {A2uiError, type A2uiRequestProcessor, type AgentToRendererMessage} from '@a2ui/agent';
export async function generateValidatedTurn(
processor: A2uiRequestProcessor,
userPrompt: string,
callLlm: (prompt: string) => Promise<string>,
maxRetries = 2,
): Promise<AgentToRendererMessage[]> {
let prompt = userPrompt;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const rawText = await callLlm(prompt);
try {
const parts = processor.parseResponse(rawText);
const messages: AgentToRendererMessage[] = [];
for (const part of parts) {
if (part.type === 'a2ui') {
messages.push(...part.a2ui);
}
}
return messages;
} catch (err) {
if (!(err instanceof A2uiError) || attempt === maxRetries) {
throw err;
}
prompt = [
userPrompt,
`Your previous response:\n${rawText}`,
`Validation failed: ${err.message}`,
'Fix the A2UI output so every component and ID matches the catalog.',
].join('\n\n');
}
}
return [];
}

Customize catalogs, examples, and inference formats

Prune large catalogs, attach few-shot A2UI JSON examples, or customize inference format options on the generator to steer layout quality and reduce prompt size.

Attach component or function pruning transformers to CatalogConfig to expose only the widgets a workflow needs, and pass canonical A2UI JSON example turns to A2uiGenerator, which validates them against the negotiated catalogs and decompiles them into the active inference format (such as <a2ui> blocks in Express). You can also pass options to ExpressFormatFactory or swap to DirectJsonFormatFactory when a model emits raw JSON inside <a2ui-json> tags (see Inference Formats). In Python, compose server-side component templates with @macro and MacroExpander so clients render new composite layouts without a client release (see Using Macros):

import {
A2uiGenerator,
type AgentToRendererMessage,
CatalogConfig,
ComponentPruningTransformer,
DirectJsonFormatFactory,
ExpressFormatFactory,
} from '@a2ui/agent';
import {basicCatalog} from '@a2ui/web_core/v1_0';
const exampleTurn: AgentToRendererMessage[] = [
{
version: '1.0',
createSurface: {
surfaceId: 'order_summary',
catalogId: basicCatalog.id,
components: [
{id: 'root', component: 'Card', child: 'status'},
{id: 'status', component: 'Text', text: 'Order ORD-4821 ready for pickup'},
],
},
},
];
export const formatFactory = new ExpressFormatFactory({
surfaceId: 'order_summary',
allowedMessages: ['createSurface', 'updateComponents', 'updateDataModel'],
});
// Or emit raw A2UI JSON inside <a2ui-json> tags:
export const jsonFormatFactory = new DirectJsonFormatFactory({
progressiveKeys: ['text'],
});
export const prunedGenerator = new A2uiGenerator(
[
new CatalogConfig(basicCatalog, [
new ComponentPruningTransformer(['Card', 'Column', 'Text', 'Button']),
]),
],
{[basicCatalog.id]: exampleTurn},
formatFactory,
);

Connect to agent frameworks and transports

Plug the A2UI generator and message processors into standard agent frameworks and protocols instead of writing custom transport plumbing.

Google ADK

Use the ADK integration package to manage per-session request processors, inject A2UI prompt instructions without colliding with ADK {variable} state templating, expose the client A2UI toolset, and bridge A2A events and function RPCs:

Terminal window
pip install "a2ui-adk[a2a]"
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutorConfig
from google.adk.agents import LlmAgent
from a2ui.adk import (
A2uiEventConverter,
A2uiInstruction,
A2uiProcessorStore,
SendA2uiToClientToolset,
a2ui_rpc_callback,
)
processors = A2uiProcessorStore()
def active_catalogs(ctx):
processor = processors.get(ctx)
return processor.active_catalogs if processor else None
order_agent = LlmAgent(
model="gemini-2.5-flash",
name="order_agent",
instruction=A2uiInstruction(
"You help customers manage pickup orders in {store_city}.",
a2ui_processor=processors.get,
a2ui_enabled=processors.is_enabled,
),
tools=[
SendA2uiToClientToolset(
a2ui_enabled=processors.is_enabled,
a2ui_processor=processors.get,
),
],
before_agent_callback=a2ui_rpc_callback(catalogs=active_catalogs),
)
executor_config = A2aAgentExecutorConfig(
event_converter=A2uiEventConverter(a2ui_processor=processors.get),
)
# In your request executor before running the session:
# processors.set(session.id, processor if a2ui_active else None)

A2A protocol

Use the A2A integration package to advertise A2UI support on your agent card, negotiate the A2A Extension Specification per request, stream A2UI messages as application/a2ui+json data parts, and answer inbound function RPCs:

Terminal window
pip install a2ui-a2a
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.types import AgentCapabilities, AgentCard
from a2ui.a2a import (
create_a2ui_parts,
get_a2ui_agent_extension,
get_a2ui_messages,
stream_response_to_parts,
try_activate_a2ui_extension,
)
from a2ui.core.basic_catalog import BasicCatalog
from a2ui.core.schema import ProtocolVersion
from a2ui.rpc import RendererToAgentMessageProcessor
basic = BasicCatalog(ProtocolVersion.V1_0)
agent_card = AgentCard(
name="Order Agent",
description="Streams interactive order summaries with A2UI.",
url="http://localhost:10002/",
version="1.0.0",
default_input_modes=["text/plain"],
default_output_modes=["text/plain", "application/a2ui+json"],
capabilities=AgentCapabilities(
streaming=True,
extensions=[
get_a2ui_agent_extension(
ProtocolVersion.V1_0,
supported_catalog_ids=[basic.catalog_id],
),
],
),
skills=[],
)
class OrderA2aExecutor(AgentExecutor):
# Given a session processor lookup and an enqueue_parts(event_queue, parts) helper:
async def execute(
self, context: RequestContext, event_queue: EventQueue
) -> None:
activated_version = try_activate_a2ui_extension(context, agent_card)
if activated_version is None:
return
processor, token_stream = await self.prepare_session(context)
inbound = (
get_a2ui_messages(context.message)
if context.message is not None
and activated_version == ProtocolVersion.V1_0
else []
)
rpc_processor = RendererToAgentMessageProcessor(processor.active_catalogs)
results = [await rpc_processor.process_message(msg) for msg in inbound]
if inbound and all(result.handled for result in results):
rpc_reply_parts = create_a2ui_parts(
[reply for result in results for reply in result.replies],
version=activated_version,
)
if rpc_reply_parts:
await enqueue_parts(event_queue, rpc_reply_parts)
return
async for a2a_part in stream_response_to_parts(
processor.open_stream(), token_stream, activated_version
):
await enqueue_parts(event_queue, [a2a_part])

AG-UI and CopilotKit

Enable A2UI on CopilotRuntime to stream A2UI surfaces over AG-UI across Python and TypeScript agent backends.

When injectA2UITool: true is set (or when the frontend supplies a catalog), CopilotKit injects the A2UI generation tool and translates tool calls and state deltas into AG-UI events across integrations including Google ADK (ag_ui_adk), LangGraph (CopilotKitMiddleware), and AWS Strands (@ag-ui/aws-strands). See the CopilotKit A2UI documentation for runtime setup details:

import {CopilotRuntime} from '@copilotkit/runtime';
const runtime = new CopilotRuntime({
agents: {
order_agent: myOrderAgent, // Your AG-UI compatible agent instance
},
a2ui: {
injectA2UITool: true, // Optional (default: false)
},
});

For Model Context Protocol tool servers that attach interactive UI resources (ui://), see Serve A2UI Layouts from an MCP Server and MCP Integration Overview.