Skip to main content
When useChat sends a message, the backend responds with a stream of AG-UI protocol events via Server-Sent Events (SSE). The hook parses these internally and updates React state — but if you need to render custom UI for specific events, do analytics, or build logging, the onEvent callback gives you access to every raw event before the hook processes it.

Event types

The hook recognizes and handles these event types:

PersonaStreamingEvent — full type

Every event in the stream is one of these shapes (verified against the backend’s actual aguiTranslator.js — not guessed from the AG-UI spec, which the backend uses a subset of):

Using onEvent for custom behavior

Pass an onEvent callback to useChat to intercept every raw event. The callback fires before the hook updates state — so you can observe the stream without interfering with it.

Logging and analytics

Rendering MCP app widgets

The hook doesn’t render anything for mcp_app CUSTOM events — they’re a signal that an MCP server has produced an interactive widget. Use onEvent to capture them and render your own UI:

Tracking subagent activity

When the agent spawns a subagent (via a task tool call), the backend streams subagent_activity CUSTOM events. Each one carries a toolCallId linking it to the parent tool call, plus a live update on the subagent’s progress:
The hook also stores these on the matching PersonaToolCall’s subagentActivity array, so you can render them from messages[i].toolCalls[j].subagentActivity without needing onEvent at all.

The streaming lifecycle in order

Here’s the typical sequence of events for a single assistant turn:
With reasoning enabled, the sequence looks like:
With HITL interrupt:
The stream pauses. The user approves via resumeInterrupt, which calls sendMessage with resume, and the stream resumes:

SSE wire format

The raw HTTP response uses standard SSE framing. Each line is data: <json>\n\n. Comment lines (: heartbeat\n\n) are sent during gaps between events (e.g. a long-running tool call) to keep the connection alive through proxy idle timeouts. The hook handles this parsing internally — you never need to deal with raw SSE unless you’re building a non-React client.