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 formcp_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 atask 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:
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:resumeInterrupt, which calls sendMessage with
resume, and the stream resumes:
SSE wire format
The raw HTTP response uses standard SSE framing. Each line isdata: <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.