Labs/AG-UI event stream
02

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.

RUN_STARTED / RUN_FINISHEDTEXT_MESSAGE_CONTENTTOOL_CALL_ARGSSTATE_SNAPSHOT / STATE_DELTAhuman-in-the-loop
event 00 / 47
No run yet
1 · Event logagent → client
  1. Press play. Every row is one event the agent streams to the browser.
2 · Agent stateempty

No state yet. The first STATE_SNAPSHOT sets it; every STATE_DELTA patches it.

Current event

Nothing streamed yet.

3 · What the person seesderived from 1 + 2
Trip panel appears when state arrives.
Plan our team offsite in Lisbon. 8 people, under $12,000.
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
}