POST /chat streams the agent’s response as Server-Sent Events (AG-UI protocol frames). The
adapter is responsible only for the wire mechanics — headers, frames verbatim, backpressure,
teardown on disconnect. The event semantics live in the
AG-UI chat reference, and useChat already parses all of it for you.
What the adapter returns
A chat request becomes aResponse whose body is a pull-based ReadableStream:
- Headers go out with the response —
Content-Type: text/event-stream,Cache-Control: no-cache, andX-Accel-Buffering: noso an nginx/ingress proxy in front of Next doesn’t sit on the stream until the run finishes. - Frames are written verbatim —
data: ...\n\npayload lines from the runtime, plus: heartbeatcomment lines on the runtime’s cadence (default 15s) so intermediaries don’t time the connection out. Comment lines are invisible to anydata:-only parser. - Backpressure is the platform’s — the stream only pulls the next frame from the runtime when the consumer is ready for it, so a slow client can’t balloon memory.
- Disconnects tear down immediately — when the client hangs up, the platform calls
cancel()on the stream and the adapter callsreturn()on the runtime subscription. No zombie pumps.
Required route config
app/api/persona/[...persona]/route.ts
Consuming the stream
TheuseChat hook does all of this. If you’re building your own client:
fetch rather than EventSource — POST bodies and custom headers rule EventSource out.
Reconnect & resume
ThePOST /chat response carries an x-persona-run-id header. If the connection drops, a
client can reattach to the same run and receive the frames it missed:
runGraceMs (default 5 minutes) — but in the memory of the process that
started them. That has a specific consequence in Next.js deployments:
This is a property of the runtime, not of this adapter — see
Reconnect & resume for the full mechanics and mitigations. On
serverless, treat a dropped connection as a lost stream and restart it; the thread’s messages are
persisted server-side either way, so
GET /threads/:id/messages recovers the conversation.
Edge runtime
The server entry is Web-standard only —Request, Response, ReadableStream, fetch,
crypto.randomUUID — and the built bundle imports nothing from node:*. It runs unchanged on the
Edge runtime:
app/api/persona/[...persona]/route.ts
Binary streams
File downloads (GET /files/:id) and any other kind: 'binary' response go through the same
pull-based writer, chunk by chunk, without buffering the whole file.
Development-mode note
Next re-evaluates route modules on hot reload, so a saved edit creates a fresh runtime instance (and drops any in-flight resumable runs). That’s dev-only behavior; a production build evaluates the module once per worker. The runtime’s eviction timer isunref’d and won’t keep a process
alive, so calling runtime.close() is optional — reach for it only if you’re creating runtimes
repeatedly in a long-lived process.