External Memory Backends
Wire an existing Mem0 / Zep / LangMem deployment as the backing store for contextweaver's optional long-lived stores (
EpisodicStore,FactStore) so contextweaver compiles per-turn prompts on top of memory you've already invested in — instead of forcing a separate in-memory or SQLite store.
Why
contextweaver and external memory services solve complementary problems:
- External memory layers (Mem0, Zep, LangMem) hold cross-session state: passive memory extraction, temporal knowledge graphs, semantic / episodic / procedural memory.
- contextweaver compiles those memories — plus the current turn's tool calls and tool results — into a phase-specific, budget-aware prompt every time the LLM is invoked.
The two compose: external memory persists across sessions; contextweaver decides what to surface this turn and how to compact oversized tool outputs (the context firewall).
Decision matrix
| Backend | Best for | Status | Install |
|---|---|---|---|
| Mem0 | Passive memory extraction from conversations, multi-tenant deployments | Available | pip install 'contextweaver[mem0]' |
| Zep / Graphiti | Temporal knowledge graphs, time-aware fact retrieval | Available | pip install 'contextweaver[zep]' |
| LangMem | LangGraph-native long-term memory shared across threads | Available | pip install 'contextweaver[langmem]' |
All three implement the same existing protocols
(EpisodicStore / FactStore from contextweaver.store.protocols)
without widening them, so the wiring is identical across backends — only
the constructor and its scope argument differ.
Boundary diagram
┌──────────────────────────────────────────────────────────────┐
│ Your agent runtime (any framework) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ contextweaver — policy layer │ │
│ │ │ │
│ │ ContextManager.build() │ │
│ │ ├─ EpisodicStore.search(query) ─┐ │ │
│ │ └─ FactStore.get_by_key(key) ─┤ │ │
│ │ │ │ │
│ └───────────────────────────────────────┼────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ External memory service (Mem0 / Zep / LangMem) │ │
│ │ • Long-lived store across sessions │ │
│ │ • Vector / graph recall │ │
│ │ • Memory extraction / consolidation │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
contextweaver never reaches outward — the adapter wraps the client
object you already hold (a configured mem0.Memory, etc.) and routes
protocol calls through it.
Mem0
contextweaver.extras.memory.mem0 ships two classes:
Mem0EpisodicStore—EpisodicStoreProtocolMem0FactStore—FactStoreProtocol
Both wrap a mem0.Memory instance scoped by a stable user_id. Writes
go through Memory.add(... infer=False) so mem0 does not run an
LLM extraction pass — the raw Episode.summary / Fact.value is stored
as-supplied. Every record is stamped with a contextweaver metadata
key (cw_episode_id / cw_fact_id) so get / delete can resolve
the canonical ID back to mem0's UUID.
Minimal wiring
from mem0 import Memory
from contextweaver.extras.memory.mem0 import Mem0EpisodicStore, Mem0FactStore
from contextweaver.context.manager import ContextManager
from contextweaver.store import StoreBundle
from contextweaver.store.episodic import Episode
from contextweaver.store.facts import Fact
# 1. Configure mem0 — bring your own LLM / vector store / reranker.
memory = Memory()
# 2. Adapt mem0 onto contextweaver's protocols. user_id is the mem0
# session id used for scope partitioning; use a stable value per
# agent / per tenant.
episodic = Mem0EpisodicStore(memory, user_id="agent:support-bot")
facts = Mem0FactStore(memory, user_id="agent:support-bot")
# 3. Plug into the StoreBundle the ContextManager uses.
bundle = StoreBundle(episodic_store=episodic, fact_store=facts)
ctx_mgr = ContextManager(stores=bundle)
# 4. From here on, use the standard contextweaver API.
episodic.add(Episode("ep-1", "User asked about refund policy for SKU-42"))
facts.put(Fact("f-tier", key="user.tier", value="enterprise"))
Search semantics
Mem0EpisodicStore.search delegates to Memory.search and inherits
mem0's vector + reranker stack — this is the main reason to choose
mem0 over the bundled InMemoryEpisodicStore. The configured
user_id scope is applied at search time so two adapter instances
constructed with different user_ids don't see each other's records.
Mem0FactStore design notes
mem0 has no first-class concept of a key separate from the content
itself, so Mem0FactStore.get_by_key and list_keys reconstruct the
answer client-side by scanning Memory.get_all. When the configured
user_id scope exceeds scan_limit (default 1000), these methods
raise NotImplementedError rather than silently truncating. Narrow
scope by partitioning across multiple user_ids or pick a dedicated
FactStore backend (in-memory or SQLite).
Out-of-scope (current cycle)
Mem0EpisodicStore.search does not currently expose mem0's
threshold / rerank parameters — both will surface once the protocol
gains a search_options parameter (tracked separately; this PR
intentionally does not widen the Protocol).
Zep / Graphiti
contextweaver.extras.memory.zep ships ZepEpisodicStore and
ZepFactStore, wrapping a zep_cloud.Zep client scoped by user_id.
Zep's knowledge graph is built around episodes (raw inputs) from which it
extracts edges (facts) and nodes (entities). Episodes are the one surface that
round-trips your exact input, so — like the Mem0 backend — this adapter uses
them as the lossless system of record: each Episode / Fact is written via
graph.add(type="json") stamped with its canonical ID, and get / delete /
all resolve that ID back to Zep's episode uuid_ by scanning
graph.episode.get_by_user_id.
from zep_cloud.client import Zep
from contextweaver.extras.memory.zep import ZepEpisodicStore, ZepFactStore
from contextweaver.store import StoreBundle
client = Zep(api_key="...") # your existing Zep client
bundle = StoreBundle(
episodic_store=ZepEpisodicStore(client, user_id="agent:support-bot"),
fact_store=ZepFactStore(client, user_id="agent:support-bot"),
)
Search note. Zep's native graph.search operates over extracted
edges/nodes, which don't map onto the Episode/Fact key-value contract, so
ZepEpisodicStore.search performs a deterministic client-side match over the
persisted episodes. Surfacing Zep's graph search is a follow-up that needs a
widened search-options Protocol (intentionally not widened here).
LangMem
contextweaver.extras.memory.langmem ships LangMemEpisodicStore and
LangMemFactStore, wrapping any LangGraph BaseStore
(langgraph.store.base) — an InMemoryStore, a PostgresStore, or
LangMem's own store-backed managers. Because a BaseStore is a faithful
namespaced KV store, the mapping is direct and lossless: the canonical
episode_id / fact_id is the store key and the value is the dataclass'
to_dict() payload. Episodes live under (*namespace, "episodes") and facts
under (*namespace, "facts"). EpisodicStore.search delegates to
BaseStore.search, giving semantic recall when the store has a vector index.
from langgraph.store.memory import InMemoryStore
from contextweaver.extras.memory.langmem import (
LangMemEpisodicStore,
LangMemFactStore,
)
from contextweaver.store import StoreBundle
store = InMemoryStore() # or any LangGraph BaseStore (Postgres, etc.)
ns = ("agent", "support-bot")
bundle = StoreBundle(
episodic_store=LangMemEpisodicStore(store, namespace=ns),
fact_store=LangMemFactStore(store, namespace=ns),
)
See also
tests/test_extras_memory_mem0.py— Reference for the wire shape and the per-method semantics.docs/integration_otel.md— Sameextras/pattern, different responsibility (observability vs. memory).- How contextweaver Fits — Where the policy layer sits relative to runtimes and persistence layers.