Skip to content

Commit cae6a8c

Browse files
authored
Merge pull request #95 from skynetcmd/feature/langchain-checkpointer
LangChain checkpointer + LCEL surfaces (release 2026.7.15.1)
2 parents d336627 + ee5b0a9 commit cae6a8c

47 files changed

Lines changed: 1653 additions & 97 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,51 @@ the policy is forward-going only.
2525

2626
---
2727

28+
## [2026.7.15.1] — 2026-07-15 — LangChain checkpointer + LCEL surfaces
29+
30+
### Added
31+
32+
- **LangGraph checkpointer (`M3Saver`)** — a `BaseCheckpointSaver` backed by
33+
m3's local engine DB, so a LangGraph run can pause at a human-in-the-loop
34+
`interrupt()`, survive a process restart, and resume/time-travel. Stored in
35+
dedicated tables that deliberately bypass the embedder/contradiction pipeline
36+
(a checkpoint is machine state, not knowledge). Sync + async surfaces.
37+
- **LCEL-native memory components**`MemoryWrite` (a pass-through `Runnable`
38+
that persists each piped value, so it composes at the tail of a chain),
39+
`MemoryRetrieve` (callable retrieval for the head), and the `with_m3_memory`
40+
decorator for zero-boilerplate capture of a callable's input/output.
41+
- **`M3Retriever.explain(query)`** — the real retrieval signal (blended
42+
relevance score, per-memory confidence, type, bitemporal validity) for
43+
attaching to a LangSmith run or logging. Reports what m3 computes; never
44+
fabricates per-component sub-scores.
45+
- **`M3_DEFAULT_USER_ID`** — an env fallback so single-user LangChain apps can
46+
drop `user_id=` from every call. Resolution stays `explicit > constructor
47+
default > env > raise`: there is still no anonymous mode, so tenancy isolation
48+
is unchanged.
49+
- **New examples** under `examples/langchain-agent/`:
50+
`graph_checkpointer.py`, `agent_with_memory_and_persistence.py`, and
51+
`lcel_memory.py`; plus `bin/mem0_scan.py`, an AST scanner that reports mem0
52+
usage and rewrites imports for migration.
53+
54+
### Fixed
55+
56+
- **LangChain integration was not shipped in built wheels.** `m3_memory/
57+
integrations/` lacked an `__init__.py` and `setup.py` excluded the whole
58+
integrations tree, so `from m3_memory.langchain import …` worked only from a
59+
repo checkout. The integration now ships as importable code (verified against
60+
an isolated release build). The `m3_memory.langchain` shim also now raises an
61+
actionable "upgrade" error instead of a cryptic namespace error when the
62+
payload is absent.
63+
64+
### Changed
65+
66+
- **LangChain docs updated** to cover all five drop-in surfaces (including the
67+
new checkpointer) and the LCEL components; the version note now states the
68+
supported `langchain-core` / `langgraph` compatibility band (major-version
69+
range) rather than describing it as a hard pin.
70+
71+
---
72+
2873
## [2026.7.15.0] — 2026-07-15 — Documentation accuracy, positioning, and a CUDA install guide
2974

3075
Documentation and repository-hygiene release. No changes to the installable

INSTALL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ wiring, chatlog hooks, and a final `doctor` verification. Power users can
8181
still run each step manually with `m3 install-m3`, `m3 embedder install`,
8282
etc. — see `m3 --help`.
8383

84-
> **Tool catalog stays small in your context.** m3 ships 87 MCP tools but
85-
> groups them into 8 domains (memory, chatlog, files, entity, agent, tasks,
86-
> conversations, admin). Only ~6 essentials load at MCP startup
87-
> (~2,400 tokens vs ~16,100 if all 87 loaded eagerly). The agent pulls in
88-
> a domain on demand — just say "load the files tools" and it does. Set
89-
> `M3_TOOLS_LAZY=0` to disable.
84+
> **Tool catalog stays small in your context.** m3 ships 100+ MCP tools but
85+
> groups them into 9 domains (memory, chatlog, files, entity, agent, tasks,
86+
> conversations, diagnostics, admin). Only the ~18 essentials load at MCP
87+
> startup (~3,540 tokens, ~1.8% of a 200K window; the full catalog loads on
88+
> demand). The agent pulls in a domain on demand — just say "load the files
89+
> tools" and it does. Set `M3_TOOLS_LAZY=0` to disable.
9090
9191
If the TL;DR worked, stop here. The rest of this file explains why and what
9292
gets installed.

