AG-UI Protocol 2026: When AI Agents Render UI Directly for Users

Posted on: 5/15/2026 9:15:16 AM

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.

16canonical AG-UI event types
3agent protocol layers: MCP · A2A · AG-UI
SSE+WSdefault transports, not mandated
2026Bedrock AgentCore native support

1. Why yet another protocol?

Step back and look at the agent stack:

  • MCP solves "agent ↔ tool/data". An agent calls search_jira without 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
Figure 1. The 2026 agent-protocol triangle: AG-UI sits on the "agent ↔ human" edge.

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
Figure 2. A typical AG-UI round-trip: streamed chunks, tool traces, state deltas, and UI invocation in one channel.

The six core event groups:

Event groupExamplesPurpose
LifecycleRUN_STARTED, RUN_FINISHED, RUN_ERRORMark the boundaries of an invocation
TextTEXT_MESSAGE_START, TEXT_MESSAGE_CHUNK, TEXT_MESSAGE_ENDToken-level streaming for dialogue
ToolTOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_ENDSurface in-flight tools, params, and results
ReasoningTHINKING_CHUNKPush reasoning traces (Claude extended thinking, o-series)
StateSTATE_SNAPSHOT, STATE_DELTASync shared state agent ↔ UI via JSON Patch
UIUI_INVOCATION, UI_INPUT_REQUESTAsk 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 emit STATE_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
Figure 3. UI Invocation: agent requests, frontend renders, user response flows back.

5. Generative UI: two schools of thought

There are two ways to do generative UI over AG-UI, each fits a different use case:

SchoolMechanismWhen to useRisk
Component Registry (closed-set)Frontend pre-registers a safe component set; agent picks by nameEnterprise products, brand-consistent, audit-friendlyAgent 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 ComponentsProducts that need to "paint" novel UI on the fly: dashboard generators, form buildersNeeds 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

CriterionAG-UIVercel AI SDK UIOpenAI Assistants StreamLangServe 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 traceTHINKING_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

2023 — Early days
Chat completion streams text only. UI = markdown render. Tool calls just emerging in GPT-3.5.
Dec 2023 — Vercel AI SDK 3.0
Vercel open-sources v0's Generative UI tech: agents start returning React Server Components instead of text.
Q3 2024 — CopilotKit shapes AG-UI
CopilotKit consolidates common SSE event shapes into the first ag-ui-protocol spec, framework-agnostic.
Q1 2026 — Native on cloud agent runtimes
AWS Bedrock AgentCore Runtime (Mar 2026) adds native AG-UI support. Microsoft Agent Framework ships integration the same month.
Mar 2026 — A2UI
Google announces A2UI — a declarative, framework-agnostic UI payload format. AG-UI is the transport, A2UI is the payload. The two specs are complementary, not competing.
Q2 2026 — Stack convergence
CopilotKit ships an A2UI renderer over AG-UI transport. LangGraph, CrewAI, and Mastra emit canonical AG-UI by default. The MCP/A2A/AG-UI triangle is complete.

10. Security and pitfalls in production

5 things to lock down before you ship

  1. 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.
  2. Sanitize props before render. Every string in props must be treated as user-controlled. dangerouslySetInnerHTML is off-limits.
  3. 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.
  4. 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.
  5. Resumable streams. Networks drop. Every event should carry a sequence number; the client resumes from lastEventId over 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