AG-UI over WebSockets
AG-UI is an open protocol for agents and the apps that show them: a run streams standard events for text, tool calls, steps, and a state shared between the agent and the UI. The protocol doesn't depend on a transport, but its standard client speaks HTTP. react-websocket/ag-ui runs it over a WebSocket, with what WebSockets add: one connection for every run, runs that resume after a reconnect or a reload, and a way back from a state delta that went wrong.
The thread
useAgUiAgent(agent) returns the thread: what the agent's events add up to.
Tool call arguments arrive as JSON text in pieces, so toolCalls[n].function.arguments grows until TOOL_CALL_END: parse it then. Events the adapter doesn't fold, such as CUSTOM and RAW, leave the thread as it is. To handle them, listen to the client's messages, or wrap the exported applyAgUiEvent in your own reducer.
agent.run(input) starts a run with the thread so far, plus input.messages, which show up at once. It sends AG-UI's RunAgentInput: the thread id, a new run id, the state, the messages, and your tools, context and forwardedProps. A run that's still streaming is stopped first.
When a state delta fails
STATE_DELTA carries JSON Patch operations against the state the client is supposed to have. If the client's state differs, because a delta was lost or the server's view drifted, a patch can fail: a path that doesn't exist, a failed test. Applying later deltas to the wrong base would only make it worse.
So the adapter keeps the last good state, marks the thread stale, and skips deltas until a STATE_SNAPSHOT arrives. With the resync option, it asks the server for one right away, and again each time a later delta fails after a snapshot; answer it with a snapshot before the run finishes. A run that finishes stale doesn't make the next one stale: that run starts from the last good state, which it sends to the server, so its deltas apply. Patches never change the state in place: what changes is copied, and the rest is shared, so React sees new objects only where something changed.
Runs that survive reconnects
Each run streams through createWebSocketRuns, so it behaves like the resumable runs in the agents guide:
- After a reconnect, the run resumes after the last event received, and events sent twice are dropped.
- After a reload, the runs that were streaming are followed again from their first event, which rebuilds their part of the thread. Send a
MESSAGES_SNAPSHOTand aSTATE_SNAPSHOTat the start of each run, and a reloaded page gets the whole thread back. policy: 'cancel'stops a run when its connection drops, for answers nobody may be waiting for.
The wire format
AG-UI doesn't define messages for WebSockets, and one socket can carry several runs, so each event needs its run and its place. The defaults are:
The server keeps each run's events for a while, starts a run only once per runId (the client sends run again after a reconnect until the first event arrives), and ends every run with RUN_FINISHED or RUN_ERROR. The run, resume, cancel, resync and event options each replace one of these formats, if your server already speaks another.
See also
react-websocket/ag-uiAPI- AI agents: browser tools, approvals, streaming and resumable runs
- AG-UI events