Skip to content
brain.GitHub
Developer docs OPEN SOURCE
Guides

Session lifecycle

Create sessions, send messages, stream events, and interrupt, resume, or close a session.

A session is one agent conversation that belongs to one tenant. You create a session with its harness, model, and tools, then send messages and stream events while each turn runs. When a turn needs to stop, you interrupt it, then resume or close the session.

Before you begin

The examples use a client for the local tenant from the Quickstart. Install the JavaScript/TypeScript or Python package as described there.

import { Brain } from "@aexhq/brain";

const brain = new Brain("http://127.0.0.1:8080", process.env.BRAIN_API_KEY!);

Session statuses

StatusMeaningWhat you can do next
idleNo turn is running.Send a message or close the session.
runningA turn is running, or messages are waiting in the queue.Send more messages (they queue), interrupt, or close.
interruptedA turn stopped before it finished: you interrupted it, the model provider returned an error, Brain stopped during the turn, or Brain loaded the session from external storage.Resume, then send a message. Or close.
failedBrain stopped the turn because of its own failure, such as a crashed harness process or a failed disk write.Resume, then send a message. Or close.
closedThe session is permanently closed.Read its event history.

Create a session

Create a session with the harness, model, instructions, and tools the agent should use. Brain saves the session right away and starts the harness when you send the first message. See Create a session for every field and status code.

FieldRequiredDescription
harnessYesA harness name from the host config, such as pi.
config.modelYesA model in provider/model form, such as openai/gpt-4.1-mini, up to 256 characters. The provider must be configured in the host config, and its API key must be set.
config.toolsYesNames of the tools to enable, up to 128. Pass [] for none. Each tool must be available to your tenant.
config.instructionsNoThe system prompt, up to input_bytes (64 KiB by default). Pi uses a generic assistant prompt when you omit it.
config.reasoning_effortNooff, minimal, low, medium, high, xhigh, or max. Pi defaults to off. Choose a level the model supports; otherwise the first turn fails with session.error.
curl http://127.0.0.1:8080/sessions \
  -H "Authorization: Bearer $BRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"harness":"pi","config":{"model":"openai/gpt-4.1-mini","instructions":"Answer in one paragraph.","tools":[]}}'

Brain returns 201 Created with the session in status idle:

{
  "id": "7d1c9e04b3a24f6e8c5a0b2d9e3f4a61",
  "harness": "pi",
  "config": { "model": "openai/gpt-4.1-mini", "instructions": "Answer in one paragraph.", "tools": [] },
  "status": "idle",
  "created_at": "2025-01-01T12:00:00Z",
  "updated_at": "2025-01-01T12:00:00Z",
  "checkpoint": { "id": "0f8e2a6c1d3b4e5f9a7c6b5d4e3f2a1b", "offset": 0, "time": "2025-01-01T12:00:00Z" }
}

Creating a session needs a token with manage that isn't limited to one session; other tokens get 403. An unknown harness returns 422. A malformed model, a provider that isn't configured, an invalid reasoning effort, more than 128 tools, or a duplicate tool returns 400. A tool that isn't available to your tenant returns 403.

Send a message

POST /sessions/{id}/events with a user.message adds the message to the session's queue and returns 202 Accepted with the saved event. The event's id is the input_id on every event of the turn it starts. Any token for the session's tenant that isn't read-only can send messages; manage isn't required.

FieldRequiredDescription
typeYesuser.message.
textYesThe message. It must not be blank and is limited to input_bytes (64 KiB by default).
keyNoAn idempotency key, up to 128 bytes.
deliveryNoqueue, the default: the message runs after the active turn and any messages queued before it.
curl http://127.0.0.1:8080/sessions/SESSION_ID/events \
  -H "Authorization: Bearer $BRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"user.message","text":"Summarize our plan.","key":"msg-0001"}'

A session runs one turn at a time. Messages sent while a turn runs wait in order, up to queue messages (16 by default).

Retry safely with idempotency keys

Send a key with each message so you can retry after a timeout or a dropped connection:

  • Same key, same message: Brain returns the original event and doesn't queue the message again.
  • Same key, different message: Brain returns 409 conflict.
  • Rejected request: A key is recorded only when Brain accepts the message, so a request rejected with 429 doesn't use up its key.

Keys are stored with the event log and still apply after Brain restarts.

Handle send errors

StatusCodeCause
400invalid_requestBlank or oversized text, a key over 128 bytes, or an unknown delivery.
409conflictThe key was used with a different message, the session is closed, or the session is interrupted or failed. Resume it before sending.
429overloadedThe queue is full, the session's event log is near its session_bytes limit, the external storage upload backlog is full, or Brain is shutting down. Retry after the Retry-After delay.

Stream events

