lemmalog README

The project's README from github.com/JordyZomer/lemmalog. As one markdown file: README.md.

Lemmalog

A Datalog engine for LLM agent memory. This repo contains the engine (Rust crate, MCP server, REPL, agent skill) plus the design document (datalog-context-engine-design.md, with an honest status log of what shipped).

The thesis: an agent's memory should be a deductive database — the agent builds a verifiable model of what it knows and mechanically reasons over how that knowledge changes, rather than "remembering better" than a vector store. Base facts are asserted at the ingestion boundary (LLM extraction); rules derive closures, temporal projections, contradiction candidates, and relevance diffusion; every fact carries provenance back to its source episodes; and each conversation turn updates derived views incrementally instead of re-deriving them (or worse, re-reasoning them in-context).

What's implemented

Design element Status
Runtime-parsed, stratified Datalog (interpreter, not proc-macro) ✅
Negation-as-absence with negative-cycle rejection ✅
Seminaive fixpoint with per-epoch delta maintenance ✅
Bi-temporal facts via valid_from/valid_to/asserted_at columns + now() ✅
Semiring annotations: confidence (product t-norm) × provenance (set union) ✅
Annotation merge on re-derivation (max conf, union prov, deduped supports) ✅
why() proof trees with cycle protection ✅
Additive arithmetic in comparisons (D = Dm + 1) with linear solving ✅
Scoped negative deltas: retraction recomputes only transitive dependents ✅
ask() — read-only datalog query surface for agents ✅
Magic-sets demand evaluation (ask_deep): point queries without full fixpoint ✅
Per-position secondary indexes; row-id lookups; WAM-style trail backtracking ✅
Epoch change-log: changes_from/since + "new in memory" context section ✅
Hybrid retrieval (context_for_query): BM25 + entity/graph boosting, budget-aware ✅
Extraction boundary: Extractor trait, memoized MockExtractor + LlmExtractor ✅
Deterministic update policy: ADD / UPDATE / NOOP / escalate ✅
Positional ContextAssembler (distilled top, verbatim provenance bottom, budget) ✅
AgentMemory facade: observe → policy → maintain → ask/ask_deep/context/why ✅
Persistence: snapshot save/load (episodes + EDB facts + rules; derived rebuilt) ✅
Semantic side index: Embedder trait, HashEmbedder, seed_mentions + near diffusion ✅
DRed-lite scoped recompute: supersession rebuilds only what actually changed ✅
Synthetic eval harness (scenario::run_eval): accuracy/token/latency vs. ground truth ✅
Aggregation: count/min/max/sum head args with group-by fold + value-change propagation ✅
Entity resolution: star-shaped aliasing, directional canonical views, conflict escalation ✅
MCP server (--features mcp): the engine as tools for Claude Code / Kimi CLI ✅
Rule registry: versioned batches, agent install/uninstall, backfill on change ✅
Hypotheticals: what_if lookahead with byte-identical store restore ✅
Streaming change feed: Added/Retracted/Cleared events for projections ✅
Indexed read paths: query/ask select buckets (point lookups ~100µs at 4M facts) ✅
Differential testing: 450 random programs vs a naive fixpoint oracle + parser fuzzing ✅
REPL: cargo run --bin lemmalog (rule / + / ? / ?? / why / run / dump / batches) ✅
Leapfrog triejoins (worst-case-optimal joins), DBSP streaming deltas 🚧 future phases

Entity resolution (canonicalization)

The LLM proposes star-shaped alias(Local, Canonical) edges; Datalog derives the closure; canonical views project facts read-side only (src/canonical.rs):

alias(Acme_Corp, Acme).                       % LLM-proposed, confidence-tagged
same_as(X, Y) :- alias(X, Y).                 % symmetric-transitive closure
same_as(X, Z) :- same_as(X, Y), same_as(Y, Z).
maps_to(X, X) :- entity(X), !aliased(X).      % directional projection:
maps_to(L, C) :- alias(L, C).                 %  exactly one canonical spelling
current_canon(S, R, O) :- current(S, R, O), maps_to(S, S2), maps_to(O, O2).

Safety properties (all tested): topology violations — a local with two canonicals, or a name both local and canonical — derive alias_conflict facts instead of merging identities; confidence propagates through the closure (weak two-hop merges are visibly low-confidence); retracting an alias edge collapses the closure and every downstream view in the same epoch. A similarity-gated LLM reconciliation pass (canonical::reconcile::reconcile_entities) offers only embedding-similar name pairs to the model.

