SSE Event Stream
AstraBox streams public Session output over Server-Sent Events (SSE) using the AI SDK UI Message Stream protocol. A single connection delivers every frame as it happens — no polling required.
Connection URL
GET /api/v1/sessions/{session_id}/ai-stream?follow=session
Request headers:
Authorization: Bearer $ACCESS_TOKEN
Accept: text/event-stream
The follow=session connection may open while the Session is idle. It waits
for output, delivers one assistant reply, and then closes. Open the next
connection with the last after_seq cursor to wait for the following reply.
Several queued inputs can produce separate replies within one platform turn;
closing a reply's connection does not stop that turn or its engine.
You can also send a message and receive that turn on one request:
POST /api/v1/sessions/{session_id}/ai-stream
The response declares x-vercel-ai-ui-message-stream: v1. Replayable semantic
frames are persisted while output streams, so a client can reconnect after a
network interruption. Live delta chunks may be coalesced in durable history.
AstraBox does not use the SSE Last-Event-ID header; resume with after_seq as
described below.
SSE Format
Each frame is a JSON object in the SSE data field:
data: {"type":"start","messageId":"RESPONSE_MESSAGE_ID","messageMetadata":{"turn_id":"TURN_ID"}}
data: {"type":"text-start","id":"text-1"}
data: {"type":"text-delta","id":"text-1","delta":"Hello"}
data: {"type":"text-end","id":"text-1"}
data: {"type":"finish","finishReason":"stop"}
data: {"type":"data-resume-cursor","transient":true,"data":{"frameSeq":42,"turnId":"TURN_ID"}}
data: [DONE]
| Field | Description |
|---|---|
type | AI SDK message-part type; it determines the rest of the JSON shape. |
id or toolCallId | Identifier shared by the related start, delta, and end frames. |
data | Payload of an AstraBox data-* part. |
Heartbeat comments may be sent to keep the connection alive.
Message Deltas
Incremental text output begins with text-start, followed by one or more
text-delta frames and text-end:
data: {"type":"text-start","id":"text-1"}
data: {"type":"text-delta","id":"text-1","delta":"Hello"}
data: {"type":"text-end","id":"text-1"}
Reasoning output, when the selected Agent program provides it, follows the same pattern:
data: {"type":"reasoning-start","id":"reasoning-1"}
data: {"type":"reasoning-delta","id":"reasoning-1","delta":"Inspecting the repository"}
data: {"type":"reasoning-end","id":"reasoning-1"}
The id is identical for all frames in the same text or reasoning block.
Tool input uses a corresponding tool-input-start, tool-input-delta, and
tool-input-available sequence linked by toolCallId.
Reconnecting During Message Deltas
Use the last frameSeq received in a data-resume-cursor part as after_seq
when reconnecting. Behavior depends on the cursor and the message structure:
- The cursor is within a reply. The stream replays that reply from its input-consumption boundary, including its stable message ID and all text and tool parts, then continues with new deltas. The AI SDK creates fresh parser state on resume; a cursor alone cannot reconstruct earlier parts.
- The cursor is at a completed reply boundary. That reply stays outside the next response. The stream waits for or replays the following reply.
- The turn has completed but its tail was not received. The stream rebuilds
the last unfinished reply through its terminal frame and cursor, then
closes with
[DONE].
Text and reasoning resumes synthesize a missing *-start frame when necessary
so the next delta remains valid AI SDK input. Clients should still process
parts by their IDs and treat replayed parts idempotently.
Reply-content replay does not repeat a data-session-store-reload notification
at or before the requested after_seq. That cursor acknowledges the notification
even when the content reader rewinds to reconstruct the SDK message. A newer
reload notification still requires a SessionStore rebuild before continuation.
Event Catalog
The stream follows the AI SDK protocol rather than defining an AstraBox event vocabulary. These are the frame families an integration normally handles:
| Frame family | Meaning | Examples |
|---|---|---|
| Message and step lifecycle | An assistant message or model step started or finished. | start, start-step, finish-step, finish |
| Text and reasoning | Incremental Agent output. | text-*, reasoning-* |
| Tool calls | Tool input, approval, and result. | tool-input-*, tool-approval-*, tool-output-* |
| Interaction | A question, decision, or approval that requires a response. | data-interaction |
| Turn correlation | Associates accepted input and result data with the turn. | data-turn-accepted, data-input-consumed, data-result, data-turn-failure |
| Child-run change | Signals that the Session's child-run view should be refreshed. | data-child-runs-changed |
| Resume | Provides a safe durable cursor. | data-resume-cursor |
| Error | Reports a stream failure. | error |
Standard AI SDK parts retain their documented shapes. data-* parts carry an
object in data; treat fields not documented by your integration as additive.
Common Event Flow
data-turn-accepted
start
data-input-consumed
start-step
reasoning-start / reasoning-delta / reasoning-end (optional)
text-start / text-delta / text-end
tool-input-* / tool-output-* (optional)
finish-step
data-result
finish
data-resume-cursor
[DONE]
Not every turn contains every frame. An Agent program can emit multiple model
steps and tool calls. Child tasks can also produce a transient
data-child-runs-changed notification; read the Session child-run endpoint for
their current state.
Model results
data-result contains the usage or cost values reported by the Agent program
for the completed turn. Available fields differ by Agent program and model
service. For example:
{
"type": "data-result",
"data": {
"total_cost_usd": 0.0137,
"usage": {
"input_tokens": 1204,
"output_tokens": 88
}
}
}
Treat the data object as additive and do not assume every Agent program
reports every metric.
Connection lifecycle
- A
finishframe withfinishReason: "stop"ends the current reply. When itsmessageMetadata.response_boundaryistrue, the platform turn continues across a FIFO response boundary. Use Session state to judge turn completion. The response includes a durable resume cursor and[DONE]before closing. - An
errorframe terminates the current response. Save any resume cursor sent before it and inspect the Session before deciding whether to retry. - A
finishframe withfinishReason: "tool-calls"can mark an interaction point inside the same turn; after the interaction is answered, that turn continues on the Session stream. - For network drops, reconnect with
after_seq. A deleted or inaccessible Session is reported as an HTTP error when the next connection is opened.
Tool call pairs
Every completed tool invocation is linked by toolCallId.
Key tool-input frames:
{
"type": "tool-input-available",
"toolCallId": "tool-call-1",
"toolName": "Bash",
"input": {"command": "pwd"}
}
The matching output uses the same toolCallId:
{
"type": "tool-output-available",
"toolCallId": "tool-call-1",
"output": "/workspace"
}
Failed or rejected calls use tool-output-error or tool-output-denied.
Process frames in stream order; do not sort tool calls by their identifier.
Tool Responses
When the stream emits a data-interaction part or tool-approval-request,
send the response to POST /api/v1/sessions/{session_id}/interaction-respond with the interaction ID:
{
"interaction_id": "INTERACTION_ID",
"answer": {
"decision": "approve"
}
}
The response shape depends on the interaction's presentation: a form carries
question answers, a decision uses one of the declared option IDs, and a tool
approval accepts approve or reject. See Permission
Modes for complete examples.
finish full schema
The terminal finish part names the AI SDK finish reason:
{
"type": "finish",
"finishReason": "stop"
}
On a follow=session connection, AstraBox follows it with a transient durable
cursor:
{
"type": "data-resume-cursor",
"transient": true,
"data": {
"frameSeq": 42,
"turnId": "TURN_ID"
}
}
The transport then sends data: [DONE] and closes the response. Save
frameSeq; do not derive a cursor from a message, block, or tool identifier.
Reconnect with after_seq
A new Session-follow connection replays durable frames from the beginning by
default. To continue from a received cursor, pass after_seq:
GET /api/v1/sessions/{session_id}/ai-stream?follow=session&after_seq=42
AstraBox emits data-resume-cursor only at a structurally safe replay point.
The server also rewinds within an active reply to include its identity and
complete content. Replace that reply by message ID; do not append its replay
as another assistant message.
For long-running connections, update the saved cursor whenever a
data-resume-cursorpart arrives and use itsframeSeqfor the next request.
Frame delivery cadence
AstraBox does not expose delta_flush_interval_ms. Text, reasoning, and tool
input frames are forwarded as the selected Agent program produces them. The
transport may send keepalive comments while no output is available.
Event History
Use the messages endpoint for durable conversation history and pagination:
curl --silent --show-error \
"$SERVICE_URL/api/v1/sessions/$SESSION_ID/messages?limit=20" \
--header "Authorization: Bearer $ACCESS_TOKEN"
The response contains saved messages rather than raw transport frames. Use
before to page backwards. To resume raw streamed output, use the
data-resume-cursor and after_seq flow above.
curl Examples
curl --no-buffer --silent --show-error \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header 'Accept: text/event-stream' \
"$SERVICE_URL/api/v1/sessions/$SESSION_ID/ai-stream?follow=session"
Resuming with after_seq:
curl --no-buffer --silent --show-error \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header 'Accept: text/event-stream' \
"$SERVICE_URL/api/v1/sessions/$SESSION_ID/ai-stream?follow=session&after_seq=42"
JavaScript EventSource Example
The browser's native EventSource uses the signed-in same-origin cookie. It
cannot add an Authorization header; bearer-token clients should use fetch
with an SSE parser instead.
let afterSeq = -1;
function followSession(sessionId) {
const url = new URL(`/api/v1/sessions/${sessionId}/ai-stream`, window.location.origin);
url.searchParams.set('follow', 'session');
if (afterSeq >= 0) url.searchParams.set('after_seq', String(afterSeq));
const stream = new EventSource(url, {withCredentials: true});
stream.onmessage = (event) => {
if (event.data === '[DONE]') {
stream.close();
followSession(sessionId);
return;
}
const part = JSON.parse(event.data);
if (part.type === 'text-delta') {
console.log('Agent:', part.delta);
}
if (part.type === 'data-resume-cursor') {
afterSeq = part.data.frameSeq;
}
if (part.type === 'error') {
console.error('Error:', part.errorText);
}
};
stream.onerror = () => {
stream.close();
followSession(sessionId);
};
}
Client Implementation Tips
- Track the last safe cursor — save
data-resume-cursor.data.frameSeqand use it asafter_seqafter disconnects. - Use an AI SDK parser — preserve the start/delta/end order and IDs for text, reasoning, and tool parts.
- Watch reply completion — after clean response EOF, open a new
follow=sessionresponse for the next reply. Inspect Session state on error. - Reconnect on transport loss — reopen from the last safe cursor; do not guess from the last text delta you rendered.
- Handle documented data parts — process the
data-*types your client needs and ignore additive fields it does not understand.
FAQ
Q: I'm getting a flood of historical events on connect. How do I avoid this?
A: Pass after_seq set to the last frameSeq from a data-resume-cursor part.
Completed replies before that cursor are omitted. An active reply is replayed
with its full content and stable identity so the SDK can replace it.
Q: Will the SSE connection close on its own?
A: A follow=session response closes at one assistant reply's boundary.
Open the next response with after_seq to wait for the following reply. While
the Session is idle, the current response stays open and receives keepalives.
Q: Can I use WebSockets instead?
A: The Session message stream uses SSE. SSE fits the one-way output stream and supports reconnection through AstraBox's durable cursor.
Q: Can I set delta_flush_interval_ms?
A: No. AstraBox forwards frames at the cadence produced by the selected Agent program and does not expose a separate flush interval.