AG-UI Protocol: Stream AI Agents Into Your Frontend — ContentBuffer guide

AG-UI Protocol: Stream AI Agents Into Your Frontend

K
Kodetra Technologies··4 min read Intermediate

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:

  1. Bracket every run with RUN_STARTED / RUN_FINISHED.
  2. Bracket every assistant message with TEXT_MESSAGE_START / _END, with the same message_id.
  3. Use delta (not text) 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 a finally block.
  • Reusing message_id across 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 CORSMiddleware with allow_origins=['http://localhost:3000'] during dev.

Quick Reference — Core Event Types

EventWhen to emitKey fields
RUN_STARTEDBeginning of every agent runthread_id, run_id
TEXT_MESSAGE_STARTBefore streaming an assistant messagemessage_id, role
TEXT_MESSAGE_CONTENTPer token or chunkmessage_id, delta
TEXT_MESSAGE_ENDAfter the message finishesmessage_id
TOOL_CALL_START / _ENDWrapping a tool invocationtool_call_id, name
STATE_DELTAPatching shared agent/UI statedelta (JSON Patch)
RUN_FINISHEDEnd of every run (success or error)thread_id, run_id

Next Steps

  • Add tool calls: emit TOOL_CALL_STARTTOOL_CALL_ARGSTOOL_CALL_END so the UI can render a live tool card.
  • Add shared state: stream STATE_DELTA events with JSON Patch ops to keep a typed store in sync between agent and UI.
  • Add human-in-the-loop: emit an INTERRUPT event, 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

Subscribe to join the conversation...

Be the first to comment