Aex Brain
Concepts

Sessions

The canonical journal, turns, suspension, recovery, and event projections.

A session is a conversation plus everything Brain did on its behalf. Its creation configuration, lifecycle, effects, transcript changes, and Agentloop state are ordered in one append-only journal under sessions/{id}/journal/.

One journal, several projections

One sequence counter orders every record. Public Events, session status, the current transcript, and Agentloop kv are projections built from that journal. They are not independent state stores. Brain opens sessions on demand. A checksummed disposable checkpoint accelerates ordinary opens; stale or damaged checkpoints rebuild from the canonical journal. Checkpoints are not a second source of truth.

Public lifecycle and effect records appear through GET /v1/sessions/{id}/events. Transcript deltas and kv remain canonical journal records rather than separate copies. When a delta replaces an existing transcript tail, Brain projects that same record as transcript_replaced; pure appends and kv changes stay private. Every projection preserves the journal sequence.

Effects happen after commit

Before Brain calls a model, invokes a Tool, or starts an Environment operation, it durably commits the corresponding *_started record. It then sends the effect once. Brain never automatically retries it.

Before the Agentloop observes the answer, Brain commits *_ended or *_failed referencing the started sequence. If a remote result is uncertain, the failure or Tool outcome says unknown; an unknown effect is not replayed onto another target.

Extension Events use the same commit path. A turn may emit at most BRAIN_MAX_EMITTED_BYTES across the serialized Event kinds and payloads, 1 MiB unless the deployment sets otherwise. Public system codes and private journal record names are reserved; extensions cannot emit them.

Every new extension Event carries Brain-assigned origin: kind is agentloop or tool, and sequence references that execution's activation_started or tool_call_started record. Payload fields cannot change the origin. Historical Events without origin remain unauthenticated; never infer authorship from their names. An origin identifies the submitting execution, not the truth of its payload or a human approval.

This makes {session_id, sequence} the stable name of an operation throughout the journal and its execution path. There is no other identifier: a record carries references, not copies. tool_call_started names the Tool, the invocation, and the deadline; where the call goes is the configuration's answer for that name, recorded once at creation. Environment records name the Environment and carry the request.

Transcript and Agentloop state

The Agentloop owns transcript contents while Brain owns persistence. set-transcript commits the difference from the saved transcript. Appends store only the tail; summaries, edits, and reorders restart at the first changed item. Recovery folds those deltas in journal order.

kv-put commits one key/value change; kv-delete commits removal from current state without erasing history. kv-read distinguishes a missing key from stored JSON null. Each operation is durable before it returns, independently of turn success. Every invocation uses a fresh Wasm instance; save state through these services. Turn completion cannot overwrite saved state with a returned snapshot.

Model calls do not change the saved transcript. Their start records retain a message count and context: { through_sequence, delta }: apply the delta to the saved transcript at that journal sequence to reconstruct the exact request. This records auxiliary contexts without copying the entire conversation on every model call. Outcomes retain the complete model result.

Turns

For a short-lived caller, session.submit(input, { idempotencyKey }) returns the { session_id, sequence } receipt of a durably committed turn_started record without waiting for completion. The HTTP operation is POST /v1/sessions/{id}/messages with Prefer: respond-async; it returns 202 and Preference-Applied: respond-async. Omitting that header preserves the synchronous response.

Submission and synchronous send have separate idempotency scopes. Retry an uncertain submission with the same key and mode; do not switch to send as a retry. Another async submission while the session is occupied is rejected. Read Events from the receipt's sequence and retrieve the session to observe its eventual status; the handle's cached state is not updated by background work.

Closing a client after acceptance does not cancel hosted execution. Host Environments still require their host process, and client-side structured-output correction requires send; put independent execution and output policy in hosted extensions. Drain waits for accepted turns. Restart records interrupted work without replay, as described below.

A turn starts when a client sends a message and ends when the Agentloop returns or fails. During the activation the loop may call Brain's model, Tool dispatch, event emission, and telemetry services in any order.

Three deployment limits bound a turn, each with a default the server ships and zero meaning no bound; see Configuration for the rest:

  • BRAIN_MAX_MODEL_CALLS limits model calls per turn.
  • BRAIN_MAX_TURN_SECS limits wall time.
  • BRAIN_MAX_TOOL_SECS is the deadline handed to every Tool call.

The wall deadline initiates cancellation. Brain records outstanding effect outcomes and waits for bounded executor cancellation before closing the turn; this cleanup can extend the response time.

