Skip to content
brain.GitHub
Developer docs OPEN SOURCE
Runtime reference

Events & streaming

Event envelope, payload schemas, SSE wire format, and cursor-based reconnection.

Every turn in a session produces a sequence of typed events. Brain appends retained events to the session's local log (events.jsonl) and broadcasts both retained and live-only events over server-sent events (SSE).

Event envelope

Every event returned by POST /sessions/{id}/events or GET /sessions/{id}/events shares the same outer JSON envelope:

{
  "id": "8f14e45fceea167a5a36dedd4bea2543",
  "type": "assistant.message",
  "time": "2026-10-15T12:00:00Z",
  "input_id": "c9f0f895fb98ab9159f51fd0297e236d",
  "data": {
    "text": "Append-only logs write new records at the end of a file without modifying existing bytes.",
    "usage": { "input": 42, "output": 19 },
    "stop_reason": "stop"
  },
  "cursor": "eyJvIjoxMjgsImkiOiI4ZjE0ZTQ1ZmNlZWExNjdhNWEzNmRlZGQ0YmVhMjU0MyJ9"
}
FieldTypePresent onDescription
idstringAll eventsUnique 32-character hex identifier for this event.
typestringAll eventsEvent type name in {domain}.{action} format (see Event types).
timestringAll eventsUTC RFC 3339 timestamp when Brain recorded or published the event.
input_idstringTurn eventsThe id of the user.message event that triggered this event. Omitted on user.message itself and on session-level status changes (such as close).
dataobjectAll eventsType-specific payload (see Event types).
cursorstringRetained eventsOpaque position token encoding the byte offset and event ID in events.jsonl. Omitted on live-only events (assistant.delta and tool.progress).

Do not construct cursors or pass id as after

Always pass the exact cursor string from a retained event (or next_cursor from a history page) as after. Passing an event id or a fabricated string returns 409 cursor_unavailable.

Event types

Brain emits eight event types. Six are saved to disk (events.jsonl) and replayed on history/stream requests; two are live-only progress signals sent to connected SSE subscribers.

TypeSaved to history?data fieldsDescription
user.messageYestype ("user.message"), text, delivery ("queue"), optional keyUser message accepted by POST /sessions/{id}/events.
assistant.deltaNo (live only)textIncremental text chunk streamed while the model generates a response.
assistant.messageYestext, plus usage and stop_reason from PiComplete assistant message emitted at the end of a model generation step.
tool.callYesid, name, argumentsTool invocation requested by the model before Brain dispatches the HTTP, SSE, or MCP call.
tool.progressNo (live only)call_id, dataIntermediate JSON progress frame emitted by an SSE tool (event: progress) or MCP progress notification.
tool.resultYescall_id, result (content[], optional is_error, optional unknown_outcome)Final outcome of a tool call returned to the harness. Each item in content has type: "text" (text) or type: "image" (data, mime_type).
session.statusYesstatus ("running", "idle", "interrupted", "failed", or "closed")Lifecycle transition for the session. When a turn finishes and the input queue is empty, Brain emits status: "idle" with input_id set to that turn's user.message id.
session.errorYescode ("provider_error", "brain_error", "conflict", etc.), messageFatal turn error or cancelled queued input. See Errors.

Each retained record in events.jsonl is capped at 4 MiB, and the total log file is capped at limits.session_bytes (default 1 GiB).

SSE wire format

Send GET /sessions/{id}/events with Accept: text/event-stream to open an SSE stream:

id: eyJvIjowLCJpIjoiYzlmMGY4OTVmYjk4YWI5MTU5ZjUxZmQwMjk3ZTIzNmQifQ
event: user.message
data: {"id":"c9f0f895fb98ab9159f51fd0297e236d","type":"user.message","time":"2026-10-15T12:00:00Z","data":{"type":"user.message","text":"Hello","delivery":"queue"},"cursor":"eyJvIjowLCJpIjoiYzlmMGY4OTVmYjk4YWI5MTU5ZjUxZmQwMjk3ZTIzNmQifQ"}

event: assistant.delta
data: {"id":"45c48cce2e2d7fbdea1afc51c7c6ad26","type":"assistant.delta","time":"2026-10-15T12:00:01Z","input_id":"c9f0f895fb98ab9159f51fd0297e236d","data":{"text":"Hello"}}

id: eyJvIjoxMjgsImkiOiI4ZjE0ZTQ1ZmNlZWExNjdhNWEzNmRlZGQ0YmVhMjU0MyJ9
event: assistant.message
data: {"id":"8f14e45fceea167a5a36dedd4bea2543","type":"assistant.message","time":"2026-10-15T12:00:01Z","input_id":"c9f0f895fb98ab9159f51fd0297e236d","data":{"text":"Hello"},"cursor":"eyJvIjoxMjgsImkiOiI4ZjE0ZTQ1ZmNlZWExNjdhNWEzNmRlZGQ0YmVhMjU0MyJ9"}

: keepalive
  • Framing: Every SSE frame includes event: <type> and data: <json-envelope>, followed by a blank line (\n\n).
  • Event IDs: Retained events include id: <cursor> before event:. Live-only events (assistant.delta and tool.progress) omit the id: line so standard EventSource clients never overwrite their last valid reconnection cursor.
  • Keepalives and deadlines: Brain writes : keepalive\n\n every 15 seconds while idle and applies a 15-second write deadline per frame or keepalive. Keepalives never touch model state or trigger LLM calls.

Reconnecting with cursors

When subscribing over SSE, Brain captures the current log boundary under lock, replays all retained events after your cursor in batches of 100, and then switches to live events without gaps or duplicates.

  1. Record event.cursor whenever it is non-empty.
  2. Reconnect with GET /sessions/{id}/events?after=CURSOR or the Last-Event-ID: CURSOR header (?after= takes precedence if both are sent; cursors longer than 512 bytes are rejected).
  3. If the session was restored from an older external checkpoint and your cursor points into an unsynced tail that no longer exists, Brain responds with 409 cursor_unavailable. Reset your local state and read from after="".

Stream lifecycle and backpressure

  • Multi-turn streams: SSE streams stay open across idle transitions and future turns until the client disconnects, the server shuts down, or the session is closed.
  • Client disconnects: Closing an SSE connection never interrupts a running turn on the server.
  • Slow-consumer eviction: Each SSE subscriber has a bounded in-memory buffer (limits.subscriber_events, default 128, and limits.subscriber_bytes, default 2 MiB). If a slow client falls behind either limit during live streaming, Brain closes that subscriber's connection without affecting the turn or other subscribers; the client can immediately reconnect with its last cursor.

On this page