Bring Your Own Model
Khwan is a cognition layer, not a model. In the BYOM flow you call the LLM; Khwan only prepares context and records the result. This page is the deep dive on that loop.
The contract
turn = kw.prepare(user_input) # Khwan → you: a ready-to-send brief
answer = my_own_llm(turn.messages) # you → your model → you: the reply
kw.record(turn, answer) # you → Khwan: persist + learnTwo things cross the boundary:
- Out of Khwan: a
Turn— most importantlyturn.messages(the brief) andturn.turn_token(an opaque handle that tiesrecordback to thisprepare). - Back into Khwan: the plain-text
answeryour model produced.
Khwan never sees your model provider key and never makes the generation call — there is no hosted model path; you always call your own model.
prepare() — what you get
kw.prepare(input) returns a Turn. It does not call an LLM; it assembles
context on the server and hands it back.
| Field | Type | Meaning |
|---|---|---|
turn.messages | list[{role, content}] | The brief to feed your model. |
turn.coherence | float | None | Coherence signal for this turn. |
turn.sources | list | Memory / retrieval sources used. |
turn.allowed | bool | False if Khwan gated the turn. |
turn.reason | str | None | Why it was gated (when allowed is False). |
turn.turn_token | str | None | Opaque handle; pass to record(). |
turn.raw() | dict | The full server payload. |
Always check turn.allowed before calling your model. When it’s False, skip
generation and surface turn.reason — the turn conflicts with the core’s
constitution or memory.
What’s inside turn.messages
A standard chat array with Khwan’s value baked into the system message:
[
{
"role": "system",
"content": "<constitution> + <retrieved memory> + <learned lessons> + <coherence>"
},
{"role": "user", "content": "remember I prefer short answers in Thai"}
]Don’t parse it — just pass it through. The constitution (identity + rules), retrieved memory for this core, distilled lessons, and coherence framing are already composed into it.
record() — closing the loop
kw.record(turn, answer)Pass back the same Turn and your model’s answer as a string. Khwan uses
turn.turn_token to match it to the original prepare, then persists the exchange
and updates what it has learned. Skip record and Khwan learns nothing from that
turn.
Writing my_own_llm
my_own_llm is just your normal LLM call: it takes turn.messages and returns a
string. Drop-in versions for common providers follow.
Anthropic (Claude)
Anthropic takes the system prompt as a separate argument, so split it out:
import anthropic
client = anthropic.Anthropic(api_key="sk-ant-...") # your key, Khwan never sees it
def my_own_llm(messages):
system = next((m["content"] for m in messages if m["role"] == "system"), "")
chat = [m for m in messages if m["role"] != "system"]
r = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system,
messages=chat,
)
return r.content[0].textOpenAI
OpenAI accepts the system message inline, so pass messages straight through:
from openai import OpenAI
client = OpenAI(api_key="sk-...") # your key
def my_own_llm(messages):
r = client.chat.completions.create(
model="gpt-4o",
messages=messages, # system + user, as-is
)
return r.choices[0].message.contentLocal Ollama
Fully local generation — the input and the answer never leave your machine except for the Khwan memory calls:
import ollama # pip install ollama, with a local Ollama server running
def my_own_llm(messages):
r = ollama.chat(
model="llama3.1",
messages=messages, # Ollama uses the same {role, content} shape
)
return r["message"]["content"]Anything else
Any callable that maps list[{role, content}] -> str works. Streaming, tool use,
and multi-model routing are all fine — Khwan only cares about the final answer
string you hand to record().
Isolate a core
The BYOM loop above runs against one brain. To run several fully separate brains —
each with its own memory, identity, and learning — pass core per client:
test = Khwan(api_key="kwk_live_xxx", user_id="alice", core="test")
client1 = Khwan(api_key="kwk_live_xxx", user_id="alice", core="client1")
# test and client1 share no memory or lessons.core is sent as the X-Khwan-Core header. Omit it for the account’s default
core. user_id still identifies the end user on each request — it does not create
a separate brain. Quota is pooled at the account level. See
Isolated cores and the cores API.