Cancellation requests stop local waiting and are forwarded to active executors. Outstanding Tool calls return cancelled; their own invocation deadlines return timeout. Neither promises that external effects were rolled back. unknown means dispatch may have happened but no reliable terminal result is available, such as a lost connection or an unfinished effect recovered after restart. These Tool results all have is_error: true and are never replayed automatically.

Suspension and recovery

By default, create and turn completion release the actor and its history indexes. Transcript and Event reads do not activate it. A message explicitly opens a new activation; the session's Environments stay set up. idleTtlMs can retain execution for a chosen interval; explicit zero retains it indefinitely. The server-wide TTL is optional and has the same zero convention.

Journal commits used as effect fences are durable before dispatch. If the process stops during a turn, the first access after restart records the interrupted turn as failed and does not rerun it. Any already committed effect record remains the source of truth.

Restart also fails interrupted creation and closes interrupted ending. Every started effect with no terminal record becomes an ambiguous failure; Brain does not resend it. Ending is recorded before detach starts, so a partly detached session cannot accept another message. A failed detach does not prevent the session from ending. Teardown remains a separate journaled operation. If teardown fails, the Environment record remains without automatic retry; an explicit session delete with a new idempotency key can request another attempt.

HTTP idempotency claims survive restart. Completed answers are retained for 24 hours. If a claimed request has no recorded answer, reusing its key returns an ambiguous error and never executes the request again. Inspect the session before choosing a new operation key. A create body contains only names, so repeating the same create sends the same body.

You can also seed a new session with transcript; Brain records those messages as its opening conversation.

Reading history without execution

session.transcript() reads the current canonical messages and through_sequence, including while suspended. It corresponds to GET /v1/sessions/{id}/transcript. Event pages and per-session live subscriptions also work without keeping an actor alive. Historical telemetry requires a configured sink; live model deltas are not silently promoted to durable history.

Acknowledged canonical records survive process and OS/power failure when the local filesystem and storage honor flushes. Storage loss is outside this guarantee. Brain refuses complete corrupt frames and incomplete nonfinal segments; only an incomplete final write is truncated. Recovery keeps committed prefixes and never replays agent effects.

Reading Events

Read committed Events from a sequence cursor:

let cursor = 0;
for await (const event of session.events(cursor)) {
  await handle(event);
  cursor = event.sequence;
  await saveCursor(cursor);
}

A finite page returns at most 1,000 Events and normally at most 8 MiB of journal frames. If the next Event alone exceeds that byte bound, Brain returns that one Event so the cursor can advance.

For live delivery, request the same endpoint as SSE:

GET /v1/sessions/{id}/events?after={cursor}
Accept: text/event-stream

SSE first catches up from the journal and then follows new commits. Each session has its own live backlog; unrelated traffic cannot make a subscriber lag. Model token deltas are live-only and have no journal sequence; reconnecting yields the completed committed model result. A consumer that needs at-least-once delivery into another system owns its saved cursor, queue, retries, and deduplication.

For recorded Events, SSE data contains the complete Event envelope, including origin. The SDK exposes the payload as event.data and provenance as event.origin on both pages and streams.

Lifecycle

session.interrupt() requests cancellation of work in flight and keeps the session available for another turn. The HTTP operation remains POST /v1/sessions/{id}/cancel. end refuses new messages and detaches the session's Environments while preserving history. delete removes the session directory, tears down its Environments, and forgets its credentials; the session must first be ended or failed.

await brain.close() releases the client's connections and local handlers. It is idempotent, rejects subsequent client operations, and does not interrupt, end or delete stored sessions. The client retains its shared host connection until close, including after failed creation or ending its last session. Put creation inside try and close the client in finally, as in the lifecycle example. Handlers should observe their cancellation signal; close does not wait for application code that ignores it. A client created by withToken has its own lifetime.

end waits for active work. SIGTERM/server shutdown stops admitting work and drains admitted operations while their callbacks remain available, then closes subscriptions and workers. Deadlines, interrupt, and forced process termination can still interrupt a turn and expose partial progress.

An owning host Tool can pass its signal to child.send(input, { signal: context.signal }). Use a fresh handle for an exclusively owned child; do not concurrently send through other owners. The SDK waits for admission before cancelling and waits for any issued cancellation request before returning. Creating a session alone does not establish ownership or affect independent sessions.

On this page