You built an AI agent that actually works. It calls tools, handles edge cases, and produces useful output. But when a user asks "what's it doing right now?", your frontend shows a spinner.
That gap -- a working agent backend with no way to show what it's doing -- has been the default for years. Teams patch it with polling, WebSocket hacks, or just shipping the full response at the end and calling it good. None of those hold up when your agent takes 30 seconds and touches six tools.
AG-UI is the protocol designed to close this gap for good.
What AG-UI Actually Is
AG-UI is the protocol that fills the last gap in the modern agent stack. It's an open, event-based spec that standardizes how agent backends stream state, messages, and tool results to any user-facing frontend in real time.
Before AG-UI, every team built their own streaming bridge. Some used WebSockets. Some polled a status endpoint every two seconds. Some pushed the whole response at the end. The result was custom code, fragile contracts, and a streaming layer you had to reinvent on every project.
AG-UI gives you a typed event stream where the frontend knows exactly what each event means and how to render it.
Think of it alongside the other protocol layers you probably already know. MCP standardizes how your agent calls tools and fetches context from backend servers -- it faces inward. A2A (Agent-to-Agent protocol) standardizes how agents hand off work to each other. AG-UI standardizes how the agent talks to the user interface -- it faces outward. You can read more about how MCP and A2A fit together at the protocol level.
Each protocol works at a different boundary:
AG-UI is framework-agnostic. The @ag-ui/core npm package gives you TypeScript types and runtime schemas. Your backend needs to emit the right SSE event stream. That's the whole contract.
The Event Model
AG-UI defines 16 core event types organized into four groups. Understanding the structure is what lets you wire it up correctly -- and test it without guessing.
Lifecycle Events
Every agent run opens with RUN_STARTED and closes with either RUN_FINISHED or RUN_ERROR. This gives the frontend a reliable contract for loading states and error handling that doesn't require any custom signaling.
import type { RunStartedEvent, RunFinishedEvent, RunErrorEvent } from "@ag-ui/core";
// Required sequence for every valid AG-UI run:
// RUN_STARTED → [content events] → RUN_FINISHED | RUN_ERRORThe optional step events -- STEP_STARTED and STEP_FINISHED -- are useful when your agent has named phases. A CX agent handling a return might have steps like "verify order", "check policy", "process refund". You can surface those in the UI so the user sees where the agent is in the process, not just a spinning indicator.
Text Message Events
Text streaming uses a three-event triad: TEXT_MESSAGE_START opens the message, TEXT_MESSAGE_CONTENT streams token chunks, and TEXT_MESSAGE_END closes it. This maps directly to how LLM streaming APIs work.
// Event sequence for a streamed assistant response:
// TEXT_MESSAGE_START { messageId, role: "assistant" }
// TEXT_MESSAGE_CONTENT x N { messageId, delta: "Hello" }
// TEXT_MESSAGE_CONTENT x N { messageId, delta: " there!" }
// TEXT_MESSAGE_END { messageId }The messageId field on every event lets the frontend accumulate tokens correctly even when multiple messages are in flight. Don't assume events for different messages are non-interleaved -- AG-UI allows it, and your renderer needs to handle it.
Tool Call Events
This is the part that matters most for agents that do real work. Tool calls stream through their own lifecycle:
// TOOL_CALL_START { toolCallId, toolCallName: "lookup_account" }
// TOOL_CALL_ARGS x N { toolCallId, delta: '{"customerId":' }
// TOOL_CALL_ARGS x N { toolCallId, delta: '"cust_8842"}' }
// TOOL_CALL_END { toolCallId }
// TOOL_CALL_RESULT { toolCallId, content: "Account found: Gold tier, 3 open orders" }TOOL_CALL_ARGS streams the JSON argument object incrementally. Your frontend can start showing a tool call preview before the arguments are even fully formed. TOOL_CALL_RESULT delivers the actual output back to the client, which is what makes "show me what the agent is doing" genuinely informative rather than just "a tool ran".
When a user watches their AI agent look up their account, check return eligibility, and schedule a callback in real time, that's AG-UI tool events driving the UI.
State Management Events
State synchronization is the most powerful part of AG-UI for complex agents. Your agent has internal state -- retrieved context, conversation history, intermediate outputs, customer profile. AG-UI gives you two ways to push that state to the frontend.
STATE_SNAPSHOT replaces the entire state object in one shot. Use it when the state changes dramatically or when you need to initialize the client.
STATE_DELTA applies incremental updates using JSON Patch (RFC 6902). For large state objects with small changes per turn, this is far more efficient:
import type { StateDeltaEvent } from "@ag-ui/core";
// Only send what changed, not the whole state:
const delta: StateDeltaEvent = {
type: "STATE_DELTA",
delta: [
{ op: "replace", path: "/lastToolResult", value: { status: "success" } },
{ op: "add", path: "/retrievedFacts/-", value: "Customer tier: Gold" }
]
};The frontend applies each patch atomically. Your UI re-renders only the components that depend on changed fields.
Building an AG-UI Backend in TypeScript
The @ag-ui/client package provides an AbstractAgent base class with one required method: run(input), returning an Observable of events. Here's a minimal implementation:
import { AbstractAgent, type RunAgentInput } from "@ag-ui/client";
import type {
BaseEvent,
RunStartedEvent,
TextMessageStartEvent,
TextMessageContentEvent,
TextMessageEndEvent,
RunFinishedEvent,
} from "@ag-ui/core";
import { Observable } from "rxjs";
import { v4 as uuid } from "uuid";
export class MyAgent extends AbstractAgent {
run(input: RunAgentInput): Observable<BaseEvent> {
return new Observable((subscriber) => {
const runId = uuid();
const messageId = uuid();
subscriber.next({ type: "RUN_STARTED", runId } as RunStartedEvent);
subscriber.next({
type: "TEXT_MESSAGE_START",
messageId,
role: "assistant",
} as TextMessageStartEvent);
// Replace this with real LLM token streaming
const tokens = ["I", " found", " your", " account."];
for (const token of tokens) {
subscriber.next({
type: "TEXT_MESSAGE_CONTENT",
messageId,
delta: token,
} as TextMessageContentEvent);
}
subscriber.next({ type: "TEXT_MESSAGE_END", messageId } as TextMessageEndEvent);
subscriber.next({ type: "RUN_FINISHED", runId } as RunFinishedEvent);
subscriber.complete();
});
}
}Your backend exposes this as an SSE HTTP endpoint. Any AG-UI-compatible frontend can consume it. The contract is in the event types, not in the framework you used to build the agent.
Wiring Up Existing Frameworks
If you're already using LangGraph or another framework with an AG-UI adapter, you don't need to rewrite your agent logic. You wrap it with the adapter and expose the HTTP endpoint.
import { LangGraphAgent } from "@ag-ui/langgraph";
import { myGraph } from "./agent-graph";
const agent = new LangGraphAgent({ graph: myGraph });
// agent.run() now emits the full AG-UI event stream automatically.
// Your LangGraph state transitions become STATE_DELTA events.
// Your tool node outputs become TOOL_CALL_RESULT events.On the frontend, @ag-ui/client handles the subscription:
import { AGUIClient } from "@ag-ui/client";
const client = new AGUIClient({ url: "https://api.example.com/agent" });
client.on("TEXT_MESSAGE_CONTENT", ({ messageId, delta }) => {
appendToken(messageId, delta);
});
client.on("TOOL_CALL_START", ({ toolCallId, toolCallName }) => {
showToolSpinner(toolCallName);
});
client.on("TOOL_CALL_RESULT", ({ toolCallId, content }) => {
showToolResult(toolCallId, content);
hideToolSpinner(toolCallId);
});
client.on("STATE_DELTA", ({ delta }) => {
applyPatch(localAgentState, delta);
syncUIFromState(localAgentState);
});
client.on("RUN_FINISHED", () => {
hideLoadingIndicator();
});
client.start({ messages: [{ role: "user", content: "I need to return an order." }] });You get granular control over every piece of agent output without maintaining a custom streaming layer.
What AG-UI Does for Observability
One of the less obvious benefits of adopting AG-UI is what it does for your monitoring setup. When every agent action emits a typed event, you get a structured trace of every run without any additional instrumentation.
You can pipe the event stream to your observability backend alongside the SSE output. Each TOOL_CALL_START becomes a span. Each STATE_DELTA is a state transition you can replay later. RUN_ERROR pinpoints the failure without guessing from logs.
This pairs directly with the analytics you're probably already doing on conversation outcomes. AG-UI events tell you how the agent behaved during the run. Conversation analytics tell you what outcomes that behavior produced. Together they close the loop from observation to improvement.
What This Changes for CX Agents
For agents handling support tickets, processing returns, or booking appointments, AG-UI changes what human oversight looks like in practice.
Today, supervisors review agents by reading transcripts after conversations end. With AG-UI, you can build a real-time dashboard showing exactly which tools the agent is calling, what state it's in, and what it's about to respond -- while the conversation is still live.
That's useful for quality review. It's more useful for catching a misbehaving agent before it does something hard to reverse. If you see a TOOL_CALL_START for process_refund that shouldn't be firing at this point in the conversation, you can intervene.
The tools your agents use stop being a black box and become a visible, auditable trail. That visibility is what makes human oversight practical beyond the prototype stage.
Testing the AG-UI Contract
The event spec AG-UI defines is also a test contract. A valid AG-UI stream must satisfy:
- First event is
RUN_STARTED - Last event is
RUN_FINISHEDorRUN_ERROR - Every
TEXT_MESSAGE_STARThas a matchingTEXT_MESSAGE_ENDwith the samemessageId - Every
TOOL_CALL_STARTis eventually resolved withTOOL_CALL_RESULTorRUN_ERROR STATE_DELTApayloads are valid RFC 6902 JSON Patch documents
You can write a validator that consumes the event stream and checks these invariants automatically:
import { MyAgent } from "./my-agent";
async function collectEvents(agent: MyAgent, input: RunAgentInput) {
const events: BaseEvent[] = [];
await new Promise<void>((resolve, reject) => {
agent.run(input).subscribe({
next: (e) => events.push(e),
error: reject,
complete: resolve,
});
});
return events;
}
test("agent produces valid AG-UI stream", async () => {
const agent = new MyAgent();
const events = await collectEvents(agent, { messages: [testMessage] });
expect(events[0].type).toBe("RUN_STARTED");
expect(["RUN_FINISHED", "RUN_ERROR"]).toContain(events.at(-1)?.type);
const starts = events.filter((e) => e.type === "TEXT_MESSAGE_START");
const ends = events.filter((e) => e.type === "TEXT_MESSAGE_END");
expect(starts.map((e) => e.messageId)).toEqual(ends.map((e) => e.messageId));
});Run this in CI and you'll catch malformed streams before they reach users. You can also use Chanl scenarios to run the agent against realistic customer conversations and validate the event stream for each -- not just that the agent gives the right answer, but that it takes a coherent path to get there.
Where AG-UI Fits in Your Stack
If you've already built your agent backend, adding AG-UI is an adapter problem. You wrap your agent in the AbstractAgent class, emit the correct events, and expose an HTTP SSE endpoint. Existing framework adapters handle most of that.
If you're starting fresh, AG-UI is worth building toward from day one. The discipline of emitting typed events for every agent action pays off across observability, testing, and frontend flexibility. You can swap your frontend framework without touching agent logic. You can pipe the same event stream to a monitoring dashboard and your user UI simultaneously.
The three protocols -- MCP for tool connections, A2A for agent coordination, AG-UI for user interfaces -- give you the full stack to build, connect, and monitor AI agents without custom glue code at every boundary. You can see how SSE streaming works at the infrastructure level if you want to go deeper on the transport layer.
The missing piece was always the user-facing layer. AG-UI is that piece.
Make your agent observable end to end
Chanl connects to your agent's event stream and gives you real-time monitoring, structured traces, and conversation analytics in one place -- across every channel.
Get Started FreeCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
The Signal Briefing
Un email por semana. Cómo los equipos líderes de CS, ingresos e IA están convirtiendo conversaciones en decisiones. Benchmarks, playbooks y lo que funciona en producción.



