AG-UI (Agent–User Interaction) gives frontends a consistent way to render agent behavior without caring which model or backend framework sits behind it. Instead of asking “what model are you using?”, the UI focuses on the event stream: runs start, messages stream in, tools are invoked, and state updates arrive.
This post walks through a frontend-first approach. We will:
- Mock the resources an agent would normally receive.
- Replay event streams to validate the UI logic.
- Finish with a full chat widget that simulates an AG-UI run end-to-end.
The frontend contract
AG-UI stays lightweight by reducing the contract to a handful of event sequences:
- Runs:
RUN_STARTED→RUN_FINISHED(orRUN_ERROR). - Messages:
TEXT_MESSAGE_START→TEXT_MESSAGE_CONTENT*→TEXT_MESSAGE_END. - Tool calls:
TOOL_CALL_START→TOOL_CALL_ARGS*→TOOL_CALL_END. - State:
STATE_SNAPSHOT(full state) andSTATE_DELTA(JSON Patch updates). - Resync:
MESSAGES_SNAPSHOTfor clean recovery.
In practice, the UI needs a reducer that can transform those events into:
- A message timeline.
- Tool call cards.
- A state panel.
- Run status indicators.
Interactive sample
AG-UI event reducer (minimal)
type ChatState = {
status: "idle" | "running" | "finished" | "error";
messages: { id: string; role: string; content: string }[];
toolCalls: { id: string; name: string; args: string }[];
agentState: Record<string, unknown> | null;
};
type StreamEvent =
| { type: "RUN_STARTED"; threadId: string; runId: string }
| { type: "RUN_FINISHED"; threadId: string; runId: string }
| { type: "TEXT_MESSAGE_START"; messageId: string; role: string }
| { type: "TEXT_MESSAGE_CONTENT"; messageId: string; delta: string }
| { type: "TEXT_MESSAGE_END"; messageId: string }
| { type: "TOOL_CALL_START"; toolCallId: string; toolCallName: string; parentMessageId?: string }
| { type: "TOOL_CALL_ARGS"; toolCallId: string; delta: string }
| { type: "TOOL_CALL_END"; toolCallId: string }
| { type: "STATE_SNAPSHOT"; snapshot: Record<string, unknown> }
| { type: "STATE_DELTA"; delta: { op: string; path: string; value?: unknown }[] }
| { type: "MESSAGES_SNAPSHOT"; messages: { id: string; role: string; content: string }[] };
const applyEvent = (state: ChatState, event: StreamEvent): ChatState => {
switch (event.type) {
case "RUN_STARTED":
return { ...state, status: "running" };
case "RUN_FINISHED":
return { ...state, status: "finished" };
case "TEXT_MESSAGE_START":
return {
...state,
messages: [...state.messages, { id: event.messageId, role: event.role, content: "" }],
};
case "TEXT_MESSAGE_CONTENT":
return {
...state,
messages: state.messages.map(message =>
message.id === event.messageId
? { ...message, content: message.content + event.delta }
: message
),
};
case "STATE_SNAPSHOT":
return { ...state, agentState: event.snapshot };
default:
return state;
}
};Mock the resources
In production, your agent might receive tools, context, and initial state from a backend (for example, a pydantic-ai service). In a demo environment, you can still model those payloads, keep them local, and reuse them for UI development and demos.
Interactive sample
Mock the agent resources
{
"agent": {
"id": "ag-ui-demo",
"threadId": "thread-demo-001",
"description": "AG-UI teaching agent for frontend teams."
},
"tools": [
{
"name": "summarize_guidelines",
"description": "Summarize AG-UI contract details for the active UI view.",
"parameters": {
"type": "object",
"properties": {
"topic": {
"type": "string"
},
"audience": {
"type": "string",
"enum": [
"product",
"engineering",
"design"
]
}
},
"required": [
"topic"
]
}
},
{
"name": "draft_component_spec",
"description": "Generate a component checklist for an AG-UI widget.",
"parameters": {
"type": "object",
"properties": {
"surface": {
"type": "string"
},
"constraints": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"surface"
]
}
}
],
"context": [
{
"description": "Audience",
"value": "Frontend teams shipping agent-native workflows."
},
{
"description": "Experience goal",
"value": "Make event semantics visible while keeping the UI calm."
}
],
"state": {
"tone": "crisp",
"draftStatus": "idle",
"lastRunAt": null,
"compliance": {
"events": "required",
"tools": "optional",
"state": "supported"
}
}
}Agent
AG-UI teaching agent for frontend teams.
Tools
Summarize AG-UI contract details for the active UI view.
ParametersShow JSON
{
"type": "object",
"properties": {
"topic": {
"type": "string"
},
"audience": {
"type": "string",
"enum": [
"product",
"engineering",
"design"
]
}
},
"required": [
"topic"
]
}Generate a component checklist for an AG-UI widget.
ParametersShow JSON
{
"type": "object",
"properties": {
"surface": {
"type": "string"
},
"constraints": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"surface"
]
}Context
Frontend teams shipping agent-native workflows.
Make event semantics visible while keeping the UI calm.
Agent stateShow JSON
{
"tone": "crisp",
"draftStatus": "idle",
"lastRunAt": null,
"compliance": {
"events": "required",
"tools": "optional",
"state": "supported"
}
}This mirrors a `RunAgentInput` payload. Keep it structured and consistent.
Replay the event stream
Once you have resources defined, the frontend just needs to react to events. Editing the event array below instantly changes the preview so you can validate tool call rendering, state updates, and message timing.
Interactive sample
Stream AG-UI events into UI
[
{
"type": "RUN_STARTED",
"threadId": "thread-demo-001",
"runId": "run-101"
},
{
"type": "STATE_SNAPSHOT",
"snapshot": {
"phase": "thinking",
"activeGoal": "Define the frontend event contract",
"compliance": {
"events": true,
"tools": true,
"state": true
}
}
},
{
"type": "TEXT_MESSAGE_START",
"messageId": "m1",
"role": "user"
},
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "m1",
"delta": "Show me how to render an AG-UI compliant chat timeline."
},
{
"type": "TEXT_MESSAGE_END",
"messageId": "m1"
},
{
"type": "TEXT_MESSAGE_START",
"messageId": "m2",
"role": "assistant"
},
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "m2",
"delta": "We will stream text events, display tool calls inline, and keep state snapshots visible for debugging."
},
{
"type": "TEXT_MESSAGE_END",
"messageId": "m2"
},
{
"type": "TOOL_CALL_START",
"toolCallId": "tool-1",
"toolCallName": "draft_component_spec",
"parentMessageId": "m2"
},
{
"type": "TOOL_CALL_ARGS",
"toolCallId": "tool-1",
"delta": "{\"surface\":\"chat-widget\",\"constraints\":[\"frontend\",\"event-driven\"]}"
},
{
"type": "TOOL_CALL_END",
"toolCallId": "tool-1"
},
{
"type": "TEXT_MESSAGE_START",
"messageId": "m3",
"role": "tool"
},
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "m3",
"delta": "Checklist: message stream, tool rail, state panel, run controls."
},
{
"type": "TEXT_MESSAGE_END",
"messageId": "m3"
},
{
"type": "STATE_DELTA",
"delta": [
{
"op": "replace",
"path": "/phase",
"value": "ready"
},
{
"op": "add",
"path": "/lastRunAt",
"value": "2025-12-21T18:15:00Z"
}
]
},
{
"type": "RUN_FINISHED",
"threadId": "thread-demo-001",
"runId": "run-101"
}
]AG-UI Chat Timeline
Event-driven UI driven by agent events.
Agent stateShow JSON
{
"phase": "ready",
"activeGoal": "Define the frontend event contract",
"compliance": {
"events": true,
"tools": true,
"state": true
},
"lastRunAt": "2025-12-21T18:15:00Z"
}Edit the event array and replay to see how a compliant frontend reconstructs messages, tool calls, and state.
Build the final chat widget
The widget below puts everything together: a chat timeline, tool rail, state panel, and run controls. The event flow mirrors what you would receive from a real AG-UI backend.