
AG-UI Protocol: Stream AI Agents Into Your Frontend
Summary
Wire Claude or any agent backend to a React UI with AG-UI's 16 event types.
AI agents stream tokens, call tools, update shared state, and pause for human input — none of that fits the request/response model your REST APIs were built for. AG-UI is the open, event-based protocol that fixes that. It's a thin wire format with ~16 standard event types that any agent backend (Claude, LangGraph, CrewAI, Pydantic AI, Mastra) can emit and any frontend can consume. As of May 2026 it has 13k+ GitHub stars and first-party integrations across Microsoft, Google ADK, AWS Strands, and more.
Think of it as the third leg of the agentic protocol stack: MCP connects agents to tools, A2A connects agents to other agents, and AG-UI connects agents to your users. This guide gets you a streaming agent → FastAPI server → terminal client in under 50 lines of code, then shows you how to swap the terminal for React.
Prerequisites
- Python 3.10+ and Node 18+
- An Anthropic API key in
ANTHROPIC_API_KEY - Basic familiarity with FastAPI and Server-Sent Events (SSE)
Step 1 — Install the AG-UI Python SDK
Scaffold a new project and install the core packages:
mkdir agui-demo && cd agui-demo
python -m venv .venv && source .venv/bin/activate
pip install ag-ui-protocol fastapi uvicorn anthropic
ag-ui-protocol ships the typed event classes (TextMessageStartEvent, TextMessageContentEvent, etc.) and an SSE encoder. You bring the agent loop.
Step 2 — Emit AG-UI Events From Your Agent
Any function that yields the 16 standard events is a valid AG-UI agent. Here is a minimal Claude-backed agent that streams a response:
# agent.py
import os, uuid, anthropic
from ag_ui.core import (
RunStartedEvent, RunFinishedEvent,
TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent,
EventType,
)
client = anthropic.Anthropic()
async def run_agent(thread_id: str, run_id: str, user_input: str):
yield RunStartedEvent(
type=EventType.RUN_STARTED, thread_id=thread_id, run_id=run_id
)
msg_id = str(uuid.uuid4())
yield TextMessageStartEvent(
type=EventType.TEXT_MESSAGE_START, message_id=msg_id, role='assistant'
)
with client.messages.stream(
model='claude-sonnet-4-6',
max_tokens=1024,
messages=[{'role': 'user', 'content': user_input}],
) as stream:
for text in stream.text_stream:
yield TextMessageContentEvent(
type=EventType.TEXT_MESSAGE_CONTENT,
message_id=msg_id, delta=text,
)
yield TextMessageEndEvent(
type=EventType.TEXT_MESSAGE_END, message_id=msg_id
)
yield RunFinishedEvent(
type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id
)
Three rules to keep your stream protocol-valid:
- Bracket every run with
RUN_STARTED/RUN_FINISHED. - Bracket every assistant message with
TEXT_MESSAGE_START/_END, with the samemessage_id. - Use
delta(nottext) on content events — clients append, they don't replace.
Step 3 — Expose It Over HTTP+SSE
AG-UI is transport-agnostic, but the reference transport is plain SSE. FastAPI gets you there in 15 lines:
# server.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from ag_ui.encoder import EventEncoder
from agent import run_agent
app = FastAPI()
@app.post('/agent')
async def agent_endpoint(body: dict):
encoder = EventEncoder()
async def event_stream():
async for event in run_agent(
thread_id=body['thread_id'],
run_id=body['run_id'],
user_input=body['messages'][-1]['content'],
):
yield encoder.encode(event)
return StreamingResponse(event_stream(), media_type='text/event-stream')
Run it:
uvicorn server:app --reload --port 8000
Smoke test with curl — you should see SSE frames stream back:
curl -N -X POST http://localhost:8000/agent \
-H 'Content-Type: application/json' \
-d '{"thread_id":"t1","run_id":"r1","messages":[{"role":"user","content":"Say hi in 5 words."}]}'
Example output (truncated):
event: RUN_STARTED
data: {"type":"RUN_STARTED","threadId":"t1","runId":"r1"}
event: TEXT_MESSAGE_START
data: {"type":"TEXT_MESSAGE_START","messageId":"...","role":"assistant"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":"Hi"}
event: TEXT_MESSAGE_CONTENT
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"...","delta":" there"}
...
event: RUN_FINISHED
data: {"type":"RUN_FINISHED","threadId":"t1","runId":"r1"}
Step 4 — Wire Up a React Client
The fastest path is CopilotKit's <CopilotChat />, which speaks AG-UI natively. Install and point it at your endpoint:
npm install @copilotkit/react-core @copilotkit/react-ui
// App.tsx
import { CopilotKit } from '@copilotkit/react-core';
import { CopilotChat } from '@copilotkit/react-ui';
import '@copilotkit/react-ui/styles.css';
export default function App() {
return (
<CopilotKit runtimeUrl="http://localhost:8000/agent">
<CopilotChat labels={{ title: 'AG-UI Demo' }} />
</CopilotKit>
);
}
That's it — tokens stream into the chat bubble live, no manual SSE parsing on your side. The same backend works with the Terminal+Agent CLI client or any other AG-UI-compatible frontend.
Common Pitfalls
- Forgetting
RUN_FINISHED. Clients keep the connection open and never render the final state. Always emit it in afinallyblock. - Reusing
message_idacross runs. Each assistant turn needs a fresh UUID, otherwise the client merges them into one bubble. - Sending full text instead of
delta. Clients concatenate deltas; if you send the whole string each event, the user sees it duplicated. - CORS. Browsers block cross-origin SSE silently — add
CORSMiddlewarewithallow_origins=['http://localhost:3000']during dev.
Quick Reference — Core Event Types
| Event | When to emit | Key fields |
|---|---|---|
| RUN_STARTED | Beginning of every agent run | thread_id, run_id |
| TEXT_MESSAGE_START | Before streaming an assistant message | message_id, role |
| TEXT_MESSAGE_CONTENT | Per token or chunk | message_id, delta |
| TEXT_MESSAGE_END | After the message finishes | message_id |
| TOOL_CALL_START / _END | Wrapping a tool invocation | tool_call_id, name |
| STATE_DELTA | Patching shared agent/UI state | delta (JSON Patch) |
| RUN_FINISHED | End of every run (success or error) | thread_id, run_id |
Next Steps
- Add tool calls: emit
TOOL_CALL_START→TOOL_CALL_ARGS→TOOL_CALL_ENDso the UI can render a live tool card. - Add shared state: stream
STATE_DELTAevents with JSON Patch ops to keep a typed store in sync between agent and UI. - Add human-in-the-loop: emit an
INTERRUPTevent, wait for the client to POST back an approval, then resume the run. - Browse the AG-UI Dojo for 50–200 line examples of every feature with LangGraph, CrewAI, Pydantic AI, and more.
AG-UI is the missing glue between your model runtime and your product surface. Once it's in place, swapping Claude for a different backend — or LangGraph for CrewAI — is a server-side change your frontend never sees.
Comments
Be the first to comment