← All posts

The math behind a deterministic agent memory

An LLM lists what each turn mentions. After that, memory is counting: one table, a few matrix products and no learned weights.

I built N-Dimensional Memory (NDM) as an external memory for LLM agents. This post explains the math inside it. The math is counting and nothing fancier: it counts which events mention which things, then uses those counts to find connections, order history, spot unclear references and choose what to show the model. There is no neural network and no learned weights, so the same input always produces the same output.

System view

Deterministic memory engine: an LLM lists entities per turn, everything after is countingDialogue turnswho said what, whenLLM reads each turn alonelists entities (the only model call)DETERMINISTIC ENGINE · counting only, no learned weights, same input → same outputentity listsIncidence matrix Bentity × event, 0 or 1BᵀB → collisionswhich events share entitiesBBᵀ → weighted tiesshared events ÷ ln(1 + event size)Row of B → trajectoryordered history, tick + date clocksCandidate count → resolve0 unknown · 1 fill in · 2+ stay openScore events for a question3·entities + words + 0.5·speakersDepictiontop events + prior turn · date orderAnswering LLMreads a few attributed lines~570 words, not ~1,730
The language model is used at the edges only. Between extraction and answering, memory is arithmetic on one table.

The language model reads each turn on its own and lists the entities it mentions. From there on the model is not needed until the final answer. Everything in between is arithmetic on those lists.

One tiny conversation

Every step below uses the same four turns between Alice and Bob:

TurnDateSpeakerWhat was said
t01 MayAliceI started a pottery class.
t11 MayBobMy pottery teacher is Carla.
t28 MayAliceCarla said my vase was lovely.
t39 MayBobI sold my bike.

Step 1: one table

Write each entity as a row and each turn (event) as a column, with a 1 wherever the entity appears. This is the incidence matrix B, and it is the only thing the engine computes on.

B[i][k] = 1 if entity i appears in event k, otherwise 0
Entityt0t1t2t3
Alice1010
Bob0101
pottery1100
Carla0110
vase0010
bike0001

Read along a row to see where an entity shows up; read down a column to see what one event involves. Two rules keep the table trustworthy. Each event gets its own column, so facts from different turns cannot blur together. Names are normalised before they become rows, so "the vase" and "vase" share a row, while "my vase" said by Alice and "my vase" said by Bob do not.

Step 2: which events connect

Two events collide when they mention the same entity. Counting shared entities for every pair of events is one matrix product:

(BᵀB)[k][l] = number of entities that events k and l share

Speakers are excluded from this count. In a two-person chat they appear in almost every turn, so counting them would connect everything to everything. They are hubs, and hubs carry no signal.

t0t1t2t3
t0—1 (pottery)00
t11 (pottery)—1 (Carla)0
t201 (Carla)—0
t3000—

The chain t0 → t1 → t2 is one connected story: a pottery class, the teacher, the teacher praising a vase. Nobody said so in one sentence; the link comes only from shared entities. The bike (t3) connects to nothing. The math says that events connect, never what the connection means. Whether Carla also teaches Alice is not stated, so the engine does not claim it.

Step 3: how strongly two entities are tied

Flip the question: count the events two entities share. It is the same product the other way round:

(BBᵀ)[i][j] = number of events in which entities i and j both appear

Not every shared event counts equally. An event that mentions two things says a lot about how they relate; an event that mentions twenty says little about any one pair. So each shared event is weighted down by how crowded it is (Adamic–Adar weighting):

w(i, j) = Σₖ B[i][k] · B[j][k] / ln(1 + entities in event k)
Event mentionsWeight of one shared event
2 entities1 ÷ ln 3 = 0.91
3 entities (t1 and t2 here)1 ÷ ln 4 = 0.72
6 entities1 ÷ ln 7 = 0.51
20 entities1 ÷ ln 21 = 0.33

It is a similarity score, not a distance: a higher number means more closely tied in this conversation, nothing more.

Step 4: history in order, with two clocks

An entity's trajectory is its row of B read in time order. Carla's is t1 (1 May, introduced as Bob's teacher), then t2 (8 May, praised Alice's vase). Every event carries two clocks:

ClockWhat it meansAnswers
Tick (t0, t1, …)The order the engine heard things; also each event's unique ID"What did we know by then?"
DateWhen the turn was said; the anchor for words like "yesterday""When did it happen?"

