Watch an agent run, one event at a time
Step through a real-shaped AG-UI run: lifecycle events, streamed text, streamed tool arguments, state snapshots and JSON Patch deltas, and a human approval in the middle.
GoalSee that a generative interface is a pure function of an ordered event log — and that the UI, the state and the transcript are three views of the same stream.
- Press play. Every row is one event the agent streams to the browser.
No state yet. The first STATE_SNAPSHOT sets it; every STATE_DELTA patches it.
Nothing streamed yet.
How a client folds this stream (the whole trick)click to collapse
Three panes, one source of truth. The transcript, the trip panel and the state JSON are all computed from the event log. Nothing in pane 3 is stored separately. That is why the scrubber works.
Two channels for UI. The venue cards come from a tool call rendered in the chat. The trip panel is bound to shared state and updates from STATE_DELTA patches. Most real apps need both.
Human-in-the-loop is a frontend tool. confirm_payment is executed by the browser, not the server. The run ends with pendingToolCallIds; your click becomes a tool message in the next RunAgentInput. AG-UI 1.0 also has formal interrupts: the run finishes with an interrupt outcome and the next run must answer every one in resume.
Streaming args render early. Venue cards fill in while TOOL_CALL_ARGS is still arriving, using a partial-JSON parser (Lab 03).
function reduce(events) {
const ui = { messages: {}, tools: {}, state: null }
for (const e of events) switch (e.type) {
case "TEXT_MESSAGE_START": ui.messages[e.messageId] = ""; break
case "TEXT_MESSAGE_CONTENT": ui.messages[e.messageId] += e.delta; break
case "TOOL_CALL_START": ui.tools[e.toolCallId] = { name: e.toolCallName, args: "" }; break
case "TOOL_CALL_ARGS": ui.tools[e.toolCallId].args += e.delta; break
case "STATE_SNAPSHOT": ui.state = e.snapshot; break
case "STATE_DELTA": ui.state = applyPatch(ui.state, e.delta); break
}
return ui // render(ui) is your interface
}