You've spent weeks getting your agent backend right. It calls tools correctly, manages context, handles edge cases. Then product asks: can we embed it in the support portal? And suddenly you're inventing an event schema, arguing about whether partial text chunks should be arrays or strings, and writing yet another piece of WebSocket glue code that only works with this one UI.
AG-UI exists because every team kept solving that same problem differently. It's the open event protocol that defines what an agent backend sends to a frontend. Combined with MCP and A2A, it makes the modern agent stack feel finished instead of half-assembled.
What AG-UI Actually Does
AG-UI gives your agent backend and your frontend a shared vocabulary. The backend emits a stream of typed events. The frontend subscribes and renders. No custom schemas, no polling, no debating message formats across team boundaries.
The event stream follows the shape of a conversation. When your agent starts processing a request, it emits RunStarted. As it generates text, it emits TextMessageStart followed by a sequence of TextMessageContent events, one per chunk, streaming to the UI in real time. When it calls a tool, ToolCallStart and ToolCallArgs announce it before the call happens. When everything finishes, RunFinished closes the stream.
Your React component (or Vue, or Svelte, or any framework) subscribes to this stream and knows exactly what to render at every moment. The agent logic and the UI never need to negotiate on anything beyond the event types.
The frontend never directly touches MCP or the model. The AG-UI endpoint is the boundary that separates agent logic from UI logic.
Where AG-UI Fits in the Protocol Stack
AG-UI completes the three-layer protocol stack that every production agent eventually needs. MCP (Model Context Protocol) filled the tool layer: your agent connects to external data sources, APIs, and services through a standardized interface. By May 2026, the MCP registry has over 9,400 entries and 97 million monthly SDK downloads.
A2A filled the coordination layer: multiple agents discover each other, delegate tasks, share status. If your triage agent hands off to a specialist agent, A2A defines how that handoff works.
AG-UI fills the last gap: the transport between your agent backend and the user-facing UI. It's not competing with MCP or A2A. Each handles a distinct concern:
| Protocol | Layer | Connects |
|---|---|---|
| MCP | Tool | Agent to external tools and APIs |
| A2A | Coordination | Agent to other agents |
| AG-UI | UI Transport | Agent backend to frontend |
A complete CX agent uses all three. MCP gives it its tools (CRM lookup, order status, ticket creation). A2A lets a triage agent delegate to a billing specialist. AG-UI carries the conversation to the chat widget the customer actually sees.
The Event Types You'll Use Every Day
The core spec covers three categories of events. You'll reach for all three once your agent goes beyond simple Q&A.
Text events handle the streaming response:
TextMessageStart: opens a new assistant message (carries amessageId)TextMessageContent: a text delta, one or more per chunk from the modelTextMessageEnd: closes the message
Tool call events surface agent activity to the UI:
ToolCallStart: announces a tool invocation withtoolNameandtoolCallIdToolCallArgs: streams the arguments being assembled (useful for showing "Searching for availability...")ToolCallEnd: signals the tool returned
State events synchronize agent-side context to the frontend:
StateSnapshotEvent: full current state as a JSON objectStateDeltaEvent: incremental JSON Patch diff (RFC 6902) for efficient updatesMessagesSnapshotEvent: full message history snapshot (useful on reconnect)
That last category, state events, is where AG-UI pulls ahead of homegrown solutions. More on that in a moment.
A Concrete Example: Customer Booking Agent
A hotel booking assistant is a good test case because it has real tools, real state, and real consequences for getting things wrong. The agent needs to check room availability, hold a reservation, and handle special requests. Here's how it looks with AG-UI.
First, the endpoint:
import { EventEmitter, RunAgentInput } from 'ag-ui-protocol'
import { mcpClient } from '@/lib/mcp'
import { llm } from '@/lib/llm'
export async function POST(req: Request) {
const input: RunAgentInput = await req.json()
const emitter = new EventEmitter()
// Return the event stream immediately; run the agent async
const stream = emitter.toReadableStream()
runAgent(input, emitter).catch((err) => emitter.error(err))
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }
})
}
async function runAgent(input: RunAgentInput, emitter: EventEmitter) {
const runId = crypto.randomUUID()
emitter.emit({ type: 'RunStarted', runId })
const tools = await mcpClient.listTools()
const llmStream = await llm.streamWithTools(input.messages, tools)
for await (const chunk of llmStream) {
if (chunk.type === 'text_delta') {
emitter.emit({ type: 'TextMessageContent', delta: chunk.content, messageId: chunk.id })
} else if (chunk.type === 'tool_call_start') {
emitter.emit({ type: 'ToolCallStart', toolCallId: chunk.id, toolName: chunk.name })
} else if (chunk.type === 'tool_call_args') {
emitter.emit({ type: 'ToolCallArgs', toolCallId: chunk.id, delta: chunk.args })
} else if (chunk.type === 'tool_call_end') {
const result = await mcpClient.callTool(chunk.name, JSON.parse(chunk.args))
emitter.emit({ type: 'ToolCallEnd', toolCallId: chunk.id })
// Feed result back into the LLM stream context
llmStream.feedToolResult(chunk.id, result)
}
}
emitter.emit({ type: 'RunFinished', runId })
}On the React side, CopilotKit's useCoAgent hook (which implements AG-UI natively) handles everything:
import { useCoAgent, CopilotChat } from '@copilotkit/react-core'
export function BookingChat() {
const { state, isRunning } = useCoAgent({
name: 'booking-assistant',
url: '/api/booking-agent',
initialState: { reservation: null, preferences: {} }
})
return (
<div className="flex gap-4">
<CopilotChat className="flex-1" />
{state.reservation && (
<ReservationPanel reservation={state.reservation} />
)}
</div>
)
}No custom event parsing. No state management library wiring. The state object comes directly from StateSnapshotEvent and StateDeltaEvent events the agent emits.
State Synchronization: The Part That Changes Everything
Streaming text gets all the attention, but AG-UI's state events are what make it genuinely useful for customer experience agents.
CX agents accumulate context throughout a conversation: what the customer has told you, what tools have run, what's been confirmed or declined. Traditionally this state lives entirely on the backend, and the frontend either polls for it or receives it as embedded text.
AG-UI state events give the backend a direct channel to drive frontend panels. When your agent looks up a customer's account, it emits a snapshot:
emitter.emit({
type: 'StateSnapshotEvent',
snapshot: {
customer: {
name: 'Maria Chen',
tier: 'premium',
openTickets: 2,
lastContact: '2026-05-08'
},
currentIssue: 'Order #4492 delayed',
suggestedActions: ['check_shipment_status', 'offer_delivery_credit']
}
})Your React UI subscribes to this and renders a live "Customer Context" panel alongside the chat. When the agent uses one of the suggested actions, it emits a diff:
emitter.emit({
type: 'StateDeltaEvent',
delta: [
{ op: 'remove', path: '/suggestedActions/0' },
{ op: 'add', path: '/actionsCompleted/-', value: 'check_shipment_status' }
]
})The panel updates in real time. No shared state library. No separate API calls. The agent drives the UI as a side effect of its normal work.
This pattern matters for CX because it keeps support agents (human or AI) in sync with what the automated agent is doing. Human reviewers can watch state evolve live, ready to step in before the customer notices a problem.
AG-UI in Multi-Agent Setups
When multiple agents coordinate via A2A, AG-UI becomes the single display surface for all of them. Your triage agent hands off to a billing specialist via A2A; both agents emit events into the same AG-UI stream that the frontend is subscribed to.
This forces one decision: does each agent have its own AG-UI endpoint that the orchestrator merges, or does the orchestrator itself handle the AG-UI stream?
For most CX setups, the orchestrator pattern works better:
async function runOrchestrator(input: RunAgentInput, emitter: EventEmitter) {
emitter.emit({ type: 'RunStarted', runId: crypto.randomUUID() })
// Triage phase
const triageResult = await triageAgent.run(input.messages)
if (triageResult.handoffTo === 'billing') {
// Announce handoff in the stream (optional; good for UI feedback)
emitter.emit({
type: 'TextMessageContent',
delta: 'Let me connect you with our billing team...',
messageId: crypto.randomUUID()
})
// Run billing agent, piping its events into our stream
await billingAgent.runIntoEmitter(input.messages, triageResult.context, emitter)
} else {
await generalAgent.runIntoEmitter(input.messages, emitter)
}
emitter.emit({ type: 'RunFinished' })
}The frontend sees one continuous stream. The A2A handoff stays invisible to the UI unless you choose to surface it. That separation is what lets you evolve your multi-agent topology without touching the frontend.
Testing Your AG-UI Agent
Standard LLM tests assert on final text output, which misses a lot. With AG-UI, every interaction is a typed event sequence, so you can assert on agent behavior precisely.
import { captureEventStream } from 'ag-ui-protocol/testing'
test('agent checks availability before holding a room', async () => {
const events = await captureEventStream('/api/booking-agent', [
{ role: 'user', content: 'Book me a room for next Friday night, king bed' }
])
const toolCalls = events
.filter((e) => e.type === 'ToolCallStart')
.map((e) => e.toolName)
// Verify ordering: must check before committing
expect(toolCalls).toContain('check_availability')
expect(toolCalls).toContain('hold_room')
expect(toolCalls.indexOf('check_availability'))
.toBeLessThan(toolCalls.indexOf('hold_room'))
})
test('agent surfaces customer tier in state before responding', async () => {
const events = await captureEventStream('/api/booking-agent', [
{ role: 'user', content: 'I need a room asap' }
])
const snapshots = events.filter((e) => e.type === 'StateSnapshotEvent')
expect(snapshots.length).toBeGreaterThan(0)
expect(snapshots[0].snapshot).toHaveProperty('customer.tier')
})You can verify tool call ordering, catch cases where sensitive tools fire unexpectedly, and confirm that state transitions happen in the right sequence. Output-only testing can't get you there.
For teams running broader simulation suites covering room unavailability, payment failures, or mid-conversation interruptions, Chanl's Scenarios lets you replay event streams against structured evaluation criteria and get pass/fail results you can track over time.
Monitoring AG-UI Agents in Production
Every AG-UI event stream is a rich telemetry source. Tool call latency, state transition frequency, session duration, abandonment rate: all derivable from the event sequence without extra instrumentation.
The pattern most teams settle on: persist the raw event stream per conversation, then run aggregate queries over it. Honest caveat: this is more storage than you probably expect. A 5-minute conversation with state deltas can produce a few hundred events. Plan capacity accordingly, or sample state deltas (text events and tool calls are usually worth keeping in full).
When a customer reports an issue, you replay the event stream for their session and see exactly what the agent did, in what order, with what state at each step. That replay path is the difference between "the agent was weird that one time" and a fixable bug report.
Getting Started
You don't need to rewrite your agent backend to adopt AG-UI. The migration is additive:
- Add one endpoint that returns
text/event-stream. Keep your existing API routes. - Start with text events only:
RunStarted,TextMessageContent,RunFinished. This already gives you streaming UI with no custom code on the frontend. - Add tool call events as your agent gains tools. Frontends that don't handle them yet ignore unknown event types gracefully.
- Introduce state events when you need the frontend to reflect agent-side context.
The AG-UI GitHub repo ships TypeScript and Python SDKs with adapters for LangGraph, CrewAI, and Mastra. If you're building on VAPI, Retell, Bland, or another orchestration platform, AG-UI handles the text and state layer while voice audio stays on its own channel, a common pattern for voice-plus-chat hybrid CX agents.
Amazon Bedrock AgentCore Runtime added native AG-UI support in March 2026. CopilotKit's useCoAgent hook is the fastest path to a production-ready UI if you're on React.
What This Means for Your Agent Stack
The same agent backend that answers questions in your support portal can now power a mobile app, an embedded chat widget, and a voice assistant overlay, without changing the agent logic. That's the practical upshot of a stable three-layer protocol stack: MCP for tools, A2A for coordination, AG-UI for frontend transport.
For the product manager who asked "can we embed it in the portal?" last month, the answer is now a standardized yes. Your agent backend adds one AG-UI endpoint. Every new surface subscribes. Build once, render anywhere, observe everywhere.
Wire your CX agent to every channel your customers use
Chanl connects your agent backend to voice, chat, and messaging, with built-in monitoring so you know what's happening in every conversation.
Start building freeCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
The Signal Briefing
Un email por semana. Cómo los equipos líderes de CS, ingresos e IA están convirtiendo conversaciones en decisiones. Benchmarks, playbooks y lo que funciona en producción.



