Store a procedure
Keep a procedure so a situation recalls it — no name to look up, no key to remember — and so several instances collapse into one vector that handles a case none of them covered. The mechanism is in Mnemonics; this page is the recipe.
import numpy as np
D = 1024 # slot budget is ~sqrt(D/32) -- 5 slots here. See step 6.
def normalize(v):
n = np.linalg.norm(v)
return v if n < 1e-12 else v / n
# Circular convolution and correlation: the engine's /vec/bind and
# /vec/unbind in numpy, so this encoding is byte-compatible with the store.
def bind(a, b):
return normalize(np.fft.irfft(np.fft.rfft(a) * np.fft.rfft(b), n=D))
def unbind(c, key):
return np.fft.irfft(np.fft.rfft(c) * np.conj(np.fft.rfft(key)), n=D)
def sym(name):
# A deterministic unit vector per symbol. Unrelated names land
# near-orthogonal, which keeps the sums below separable at read time.
import hashlib
seed = int.from_bytes(hashlib.sha256(name.encode()).digest()[:8], "big")
return normalize(np.random.default_rng(seed % (2 ** 63)).standard_normal(D))
1. Encode situation and procedure as role-filler bundles
Both sides must be structured: a single opaque embedding of the whole task has no separable parts, so nothing cancels in step 3 and nothing derives in step 4. Bind each fact to its role, each command to the step it occupies.
ROLES = ("installer", "tester", "separator", "lang")
ROLE = {k: sym("role:" + k) for k in ROLES}
SLOT = [sym("slot:%d" % i) for i in range(8)]
def situation(facts):
# The task's discriminative facts, each in its own role. Facts the
# procedure never uses (lang) are harmless noise; no filtering needed.
return normalize(sum(bind(ROLE[k], sym(v)) for k, v in facts.items()))
def procedure(steps):
# The commands, each bound to its step index: order is carried by the slot
# key, not by the position of anything in memory.
return normalize(sum(bind(SLOT[i], sym(s)) for i, s in enumerate(steps)))
def eco(inst, test, sep, lang):
return dict(installer=inst, tester=test, separator=sep, lang=lang)
def steps_of(f):
# An instance's steps are its own commands -- why they cancel.
return [f["installer"], f["tester"], f["separator"]]
rust = eco("cargo add", "cargo test", "@", "rust")
s_rust, p_rust = situation(rust), procedure(steps_of(rust))
2. Store one instance, recalled by situation
Write the relation unbind(p_rust, s_rust) into a collection whose address is
the situation, via
/db/{db}/collections/procedures/write. Recall then works
by describing the task, never by naming the procedure. One instance already
reproduces its own procedure — and answers only tasks shaped like itself.
3. Derive the general form from several instances
Sum the relations. Each instance's own tokens cancel inside its own relation, so what accumulates is the wiring the instances share.
def relation(facts, steps):
# Divide the procedure by the situation. A token on both sides (the
# installer is in the task AND in the command) cancels here, leaving
# "which role feeds which slot" with no value attached.
return unbind(procedure(steps), situation(facts))
def mnemonic(instances):
# The general form: one vector for the whole family.
return normalize(sum(relation(f, s) for f, s in instances))
FAM = [eco("cargo add", "cargo test", "@", "rust"),
eco("pip install", "pytest", "==", "python"),
eco("npm install", "npm test", "@", "node"),
eco("go get", "go test", "@", "go")]
M = mnemonic([(f, steps_of(f)) for f in FAM])
Sixteen instances is where the curve flattens: recovery over a 4-slot schema runs 49% (2 instances), 74% (4), 92% (8), 97% (16), 98% (64). More never costs accuracy, so there is no reason to prune a family.
4. Apply it in a context it was not built from
Bind the general form onto a new situation and clean up each slot. The new situation's own tokens flow into the slots.
ruby = eco("bundle add", "rspec", ",", "ruby")
derived = bind(M, situation(ruby)) # a noisy procedure vector
# Cleanup codebook: every command token, including ruby's unseen ones.
labels = sorted({v for f in FAM + [ruby] for v in f.values()})
codebook = np.stack([sym(l) for l in labels])
def read_slot(proc_vec, i, codebook, labels):
# Unbind the slot key, snap to the nearest known token. This is the
# cleanup /read performs; done locally it also exposes the score.
noisy = normalize(unbind(proc_vec, SLOT[i]))
sims = codebook @ noisy
j = int(np.argmax(sims))
return labels[j], float(sims[j])
# Held out of M's construction, every one of ruby's commands is recovered:
# whole-procedure recovery is 100% over 240 held-out members (40 seeds), and
# 0% for the nearest stored instance -- some other ecosystem's commands.
Where the codebook is a collection rather than an array, post
unbind(derived, SLOT[i]) to read with
"strategy": "fast" — a single step keeps the distance step 5 needs.
5. Gate the result on confidence
A derived procedure is worth as much as the match that produced it. A situation from outside the family still returns something; what says whether to run it is the first-contact similarity between the slot query and what the read landed on.
def confident(proc_vec, i, codebook, labels, tau):
# Below tau the slot is unresolved, and one unresolved slot abstains the
# whole procedure: a half-derived recipe is worse than none.
label, score = read_slot(proc_vec, i, codebook, labels)
return (label, score) if score >= tau else (None, score)
Derive tau from a held-out sample rather than choosing it — the procedure,
the statistic, and the coverage/error trade are in
Calibrate a gate. Two of its rules apply directly:
gate on the first-contact similarity, not a post-iteration one, and
re-derive the threshold when the codebook grows.
6. Budget the slots before the family
The bound is the number of steps in the procedure, not the number of
instances. Recovery over a 16-instance family at D = 1024: 100% at 2–3 slots,
94% at 4, 22% at 6, 0% at 8. Dimension moves the wall — the same 8-slot schema
reads 0% at 1024, 15% at 2048, 96% at 4096, 100% at 8192 — because a schema
fits when n_slots ≲ √(D/32). A longer recipe has two remedies: a larger
dimension, or a chain of shorter mnemonics, which
holds to depth 8 with a cleanup read between hops (100% at every depth
measured) against 38% at depth 2 chained naked.
7. Add procedures continually
New procedures accumulate; they do not overwrite. Three cases:
A new family. The write routes on the situation, so a task unlike anything stored matches no existing address and starts its own family. Same-family situations are similar (0.49) where cross-family ones are not (−0.02) — so the routing needs no labels.
A new member of an existing family. Route by situation, add the relation into the counter at that address. The mnemonic sharpens in place, with no rebuild and no effect on the other families: independent counters are why a ten-way stream costs nothing on the classes already learned (91.3% all-seen after ten rounds, 100% retention of the first).
A procedure that is now wrong. Contradiction is the case addition cannot
handle: adding A → C on top of A → B leaves both, and the read returns
their blend (48% correct inversion). Write the residual instead —
bind(A, C − read(A)) — which cancels the stale term and installs the new
one (100% inversion, 100% retention of everything not contradicted).
Two checks. Self-regeneration — ask the mnemonic for the procedures it was built from; full recovery means the instances are redundant and can be dropped (100% for families up to 128 members, a 128:1 compression), and a drop is the signal to split the family or raise the dimension. The cache baseline — score nearest-stored-instance recall on the same held-out cases; a mnemonic that does not beat it has no cancelling fillers in its encoding.
Related
- Mnemonics — why cancellation leaves the rule · Calibrate a gate — the threshold in step 5 · Build a world model — the same write over transitions
- Build an agent memory · Choose a dimension · Compose memories · bind · unbind · online learning