Table of Contents
The last-mile problem
You’ve built a LangGraph agent that reasons, calls tools, and produces useful results. Now it needs a real-time UI that can show tool calls as they happen and stream tokens as they arrive. Otherwise, users spend 12 seconds staring at a spinner with no idea what the agent is doing.
Building that connection yourself gets tedious quickly. You write a FastAPI endpoint that streams SSE and invent an event schema, probably something like {"type": "token", "content": "..."}. Then you add a React useEffect that opens an EventSource, parses events, and distributes them to component state. Reconnect logic, tool-call events, and state synchronization all need their own code. Six months later, the next project needs a different schema, so you build most of it again.
The underlying problem is the missing contract between agent and UI. LangGraph and CrewAI have different streaming formats, while an SSE shape made for one project may not fit the next. Backend changes then spill into the frontend.
If you’ve written a custom EventSource handler that parses data: {"type": "token", "content": "..."} from an SSE stream, you’ve already built a small, one-off version of AG-UI.
The AG-UI protocol
AG-UI specifies how agents communicate with frontend interfaces. Like HTTP or MCP, it defines a contract rather than an implementation. Any compatible frontend can consume events from an agent backend that follows the protocol, without a custom integration for each framework.
The protocol defines 16 event types across 7 categories:
| Category | Events |
|---|---|
| Lifecycle | RUN_STARTED, RUN_FINISHED, RUN_ERROR |
| Text Message | TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END |
| Tool Call | TOOL_CALL_START, TOOL_CALL_ARGS_DELTA, TOOL_CALL_END |
| Tool Result | TOOL_CALL_RESULT |
| State Management | STATE_SNAPSHOT, STATE_DELTA |
| Activity | STEP_STARTED, STEP_FINISHED |
| Special | MESSAGES_SNAPSHOT, RAW |
Those events cover the full interaction between an agent and its UI.
The event model is independent of the transport. SSE, WebSockets, and HTTP streaming can all carry the same events. SSE works well for request-response agent calls, while WebSockets often make more sense for persistent sessions. AG-UI leaves that choice to the application.
For state synchronization, STATE_DELTA events use JSON Patch (RFC 6902), an established format for describing changes to a JSON document. The agent sends a diff instead of its full state on every update. You do have to model that state explicitly up front, which takes some design work.
The protocol has adapters for LangGraph, CrewAI, Google ADK, AWS Agents, Pydantic AI, and Microsoft Agent Framework. Each adapter maps framework-native events to AG-UI events, allowing the agent code to stay as it is.
Every agent run starts with RUN_STARTED and ends with RUN_FINISHED. Steps have a similar STEP_STARTED and STEP_FINISHED envelope. A tool call emits a start event, one or more argument deltas, and an end event when it is ready to execute. The frontend can respond at each boundary instead of waiting for the final result.
Backend integration
This minimal FastAPI server connects AG-UI to Google ADK:
The demo app source includes the complete example.
from ag_ui_adk import ADKAgent, add_adk_fastapi_endpoint
from fastapi import FastAPI
from tutor_agent import tutor_agent # Standard Adk LlmAgent
# Create ADK middleware agent instance
adk_tutor_agent = ADKAgent(
adk_agent=tutor_agent,
user_id="demo_user",
session_timeout_seconds=3600,
use_in_memory_services=True,
)
# Create FastAPI app
app = FastAPI(title="ADK Middleware Tutor Agent")
# Add the ADK endpoint
add_adk_fastapi_endpoint(app, adk_tutor_agent, path="/tutor")
This setup replaces the reconnect logic, event ID sequencing, custom JSON schema validation, and cross-event state coordination you would otherwise write yourself. EventEncoder handles content-type negotiation and serialization, while RunAgentInput provides typed input and thread management.
The connection from the agent framework to the React component looks like this:
flowchart TD
AF[Agent Framework\nLangGraph / CrewAI / ADK]
SDK[ag-ui SDK\nEvent Mapping + Encoding]
EP[FastAPI Endpoint\nSSE / WebSocket]
RT[CopilotKit Runtime\nAuth + Routing + Provider]
HOOK[useCoAgent Hook\nReact State Sync]
UI[Your UI Components\nCopilotSidebar / CopilotChat]
AF --> SDK
SDK --> EP
EP --> RT
RT --> HOOK
HOOK --> UI
The SDK adapts your framework to the protocol. Your agent code sits above that boundary, and the standardized UI integration sits below it.
CopilotKit handles the React side