Building this surfaced and fixed two long-lived engine bugs: the scoped recompute never processed same-stratum dependents (latent stale-fact bug), fixed by SCC-condensation stratification plus a recompute fixpoint; and the invalidation pass ran before lower strata were materialized on first run, fixed by moving invalidation after evaluation. Both caught by the differential harness.

The lemmalog skill

skills/lemmalog/SKILL.md in this crate is a generic agent skill that makes the engine the task's working memory for any long-running work — investigations, debugging, audits, multi-agent searches — not just one hardcoded workflow. It encodes the discipline the live experiments converged on (assert-as-you-verify with anchors and confidence, rules as experiments, query before re-reasoning, why before trusting, hypothesis lifecycles, decide-from-queries, report-from-the-engine), the minimal interop schema (located, describes, hypothesis/status, decision), the grammar gotchas, and the anti-patterns. Install per CLI:

# Claude Code (user scope)
mkdir -p ~/.claude/skills && cp -r skills/lemmalog ~/.claude/skills/

# Kimi CLI: copy the same folder into its skills directory
# (e.g. ~/.kimi/skills/lemmalog/ — see its skills docs)

Task prompts then stay domain-specific and reference the skill in one line.

MCP server: use from Claude Code or Kimi CLI

cargo build --release --features mcp

Register the server (stdio JSON-RPC, 12 tools):

# Claude Code (project or user scope)
claude mcp add lemmalog -- $(pwd)/target/release/lemmalog-mcp

# Kimi CLI
kimi mcp add lemmalog -- $(pwd)/target/release/lemmalog-mcp

Persistence across sessions: set the environment when registering (both CLIs support --env KEY=VALUE on add):

claude mcp add lemmalog --env LEMMALOG_MCP_PATH=/tmp/lemmalog.snapshot -- \
  $(pwd)/target/release/lemmalog-mcp

Sub-agents that can't reach MCP (Kimi CLI sub-agents need mcp__lemmalog__* in their agent profile's tools list — the bare mcp__lemmalog form matches nothing) and scripts/cron can use the headless CLI on the same snapshot:

LEMMALOG_MCP_PATH=/tmp/lemmalog.snapshot lemmalog-cli observe --facts 'S --rel--> O'
LEMMALOG_MCP_PATH=/tmp/lemmalog.snapshot lemmalog-cli query --goal 'current("s", R, O)'

Mutations are visible to the MCP server on its next load and vice versa; the two hold separate in-process copies, so don't write from both simultaneously (have the parent read while a sub-agent writes, or route every writer through the CLI).

The intended division of labor: the host model (Claude/Kimi) reads the conversation and asserts triples via lemmalog_observe (line protocol S --rel[conf]--> O); Lemmalog derives closures, temporal views, canonicalizations and aggregations deterministically. Typical session:

lemmalog_observe      {"facts": "Alice --works_at--> Acme\nAlice --manager--> Bob", "ts": 100}
lemmalog_install_rules {"rules": "reports_to(X,Y) :- current(X,\"manager\",Y).\n trans: ..."}
lemmalog_query        {"goal": "reports_to(\"Alice\", Y)"}        -> Y=Bob, Y=Carol
lemmalog_why          {"fact": "reports_to(Alice, Carol)"}          -> proof tree to episodes
lemmalog_what_if      {"facts": "Dana --manager--> Alice", "goal": "reports_to(\"Dana\", Y)"}
lemmalog_canonicalize {"facts": "Acme_Corp --alias_of[0.9]--> Acme"}

Also available: lemmalog_query_deep (magic sets), lemmalog_dump, lemmalog_batches/lemmalog_uninstall (revertable rule batches), lemmalog_save, lemmalog_run. Note the goal/fact grammar: bare capitalized words are variables — quote entity names (reports_to("Alice", Y)).

Error semantics are built for self-correction. Recoverable input errors (unparseable goals, rejected rule batches, unknown batch ids) return as tool results with isError: true — category prefix, the offending input, the precise reason, and a hint or corrected example (e.g. the quote-entity-names hint on every parse failure). Silent zero-fact ingestion is impossible: lemmalog_observe reports every dropped line with its reason (pronoun/role-word subjects, prose contamination, missing --rel--> structure), so a malformed extraction batch is loud, not lost.

LongMemEval (oracle split) — live results

The 15 MB oracle split is not committed; download it once:

mkdir -p data && curl -sL \
  https://huggingface.co/datasets/xiaowu0162/longmemeval/resolve/main/longmemeval_oracle \
  -o data/longmemeval_oracle.json

examples/longmemeval.rs runs the benchmark end-to-ready: evidence sessions -> chunked live extraction -> update policy -> memory -> answers in two modes (structured memory block vs raw transcript, same model) -> SQuAD-style F1. Final configuration: Claude Opus 4.8, 5 per type, with role-aware pronoun resolution, stated-date extraction feeding derived ordering rules, answer-format discipline, and a question-time recall fallback:

                         memory F1   transcript F1   EM
