TypeScript SDK
The @khwan/client package is a thin wrapper over the
REST API for Node and the browser. It mirrors the
Python client one-to-one, with async methods.
npm install @khwan/clientimport { Khwan, KhwanError } from '@khwan/client'Every method returns a Promise — prepare, record, sync, memory,
cores, and metrics are all async.
new Khwan(options)
const kw = new Khwan({
apiKey: 'kwk_live_xxx', // required
userId: 'alice', // required — sets X-Khwan-User
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 | — | Required. End-user id; sent as X-Khwan-User. Identifies the user, not a separate brain. |
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.
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.
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.
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)
import { Khwan, KhwanError } from '@khwan/client'
import Anthropic from '@anthropic-ai/sdk'
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
}