Memory. One
line on top of pip install khora[crewai]:
Install
crewai>=1.10,<2.0 plus a stable khora.
Quickstart
The block below is byte-identical toexamples/integrations/crewai/example.py.
CI fails if they diverge.
example.py
Public surface
KhoraMemory(kb, namespace, *, user_id, app_id="crewai", scope_root="/", **memory_kwargs)- factory returning a
crewai.Memorywired against khora.
- factory returning a
KhoraStorageBackend: the duck-typedcrewai.memory.storage.backend.StorageBackendimplementation. Exposed for advanced users who want to construct the CrewAIMemorythemselves.
Scope ↔ namespace + tags mapping
CrewAI organises memories under a hierarchicalscope path
(/crew/research/<session>) plus a flat categories list. Khora has
a single namespace_id per memory. The adapter resolves the two like
this:
| CrewAI | Khora |
|---|---|
namespace arg to KhoraMemory | Document.namespace_id |
MemoryRecord.scope | Document.metadata["crewai_scope"] |
| trailing UUID on the scope path | Document.session_id (and Chunk.session_id) |
MemoryRecord.categories | Document.metadata["crewai_categories"] |
MemoryRecord.importance | Document.metadata["crewai_importance"] |
MemoryRecord.source | Document.metadata["crewai_source"] |
user_id arg to KhoraMemory | Document.metadata["crewai_user_id"] |
scope_prefix / categories / metadata_filter in
search and list_records is performed post-recall against
Document.metadata - khora has no per-document scope or
category columns to push the filter down into. For typical CrewAI
working-set sizes (hundreds to low thousands of records per
namespace), the post-filter is fast enough. Deployments with deep
scope trees and millions of records should partition by
KhoraMemory.namespace instead of relying on scope filters.
user_id validation
The factory rejects the following user_id values with
khora.exceptions.KhoraIntegrationError:
- empty string
"default"- any value shorter than 8 characters
Caveats
Pre-computed embeddings are ignored
CrewAI’sMemory.recall computes a query embedding via its own
embedder, then calls StorageBackend.search(query_embedding, …),
passing only the vector, not the source text. The adapter
discards that embedding and threads the original query text into
khora’s recall() via a stashing embedder installed at factory
construction. Two consequences:
- khora runs its own embedding step on the text. The embedder
configured on
Khora()(its dimension, model, normalisation) wins; the embedder configured oncrewai.Memoryonly contributes the text-stashing side channel. - CrewAI’s HyDE / rerank step (in
recall_flow.py) and khora’s HyDE/rerank/temporal-anchor stack both execute. Operators paying the LLM bill should be aware: a single CrewAIrecall()can spend tokens at both layers.
crewai.Memory(...) with query_analysis_threshold=10_000 so CrewAI
skips its own analysis on most queries, leaving HyDE to khora alone.
CrewAI’s encoding LLM is not duplicated
CrewAI’sMemory.remember runs its own LLM-driven scope / categories
/ importance analysis before calling StorageBackend.save([record]).
The adapter forwards those fields directly via
Document.metadata. kb.remember is called with
entity_types=[] and relationship_types=[] so khora does not
trigger a second extraction LLM call.
If you want khora’s entity extraction to run on CrewAI records
anyway, construct KhoraStorageBackend directly and pass non-empty
entity_types / relationship_types via the extraction_params on
your Khora() config.
Sync entry point only - no async loops above the adapter
KhoraStorageBackend is a sync class. Every async call into khora is
dispatched through khora.integrations._sync.run_sync, which refuses
to run from inside an existing asyncio loop (deadlock surface). Do not
call KhoraMemory(...) or any of its methods from inside an async def. Call it from a sync entry point or a worker thread.