AG-UI Protocol 2026: When AI Agents Render UI Directly for Users
Posted on: 5/15/2026 9:15:16 AM
Table of contents
- 1. Why yet another protocol?
- 2. Anatomy of an AG-UI event stream
- 3. State synchronization: the hardest part of AG-UI
- 4. UI Invocation: the agent "paints" a live component
- 5. Generative UI: two schools of thought
- 6. Side-by-side with other options
- 7. Code sample: backend FastAPI emitting an AG-UI stream
- 8. Code sample: React frontend with CopilotKit AG-UI
- 9. Timeline: from plain chat to AG-UI
- 10. Security and pitfalls in production
- 11. When NOT to use AG-UI
- 12. Conclusion
- References
Through 2025 and early 2026, two agent protocols dominated every architecture discussion: MCP (Model Context Protocol) standardizes how agents call tools and access data, while A2A (Agent-to-Agent) standardizes how agents from different vendors collaborate. But one corner of the triangle was missing: how does an agent talk to the human in a way richer than a few lines of markdown? That gap is exactly what AG-UI (Agent-User Interaction Protocol) was born to fill — and in just a few months it has become the default transport layer for every agentic frontend from CopilotKit and Microsoft Agent Framework to AWS Bedrock AgentCore.
This post breaks down AG-UI at the architecture level — the 16 canonical event types, SSE/WebSocket transport, the state-synchronization model, and how to pair it with Vercel AI SDK and A2UI to build a production-ready agentic frontend stack. We include working code, flow diagrams, a head-to-head comparison of the major libraries, and a 2026 security checklist.
1. Why yet another protocol?
Step back and look at the agent stack:
- MCP solves "agent ↔ tool/data". An agent calls
search_jirawithout caring where Jira is hosted. - A2A solves "agent ↔ agent". A PM agent delegates work to a QA agent built by another vendor, with no shared code base.
- AG-UI solves "agent ↔ human user". The agent needs to stream tokens, push reasoning traces, surface in-flight tool calls, synchronize shared state, and sometimes render a React/Vue component right inside the chat so the user can approve an action — all over one framework-agnostic protocol.
Before AG-UI, every frontend rolled its own event schema. Vercel AI SDK had a Vercel format, LangServe had another, OpenAI Assistants streamed OpenAI's way, every MCP client invented partial-message wire formats... The consequence: swapping the agent backend forced you to rewrite the frontend. AG-UI is "OpenTelemetry for agent UIs" — one event spec, every runtime emits it, every frontend lib understands it.
flowchart LR
User([Human user])
UI[Frontend React/Vue
CopilotKit · AI SDK UI]
AGUI{AG-UI
Event Stream}
Agent[Agent Runtime
LangGraph · CrewAI · Mastra]
A2A[A2A Protocol]
PeerAgent[Cross-vendor Agent]
MCP[MCP Servers
Tools · Data]
User <-->|"input · feedback"| UI
UI <-->|"SSE/WS events"| AGUI
AGUI <--> Agent
Agent <-->|"agent collab"| A2A
A2A <--> PeerAgent
Agent <-->|"tools"| MCP
style AGUI fill:#e94560,stroke:#fff,color:#fff
style Agent fill:#16213e,stroke:#fff,color:#fff
style A2A fill:#2c3e50,stroke:#fff,color:#fff
style MCP fill:#2c3e50,stroke:#fff,color:#fff
style UI fill:#f8f9fa,stroke:#e94560,color:#2c3e50
2. Anatomy of an AG-UI event stream
AG-UI defines 16 event types grouped into six categories. Every event is a JSON object tagged with type, messageId, and timestamp, transported over SSE or WebSocket — pick whichever, depending on whether you need bidirectional traffic. A typical interaction looks like this:
sequenceDiagram
participant U as User
participant FE as Frontend
participant AG as AG-UI Layer
participant BE as Agent Runtime
U->>FE: types "Plan a Q3 sprint"
FE->>AG: POST /run (input + thread state)
AG->>BE: invoke agent
BE-->>AG: RUN_STARTED
AG-->>FE: RUN_STARTED
BE-->>AG: TEXT_MESSAGE_CHUNK ("Reading backlog...")
AG-->>FE: stream chunk
BE-->>AG: TOOL_CALL_START (jira.search)
AG-->>FE: show "calling Jira" spinner
BE-->>AG: TOOL_CALL_END (24 tickets)
BE-->>AG: STATE_DELTA (sprint draft)
AG-->>FE: update side panel
BE-->>AG: UI_INVOCATION (SprintApprovalCard)
AG-->>FE: render dynamic component
U->>FE: clicks "Approve"
FE-->>AG: USER_RESPONSE (approved=true)
AG-->>BE: resume flow
BE-->>AG: RUN_FINISHED
The six core event groups:
| Event group | Examples | Purpose |
|---|---|---|
| Lifecycle | RUN_STARTED, RUN_FINISHED, RUN_ERROR | Mark the boundaries of an invocation |
| Text | TEXT_MESSAGE_START, TEXT_MESSAGE_CHUNK, TEXT_MESSAGE_END | Token-level streaming for dialogue |
| Tool | TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END | Surface in-flight tools, params, and results |
| Reasoning | THINKING_CHUNK | Push reasoning traces (Claude extended thinking, o-series) |
| State | STATE_SNAPSHOT, STATE_DELTA | Sync shared state agent ↔ UI via JSON Patch |
| UI | UI_INVOCATION, UI_INPUT_REQUEST | Ask the frontend to render a component and await a user response |
Design tip
If you're rolling your own agent runtime, you don't have to emit all 16 events. Lifecycle + Text alone make you compatible with any AG-UI client. Tool / Reasoning / State / UI are "progressive enhancements" — light them up as your backend grows.3. State synchronization: the hardest part of AG-UI
Unlike traditional chat APIs that only stream text, AG-UI lets the agent and the UI share a single JSON state object. When the agent mutates state (say, appending a row to the sprint draft), it emits a STATE_DELTA in JSON Patch (RFC 6902) format. The frontend applies the patch to its local state, React re-renders — no extra round trip needed.
// Backend emit (Python pseudocode)
state_delta_event = {
"type": "STATE_DELTA",
"messageId": msg_id,
"timestamp": now(),
"delta": [
{"op": "add", "path": "/sprint/tickets/-", "value": {"id": "AT-128", "title": "Refactor auth"}},
{"op": "replace", "path": "/sprint/totalPoints", "value": 42}
]
}
yield sse_format(state_delta_event)
// Frontend hook (React + CopilotKit AG-UI)
const { state, applyDelta } = useAGUIState<SprintState>({
initial: { sprint: { tickets: [], totalPoints: 0 } }
});
useAGUIEvent("STATE_DELTA", (evt) => {
applyDelta(evt.delta); // apply JSON Patch
});
// state.sprint.tickets updates automatically; component re-renders
return <SprintBoard tickets={state.sprint.tickets} />;
Why AG-UI chose JSON Patch over full-state replacement:
- Bandwidth — for a 100-ticket sprint where only one field changed, you ship one patch instead of 100 records.
- Idempotency — if the client reconnects and replays from event #42, the state still converges correctly.
- Smooth UI — React's reconciler only patches the DOM at the changed node, no full board re-render.
Common trap
Don't emitSTATE_DELTA at very high frequency (e.g. once per token). Batching every 50–100 ms — or once per "logical change" (one ticket added) — is the sweet spot. Too high and React's render queue chokes, animations stutter, especially with deep state trees.
4. UI Invocation: the agent "paints" a live component
This is AG-UI's killer feature — and the cleanest break from every traditional chat protocol. When the agent needs a human decision, it doesn't return markdown text "do you want to approve (y/n)?". Instead it emits:
{
"type": "UI_INVOCATION",
"componentName": "SprintApprovalCard",
"props": {
"sprintId": "Q3-2026-S1",
"ticketCount": 24,
"estimatedPoints": 42,
"risks": ["3 tickets without acceptance criteria"]
},
"awaitResponse": true,
"responseSchema": {
"type": "object",
"properties": { "decision": { "enum": ["approve", "reject", "edit"] } }
}
}
The frontend has pre-registered a map {componentName: ReactComponent}. It renders SprintApprovalCard inline in the chat, waits for the user to interact, then sends a USER_RESPONSE back to the agent. The whole loop is human-in-the-loop natively — no extra REST endpoint, no separate WebSocket for approvals.
flowchart TB
A[Agent decides:
need user approval] --> B[Emit UI_INVOCATION
componentName + props]
B --> C{Frontend lookup
componentRegistry}
C -->|found| D[Render component
pass props]
C -->|not found| E[Fallback: render JSON]
D --> F[User interacts]
F --> G[Frontend emits
USER_RESPONSE]
G --> H[Agent resumes flow
with new input]
style A fill:#16213e,stroke:#fff,color:#fff
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#2c3e50,stroke:#fff,color:#fff
style E fill:#f8f9fa,stroke:#ff9800,color:#2c3e50
style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style G fill:#e94560,stroke:#fff,color:#fff
style H fill:#16213e,stroke:#fff,color:#fff
5. Generative UI: two schools of thought
There are two ways to do generative UI over AG-UI, each fits a different use case:
| School | Mechanism | When to use | Risk |
|---|---|---|---|
| Component Registry (closed-set) | Frontend pre-registers a safe component set; agent picks by name | Enterprise products, brand-consistent, audit-friendly | Agent is constrained — can't "invent" new UI |
| Declarative JSON UI (open-set, A2UI-style) | Agent emits a JSON tree describing layout; frontend's generic renderer turns it into React/Vue/Web Components | Products that need to "paint" novel UI on the fly: dashboard generators, form builders | Needs a tight — agent can emit weird trees, risk of XSS or bad UX |
In the Q2/2026 stack, A2UI (announced by Google in March 2026) is becoming the standard "payload format" for the second school, while AG-UI remains the "transport". The two specs were designed to compose: AG-UI is the pipe, A2UI is what flows through it. CopilotKit, Microsoft Agent Framework, and AWS Bedrock AgentCore have all shipped integrations with both.
6. Side-by-side with other options
| Criterion | AG-UI | Vercel AI SDK UI | OpenAI Assistants Stream | LangServe stream |
|---|---|---|---|---|
| Open spec, runtime-agnostic | ✅ open spec, agnostic | ⚠️ tied to Next.js/RSC | ❌ vendor-locked to OpenAI | ⚠️ LangChain-only |
| State sync (JSON Patch) | ✅ native | ⚠️ DIY | ❌ | ❌ |
| Native UI Invocation | ✅ | ✅ (Generative UI 3.x) | ❌ (text + tool only) | ❌ |
| Reasoning trace | ✅ THINKING_CHUNK | ✅ (reasoning UI) | ✅ (o-series only) | ⚠️ |
| Bidirectional (WebSocket) | ✅ optional | ❌ SSE only | ❌ | ❌ |
| MCP/A2A integration | ✅ design-friendly | ⚠️ partial | ⚠️ | ⚠️ |
7. Code sample: backend FastAPI emitting an AG-UI stream
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import json, time, uuid
app = FastAPI()
def sse(event: dict) -> str:
return f"data: {json.dumps(event)}\n\n"
async def run_agent(user_input: str):
msg_id = str(uuid.uuid4())
yield sse({"type": "RUN_STARTED", "messageId": msg_id})
# text stream
for token in ("Reading ", "the ", "Jira ", "backlog..."):
yield sse({"type": "TEXT_MESSAGE_CHUNK",
"messageId": msg_id, "delta": token})
# tool call
yield sse({"type": "TOOL_CALL_START",
"messageId": msg_id, "toolName": "jira.search"})
tickets = await jira_client.search("sprint=Q3")
yield sse({"type": "TOOL_CALL_END",
"messageId": msg_id, "result": {"count": len(tickets)}})
# state delta — push tickets into shared state
yield sse({"type": "STATE_DELTA", "messageId": msg_id,
"delta": [{"op": "add", "path": "/sprint/tickets",
"value": [t.to_dict() for t in tickets]}]})
# UI invocation — wait for user approval
yield sse({"type": "UI_INVOCATION", "messageId": msg_id,
"componentName": "SprintApprovalCard",
"props": {"ticketCount": len(tickets)},
"awaitResponse": True})
yield sse({"type": "RUN_FINISHED", "messageId": msg_id})
@app.post("/agui/run")
async def run(req: dict):
return StreamingResponse(run_agent(req["input"]),
media_type="text/event-stream")
8. Code sample: React frontend with CopilotKit AG-UI
import { useAGUIRun, useAGUIComponentRegistry } from "@copilotkit/agui-react";
// 1. Register the components the agent is allowed to invoke
useAGUIComponentRegistry({
SprintApprovalCard: ({ ticketCount, onResponse }) => (
<div className="card">
<p>The agent picked {ticketCount} tickets for the new sprint.</p>
<button onClick={() => onResponse({ decision: "approve" })}>Approve</button>
<button onClick={() => onResponse({ decision: "reject" })}>Reject</button>
</div>
),
});
// 2. Run the agent and bind its stream to the UI
function SprintPlanner() {
const { messages, state, runAgent, isRunning } = useAGUIRun({
endpoint: "/agui/run",
});
return (
<>
<ChatThread messages={messages} />
<SprintBoard tickets={state.sprint?.tickets ?? []} />
<button onClick={() => runAgent("Plan sprint Q3")}
disabled={isRunning}>
Plan sprint
</button>
</>
);
}
9. Timeline: from plain chat to AG-UI
10. Security and pitfalls in production
5 things to lock down before you ship
- The component registry must be a strict allowlist. Don't let the agent name any component freely — it could be prompt-injected into rendering a sensitive admin panel. Declare a static
Map<string, Component>on the frontend. - Sanitize props before render. Every string in props must be treated as user-controlled.
dangerouslySetInnerHTMLis off-limits. - Rate-limit STATE_DELTA. A buggy agent can spam patches and DOM-thrash. Batch at least every 50 ms, drop if queue exceeds a threshold.
- JSON Patch paths need a schema guard. Don't trust agent-supplied paths. Validate via AJV/Zod before applying to prevent patches from clobbering admin keys.
- Resumable streams. Networks drop. Every event should carry a sequence number; the client resumes from
lastEventIdover SSE.
AG-UI production-ready checklist
- ✅ Backend emits the full Lifecycle + Text + Tool event set
- ✅ STATE_DELTA uses RFC 6902 JSON Patch with a schema guard
- ✅ Component registry allowlisted, props sanitized
- ✅ UI_INVOCATION carries
responseSchema; FE validates the response - ✅ SSE resumable via
Last-Event-Id - ✅ Observability: log every event to Langfuse / OTel for debugging
- ✅ Graceful fallback when a component is missing from the registry
- ✅ Rate limits and budgets on streams (prevent runaway agent loops)
11. When NOT to use AG-UI
Not every product needs AG-UI. A few cases where you can skip it:
- Single-shot Q&A agents — a chat completion API + markdown is enough; AG-UI is overkill.
- Backend already locked into OpenAI Assistants / Vercel SDK — moving to AG-UI now is a heavy refactor; weigh carefully.
- Server-only apps with no UI (cron, batch jobs) — you need A2A or a message queue, not AG-UI.
- Strict compliance domains (medical, banking) that ban dynamic UI rendering — fall back to pre-built forms, it's safer.
12. Conclusion
2026 is the year the agent-protocol triangle completes. MCP has matured, A2A is spreading, and AG-UI is the final piece — the open spec for how agents speak to humans. AG-UI's edge isn't any single "new" event; it's that it's the only open spec that unifies text streaming, tool tracing, reasoning, state sync, and component invocation into one protocol — so any agent runtime can plug into any frontend lib without vendor lock-in.
If you're building a real agent product with a real UI — not just a console chat — the call for 2026 is clear: emit AG-UI on the backend, use CopilotKit or roll your own renderer on the frontend. Pair it with A2UI for generative UI, MCP for tools, A2A for inter-agent collaboration, and you have a production-ready, framework-agnostic stack that should hold up for the next few years.
References
- AG-UI Overview — Agent User Interaction Protocol
- ag-ui-protocol/ag-ui (GitHub)
- CopilotKit — AG-UI Protocol
- Microsoft Agent Framework — AG-UI Integration
- Amazon Bedrock AgentCore Runtime — AG-UI support
- Google Developers Blog — Introducing A2UI
- A2UI v0.9 — Framework-Agnostic Generative UI
- Vercel AI SDK 3.0 — Generative UI
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.