Skip to Content
CrewAI

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 preparerecord 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 seamwhat to do there
a Flow step before generationPOST /prepare — prepend the returned context to the prompt
Agent.kickoff() / Crew.kickoff()nothing — this is your model
after kickoff returnsPOST /verify (optional), then send
after the sendPOST /record
task_callback / step_callbackper-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 client
  • X-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.

Last updated on