single-session-user   5/5  0.80          0.80       4/5 vs 4/5
knowledge-update      5/5  0.60          0.41       3/5 vs 1/5
multi-session         5/5  0.21          0.38       0/5 vs 1/5
temporal-reasoning    5/5  0.57          0.61       2/5 vs 2/5
single-session-assistant  0.64          0.74       2/5 vs 2/5
single-session-preference 0.06          0.13       0/5 vs 0/5
OVERALL               30    0.48          0.51      11/30 vs 10/30

(One scored run — see the measurement caveat in the retrieval-results section before quoting these numbers comparatively; run-to-run variance without temperature control is ~±0.3 F1 per type at n=5.)

Per-fix effects, measured on the failing instances before the full run:

Honest trade-off now visible in the data: richer extraction grows memory contexts (dated facts, assistant facts), compressing the token advantage from 4-12x to 1-5x on heavy instances — recall vs context size is a dial, not a free lunch. The stable per-type structure across five full runs: knowledge-update is the memory's decisive category (transcript answers stale values or hedges both), user-stated discrete facts are near-perfect, preference gold answers are unmatchable prose for both modes, and the one remaining frontier is indirect mentions.

Hybrid retrieval (src/retrieval.rs)

The answer to the trade-off above: selection, not extraction, is the bottleneck at the context boundary. AgentMemory::context_for_query replaces dump-everything assembly with a three-signal ranker:

Selection is O(facts) per query — rebuild-on-demand is fine at agent scale — and the internal bookkeeping relations (canonicalization plumbing, aggregation temps, entity seeds) are excluded. The LongMemEval runner now answers from retrieved context (question-relevant facts + dated edge history only for the entities the selection touches), and the MCP server exposes it as lemmalog_context — the skill teaches it as the default over lemmalog_dump for grounded answering.

Retrieval results (live, same 30-instance protocol)

Memory-mode context switched from dump-everything to context_for_query (1800-token budget, budgeted dated-history append). Assembled from focused runs (Claude Opus 4.8):

                         memory F1   transcript F1
knowledge-update      5/5  0.80          0.57
single-session-user   5/5  ~1.00         ~0.80
single-session-assistant  0.70          0.74
multi-session         5/5  0.32          0.33
single-session-preference 0.11          0.11
temporal-reasoning    5/5  high variance (see below)

Measurement caveat, learned the hard way: opus-4-8 rejects the temperature parameter, so answers sample at the API default — a single scored run at n=5/type has ~±0.3 F1 noise per category (the bike-vs-car question flipped 1.00 → 0.00 across two same-configuration runs). Type comparisons below ~0.3 are not evidence. The findings that held across every configuration:

Reproduction is nearly free after the first run: LEMMALOG_CACHE_DIR persists extraction results by episode hash (reruns pay only for answering), LEMMALOG_DUMP_CTX + LEMMALOG_NO_ANSWER=1 assemble and dump contexts with zero API calls — context-assembly changes can be validated offline by diffing the dumped files.

MemEval: the standardized comparison (102 questions, split s)

The headline run: ProsusAI MemEval's stratified 102-question LongMemEval protocol (17 per category, the s haystack, ~50 sessions per question) with their standardized reader (gpt-4.1) and native binary judge (gpt-4o). Lemmalog plugged in as an adapter; extraction is Claude Sonnet 4.6 (ingestion is architectural, like Memory-R1's local model), chunked, file-cached. Published numbers are F1 on their leaderboard.

System              F1 (answer tokens)
PropMem (pub)        0.550   (23.1M all-phase)
SimpleMem (pub)      0.480   (20.8M all-phase)
lemmalog             0.487 ± 0.011 (3 runs)  (500K answer-phase)
OpenClaw (pub)       0.244   ( 0.7M)
fullcontext (ours)   0.197   (10.6M)
fullcontext (pub)    0.222   (10.6M)

Binary accuracy 0.566 ± 0.009 (3 runs, gpt-4o judge). Per-category (single run, F1 / accuracy):

Single-Session User         0.919 / 0.941
Knowledge Update            0.497 ± 0.003 / 0.706
Single-Session Assistant    0.649 / 0.882
Temporal Reasoning          0.410 / 0.412
Multi-Session               0.365 / 0.412
Single-Session Preference   0.116 / 0.235

Reading it honestly:

LoCoMo: the second standardized benchmark (10 conversations, 1,986 questions)

