<!-- Source: https://docs.khwan.ai/sdk-ts -->
<!-- The whole documentation in one file: https://docs.khwan.ai/llms-full.txt -->

# TypeScript SDK

The `@khwan/client` package is a thin wrapper over the
[REST API](/api-reference) for Node and the browser. It mirrors the
[Python client](/sdk-python) one-to-one, with `async` methods. Open source:
[github.com/khwanlabs/khwan-client-ts](https://github.com/khwanlabs/khwan-client-ts).

```bash
npm install @khwan/client
```

```ts

```

  Every method returns a `Promise` — `prepare`, `record`, `sync`, `memory`,
  `cores`, and `metrics` are all `async`.

## `new Khwan(options)`

```ts
const kw = new Khwan({
  apiKey: 'kwk_live_xxx',            // required
  userId: 'alice',                   // optional — X-Khwan-User → isolated per-user sub-brain
  core: 'client1',                   // optional — sets X-Khwan-Core (isolated brain)
  baseUrl: 'https://api.khwan.ai',   // optional; override for on-prem
  model: undefined,                  // optional session model hint
  constitution: undefined,           // optional named constitution profile
  timeout: 60000,                    // optional request timeout (ms)
})
```

| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `apiKey` | `string` | — | **Required.** Your Khwan key (`kwk_...`). |
| `userId` | `string` | `undefined` | Optional. End-user id; sent as `X-Khwan-User`. Omit for one shared brain per account/core; set it to give each end-user an **isolated per-user sub-brain** (paid — the free plan includes a few). Composes with `core`. |
| `core` | `string` | `undefined` | Core slug; sent as `X-Khwan-Core`. Selects an isolated brain. Omit for the default core. |
| `baseUrl` | `string` | `https://api.khwan.ai` | Override for an on-prem instance. |
| `model` | `string` | `undefined` | Forwarded on `prepare`. |
| `constitution` | `string` | `undefined` | Named constitution profile reference. |
| `timeout` | `number` | `60000` | Per-request timeout in ms. |

  `memory` and `embedder` are **not** configurable — they are server-managed and only
  exist in the on-prem engine.

### Isolated cores

Each named `core` is a fully separate brain (own memory, identity, learning). Omit
`core` for the account's default core. Quota is pooled at the account level.

```ts
const client1 = new Khwan({ apiKey: 'kwk_live_xxx', userId: 'alice', core: 'client1' })
```

## Methods

### `prepare(input): Promise<Turn>`

Builds the turn context (memory + constitution + coherence). No LLM call. `POST /prepare`.

```ts
const turn = await kw.prepare('remember I prefer short answers in Thai')
```

### `record(turn, answer): Promise<Record<string, unknown>>`

Hands your model's answer back to persist and learn. `POST /record`.

```ts
await kw.record(turn, answer)
```

### `sync(): Promise<Record<string, unknown>>`

Triggers a learning / consolidation pass. `POST /sync`.

### `memory(limit = 20): Promise<Record<string, unknown>>`

Recent memory entries for the selected core. `GET /memory?limit=...`.

### `cores(): Promise<Record<string, unknown>>`

Lists the account's cores. `GET /cores`.

### `metrics(): Promise<Record<string, unknown>>`

Coherence / learning / usage metrics for the selected core. `GET /metrics`.

## `Turn`

Returned by `prepare()`.

| Property | Type | Notes |
| --- | --- | --- |
| `messages` | `Array<{ role: string; content: string }>` | Brief for your model. |
| `coherence` | `number \| null` | Coherence signal. |
| `sources` | `unknown[]` | Sources used. |
| `allowed` | `boolean` | `false` if the turn was gated. |
| `reason` | `string \| null` | Why it was gated. |
| `turnToken` | `string \| null` | Opaque handle used by `record()`. |
| `raw` | `Record<string, unknown>` | Full server payload. |

## `KhwanError`

Thrown on any non-2xx response. Has a `.status` number (the HTTP code).

## Full example (BYOM with the Anthropic SDK)

```ts

const kw  = new Khwan({ apiKey: 'kwk_live_xxx', userId: 'alice', core: 'client1' })
const llm = new Anthropic({ apiKey: 'sk-ant-...' }) // your key, Khwan never sees it

async function myOwnLlm(messages: Array<{ role: string; content: string }>) {
  const system = messages.find((m) => m.role === 'system')?.content ?? ''
  const chat = messages
    .filter((m) => m.role !== 'system')
    .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }))

  const r = await llm.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    system,
    messages: chat,
  })
  const block = r.content[0]
  return block.type === 'text' ? block.text : ''
}

try {
  const turn = await kw.prepare('remember I prefer short answers in Thai')
  if (turn.allowed) {
    const answer = await myOwnLlm(turn.messages)
    await kw.record(turn, answer)
    console.log(answer)
  } else {
    console.log('blocked:', turn.reason)
  }
} catch (e) {
  if (e instanceof KhwanError) console.error('Khwan error', e.status, e.message)
  else throw e
}
```
