# TypeScript SDK (/brain/docs/sdks/typescript)

`Brain` is a zero-dependency TypeScript client for the Brain HTTP API and server-sent event (SSE) streams. It runs on Node.js 24+ using standard `fetch` and `AsyncGenerator` streams.

## Install [#install]

```sh
npm install --save-exact @aexhq/brain@2.0.0
```

## Declare runtime tools [#declare-runtime-tools]

Pass ordinary functions when creating a session. The SDK registers descriptors and opens a callback connection before admitting the session. Functions and captured credentials stay in your Node process.

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

const client = new Brain("http://127.0.0.1:8080", process.env.BRAIN_API_KEY!);
const session = await client.create({
  harness: "pi",
  config: { model: "openai/gpt-4.1-mini", tools: [] },
  runtime_transport: "sse",
  runtime_tools: [{
    name: "lookup", description: "Read application data",
    input_schema: { type: "object", additionalProperties: false },
    output_schema: { type: "object", required: ["value"] },
    async run(_args, { signal, scope, progress }) {
      signal.throwIfAborted();
      await progress({ stage: "reading" });
      return { value: `data authorized for ${scope.tenant_id}` };
    },
  }],
});
await client.send(session.id, { type: "user.message", text: "Use runtime_lookup." });
for await (const event of client.observe(session.id)) {
  console.log(event);
  if (event.type === "session.status" && (event.data as { status: string }).status === "idle") break;
}
await client.close(session.id);
await client.dispose();
```

The alias `runtime` prefixes each function name. Return a JSON value; the SDK places it in `structured_content` and renders it as text. Throw an error to return a tool failure. Honor `signal` to stop local work after cancellation. Use `runtime_transport: "rpc"` for the WebSocket carrier.

Keep the client process alive while callbacks are needed. `dispose()` closes its executors without closing saved sessions. After restart or restore, call `client.rebind(sessionID, matchingTools, "sse")` and explicitly resume/send as required by the session status. Rebinding changes the connection, never replays an action. An unavailable executor fails before dispatch; a connection lost after dispatch reports an unknown outcome. Reading history or observing events cannot execute callbacks.

JavaScript and TypeScript use the same ESM package, including generated type declarations.
No checkout or compilation is needed. Version 1 replaces the predecessor's incompatible
0.x SDK; use it with the current Go session server. Pin the client and matching
server commit from the release workflow.

## Create a client [#create-a-client]

Pass the Brain server base URL and your bearer token to `new Brain(url, token, fetchImpl?)`:

```ts
import { Brain } from '@aexhq/brain';

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

You can pass a custom `fetch` implementation as the third argument to add custom headers, logging, or test doubles.

## Create a session and send a message [#create-a-session-and-send-a-message]

Call `create()` with a harness name and the session configuration: `model`, `tools`, and optional `instructions` and `reasoning_effort`. Then call `send()` to queue a `user.message`:

```ts
const session = await brain.create({
  harness: 'pi',
  config: {
    model: 'openai/gpt-4.1-mini',
    instructions: 'Answer in two sentences or fewer.',
    reasoning_effort: 'low',
    tools: [],
  },
});

const input = await brain.send(session.id, {
  type: 'user.message',
  text: 'Explain append-only storage.',
  key: 'message-1',
});
```

`send()` returns the recorded `user.message` event once Brain writes it to disk (`202 Accepted`). Keep `input.id` to match status updates and errors produced during that turn.

## Stream events [#stream-events]

Call `observe(session.id)` to iterate over the session's SSE stream. Brain replays retained events first, then streams live events across turns. The stream stays open until you break out of the loop or abort the signal, even after the session closes, so stop when you see the status you're waiting for.

Run this `.mjs` script from the repository root while Brain is running (see `examples/session.mjs` for the CLI version):

```js
import { Brain } from '@aexhq/brain';

const brain = new Brain('http://127.0.0.1:8080', process.env.BRAIN_API_KEY);
const session = await brain.create({
  harness: 'pi',
  config: { model: 'openai/gpt-4.1-mini', tools: [] },
});
const input = await brain.send(session.id, {
  type: 'user.message',
  text: 'Explain append-only storage.',
  key: 'message-1',
});

for await (const event of brain.observe(session.id)) {
  if (event.type === 'assistant.delta') {
    process.stdout.write(event.data.text);
  }
  if (event.type === 'assistant.message') {
    console.log(event.data.text);
  }
  if (event.input_id === input.id && event.type === 'session.error') {
    throw new Error(event.data.message);
  }
  if (
    event.input_id === input.id &&
    event.type === 'session.status' &&
    ['idle', 'failed', 'interrupted', 'closed'].includes(event.data.status)
  ) {
    break;
  }
}
```

<Callout type="info" title="Breaking out of observe()">
Exiting the `for await` loop cancels the client's HTTP stream without stopping the session on the server. To stop an active turn, call `await brain.interrupt(session.id)`.
</Callout>