The clocks can disagree. If Bob says on 9 May, "Last month I sold my bike", the event gets the newest tick (t3) but its date anchor is 9 May, so "last month" means April. This mattered in practice: an early version stored ticks but not dates and answered 1 of 7 time questions on a real benchmark conversation. With dates added it answered 5 of 5 on a new conversation. Those are small samples, so read them as a design signal, not a measurement.

Step 5: unclear words become a count

Suppose a fifth turn arrives on 10 May: Alice says, "She said it was my best work." The model reading that sentence alone cannot know who "she" is or what "it" is, so it marks both unresolved. The engine counts the candidates in the last three events:

WordLooking forCandidates (t2–t4)CountWhat happens
shea person other than the speakerCarla1Filled in as Carla with a note citing t2; a later correction can undo it
ita thingvase, bike2Stays open: memory shows “it = vase or bike?” instead of guessing

The whole rule is a count: one candidate fills it in and records the evidence; two or more keep the question open until someone says which; zero records it as unknown. A guess never becomes a fact unless the count is unambiguous or a person states the answer.

Step 6: what to show the model

When a question arrives, the engine scores every event and shows the model only the best ones, in date order, within a word budget:

score = 3 × (non-speaker entities from the question in the event) + (question words in the event) + 0.5 × (speakers from the question in the event)

For “When was Alice's vase praised?”, the vase is a non-speaker entity and Alice is a speaker:

EventVase × 3Question words foundAlice × 0.5Score
t232 (vase, Alice)0.55.5
t001 (Alice)0.51.5
t10000
t30000

Then four fixed steps: keep the top events (t2, then t0); add the turn just before each kept event, because in dialogue an answer often depends on the line before it; sort by date, label who said each line, and cut the lowest scores first if over budget; hand the result to the model.

[1 May] t0 Alice said: about Alice (themself): started a pottery class [1 May] t1 Bob said: about Carla: Bob's pottery teacher [8 May] t2 Alice said: about Carla: said Alice's vase was lovely

The model answers “8 May” from three lines instead of the whole conversation.

Step 7: measuring whether it works

Stability. The model may word things differently each time it reads the same conversation, so two runs may build different connections. Compare the sets of colliding event pairs with the Jaccard index:

J(A, B) = |A ∩ B| / |A ∪ B|

If run A finds {t0–t1, t1–t2} and run B writes “pottery class” in t0 but “pottery” in t1, the t0–t1 link disappears and run B finds only {t1–t2}. J = 1 ÷ 2 = 0.5. On a real conversation the measured value was 0.22, which is how name drift was discovered and then fixed by normalising names (Step 1).

Accuracy. Benchmark questions (LoCoMo, with answers written by other researchers) are answered three ways, and every answer is read and scored by hand: correct = 1, partly correct = 0.5, wrong = 0.

What the model readScore of 33Words per question
Full conversation261,734
All of the engine's memory274,080
Depiction (Step 6)24571

The two headline ratios are plain division: 24 ÷ 26 ≈ 92% of the accuracy, using 571 ÷ 1,734 ≈ 33% of the words.

What the math does not do

  • It does not understand language. The model reads each sentence; the math only counts what the model listed.
  • It does not guess. Connections come from shared entities, and unclear words are filled in only when exactly one candidate exists.
  • It does not learn or drift. There are no trained weights or embeddings in the engine, so the same conversation always gives the same table, the same connections and the same depiction.
  • It does not prove meaning. A connection says two events share something; what it means is left to the model reading the depiction, with every line attributed to whoever said it.

An honest caveat. At equal size this design only tied plain BM25 retrieval, so the project is documented and closed as a negative result. Its strengths showed up where retrieval is weakest, namely speaker attribution and dates. The sample sizes above are small. This post explains the mechanism, not a claim that it beats retrieval.

The method on one page

StepMathPlain meaning
1Incidence table BWhich entity appears in which event
2BᵀBWhich events share entities (collisions)
3BBᵀ, weighted by 1 ÷ ln(1 + event size)How strongly two entities are tied
4A row of B, sorted by dateAn entity's history (trajectory)
5Count candidates: 0, 1, 2+Unknown, filled in, or left open
6Score = 3·entities + words + 0.5·speakersWhat to show the model for a question
7Jaccard index; hand-scored accuracy ratiosIs it stable? Does it answer correctly?