# Drop m3 into your LangChain stack **Change one import. Keep every line of your mem0 code. Get memory that remembers *better*.** ```diff - from mem0 import Memory + from m3_memory.langchain import Memory ``` That's the whole migration. Your `Memory()`, your `.add()`, your `.search()` — untouched. But now every fact is stored **on your machine** (no server, no vector DB, no API key, no data leaving your laptop), and you get four things mem0 and LangMem simply don't have: **contradiction handling, time-travel queries, commanded forgetting, and hybrid + graph retrieval.** m3 is a local-first agent-memory system — hybrid FTS5 + vector + MMR recall, a bitemporal model, knowledge-graph supersession (real contradiction handling, not flat dedup), and async fact extraction — that speaks five standard LangChain surfaces: | You use… | Swap in… | Section | |---|---|---| | **mem0** | `from m3_memory.langchain import Memory` — one import | [§1](#1-replacing-mem0) | | **LangMem** / raw LangGraph | `store=M3Store()` | [§2](#2-replacing--backing-langmem) | | chat history / RAG retriever | `M3ChatMessageHistory` · `M3Retriever` | [§2](#short-term-chat-history--rag-retrieval) | | **LangGraph checkpointer** | `checkpointer=M3Saver()` — pause / resume / time-travel | [§2](#langgraph-checkpointing-with-m3saver) | | *the extras nothing else has* | `.supersede` · `as_of=` · `.forget` · `.related` | [§3](#3-what-you-gain-the-m3-native-extras) | > **Install** — `pip install "m3-memory[langchain]"`. m3 self-configures on first > use (in-process embedder; the SQLite DB is created and migrated for you). No > wizard, no infra, nothing to provision. --- ## 1. Replacing mem0 The migration is **one import line**. Everything below it is byte-identical to your existing mem0 program — same class name, same constructor, same method calls, same return shape. ### Before (mem0) → After (m3) ```python from mem0 import Memory # ← delete this line from m3_memory.langchain import Memory # ← add this one. Nothing else changes. memory = Memory() memory.add( [ {"role": "user", "content": "I prefer dark mode and I'm allergic to peanuts."}, {"role": "assistant", "content": "Noted!"}, ], user_id="alex", ) relevant = memory.search("dark mode", user_id="alex", limit=3) print("\n".join(f"- {m['memory']}" for m in relevant["results"])) ``` `.add(messages, *, user_id=…)` and `.search(query, *, user_id=…, limit=…, filters=…)` mirror mem0's OSS signatures (`filters` narrows by `type`, like mem0's metadata filter), and the return shape (`{"results": [{"id", "memory", "score", ...}]}`) matches mem0 — so code that reads mem0 responses keeps working unchanged. > **Read-your-writes:** `memory.add(...)` writes the turn synchronously (so an > immediate `.search()` finds it) *and* kicks off m3's deeper async fact-extraction > in the background — you get mem0's instant behavior plus richer long-term memory a > moment later. Pass `extract=False` to skip the async pass. ### `Memory.from_config(...)` — your existing mem0 config just works m3 supplies its own embedder and store, so it **accepts and ignores** mem0's `embedder` / `vector_store` / `llm` config blocks (logging one line about what it overrode). You do not need to remove your config to migrate. ```python from m3_memory.langchain import Memory # a mem0-style config dict — infra keys are harmlessly ignored by m3 memory = Memory.from_config({ "vector_store": {"provider": "qdrant", "config": {...}}, # ignored — m3 owns the store "embedder": {"provider": "openai", "config": {...}}, # ignored — m3 embeds in-process }) ``` ### Hosted mem0 (`MemoryClient`) ```python # before: from mem0 import MemoryClient from m3_memory.langchain import MemoryClient # same methods, local-first, no API key client = MemoryClient() ``` --- ## 2. Replacing / backing LangMem LangMem runs on a LangGraph **`BaseStore`**. m3 provides one — `M3Store` — so you keep LangMem's tools and managers and simply **point them at m3**. Verified against LangMem source: it calls only `asearch` / `aput` / `adelete` / `aget`, all of which `M3Store` implements, and it lets the store do the embedding — which is exactly what m3 does. **No shim, no `index=` config needed.** ### Before (LangMem on the in-memory store) ```python from langgraph.prebuilt import create_react_agent from langgraph.store.memory import InMemoryStore from langmem import create_manage_memory_tool, create_search_memory_tool store = InMemoryStore( index={"dims": 1536, "embed": "openai:text-embedding-3-small"}, # needed: InMemoryStore can't embed itself ) agent = create_react_agent( "anthropic:claude-3-5-sonnet-latest", tools=[ create_manage_memory_tool(namespace=("memories",)), create_search_memory_tool(namespace=("memories",)), ], store=store, ) ``` ### After (LangMem on m3) — swap the store; drop the embed config ```python from langgraph.prebuilt import create_react_agent from langmem import create_manage_memory_tool, create_search_memory_tool from m3_memory.langchain import M3Store # ← the store change store = M3Store() # m3 embeds in-process — no index/embed config agent = create_react_agent( "anthropic:claude-3-5-sonnet-latest", tools=[ create_manage_memory_tool(namespace=("memories",)), create_search_memory_tool(namespace=("memories",)), ], store=store, ) ``` Everything else — LangMem's manage/search tools — works unchanged, now persisted locally in m3 (survives restart, unlike `InMemoryStore`). #### LangMem's background manager on m3 LangMem's `create_memory_store_manager` auto-extracts, consolidates, and versions memories from a message stream. It takes a `store` argument (or reads it from the LangGraph config), so pointing it at m3 is one change. *(Signature verified against langmem source, 2026-07-13.)* ```python from langmem import create_memory_store_manager from m3_memory.langchain import M3Store store = M3Store() # no index/embed config needed manager = create_memory_store_manager( "anthropic:claude-3-5-sonnet-latest", namespace=("memories", "{langgraph_user_id}"), # LangMem's default template store=store, # ← m3 backs the manager ) # Feed it a conversation; it extracts durable facts on its own, into m3. manager.invoke({ "messages": [ {"role": "user", "content": "I moved from Python to Rust for the perf work."}, {"role": "assistant", "content": "Got it — Rust for the performance path."}, ] }) # The extracted memories are searchable through the same m3 store. hits = store.search(("memories", "default"), query="programming language", limit=5) ``` Because m3 handles embedding, contradiction (supersession), and temporal versioning natively, LangMem's "maintains a versioned history of all changes" runs on m3's real bitemporal + KG model rather than a flat store. ### Raw LangGraph (no memory library) `M3Store` is a plain `BaseStore`, so you can give any LangGraph agent persistent cross-session memory in one line: ```python from langgraph.prebuilt import create_react_agent from m3_memory.langchain import M3Store agent = create_react_agent(model, tools, store=M3Store()) # done ``` ### Namespaces ⇄ users m3 maps the LangGraph namespace tuple to its tenancy model. The only concept you need is a **user id**: ```python ("alex",) # everything for user "alex" ("alex", "work-project") # optional second element = an isolated scope within the user ``` ### Short-term chat history & RAG retrieval Two more standard slots, both backed by m3: ```python from langchain_core.messages import HumanMessage, AIMessage from m3_memory.langchain import M3ChatMessageHistory, M3Retriever, with_m3_history # BaseChatMessageHistory — chat turns persist locally AND feed m3's async fact # extraction. session_id / thread_id both map to m3's conversation_id. history = M3ChatMessageHistory("session-42", user_id="alex") history.add_messages([HumanMessage(content="I'm vegetarian"), AIMessage(content="Noted.")]) history.messages # oldest -> newest, roles preserved history.clear() # wipe just this conversation # One-liner to wrap a runnable so its history persists to m3: chain = with_m3_history(prompt | model, user_id="alex") chain.invoke({"input": "hi"}, config={"configurable": {"session_id": "conv-1"}}) # BaseRetriever — hybrid recall as LangChain Documents, with m3's bitemporal + # confidence signal in each Document.metadata. retriever = M3Retriever(user_id="alex", k=4) docs = retriever.invoke("deadline") # -> [Document(page_content=…, metadata={id, score, confidence, valid_from, …})] ``` ### LangGraph checkpointing with `M3Saver` `M3Store` (above) is *long-term memory* — what your agent knows. A **checkpointer** is a different slot: the *machine state* LangGraph needs to pause, resume, and time-travel a run. `M3Saver` implements LangGraph's `BaseCheckpointSaver`, so a graph can hit a human-in-the-loop `interrupt()`, the process can exit, and a later run resumes exactly where it stopped — persisted to m3's local engine DB, no external checkpoint store to run. ```python from m3_memory.langchain import M3Saver from langgraph.types import Command # for resuming after an interrupt() graph = builder.compile(checkpointer=M3Saver()) cfg = {"configurable": {"thread_id": "t-1"}} # thread_id is the resume key graph.invoke({"input": "…"}, cfg) # runs until an interrupt() pauses it graph.get_state(cfg) # inspect the persisted state graph.invoke(Command(resume="yes"), cfg) # resume to completion list(graph.get_state_history(cfg)) # every super-step — replay / time-travel ``` A checkpoint is opaque graph state, not knowledge, so `M3Saver` stores it in its own tables and deliberately **bypasses** m3's embedder and contradiction pipeline (two checkpoints of a thread aren't contradictions to reconcile). An optional `user_id` in `configurable` scopes reads to that user's threads. Sync and async (`aget_tuple` / `aput` / `alist`) are both implemented. See a runnable end-to-end example in [`examples/langchain-agent/graph_checkpointer.py`](../../examples/langchain-agent/graph_checkpointer.py). ### Which surface for which job — short-term vs. long-term vs. state The three surfaces are easy to conflate; they do genuinely different jobs, and a real agent often uses two or three together: | Surface | Holds | Lifetime | Use it for | |---|---|---|---| | `M3ChatMessageHistory` | raw chat turns | one conversation | short-term context; the turns also feed m3's async fact extraction into long-term | | `M3Store` / `Memory` | distilled facts & preferences | across conversations | long-term knowledge — what the agent *knows*; contradiction-superseded, bitemporal | | `M3Saver` | serialized graph state | one thread, resumable | pausing/resuming a run — the agent's *machine state*, not its knowledge | Rule of thumb: **history** is what was *said*, **store/Memory** is what is *true*, **saver** is where the *run* is. They compose — see [`agent_with_memory_and_persistence.py`](../../examples/langchain-agent/agent_with_memory_and_persistence.py) for `M3Store` (memory) + `M3Saver` (persistence) in one `create_react_agent`. All three share one local m3 engine, so a single `memory_search_multi_db` can search chat-log and curated memory together when you want unified recall. ### LCEL-native memory — compose reads and writes into a chain `M3Retriever` is already a `Runnable`, so retrieval pipes straight into LCEL. For the **write** side — which LangChain has no standard Runnable for — m3 adds `MemoryWrite` (persist-and-pass-through) and `MemoryRetrieve` (a callable head): ```python from m3_memory.langchain import MemoryRetrieve, MemoryWrite # recall → prompt → llm → persist the turn, all in one pipe: chain = MemoryRetrieve(user_id="alex", k=4) | prompt | llm | MemoryWrite(user_id="alex") chain.invoke("what did we decide about the API?") # llm output written to m3 ``` `MemoryWrite` returns its input unchanged (so it composes at the *end* of a chain) and writes synchronously — immediately FTS-searchable, and it enqueues m3's async fact extraction like `Memory.add`. For zero-boilerplate capture, the `with_m3_memory` decorator records a callable's input and output with no body changes: ```python from m3_memory.langchain import with_m3_memory @with_m3_memory(user_id="alex") # or set M3_DEFAULT_USER_ID and drop user_id= def answer(question: str) -> str: return agent.invoke(question) # question + answer both captured ``` ### Observability — honest retrieval explanation (LangSmith) `M3Retriever(...).explain(query)` returns the **real** signal behind a retrieval — the blended relevance `score`, plus per-memory `confidence`, `type`, and bitemporal validity — for attaching to a LangSmith run or logging: ```python r = M3Retriever(user_id="alex", k=4) graph.invoke(inp, config={"metadata": {"m3_retrieval": r.explain(query)}}) ``` m3's search produces a single blended score (hybrid FTS5 + vector + MMR + optional recency), not separated per-component sub-scores — so `explain()` reports what m3 actually computes and never invents a component breakdown. Every `Document` from the retriever also carries this signal in `.metadata` (`score`, `confidence`, `valid_from`/`valid_to`, `type`) for in-chain filtering or reranking. ### Single-user apps — `M3_DEFAULT_USER_ID` m3 enforces per-user tenancy: `user_id` is required and there is no anonymous mode (a missing one raises, by design — it's how agents' private notes stay private). For a **single-user** app that's friction, so set `M3_DEFAULT_USER_ID` once and drop `user_id=` from your calls: ```bash export M3_DEFAULT_USER_ID=alex ``` Resolution order is **explicit arg → constructor default → `M3_DEFAULT_USER_ID` → raise**. A multi-tenant app simply leaves the env unset and keeps passing `user_id` per call, so the safety guarantee is unchanged — the env only fills the gap for the single-user case, it never invents a tenant. --- ## 3. What you gain — the m3-native extras Ever had an agent confidently repeat a fact the user corrected three turns ago? Surface a preference that changed months back? Get asked to "forget that" and have no verb for it? These are the things flat vector memory can't do — and they're first-class methods on the *same* `Memory` object, so your mem0-style code stays untouched while these are just *there* when you reach for them. > **On `.search()`:** the mem0-compatible call is `memory.search(query, *, > user_id=…, limit=…, filters=…)` (§1). The extras below are either extra > keyword args on that *same* method (`as_of=`, `recency_bias=`) or additional > typed methods (`.supersede`, `.forget`, `.related`) — all optional, so mem0 > code keeps working untouched while new code reaches for them. ```python from m3_memory.langchain import Memory memory = Memory(user_id="alex") # 1) CONTRADICTION — evolve a fact instead of stacking a conflicting copy # ("I use Python" -> "I switched to Rust" without both living forever) memory.supersede(old_id, "I switched to Rust") # 2) TEMPORAL — ask what was true at a point in time (native bitemporal query). # `as_of` is an m3-native keyword on the SAME .search() method. memory.search("stack", as_of="2026-01-01") # 3) FORGETTING — commanded erase (GDPR Article 17); mem0 has no forget verb. memory.forget() # wipe THIS user's memories (forget(user_id=…) for another) # 4) GRAPH retrieval — go beyond vector similarity. (Search is ALWAYS hybrid # FTS5 + vector + MMR — there is no vector-only mode to opt into.) memory.related(memory_id) # knowledge-graph neighbors of a memory # 5) RECENCY — tilt ranking toward recent facts (an m3-native .search() arg) memory.search("preferences", recency_bias=0.3) ``` Every `.search()` result also carries m3's real signal in its metadata — `confidence`, `valid_from` / `valid_to` — so even plain mem0-style searches silently benefit from temporal + confidence ranking. **Storage topology is yours to choose.** With mem0 and LangMem, conversation history and long-term memory are fixed, separate layers. m3 lets you run them as **one unified store, two independent stores, or two stores searched together** — set by configuration (the chatlog DB path), not code. `M3ChatMessageHistory` (short-term turns) and `Memory`/`M3Store` (long-term facts) route through m3's topology resolver automatically, so the same code works whether they share a DB or not. ### Escape hatch — reach *any* m3 tool For power users: `.call()` dispatches any m3 catalog tool by name (typed methods cover the 95% case; this is the other 5%). ```python memory.call("memory_search_routed", query="...", k=5) memory.call("chatlog_status") ``` > **Note — destructive tools via `.call()` are gated.** Unlike the typed methods > (`.delete()`, `.forget()`, `.clear()`), which run your explicit request directly and > are **not** gated, `.call()` goes through m3's guarded dispatcher — the same one the > MCP/LLM surface uses. So a destructive call by raw name (e.g. > `.call("memory_delete", id=…)`) returns `{"ok": false, "error": "destructive_gated"}` > unless `MCP_PROXY_ALLOW_DESTRUCTIVE=1` is set in the environment. This is deliberate: > the guard exists to stop an agent/LLM from deleting by surprise through the generic > tool surface. **For deletes, prefer the typed `.delete()` / `.forget()` / `.clear()` > methods** — they express explicit user intent and need no flag. --- ## Feature comparison | | mem0 | LangMem | **m3** | |---|---|---|---| | Local-first, no server / no API key | partial | ✗ (needs store infra) | **✓** | | Contradiction handling (supersession) | ✗ | ✗ | **✓** | | Temporal / bitemporal `as_of` queries | ✗ | ✗ | **✓** | | Commanded forgetting (GDPR) | ✗ | ✗ | **✓** | | Hybrid (FTS5+vector+MMR) + graph retrieval | vector | vector | **✓** | | Async fact extraction on write | ✓ | ✓ | **✓** | | Storage topology: unify / separate / search-together, by config | ✗ | ✗ | **✓** | | One-line drop-in for mem0 | — | ✗ | **✓** | | Backs LangMem / any LangGraph `BaseStore` | ✗ | native | **✓** | --- ## Notes & caveats - **Runnable examples:** see [`examples/langchain-agent/`](../../examples/langchain-agent/) — `mem0_migration.py`, `native_store.py`, `langmem_on_m3.py`, `history_and_retriever.py`, `graph_checkpointer.py`, plus a `perf_baseline.py` and its committed numbers. - **Versions:** the `[langchain]` extra requires `langchain-core>=0.3.0,<1` and `langgraph>=0.2.0,<1` — a major-version compatibility band, not a hard pin, so any release inside that range is supported. Verified against langgraph 0.6.x / langgraph-checkpoint 3.x. Across a `<1` range some API drift is possible; if you hit it, the extra's floor/ceiling mark the tested boundary. The mem0 mirror tracks mem0's OSS `.add`/`.search`/`.get_all`/`.delete` shapes — m3 never imports mem0 (a clean reimplementation of the interface). - **Read-your-writes** is via m3's FTS index (a query sharing words with the stored text matches immediately); purely SEMANTIC matches sharpen a beat later as the async vector backfill completes. `get_all()` is deterministic and always current. - **"Replacing LangMem"** means backing it with m3, not deleting it — your code still calls LangMem's functions, now stored in m3. mem0 is a true delete-and-replace. - **Per-user isolation:** m3 enforces tenancy at the SQL layer, so a `user_id` is mandatory on every call — there is no anonymous or global mode, and omitting it raises rather than silently sharing memory across users.