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
| Status | Meaning | What you can do next |
|---|---|---|
idle | No turn is running. | Send a message or close the session. |
running | A turn is running, or messages are waiting in the queue. | Send more messages (they queue), interrupt, or close. |
interrupted | A 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. |
failed | Brain 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. |
closed | The 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.
| Field | Required | Description |
|---|---|---|
harness | Yes | A harness name from the host config, such as pi. |
config.model | Yes | A 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.tools | Yes | Names of the tools to enable, up to 128. Pass [] for none. Each tool must be available to your tenant. |
config.instructions | No | The system prompt, up to input_bytes (64 KiB by default). Pi uses a generic assistant prompt when you omit it. |
config.reasoning_effort | No | off, 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.
| Field | Required | Description |
|---|---|---|
type | Yes | user.message. |
text | Yes | The message. It must not be blank and is limited to input_bytes (64 KiB by default). |
key | No | An idempotency key, up to 128 bytes. |
delivery | No | queue, 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
429doesn't use up its key.
Keys are stored with the event log and still apply after Brain restarts.
Handle send errors
| Status | Code | Cause |
|---|---|---|
400 | invalid_request | Blank or oversized text, a key over 128 bytes, or an unknown delivery. |
409 | conflict | The key was used with a different message, the session is closed, or the session is interrupted or failed. Resume it before sending. |
429 | overloaded | The 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 tolimitretained events (1 to 1000, default 100). Passnext_cursorasafterto read the next page. - Server-sent events: With
Accept: text/event-stream, Brain replays retained events after theaftercursor (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.
| Event | Retained | Description |
|---|---|---|
user.message | Yes | A message you sent. Its id is the input_id for the turn it starts. |
session.status | Yes | The session's new status, such as running or idle. |
assistant.delta | No | A chunk of assistant text while the model writes. |
assistant.message | Yes | A complete assistant message. A turn can contain several. |
tool.call | Yes | A tool call, with the id Brain assigned, the tool name, and arguments. |
tool.progress | No | A progress update from a streaming tool. |
tool.result | Yes | The tool's result, including errors. |
session.error | Yes | Why 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.statuswithidleand the sameinput_id. - It completes and another message is queued:
session.statuswithrunningand the next message'sinput_id. There's noidlein between. - It stops early:
session.errorwith the message'sinput_id, followed bysession.statuswithinterrupted,failed, orclosed. Each queued message also gets asession.errorwith codeconflict.
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 /sessionsreturns{"sessions": [...]}for your tenant, sorted by ID. Pass the last ID asafterto get the next page, withlimitfrom 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).