In TypeScript, `event.data` is typed as `unknown`. Check `event.type`, then cast `event.data` to that event's payload from [Events & streaming](/brain/docs/reference/events). All wire types (`Session`, `SessionConfig`, `CreateSession`, `Input`, `Event`, `EventList`, `SessionList`, `Tool`, `ToolCatalog`, `Call`, `Content`, `Result`, `ToolResult`, `Checkpoint`, `Restore`, `Status`, `SyncStatus`, `ErrorDetail`, `ErrorResponse`) are re-exported from the package entrypoint.

## Reconnect with cursors [#reconnect-with-cursors]

Every retained event includes an opaque `event.cursor`. Save the latest non-empty cursor as you consume the stream, and pass it in `{ after }` to resume from that exact log position without re-reading earlier history:

```ts
let cursor: string | undefined;
const controller = new AbortController();

for await (const event of brain.observe(session.id, {
  after: cursor,
  signal: controller.signal,
})) {
  if (event.cursor) cursor = event.cursor;
  if (event.input_id !== input.id) continue;
  if (event.type === 'session.status' && (event.data as { status: string }).status === 'idle') break;
}
```

Live-only events (`assistant.delta` and `tool.progress`) do not carry a `cursor`. You can also fetch paginated retained history without opening an SSE stream by calling `await brain.history(session.id, after, limit)`.

## Handle errors and timeouts [#handle-errors-and-timeouts]

Non-2xx HTTP responses throw `BrainError`:

```ts
import { Brain, BrainError } from '@aexhq/brain';

try {
  await brain.send(session.id, { type: 'user.message', text: 'Continue.' });
} catch (err) {
  if (err instanceof BrainError) {
    console.error(err.status, err.code, err.message, err.requestId);
  }
  throw err;
}
```

- **`BrainError` fields:** `status` (HTTP status number), `code` (machine-readable string such as `"conflict"` or `"overloaded"`, falling back to `"http_error"`), `message`, and `requestId` (from the response body's `request_id`).
- **Tool failures vs. HTTP errors:** Failed or denied tool calls do not throw `BrainError`; they arrive in the event stream as `tool.result` events with `result.is_error: true`. Model provider or harness crashes arrive as `session.error` events. See [Errors](/brain/docs/reference/errors).
- **Timeouts:** Standard requests default to `AbortSignal.timeout(30_000)` (30 seconds). `flush()` and `restore()` default to `AbortSignal.timeout(60_000)` (60 seconds). `observe()` has no overall wall-clock timeout and rejects individual SSE events larger than 4 MiB or streams that end mid-frame.
- **No automatic retries or redirects:** The client sets `redirect: "error"` and never retries failed requests automatically.

## Methods reference [#methods-reference]

| Method | Signature | Returns | Description |
| --- | --- | --- | --- |
| `create` | `create(input: CreateSession, signal?: AbortSignal)` | `Promise<Session>` | Creates a session (`POST /sessions`). Default timeout: 30s. |
| `get` | `get(id: string, signal?: AbortSignal)` | `Promise<Session>` | Fetches session metadata and status (`GET /sessions/{id}`). Default timeout: 30s. |
| `list` | `list(after = "", limit = 100)` | `Promise<{ sessions: Session[] }>` | Lists tenant sessions in ascending ID order (`GET /sessions`). |
| `send` | `send(id: string, input: Input, signal?: AbortSignal)` | `Promise<Event>` | Queues a `user.message` and returns the recorded event (`POST /sessions/{id}/events`). Default timeout: 30s. |
| `interrupt` | `interrupt(id: string)` | `Promise<void>` | Sends `{"type": "user.interrupt"}` to stop the active turn (`POST /sessions/{id}/events`). |
| `history` | `history(id: string, after = "", limit = 100)` | `Promise<{ events: Event[]; next_cursor: string }>` | Returns a page of retained events and the next cursor (`GET /sessions/{id}/events`). |
| `observe` | `observe(id: string, options?: { after?: string; signal?: AbortSignal })` | `AsyncGenerator<Event>` | Streams retained and live events over SSE (`GET /sessions/{id}/events`). |
| `resume` | `resume(id: string)` | `Promise<Session>` | Moves an `interrupted` or `failed` session back to `idle` (`POST /sessions/{id}/resume`). |
| `close` | `close(id: string)` | `Promise<void>` | Permanently closes a session (`POST /sessions/{id}/close`). |
| `flush` | `flush(id: string, signal?: AbortSignal)` | `Promise<SyncStatus>` | Uploads local state to external storage immediately (`POST /sessions/{id}/flush`). Default timeout: 60s. |
| `sync` | `sync(id: string)` | `Promise<SyncStatus>` | Reads background checkpoint sync status (`GET /sessions/{id}/sync`). |
| `restore` | `restore(id: string, checkpoint: string, signal?: AbortSignal)` | `Promise<Session>` | Replaces local session state with a specific external checkpoint (`POST /sessions/{id}/restore`). Default timeout: 60s. |
| `catalog` | `catalog()` | `Promise<{ tools: Tool[] }>` | Lists tools available to the caller's tenant (`GET /catalog`). |