Same harness, LoCoMo's standardized gpt-4.1-mini reader. F1 vs. their published leaderboard:

Rank  System          F1      Tokens (all-phase)
 1    PropMem (pub)   0.605     5.9M
 —    lemmalog        0.573 ± 0.002 (3 runs)  11.3M
 2    OpenClaw (pub)  0.557    16.4M
 3    FullCtx (pub)   0.542    37.5M
 4    Hindsight(pub)  0.489    24.2M
 5    Graphiti (pub)  0.416     5.1M
 6    Memory-R1(pub)  0.389     3.4M
 7    SimpleMem(pub)  0.358    11.4M

2nd of 10 — ahead of OpenClaw, full-context, Hindsight, Graphiti, Memory-R1, SimpleMem, Mem0, and MemU; behind only PropMem. Run-to-run σ is 0.002 (three full 1,986-question runs). The journey from 0.483

lemmalog (JordyZomer/lemmalog)

Searching Database…

这是什么?一个面向 LLM 智能体记忆的 Datalog 引擎:它不是在向量存储的基础上'记得更好',而是把记忆变成一个演绎数据库——带来源追踪的事实、分层规则、增量推导。以 Rust crate、MCP 服务器、REPL 和智能体技能的形式发布。
那它到底是一个库、一个服务器,还是一个命令行工具?三层都是:一个以 AgentMemory 门面封装的 Rust 库,一个向 Claude Code 或 Kimi CLI 暴露 12 个工具的 MCP 服务器,还有一个终端 REPL。另外还附带一个通用的智能体技能,适合长时间运行的任务。
我怎么把它跑起来?用 cargo 克隆并构建:'cargo run --bin lemmalog' 会打开 REPL。要在智能体中使用,需用 --features mcp 构建,在 Claude Code 或 Kimi CLI 里注册该服务器(传入 --env KEY=VALUE 以启用持久化),并按所用 CLI 的方式安装附带的技能。
它需要调用什么大模型吗,还是能独立运行?引擎本身不需要任何 API 密钥——推导过程是完全确定性的。LLM 只在数据写入时派上用场:由你的宿主智能体(Claude/Kimi)通过 MCP 写入三元组;测试用的 MockExtractor 也有提供,基准测试是用 Claude Opus 4.8 跑的。
那有什么坑?在查询语法中,首字母大写的裸单词是变量,所以实体名必须加引号:reportsto('Alice', Y)。另外 MCP 服务器和 CLI 各自持有独立的进程内副本——只能从其中一侧写入,否则更新可能产生竞态。
它能在我的电脑上运行吗?靠什么承载?它是一个从源码构建的 Rust crate 和命令行工具;仓库标注支持 Windows、macOS 和 Linux。作为服务器时,它可以接入各类 MCP 宿主——README 介绍了 Claude Code 和 Kimi CLI 的用法,还提供了一个供子智能体和 cron 脚本使用的无界面 CLI。
为什么用 Datalog 引擎,而不是直接用向量存储?作者的核心理念:记忆应该是一个可验证的演绎数据库,而不是模糊的相似度匹配——规则以机械方式推导出闭包、时序视图和矛盾候选,每条事实都保留指向原始对话片段的来源。在 LongMemEval 上,这种记忆模式在知识更新类问题上超过了原始转录基线,0.80 对 0.57——转录式回答在这些场景下会过时。
智能体在每轮对话中实际怎么使用它?先观察、后查询:宿主模型通过 lemmalogobserve 写入三元组,然后直接读取推导出的视图,而不用重新推理——用 lemmalogquery 或 askdeep 做精确查询,用 lemmalogcontext 获取有据可依的上下文,在信任一条事实前用 why() 查看证明树,用 whatif 做支持字节级完全回滚的假设推演。快照可以跨会话持久保存。
这些事实的'双时间轴'是什么意思?每条事实都带有 validfrom/validto/assertedat 列以及一个 now() 参考点,因此引擎能把'某件事何时为真'和'它何时被记录'区分开来。规则可以推导出时间上的投影和排序——比如 happenedbefore 就是基于对所述日期的比较规则得出的。
它现在已经能用于生产环境了吗?项目还很年轻(2026 年 8 月,338 颗星),但纪律性出奇地好:设计文档里维护着一份坦诚的状态日志,除了 leapfrog triejoin 和 DBSP 流式增量(已标记为未来阶段)之外,列出的功能全部已实现。作者还提示基准测试数字存在约 ±0.3 的 F1 运行间波动,所以对各种比较结论要保持谨慎。
Creator
JordyZomer
Size
0.3 MB
Stars
340
Updated
CommitOct 01
Age
1mo
Commits · 90 Days