LMM — Living Memory Model
A verifiable memory layer for language models. Facts live in a graph, every claim carries its source, and an answer the memory does not support cannot leave the system.
📚 Documentation — concepts, guides, the full API surface, and every measurement with its receipts.
📄 Scaling Memory Instead of Scaling Parameters — why this exists, measured against GraphRAG, and the two bugs I found in my own scorer.
In an LLM, knowledge is frozen into weights at training time. In LMM, knowledge lives in a memory you can write to, inspect and audit — which is what makes "I don't know" trustworthy rather than polite.
pip install living-memory-model # the import is `lmm`
from lmm import Memory
m = Memory("mind.lmm") # persistent; Memory() is transient
m.learn("manual.pdf") # pdf · docx · xlsx · csv · md · txt · or text
print(m.ask("what is the screen's diagonal?"))
m.save()
That is the whole API. One method reads every format, and the file extension picks the adapter.
How it works
The gate is drawn twice on purpose. On the way in it decides what may become a record; on the way out it re-reads the sentence the engine produced and drops any claim the memory does not support. Everything else in this README — the abstention rate, the audit trail, the cost — follows from those two edges.
Two details worth naming. A spreadsheet reaches the graph with no model call at all, because a table already states its own structure; that is why a sheet loads in milliseconds. And the engine is drawn smaller than the memory because it is smaller: it phrases, it does not know.
Why not embedding RAG
| Embedding RAG | LMM | |
|---|---|---|
| Ingestion | chunks + embeddings | facts into a graph + sentences into an evidence index |
| Retrieval | similarity gamble | deterministic word/graph lookup, explainable |
| Multi-hop | fails when chunks don't co-retrieve | derives new facts symbolically (µs, no model call) |
| Fabrication | a plea in the prompt | structural gate: unsupported claims cannot leave |
| Provenance | none | every fact carries its source; uncertain answers are flagged |
| Tables | flattened into text | rows go straight into the graph — zero model calls |
| A 350-page manual | seconds of embedding, lossy retrieval | queryable in ~2 s |
The core install has no dependencies at all. The graph, the gate, the trust ordering and the derivation are pure python — no model, no network, no GPU.
Measured
Same questions to both sides. RAG baseline: LangChain + multilingual embeddings
- Chroma + gpt-4o-mini, best-practice grounding prompt. LMM's engine here is the same gpt-4o-mini, so the column compares architectures, not model sizes. LMM figures are medians of three samples — these benchmarks are noisy and a single run is not a measurement.
| Benchmark | RAG | LMM |
|---|---|---|
| Fictional corpus, 17 q (no parametric leakage possible) | 14/17 | 17/17 |
| A strategy document, 16 q | 13/16 | 14/16 |
| A 350-page device manual, 30 q | 14/30 | 27/30 — ingested in 2.3 s vs 9.2 s |
| An inspection spreadsheet, 15 q | 15/15 | 14/15 — ingested in 86 ms vs 4.7 s |
| Wrong facts asserted, all runs | 1 | 0 |
The bigger the document, the wider the gap: retrieval collapse is RAG's structural ceiling, composition is LMM's structural strength.
Four corpora, one code path
The table above is one document at a time. These are the sets the answer path is developed against — three runs each, medians, same engine:
| Corpus | factual | comparisons | frontier* | traps (fabrication) |
|---|---|---|---|---|
| 62 sibling training outlines, 73 q (customer's, not redistributable) | 40/40 | 15/15 | 7/8 | 10/10 |
| 12 hardware specifications, 19 q — a corpus the system had never seen, no adaptation | 8/8 | 4/4 | 3/3 | 4/4 |
| 1,000 generated specifications, 50 q (questions derived from the corpus, not written by hand) | 25/25 | — | — | 25/25 |
| NIST SP 800-63B (public PDF), 13 q · fictional EN corpus, 17 q | 11/13 · 17/17 | — | — | — |
Every category above is a median of three runs with zero flips — no question changed its verdict between runs — except the single frontier miss.
* frontier: questions whose discriminating word the documents never write ("the priciest" against a column headed LIST PRICE).
Against three memory frameworks, same corpus, same engine
LangChain (RAG), LlamaIndex and Mem0 answering the same questions over
the same 62 documents, each with Azure gpt-4o-mini behind it, two
repeats:
| factual | comparisons | frontier | traps | wrong facts asserted | p50 latency | ingestion | |
|---|---|---|---|---|---|---|---|
| LangChain | 21/40 | 14/15 | 4/8 | 9/10 | 1 | 1.9 s | 75.6 s |
| LlamaIndex | 29/40 | 13/15 | 6/8 | 9/10 | 1 | 3.4 s | 12.9 s |
| Mem0 | 25/40 | 15/15 | 5/8 | 8/10 | 2 | 2.2 s | 161 s |
| LMM | 40/40 | 15/15 | 7/8 | 10/10 | 0 | 5.9 s | 2.1 s |
The shape of that table is the design: LMM is ahead on every accuracy column and alone at zero fabrications, loads a corpus in seconds rather than minutes, and pays for its verification in per-question latency — three model calls to answer, three more to audit the answer, where a RAG pipeline makes one and asserts whatever comes back. The questions it can answer from a record cost nothing at all (0.01 s), which is what pulled the median down from 11.2 s.
Speed. A question that names a document and one of the corpus's own field heads is answered from the record itself — no model call, no composition, the document's line and its stamp: 0.01 s where the same question used to take 10.4 s and twelve calls. It fires on 33 of the 40 factual questions above and on none of the comparisons, frontier questions or traps, which is the shape it should have. Questions that need the engine still cost what verification costs: 8 s for a comparison, 7 for an honest refusal.
Scale, measured on the 1,000-document store (25k sentences): ingestion 2.2 s, 82 MB resident, retrieval 121 ms median with the named document's line recalled 50/50. Conversation depth: the same twelve questions scored 11/12 asked to a fresh session and 11/12 asked after twenty-four turns of other documents, pointers and topic switches — the same single failure in both.
Only the invented corpus is in this repository. The other three documents belong
to their owners — a manufacturer's product manual, an institution's internal
strategy document, an inspection report naming real people — and are not ours to
redistribute. benchmarks/corpus*.txt is a fictional world written so that no
answer can come from what a model already knows.
These numbers are lower than this file used to show, and that is the point. An audit removed everything that had been fitted to the documents being scored: few-shot examples quoting the manual's own voltages, a prompt clause teaching the exact distinction one question turned on, and thresholds (a 700-character window, a 40-character cell, a 0.75 similarity) chosen while reading one PDF. The honest cost was 62/63 → 59/63, of which the prompt content alone was 4 points — that was the share of the score that came from having seen the test. Removing the last Turkish from the runtime cost one point more.
Against GraphRAG, measured
Beating chunk-and-embed is the obvious comparison. The one that matters is
against graph-structured memory, so this runs Microsoft's own package —
pip install graphrag, its indexer, its prompts, its defaults, local search —
on the same corpora, with the same engine, scored by the same side-blind
evaluator. Nothing is reimplemented, so nothing here is a strawman.
benchmarks/graphrag_side.py.
| Same engine (gpt-4o-mini), 3 samples each | LMM | GraphRAG |
|---|---|---|
| Fictional corpus (1.6 KB), 17 q | 17/17 · spread 0 | 16/17 · spread 0 |
| NIST SP 800-63B (45k tokens), 13 q | 11/13 · spread 0 | 7/13 · spread 2 |
The small corpus nearly saturates both. The real standard separates them, and the shape of the losses matters more than the totals.
GraphRAG loses specific values. Three of its misses have the answer sitting in the document while the reply talks around it — "verifiers should establish minimum length requirements … to enhance security" where the answer is 64, which never appears. That is its indexer working as designed: entity descriptions are summarised, and a summary abstracts the number away. It also asserted one confident wrong citation, OMB Memorandum M-04-04 where the document says Circular A-130.
LMM's two misses are refusals. Both are reworded questions whose answer is in the document and was not retrieved; both came back "I do not know" rather than approximated. Retrieval recall on synonyms is the open work item — and nothing was fabricated to cover it. The offline expansion channel was built for exactly this class and has since been measured against it: it does not close these two. See Honest limits for what was measured and why.
Cost, on the 45k-token standard:
| LMM | GraphRAG | |
|---|---|---|
| Ingestion | 0 model calls (shallow — the mode a large document is for) | 220 calls · 498,230 prompt tokens |
| Per question | ~5 calls | 2 calls · 10,132 prompt tokens |
One correction this measurement forced on an earlier claim: "5× the model calls" was against vanilla RAG at one call per question. Against the real competitor at two, it is about 1.3×, and on the small corpus the per-question prompt token counts are nearly level (3,840 vs 3,306). Ingestion runs the other way in graph mode — 20× the calls — which is what per-fact extraction through the gate costs against batch extraction.
Two scorer bugs were found while running this, both against the baseline, both fixed. The abstention fallback was a Turkish-only phrase list, so GraphRAG's honest English refusals scored as fabrications (13/17 → 16/17 once fixed). And a token echoed from the question — "…required by the GDPR" — was read as a claimed designation, so refusing an absence question scored as inventing an answer. Handing points back to a competitor is not generosity; a benchmark that takes them by accident is not evidence.
Language independence, measured
No hand-written sentences, no word lists, no per-language rules anywhere in the pipeline. That used to be an argument about the code; every benchmark was Turkish, so nothing tested it.
corpus_en.txt and corpus_es.txt are the fictional corpus line for line — the
same facts in the same order, the same 17 questions, the same gold keys
translated. The invented names (zerbalit, vorlin, norgul, Nortlann) are
identical in all three, so no answer in any language can come from a model's own
knowledge. Only the language moves.
| The same world, the same 17 questions | median of 3 | samples |
|---|---|---|
| Turkish | 17/17 | 17 · 17 · 17 |
| English | 16/17 | 15 · 16 · 16 |
| Spanish | 15/17 | 15 · 15 · 15 |
The measuring tool was the language-bound part. Scoring an abstention meant
recognising "I don't know" — a phrase list, in one language. Against an English
document every honest refusal would have scored as a fabrication. So the
judgement moved inside: a turn now records whether it asserted anything,
decided by re-extracting the claims from what was spoken. explain=True
surfaces that stamp.
The two-point gap is one defect. Every remaining loss in English and Spanish is a refusal that keeps talking — "Vorlin is not a recognized substance", which the corpus flatly contradicts. Retrieval, derivation and the gates behave identically in three languages (the graph ingests 61–62 facts and derives 8 in each); the engine's stylistic habit of explaining its refusals varies, and this system's scoring is strict enough to charge it.
Cost, measured
Accuracy is only half of a comparison. The other half is what it costs, so the
same two architectures were run again with a counter wrapped around the model
clients — real usage tokens from the server, median of three repeats, both
corpora. benchmarks/COST.md has the full tables; the
short version is not flattering to us:
| Per question, TR corpus | RAG | LMM deep=True |
|---|---|---|
| model calls | 1.0 | 5.5 |
| prompt tokens | 593 | 3,944 |
| wall clock | 1.6 s | 7.5 s |
| Ingestion (once) | 0 API tokens (local embedding, ~0.2 s CPU) | 42,449 + 1,174 tokens — or 0, with deep=False |
| Dependencies on disk | 114 packages, 1.35 GB + 458 MB weights | 1 package, 1.1 MB |
On a hosted per-token engine, embedding RAG is still cheaper than LMM and there is no break-even — roughly 5-6x the calls and 6-7x the prompt tokens per question, and the gap widens with every question asked. Most of the remaining cost is the verification read-back: the gate re-extracting the claims out of a sentence before it may leave. That is fabrication-0 being paid for in tokens.
What the tokens buy is the other column of the table above — provenance, the refusal guarantee, symbolic multi-hop derivation, and a core with no dependencies. And on the local engine this project is actually built for, the dollar figure is zero and the cost is your own CPU seconds instead.
Zero-call answers — questions the graph answers with no model call at all —
were 0 of 17 at every commit until lmm/lookup.py. The architecture always
permitted a derived fact or a table row to be spoken as it stands; nothing in
the code ever took that path, because respond always spent a call turning a
held triple into a sentence and more calls reading that sentence back.
lookup answers the question directly from the graph — the record, not a
sentence — for the two shapes it can settle without guessing: a value named on
a relation the graph has closed under derivation, or a field with one value
left in it. Measured: 3 of 17 questions, in TR and in EN alike — 18% —
now cost nothing at all, and they are exactly the multi-hop questions
("is vorlin a liquid", answered from the derived vorlin -[type]-> liquid).
Everything the graph cannot settle safely still falls through to the paid
path unchanged; see benchmarks/COST.md §3 for what that guarantee costs.
On a document whose facts are a TABLE, that floor is most of the questions.
A spreadsheet's row label is usually several words — united states, new hampshire — and a scan of the question one word at a time could never name
one, so those questions went to the engine. Naming a node by the question's own
contiguous word runs settles them from the graph instead. Measured on the US
Census spreadsheet fetch_documents.py pulls down, 10 questions, 3 samples,
median (benchmarks/COST.md §8): 5 correct → 9 correct, 0 zero-call → 6,
6.1 calls per question → 2.7, with zero wrong answers in every arm. The
three invented corpora do not move — their subjects are single invented words,
so there is nothing multi-word to name — and they lose nothing either.
A row is only reachable by the name a question calls it. That sheet nests
its rows by drawing the indentation with a character — .Alabama,
.Puerto Rico, because a cell has no margin — and every nested row was
invisible to the name anyone would use for it. The plain name is now an alias
of the row: Unicode's category decides what is layout, and the cell keeps its
own text, so nothing the document wrote is edited. Same protocol, 5 documents
(benchmarks/COST.md §8.4): 48/50 → 49/50 correct, 15 → 16 answered with no
model call, 0 wrong answers, the four other documents unmoved. The question
that moved had been refused at every commit in this history and is now free.
A question with four right answers gets four right answers. That sheet's
header occupies two rows, and reading only one of them overwrote three of its
four population columns out of existence — silently. Reading the block (the
sheet's own merges say where the header ends) keeps them, and then a question
naming no year names four columns equally well; the answer path used to pick
one and say nothing. It no longer chooses: a reading that names several records
is answered with all of them, each under the full field name the SHEET wrote,
which is where the year is. Four census questions that name a year were added
first, because the old set never asked for a column the single-row reader
destroys (benchmarks/COST.md §8.4.1, four arms, 5 documents × 3 samples):
census 10/14 with 3 wrong answers → 14/14 with none, 6 → 10 of them free,
field total 53/54 correct, 0 wrong, 19 zero-call, the four other documents
unmoved. Both halves ship on; either can be switched off
(LMM_XLSX_HEADER_BLOCK=0, LMM_LOOKUP_CANDIDATES=0).
And the same question over a memory that has not moved is free. Asking the same ten questions a second time costs 0.0 calls and 0.0 seconds on both field documents, every answer byte-identical. On the invented corpora it is 0.9 calls per question rather than 0.0, because two of the ten sit inside the research-offer flow and a replayed answer would leave the offer standing — so those two bypass the cache on purpose.
That floor is a property of ingestion, not only of lookup. The same
corpus ingested by a weak local 3B model derived zero facts instead of
Azure's 8, so lookup had nothing to answer from. It never guessed from the
noisy graph it was given; it declined every time, correctly. The path itself
spends no token once the graph exists — whether the graph closes at all still
depends on what read it in.
Two defects stand between that engine and the floor, and neither is
sufficient alone (benchmarks/COST.md §7.1, measured by replaying the local
engine's cached readings through the real ingestion): its extractor names whole
clauses as subjects, and — the larger one — its causal classifier answers YES
for 41 of 61 sentences of this corpus, including plain is-a sentences the
prompt's own examples cover, where gpt-4o-mini gets every one right. A
sentence read as causal is written under #causes and never reaches the
taxonomy, so the taxonomy is split across two predicates and no triangle can
close, however clean the subjects are. A subject-normalization pass was built,
measured against a PERFECT-normalization ceiling, found to buy nothing on
either side of that split, and reverted. The remaining work is extraction
quality on the local engine, not the wiring behind it.
Both of those were then attacked directly, and both attempts were reverted
too (benchmarks/COST.md §7.1.1). llama.cpp can constrain its DECODER with a
grammar, so a per-sentence GBNF was built in which a subject can only be a
contiguous run of that sentence's own words and never the whole sentence — the
wrong answer made unsayable rather than asked against — and beside it a
sentence began to be written BOTH ways, as a causal edge and as a triple, so
the taxonomy stopped being split. The engine answered by saying the longest
thing still allowed (kelvit bir metaldir → kelvit bir). It is the first
local run in which a predicate ever became transitive, and it derived nothing
all the same, because the triangles that qualified it are triangles the corpus
states outright. The 61 cached readings are kept beside the section so the next
attempt starts from the measurement.
Architecture
┌──────────── the GATE decides everything ────────────┐
user/message → extract (engine) → gate.admit → GRAPH (facts, sources,
trust, contradictions,
document → learn_text ──────→ evidence index transitive derivation)
spreadsheet → learn_rows ──────→ graph directly (no model calls)
│
question → lookup ──────────→ THE RECORD, spoken as it stands
(graph only) no engine, no tokens, no wobble
│ not settleable
↓
graph lookup + evidence retrieval → answer (engine)
│
coverage gate → digit discipline → support check → verify
(an answer whose claims are not in the given evidence DROPS)
- The engine cannot write records and cannot speak unsupported facts. It produces candidates; the gate admits, the gate releases. Every other property here is downstream of that one sentence.
- Two speeds. Symbolic reasoning — derivation, causality, contradiction — runs in microseconds on the graph. The engine is only the language I/O.
- The graph answers first, where it can settle the question by itself. If
the question NAMES a value on a relation the graph has closed under its own
derivation ("is vorlin a liquid"), or names a field with one value in it, the
answer is that record — returned before the extractor is reached, with no
model call anywhere in the path and therefore nothing for a model to invent.
Everything else falls through unchanged. It declines far more often than it
answers, on purpose: nothing structural separates a question naming no field
from one naming a field the graph has never heard of, so both are paid for.
A node whose name is SEVERAL words — a spreadsheet's row label, a country, a
spec table's field — is named by the question's own contiguous word runs, so
united statesis one name and not two misses. - The same question over a memory that has not moved is not paid for twice.
The answer is kept and re-spoken; teach the memory anything and it is dropped
rather than repeated. What counts as "moved" is not a record count — it is
every mutation the graph has, including a second source reinforcing a fact
and a
sleep()fading one, because a stale answer is worse than an expensive one.Memory(..., cache=False)turns it off. - A document is two materials. Tables state their own structure and go straight to the graph with no model call; prose goes to the evidence index, where ranges and qualifications that don't fit a triple survive verbatim.
- Growth without retraining. Knowledge goes to the graph, never to weights. A periodic LoRA pass refreshes behaviour from gate-approved logs — never from the model's own raw output.
Full description: docs/architecture.md.
Install
pip install living-memory-model # core: pure python, zero dependencies
pip install 'living-memory-model[pdf]' # pdfplumber + pypdf
pip install 'living-memory-model[xlsx]' # pandas + openpyxl
pip install 'living-memory-model[docx]' # python-docx
pip install 'living-memory-model[local]' # torch + transformers + peft (local engine)
pip install 'living-memory-model[gguf]' # llama.cpp, CPU
pip install 'living-memory-model[openai]' # OpenAI, OpenRouter, Groq, vLLM, Ollama
pip install 'living-memory-model[azure]' # Azure OpenAI
Extras are opt-in and lazy: nothing is imported until you hand LMM a file of that kind, and a missing reader reports the exact install line rather than a traceback.
Bring your own engine
LMM does not ship a model and does not want to pick one for you. It needs an engine to turn a retrieved record into a sentence, and any of these will do:
# nothing set — a local Qwen2.5-3B-Instruct, on your own machine
export LMM_MODEL_PATH=/path/to/any-instruct-model # ...or any HF model you have
# llama.cpp / int4 — the fast option on cheap hardware
export LMM_BACKEND=gguf
# OpenAI
export LMM_BACKEND=openai
export OPENAI_API_KEY=sk-...
export LMM_MODEL=gpt-4o-mini
# OpenRouter, Groq, Together, a vLLM or Ollama server — same client, one URL
export LMM_BACKEND=openai
export OPENAI_BASE_URL=https://openrouter.ai/api/v1
export OPENAI_API_KEY=...
export LMM_MODEL=meta-llama/llama-3.3-70b-instruct
# Azure OpenAI — its own client, because a deployment name is Azure's shape
export LMM_BACKEND=azure
Nothing about the architecture changes with the engine, and that is the point. The graph, the gate and the derivation are plain python; they never call the engine and never learn which one is running. What the engine decides is how a record is worded — not whether it may be spoken. So a weaker engine costs you fluency, never provenance: an unsupported claim is dropped by the same rule whichever model produced it, and the calls that read, derive and verify are free on every one of these.
The gguf engine offloads to the accelerator when the installed llama.cpp
build has one (it asks the library, not the platform); set
LMM_N_GPU_LAYERS=0 to force the CPU, or a smaller number when the model does
not fit in VRAM. Measured on an Apple M5, same call: 1.8-2.3 s on the CPU
against 0.6-0.7 s offloaded (benchmarks/COST.md §7.2).
Using it
from lmm import Memory
m = Memory("mind.lmm")
m.learn("spec.pdf") # tables → graph, prose → evidence
m.learn("inspection.xlsx") # rows → graph, milliseconds
m.learn("report.docx")
m.learn("The probe was built on Nortlann in 2031.") # plain text works too
answer = m.ask("where was the probe built?", explain=True)
print(answer) # it IS the answer string
print(answer.abstained) # did this turn assert anything?
print(answer.sources) # the stamps it rests on
m.save()
learn() returns a small report (.facts, .tables, .adapter, .source).
The operator's knobs
Everything a RAG system prompt bundles into one block is a named parameter here, and each travels exactly where it belongs — none of them can reach the verifiers, because a judge whose thermostat the caller can turn is not a judge:
m = Memory("mind.lmm",
persona="You are Ada, a warm onboarding coach. Ask before you advise.",
style="One section per course: name, duration, audience, outcomes.",
warmth=0.6, # temperature of the VOICE surfaces only
reply_tokens=1200) # length cap of the voice surfaces only
personais the voice. It rides in front of the phrasing prompts — chat, answer, refusal — and stops at the document's edge: the composer never sees it (measured: persona-flavoured rephrasing dies at the verbatim gate).styleis the document's shape — the persona's mirror twin. It travels ONLY to the composer's request: sections, headings, ordering are the operator's; every fact beneath them still has to be attested by the material.warmth/reply_tokenstune temperature and length of the same voice surfaces. Unset, every surface keeps its measured default. The classifiers and the gate keep their pinned settings regardless.
Serving it to more than one person
A memory that lives in one process is a library. lmm.serve is the same
three verbs over HTTP, with one memory per user:
python -m lmm.serve --root ./stores --port 8000 # localhost by default
curl -s localhost:8000/learn -d '{"user":"ada","text":"Ada leads R&D."}'
curl -s localhost:8000/ask -d '{"user":"ada","question":"who leads R&D?"}'
# {"answer": "...", "abstained": false, "sources": ["#doc:team.txt"], ...}
It is a transport, not a second brain: no gate, no rephrasing, no fallback
text is added, so the abstention and the source stamps cross the wire
intact and an HTTP caller can verify what an in-process caller can. Every
user's store is chosen by their id and nothing else, no route reads across
users, and an id that could climb a path is refused before it becomes a
filename. Each memory holds its own lock — one writer at a time per user,
different users in parallel. Set LMM_TOKEN to require a bearer token.
TypeScript callers get the same object rather than a bare string
(sdk/typescript, no dependencies):
const m = new LMM({ base: "http://localhost:8000", user: "ada" });
const a = await m.ask("who leads R&D?");
if (a.abstained) console.log("memory does not hold this");
else console.log(a.answer, a.sources);
Inside a LangChain application
from lmm.adapters.langchain import retriever, tool
chain_retriever = retriever(m) # passages, each stamped with its document
agent_tool = tool(m) # an AUDITED answer, with its sources
The retriever is ordinary retrieval — the gate is not in play, and what
the caller's own model then says about those passages carries the caller's
own guarantees. The tool is the one worth having: the agent receives the
answer that passed the gate, and receives the refusal as itself, because
an agent handed an empty string writes its own answer over the silence.
LangChain is not a dependency; without langchain_core installed the same
calls return duck-typed equivalents.
A conversation that may not teach
m.session.respond(msg, teach=False) is the consultation surface — a
customer-facing bot that listens, remembers the conversation, and delivers,
without ever letting a visitor write into the operator's memory:
- a statement ("we are a bank, team of ten") is CONTEXT — it routes to the chat voice and its terms accumulate in the consultation's brief;
- a question searches with the whole consultation riding along, so "the best three-hour material" still knows the topic named two turns earlier;
- a delivery request ("you decide, put a programme together") goes straight to the composer, which returns a source-stamped catalogue built from the brief — and echoing the user's own words back is conversation, not assertion, so the gate lets a consultant sound like one.
Pass on_line= to respond or compose and the draft STREAMS: each line
is judged by the composer's gate the moment its newline arrives and handed
to your callback while the engine is still writing. A line the gate refuses
is never seen — an invented number does not become printable by arriving
early. On a backend that cannot stream, the same code degrades to batch by
itself.
The whole surface
Five methods cover ingesting and asking. Everything else this README claims —
contradiction, causality, the trust ladder, the memory dynamics — lives on
m.session, which is public and is the same object Memory is built on. It is
listed here because a feature with no documented entry point is not a feature.
m.learn(what) |
teach it a file or a string | no engine for tables |
m.ask(q, explain=True) |
answer, with .sources and .abstained — cannot write memory |
engine |
m.about(label) |
the records held on a concept | no engine |
m.facts |
how many records exist | no engine |
m.save(path) |
graph and evidence, both | no engine |
m.compose(brief, topics=, on_line=) |
a structured draft from the evidence — per-topic gathering, gated line by line, streamed to on_line as lines survive, returned with its sources |
engine |
m.where(term) |
which documents mention this — names and counts, the census | no engine |
m.session.respond(msg, teach=False, on_line=) |
a conversational turn — teach=False is the consultation surface (context, brief, delivery; cannot write memory) |
engine |
m.session.learn_rows(rows) |
[{column: value}] straight to the graph |
no engine |
m.session.learn_cause(a, b) |
record that a causes b | no engine |
m.session.root_causes(x) |
walk the causal chain back | no engine |
m.session.causes_of(x) / .effects_of(x) |
one step either way | no engine |
m.session.tension() |
the contradictions it is holding | no engine |
m.session.curiosity() |
what it has been asked and cannot answer | no engine |
m.session.sleep() |
fade, reinforce, settle episodic into semantic | no engine |
m.session.verdict(old, new) |
arbitrate two rival values | engine |
A record is a core.memory.Record: .subject, .predicate, .value are
concept keys, not strings, because one spelling can be two entities and one
entity can carry labels in several languages — read the labels through
m.session.memory.identities. .sources and .trust are the provenance the
gate reads on the way out.
The column that says no engine is not a footnote. Those calls are plain python over a dict — microseconds, no network, no key — which is why a spreadsheet loads in milliseconds and why the reasoning half of this system costs nothing to run.
See examples/ — including one that runs with no engine at all.
Honest limits
Kept current, and deliberately specific.
- Answer selection can still pick a true-but-off-target sentence. The gate guarantees non-fabrication, not perfect relevance.
- Spec lines reachable only through very common words ("how many inches is the
screen", where the line reads
Screen 15.6" LCDand never says inches) are a lexical-retrieval ceiling. One such case is rescued by an append-only mechanism; the class is not closed. - A question that names PART of a column's name and nothing that tells the
columns apart ("the 2021 population estimate", where the sheet's column is
Population Estimate (as of July 1) 2021) is not answered from the graph at all — it is one reading of several and the widest reading holds one record, which this path does not speak. It goes to the engine like any other question and is answered there, at the engine's price. What is guaranteed is that the three columns the question ruled out are never spoken over it (benchmarks/COST.md§8.4.1). - The offline expansion channel does not work on a large document, and the
reason is structural. It generates the questions each line answers and
indexes them, filtering with the document: a generated query is kept when it
reaches its own line better than any other. Measured on NIST SP 800-63B, that
filter is inverted. The line reads "Memorized secrets SHALL be at least 8
characters in length"; a reader asks "what is the shortest password" —
which shares not one distinguishing word with it. A real rephrasing never
reaches its own line, so the filter keeps only the queries that COPY the
line, which are the ones that buy nothing. Handed the perfect queries by
hand, it kept zero of them.
session.expand()still exists and is off unless asked for; it is not counted as a capability here until this is solved, and solving it needs a relevance judgement rather than a lexical one. - Two benchmark questions are stable failures and are named rather than hidden: one whose answer sits in a table row sharing a single stem with the question, and one needing a heading plus a line eight sentences below it in one window.
- The engine sometimes appends an explanation to a refusal that the memory
contradicts. This is detected — the turn is scored as having asserted
something, and loses the point — but not prevented. It is the whole of the
remaining English/Spanish gap. (The other source of appended text, a second
model call after the gate, is gone: the hedge is now the stored source stamp
in brackets,
(~ #pdf:manual.pdf), and nothing is appended to a refusal at all.) - LMM does not OCR. A PDF without a text layer — what an office scanner
produces — teaches it nothing, and
learn()now says so instead of reporting a cheerful zero. Run OCR first and learn the result. - There is no HTML reader. An
.htmlfile is read as plain text and its markup lands in the evidence index;learn()warns, with the share of characters that were tags. Convert to text first. - Table columns are still flattened by the PDF reader: a cell can reach the index next to a neighbouring column's value, and no gate can reject that, because the claim really is in the evidence. Rows are repaired; columns are not.
- A spreadsheet whose header spans more than one row loses the unnamed columns.
- An engine call is bounded by
LMM_TIMEOUT(90 s by default; 0 removes the limit) on the API backend, which raisesEngineTimeoutrather than waiting out a quota. The local engine's generation loop is not interruptible, so the budget does not cover it. - The coverage gate cannot distinguish negation affixes at word level (language lists are forbidden by design); the engine-level support check covers most of that class.
- Pre-1.0. The API surface may still move.
Contributing
Please read CONTRIBUTING.md — it leads with the two rules
that make this project what it is: no document-specific constants and no
hand-written language rules. Both are stated with what enforcing them cost.
python3.11 tests/test_core.py # 63 tests · no model · no GPU · no network
Every test is a pathology that was measured on this code, written back as an assertion so it cannot return.
Licence and attribution
Apache 2.0 — see LICENSE and NOTICE.
LMM is not a language model trained from scratch. It is a memory and reasoning layer on top of one. The default engine is Qwen2.5-3B-Instruct (Apache 2.0, Alibaba Cloud), downloaded by the user from its own source; any LoRA adapter this project trains is a derivative of it. What is original here is the layer — the graph, the gate, the evidence index and the adapters. The base model is credited, never hidden.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file living_memory_model-0.3.6.tar.gz.
File metadata
- Download URL: living_memory_model-0.3.6.tar.gz
- Upload date:
- Size: 380.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d41aad2f1c2cefe8a3aff5b7de5b97c34551be446024b0be6f09d1bff772f214
|
|
| MD5 |
ed0fba30a5a21856ed2778e9973fa551
|
|
| BLAKE2b-256 |
6ce8d06fa1b43c4b677b023177a1720726f7893da1bc875da5cc89578d178964
|
File details
Details for the file living_memory_model-0.3.6-py3-none-any.whl.
File metadata
- Download URL: living_memory_model-0.3.6-py3-none-any.whl
- Upload date:
- Size: 287.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ac55717be6a4280eda57221824cf1d93bd22489408855ac98a082b2dc456e61
|
|
| MD5 |
8923a6f61632e4452d09885252c1d4f0
|
|
| BLAKE2b-256 |
dc9e8b6a4399f18250c5381f2f03e6489bedaf45488f6e3663439f98d9f07da8
|