MANIFEST.in

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,11 @@ recursive-include memory/migrations *.sql
2727
global-exclude __pycache__/*
2828
global-exclude *.py[cod]
2929
global-exclude *.pdb
30+
31+
# Prune colocated hermetic tests (e.g. integrations/langchain/test_provider_logic.py)
32+
# from BOTH the sdist and the wheel. Verified against an ISOLATED release build
33+
# (`python -m build`, which fetches the pinned setuptools>=77,<82): the resulting
34+
# wheel contains 0 test_*.py. (A non-isolated build using a newer local setuptools
35+
# — e.g. 82.x, outside our pin — can ignore this exclude for wheel package modules,
36+
# so always validate packaging with an isolated build, not the dev setuptools.)
37+
global-exclude test_*.py

bin/catalog/tools_admin.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
name="tools_list_domains",
2727
description=(
2828
"List m3 tool domains (memory, chatlog, files, entity, agent, tasks, "
29-
"conversations, admin) and their tool counts. Call `tools_load_domain` "
30-
"to expose a domain's full tool surface."
29+
"conversations, diagnostics, admin) and their tool counts. Call "
30+
"`tools_load_domain` to expose a domain's full tool surface."
3131
),
3232
parameters={"type": "object", "properties": {}, "required": []},
3333
impl=_tool_loader.list_domains,
@@ -43,7 +43,7 @@
4343
"Use when you need tools beyond the essentials (memory_search, "
4444
"memory_write, memory_get, chatlog_search, chatlog_write, files_search). "
4545
"Valid domains: memory, chatlog, files, entity, agent, tasks, "
46-
"conversations, admin."
46+
"conversations, diagnostics, admin."
4747
),
4848
parameters={
4949
"type": "object",
@@ -96,7 +96,8 @@
9696
"(name/type/required). Use this to discover the exact args for any "
9797
"tool before calling it via m3_call — cheaper than a failed call. "
9898
"Read-only catalog metadata; never returns tool output. Domains: "
99-
"memory, chatlog, files, entity, agent, tasks, conversations, admin."
99+
"memory, chatlog, files, entity, agent, tasks, conversations, "
100+
"diagnostics, admin."
100101
),
101102
parameters={
102103
"type": "object",

bin/gen_features_json.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,11 @@ def main() -> int:
131131
"install_extra": "pip install m3-memory[langchain]",
132132
"mem0_drop_in": True, # Memory/M3Memory/MemoryClient, one-line import swap
133133
"langgraph_basestore": True, # M3Store — backs LangMem / any BaseStore consumer
134+
"langgraph_checkpointer": True, # M3Saver — BaseCheckpointSaver (pause/resume/time-travel)
134135
"langmem_compatible": True,
135136
"chat_message_history": True, # M3ChatMessageHistory, with_m3_history
136137
"rag_retriever": True, # M3Retriever (LangChain BaseRetriever)
138+
"lcel_runnables": True, # MemoryWrite / MemoryRetrieve / with_m3_memory
137139
"docs": "docs/integrations/LANGCHAIN.md",
138140
"examples": "examples/langchain-agent/",
139141
"note": (

bin/gen_mcp_inventory.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def get_category_map():
144144
}
145145
return mapping
146146

147-
def generate_markdown(all_tools):
147+
def generate_markdown(all_tools, counts=None):
148148
cat_map = get_category_map()
149149

150150
# Sort tools by category then name
@@ -165,7 +165,21 @@ def generate_markdown(all_tools):
165165
]
166166

167167
md = "# MCP Tool Inventory\n\n"
168-
md += f"This document provides a comprehensive inventory of all {len(all_tools)} MCP tools available in the M3 Memory system.\n\n"
168+
md += (
169+
f"This document provides a comprehensive inventory of all {len(all_tools)} "
170+
"MCP tools available in the M3 Memory system.\n\n"
171+
)
172+
if counts:
173+
md += (
174+
f"> **How this reconciles with other docs.** The **{counts['catalog']}** "
175+
"in-catalog tools (the number the README/COMPARISON quote as \"100+\", and "
176+
f"the `count` in `docs/tools/MCP_CATALOG.json`, which excludes the two "
177+
f"`tools_*` meta-tools → **{counts['catalog'] - 2}** there) plus "
178+
f"**{counts['proxy']}** MCP-proxy protocol/debug tools (`mcp_proxy.py`) = "
179+
f"**{len(all_tools)}** total listed here. The prose \"100+ tools\" refers to "
180+
"the in-catalog surface; this inventory additionally documents the proxy "
181+
"transport tools.\n\n"
182+
)
169183

170184
# Summary Table
171185
md += "## Summary Table\n\n"
@@ -230,7 +244,10 @@ def main():
230244
# 2. Proxy tools
231245
protocol_tools, debug_tools = extract_proxy_tools()
232246

233-
all_tools = catalog_tools
247+
# NOTE: copy, don't alias — `all_tools = catalog_tools` would make the two the
248+
# SAME list, so appending proxy tools below would also grow catalog_tools and
249+
# corrupt the len(catalog_tools) used for the reconciliation counts.
250+
all_tools = list(catalog_tools)
234251
for t in protocol_tools:
235252
f = t['function']
236253
all_tools.append({
@@ -257,8 +274,14 @@ def main():
257274
if len(all_tools) != EXPECTED_TOOL_COUNT:
258275
print(f"Warning: Expected {EXPECTED_TOOL_COUNT} tools, found {len(all_tools)} — update EXPECTED_TOOL_COUNT in gen_mcp_inventory.py if a tool was added/removed.")
259276

260-
# 3. Render
261-
markdown = generate_markdown(all_tools)
277+
# 3. Render — pass the catalog/proxy split so the intro can disclose exactly
278+
# what the total counts (otherwise "122 tools" silently disagrees with the
279+
# catalog's 111 and the "100+" prose — three numbers, no reconciliation).
280+
counts = {
281+
"catalog": len(catalog_tools),
282+
"proxy": len(protocol_tools) + len(debug_tools),
283+
}
284+
markdown = generate_markdown(all_tools, counts)
262285

263286
# 4. Save
264287
output_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "docs", "MCP_TOOLS.md")

bin/gen_star_history.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ def _get(url: str, token: str) -> tuple[list, dict]:
3737
req.add_header("Authorization", f"Bearer {token}")
3838
req.add_header("X-GitHub-Api-Version", "2022-11-28")
3939
req.add_header("User-Agent", "m3-star-history-generator")
40-
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 (trusted host)
40+
# nosec B310 — URL is the hardcoded https://api.github.com host (fixed scheme),
41+
# not attacker-controlled; noqa suppresses ruff, nosec suppresses bandit.
42+
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 # nosec B310
4143
body = json.loads(resp.read().decode("utf-8"))
4244
return body, dict(resp.headers)
4345

0 commit comments

Comments
 (0)