CrewAI
CrewAI gives agents a plan and the tools to execute it. It does not give them a
memory that survives the process. Khwan is that memory — and because Khwan never
runs a model, your Agent’s LLM stays exactly as you configured it.
CrewAI’s seams are ordinary Python, so the integration is a small client you call
around kickoff() — nothing has to be patched or monkey-patched.
A packaged version of everything below is available as
khwan-crewai — the three tools and
a prepare → record wrapper, so you can install it rather than copy it. The
page still spells out what it does, because that is what you are trusting.
Where the three calls go
Flow step / before kickoff ──► prepare(turn) → grounding block, turn_token (no LLM)
Agent.kickoff(messages) ← CrewAI runs your model here
after the answer ────────────► verify(draft) → optional answer-gate
after you send ──────────────► record(answer) → persist + learn| CrewAI seam | what to do there |
|---|---|
a Flow step before generation | POST /prepare — prepend the returned context to the prompt |
Agent.kickoff() / Crew.kickoff() | nothing — this is your model |
after kickoff returns | POST /verify (optional), then send |
| after the send | POST /record |
task_callback / step_callback | per-task write-back in a multi-task crew |
Crew.kickoff() and Agent.kickoff() are blocking. If your app is async
(FastAPI, a webhook handler), run them in a worker thread —
anyio.to_thread.run_sync(lambda: agent.kickoff(messages)) — and keep the Khwan
calls on the event loop.
A minimal client
import anyio
import httpx
from crewai import Agent
BASE = "https://api.khwan.ai"
def headers(scope: dict) -> dict:
h = {"X-API-Key": scope["api_key"], "Content-Type": "application/json"}
if scope.get("core"):
h["X-Khwan-Core"] = str(scope["core"])
if scope.get("user"):
h["X-Khwan-User"] = str(scope["user"])
return h
async def run_turn(client: httpx.AsyncClient, agent: Agent, text: str, scope: dict) -> str:
# 1) prepare — no model call
prep = (await client.post(
"/prepare", json={"input": text}, headers=headers(scope)
)).json()
turn_token = prep.get("turn_token")
if prep.get("allowed") is False:
return f"I can't answer that: {prep.get('reason')}"
grounding = "\n".join(
m["content"] for m in prep.get("messages", []) if isinstance(m.get("content"), str)
)
# 2) your model — CrewAI runs it, Khwan never touches it
answer = await anyio.to_thread.run_sync(lambda: agent.kickoff(grounding + "\n\n" + text))
# 3) record — consume the token exactly once, and never raise
if turn_token:
try:
await client.post(
"/record",
json={"turn_token": turn_token, "answer": str(answer)},
headers=headers(scope),
)
except Exception:
pass # a failed learn must never break the send
return str(answer)Gating what the crew is about to say
CrewAI plans, then acts. If an action is “send this to a customer”, you usually want
one check between the draft and the send. That is
POST /verify:
verdict = (await client.post(
"/verify",
json={"answer": draft, "turn_token": turn_token},
headers=headers(scope),
)).json()
if not verdict.get("ok"):
... # regenerate, or route to a human with verdict["reason"]/verify is non-destructive — it never consumes turn_token, so the later /record
still works. Make it fail open: if the call errors, treat the draft as supported
rather than blocking the send on a transport hiccup.
Five rules, each one a bug we shipped
These come from running this integration in production. They are not style advice.
Hold turn_token on the turn, not in a session dict
Keying pending tokens by a session id the framework owns means the answer of turn N can be recorded against the input of turn N-1. Put the token on a per-turn object that travels with the request; the mispairing then cannot be expressed.
Consume the token before you dispatch
Set turn.turn_token = None before awaiting /record, not after. Otherwise a
retry or a concurrent path records the same turn twice.
record must never raise
Wrap it and swallow. A failed learn costs one turn of memory; a raised exception costs the user their reply.
In multi-tenant, a missing tenant key is an error — never a fallback
Do not fall back to the process-wide KHWAN_API_KEY when a tenant’s key is absent.
That key belongs to a different workspace, so the fallback reads and writes one
customer’s brain on behalf of another. Fail the call instead.
Record the text that actually shipped
If a human approves or edits the draft before it goes out, record the edited text. The correction is the single most valuable thing the brain can learn — and recording the draft instead teaches it the opposite of what happened.
Scoping a crew to your customers
If your crew runs on behalf of many end users, you do not build that isolation yourself — it is two headers:
X-Khwan-Core— one brain per tenant, project, or clientX-Khwan-User— a sub-brain per end user inside that core (account::core::@user)
A shared functional agent can point several people at one sub-brain by sending the
same X-Khwan-User; a personal agent sends the individual’s id. See
Cores.
Related
- Connect your agent — the framework-agnostic version of this page.