AG-UI defines what travels over the wire. CopilotKit implements the React client and adds a middleware layer for the infrastructure around it.
The Copilot Runtime sits between the frontend and the agent endpoint. It handles authentication, routes requests to different agent backends, and hides differences between LLM providers. Once an application has several agents, the runtime can route requests without making those agents aware of one another.
On the React side, CopilotKit gives you three drop-in UI components:
CopilotChatprovides a full-page chat interface.CopilotPopupprovides a floating overlay for assistant-style UIs.CopilotSidebaradds a side panel alongside the existing application.
The useCoAgent hook gives you programmatic access to agent state when you need to build custom UI around it.
A basic setup looks like this:
import {
CopilotKit,
CopilotSidebar,
} from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
function App() {
return (
// Point CopilotKit at your Copilot Runtime endpoint
<CopilotKit runtimeUrl="/api/copilotkit">
<YourExistingApp />
{/* Sidebar renders alongside your app, not replacing it */}
<CopilotSidebar
defaultOpen={false}
labels={{ title: "Assistant" }}
/>
</CopilotKit>
);
}
For a sidebar chat interface, that is the entire integration. The CopilotKit provider connects to the runtime and exposes agent state to its child components.
When you need to read or update shared agent state, useCoAgent gives you bidirectional access:
import { useCoAgent } from "@copilotkit/react-core";
// AgentState is your typed state shape -- matches what
// the agent emits via STATE_SNAPSHOT and STATE_DELTA events
interface AgentState {
currentStep: string;
searchResults: SearchResult[];
isResearching: boolean;
}
function ResearchPanel() {
const { state, setState } = useCoAgent<AgentState>({
name: "research_agent",
// Initial state before agent emits its first STATE_SNAPSHOT
initialState: { currentStep: "", searchResults: [], isResearching: false },
});
// state.searchResults updates in real-time as STATE_DELTA events arrive
// setState sends state updates back to the agent
return (
<div>
{state.isResearching && <Spinner label={state.currentStep} />}
<ResultsList results={state.searchResults} />
</div>
);
}
The state object updates as the agent emits STATE_DELTA events, and setState sends changes in the other direction. No custom WebSocket handler is required.
Discrete tool-call events also make human approval easier to implement. The UI can intercept the gap between TOOL_CALL_START and TOOL_CALL_RESULT, ask for confirmation, and let the agent continue afterward. CopilotKit supports this pattern directly. Without it, you would usually need to add an approval state machine to the agent loop.
What the protocol enables
Avoiding streaming glue code is useful on its own, but a stable event contract also enables richer interfaces.
Generative UI becomes manageable because the frontend receives a structured event stream. Tool calls are distinct from text messages, and state changes are explicit. A TOOL_CALL_START for a search tool can trigger a search animation, while a STATE_DELTA that updates currentDocument can open a document panel. A raw token stream cannot provide those signals reliably.
Tool-call visibility gives users feedback before a result arrives. During a workflow that takes 15 to 30 seconds, messages such as “searching the web for X” and “reading document Y” explain the wait better than a spinner.
Cross-framework portability means a move from LangGraph to CrewAI, or an application that runs both, does not require a new frontend. The adapter handles the event mapping. MCP applies a similar idea to tool interfaces: a tool implements one contract so different agent frameworks can call it. AG-UI provides that contract between agents and UI clients. The two protocols cover different layers of the same stack.
Trade-offs
AG-UI is young. The specification is at v0, and its ecosystem is still small. CopilotKit is the primary production consumer today, so adopting the protocol also means relying on CopilotKit’s priorities. Support outside React is incomplete. A Vue or Svelte application will need its own client implementation based on the specification.
The event model assumes streaming. An agent that produces batch output, such as a document pipeline that runs for a minute and returns one complete artifact, takes on the event overhead without much benefit. Splitting an already complete response into TEXT_MESSAGE_CONTENT chunks adds complexity without improving the experience.
JSON Patch state synchronization works well once the state has a clear shape, but it requires a typed JSON document shared by the agent and UI. If the agent’s state is implicit or scattered across several variables, you will need to consolidate it before AG-UI can synchronize it usefully.
The LangGraph adapter maps events automatically, but that abstraction can hide framework-specific streaming behavior. If you depend on LangGraph-specific events, confirm that the adapter exposes them before adopting it.
Demo project
I built a school tutor agent that runs this stack end to end, with Google ADK on the backend and CopilotKit in the React frontend. Clone the repository, run pnpm install, set your API key, and start it with pnpm dev.
The code is available in the school-tutor-agent repository.