GET /sessions/{id}/events returns the session's events in two ways:

  • JSON pages: By default, Brain returns {"events": [...], "next_cursor": "..."} with up to limit retained events (1 to 1000, default 100). Pass next_cursor as after to read the next page.
  • Server-sent events: With Accept: text/event-stream, Brain replays retained events after the after cursor (or from the start of the session), then sends new events as they happen.

Each SSE message has an event: line with the event type and a data: line with the event as JSON. Retained events also have an id: line with their cursor. Brain sends a : keepalive comment every 15 seconds while the stream is idle.

EventRetainedDescription
user.messageYesA message you sent. Its id is the input_id for the turn it starts.
session.statusYesThe session's new status, such as running or idle.
assistant.deltaNoA chunk of assistant text while the model writes.
assistant.messageYesA complete assistant message. A turn can contain several.
tool.callYesA tool call, with the id Brain assigned, the tool name, and arguments.
tool.progressNoA progress update from a streaming tool.
tool.resultYesThe tool's result, including errors.
session.errorYesWhy a turn or a queued message stopped.

Live-only events (assistant.delta and tool.progress) reach only clients connected when they happen; they never appear in JSON pages or replays. See Events & streaming for each event's payload.

curl -N http://127.0.0.1:8080/sessions/SESSION_ID/events \
  -H "Authorization: Bearer $BRAIN_API_KEY" \
  -H "Accept: text/event-stream"

Know when a turn ends

Brain emits session.status with running and the message's input_id when a turn starts. A turn ends in one of three ways:

  • It completes and the queue is empty: session.status with idle and the same input_id.
  • It completes and another message is queued: session.status with running and the next message's input_id. There's no idle in between.
  • It stops early: session.error with the message's input_id, followed by session.status with interrupted, failed, or closed. Each queued message also gets a session.error with code conflict.

Reconnect after a disconnect

Disconnecting doesn't stop the turn. To resume streaming after a disconnect, pass the cursor of the last retained event you processed as { after: cursor } in TypeScript, after=cursor in Python, or the after query parameter or Last-Event-ID header over HTTP.

Slow consumers

Brain buffers up to subscriber_events events (128) and subscriber_bytes (2 MiB) for each stream. A client that falls further behind is disconnected and must reconnect from its last cursor. When subscribers streams (128) are already open, a new stream gets 429. A cursor outside the session's retained history returns 409 cursor_unavailable; this can happen after a restore drops an unsynced tail.

Interrupt a turn

Send {"type":"user.interrupt"} to the events endpoint to stop the running turn. Brain returns 202 Accepted with {"status":"interrupt_requested"}. If no turn is running, the request does nothing. Like sending, interrupting doesn't require manage.

curl http://127.0.0.1:8080/sessions/SESSION_ID/events \
  -H "Authorization: Bearer $BRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type":"user.interrupt"}'

Brain asks the harness to stop, cancels the turn, and emits session.error with code conflict for the turn and for each queued message. The session then moves to interrupted. An interrupt request accepts no other fields.

Remote work may continue

Interrupting cancels in-flight tool requests, though a tool service may already have started work, such as a sandbox command or an external job. Brain sends each tool call at most once; your tool service owns retries and cleanup.

Resume after an interruption or failure

POST /sessions/{id}/resume moves an interrupted or failed session back to idle. Brain restores the harness's native state and the session config from the last completed turn's checkpoint, then emits session.status with idle and no input_id. Resuming doesn't call the model or re-run tools. Events from the stopped turn stay in the history, but the harness no longer sees them.

curl -X POST http://127.0.0.1:8080/sessions/SESSION_ID/resume \
  -H "Authorization: Bearer $BRAIN_API_KEY"

Resuming needs a token with manage and returns the session. An idle session is returned unchanged. A running or closed session returns 409. After resuming, send a new message to continue.

Close a session

POST /sessions/{id}/close permanently closes a session and returns 202 Accepted with {"status":"close_requested"}. An idle session closes immediately. If a turn is running, Brain interrupts it and fails the queued messages first. Closing needs a token with manage.

curl -X POST http://127.0.0.1:8080/sessions/SESSION_ID/close \
  -H "Authorization: Bearer $BRAIN_API_KEY"

The event history stays readable after you close a session; new messages return 409. Resources that tools created, such as sandboxes, belong to the tool services, which clean them up.

List and inspect sessions

  • List: GET /sessions returns {"sessions": [...]} for your tenant, sorted by ID. Pass the last ID as after to get the next page, with limit from 1 to 1000 (default 100). With a PostgreSQL index configured, the list includes sessions stored by other Brain instances.
  • Inspect: GET /sessions/{id} returns the session with its current status and latest checkpoint.

A session that belongs to another tenant returns 404, the same as a session that doesn't exist. A token limited to one session sees only that session. The SDKs expose these routes as list(after, limit) and get(id).

Next steps

On this page