hrr-memory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # hrr-memory
2
+
3
+ **Your AI agent already has RAG. Now give it instant fact recall.**
4
+
5
+ hrr-memory stores structured facts as `(subject, relation, object)` triples and retrieves them in under 2 milliseconds — no vector database, no embeddings API, no dependencies. It complements RAG, not replaces it.
6
+
7
+ <p align="center">
8
+ <img src="assets/hrr-diagram.svg" alt="How HRR Memory Works" width="800" />
9
+ </p>
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install github:Joncik91/hrr-memory
15
+ ```
16
+
17
+ ## 30-Second Demo
18
+
19
+ ```js
20
+ import { HRRMemory } from 'hrr-memory';
21
+
22
+ const mem = new HRRMemory();
23
+
24
+ mem.store('alice', 'lives_in', 'paris');
25
+ mem.store('alice', 'works_at', 'acme');
26
+ mem.store('bob', 'lives_in', 'tokyo');
27
+
28
+ mem.query('alice', 'lives_in'); // → { match: 'paris', confident: true }
29
+ mem.query('bob', 'lives_in'); // → { match: 'tokyo', confident: true }
30
+
31
+ mem.querySubject('alice');
32
+ // → [{ relation: 'lives_in', object: 'paris' },
33
+ // { relation: 'works_at', object: 'acme' }]
34
+
35
+ mem.save('memory.json');
36
+ ```
37
+
38
+ ## Why Not Just RAG?
39
+
40
+ | Query | RAG | hrr-memory |
41
+ |-------|-----|------------|
42
+ | "What is Alice's timezone?" | Returns paragraphs, maybe | Returns `cet` in <1ms |
43
+ | "Find notes about deployment" | Ranked chunks | Can't — not a triple |
44
+
45
+ Use both. HRR for structured facts, RAG for semantic search. [See the architecture guide →](docs/architecture.md)
46
+
47
+ ## Performance
48
+
49
+ | Facts | Accuracy | Query | RAM |
50
+ |-------|----------|-------|-----|
51
+ | 100 | 100% | <1ms | 0.1 MB |
52
+ | 1,000 | 100% | 1.5ms | 4 MB |
53
+ | 10,000 | 100% | 1.8ms | 86 MB |
54
+
55
+ ## Documentation
56
+
57
+ - **[Getting Started](docs/getting-started.md)** — installation, first facts, persistence
58
+ - **[API Reference](docs/api.md)** — every method, parameter, and return type
59
+ - **[Architecture](docs/architecture.md)** — how HRR works, auto-sharding, RAG integration
60
+ - **[Performance](docs/performance.md)** — benchmarks, scaling limits, tuning
61
+
62
+ ## License
63
+
64
+ MIT
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "hrr-memory",
3
+ "version": "0.1.0",
4
+ "description": "Holographic Reduced Representations for structured agent memory. Zero dependencies. Complements RAG with algebraic fact queries.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src/"
12
+ ],
13
+ "scripts": {
14
+ "test": "node --test test/",
15
+ "bench": "node bench/benchmark.js"
16
+ },
17
+ "keywords": [
18
+ "hrr",
19
+ "holographic",
20
+ "memory",
21
+ "agent",
22
+ "ai",
23
+ "rag",
24
+ "vector",
25
+ "structured-memory",
26
+ "fact-store",
27
+ "llm"
28
+ ],
29
+ "author": "Jounes",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/joncik/hrr-memory"
34
+ }
35
+ }
package/src/bucket.js ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bucket — a single HRR memory vector with fixed capacity.
3
+ * Auto-splits are managed by HRRMemory, not by the bucket itself.
4
+ */
5
+
6
+ export const MAX_BUCKET_SIZE = 25;
7
+
8
+ export class Bucket {
9
+ constructor(name, d = 2048) {
10
+ this.name = name;
11
+ this.d = d;
12
+ this.memory = new Float32Array(d);
13
+ this.count = 0;
14
+ this.triples = [];
15
+ }
16
+
17
+ /** Add a pre-computed association vector */
18
+ storeVector(association, triple) {
19
+ for (let i = 0; i < this.d; i++) this.memory[i] += association[i];
20
+ this.count++;
21
+ this.triples.push(triple);
22
+ }
23
+
24
+ /** Whether the bucket has reached max capacity */
25
+ get isFull() { return this.count >= MAX_BUCKET_SIZE; }
26
+
27
+ /** Serialize */
28
+ toJSON() {
29
+ return {
30
+ name: this.name,
31
+ memory: Array.from(this.memory),
32
+ count: this.count,
33
+ triples: this.triples,
34
+ };
35
+ }
36
+
37
+ /** Deserialize */
38
+ static fromJSON(data, d) {
39
+ const b = new Bucket(data.name, d);
40
+ b.memory = new Float32Array(data.memory);
41
+ b.count = data.count || 0;
42
+ b.triples = data.triples || [];
43
+ return b;
44
+ }
45
+ }
package/src/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * hrr-memory — Holographic Reduced Representations for structured agent memory.
3
+ *
4
+ * Zero dependencies. Pure JS. Float32 storage. Auto-sharding.
5
+ *
6
+ * Usage:
7
+ * import { HRRMemory } from 'hrr-memory';
8
+ * const mem = new HRRMemory();
9
+ * mem.store('alice', 'lives_in', 'paris');
10
+ * mem.query('alice', 'lives_in'); // → { match: 'paris', score: 0.3, confident: true }
11
+ *
12
+ * Based on Plate (1994): "Distributed Representations and Nested Compositional Structure"
13
+ */
14
+
15
+ export { HRRMemory } from './memory.js';
16
+ export { SymbolTable } from './symbols.js';
17
+ export { Bucket } from './bucket.js';
18
+ export { bind, unbind, similarity, randomVector, normalize } from './ops.js';
package/src/memory.js ADDED
@@ -0,0 +1,258 @@
1
+ /**
2
+ * HRRMemory — auto-sharded holographic memory store.
3
+ *
4
+ * Stores (subject, relation, object) triples in sharded buckets.
5
+ * Each bucket holds max 25 facts. When full, a new overflow bucket
6
+ * is created automatically. Queries scan all buckets for a subject.
7
+ *
8
+ * Symbol vectors are shared across all buckets.
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
12
+ import { bind, unbind, similarity } from './ops.js';
13
+ import { SymbolTable } from './symbols.js';
14
+ import { Bucket, MAX_BUCKET_SIZE } from './bucket.js';
15
+
16
+ export class HRRMemory {
17
+ /**
18
+ * Create a new HRR memory store.
19
+ * @param {number} d - Vector dimensions (default 2048). Higher = more capacity per bucket.
20
+ */
21
+ constructor(d = 2048) {
22
+ this.d = d;
23
+ this.symbols = new SymbolTable(d);
24
+ this.buckets = new Map();
25
+ this.routing = new Map(); // subject → [bucket_id, ...]
26
+ }
27
+
28
+ // ── Bucket management ──────────────────────────────
29
+
30
+ /** Get the active (non-full) bucket for a subject, splitting if needed */
31
+ _activeBucket(subject) {
32
+ const key = subject.toLowerCase().trim();
33
+ const ids = this.routing.get(key);
34
+
35
+ if (ids) {
36
+ const lastId = ids[ids.length - 1];
37
+ const last = this.buckets.get(lastId);
38
+ if (!last.isFull) return last;
39
+
40
+ // Overflow: create new bucket
41
+ const newId = key + '#' + ids.length;
42
+ const nb = new Bucket(newId, this.d);
43
+ this.buckets.set(newId, nb);
44
+ ids.push(newId);
45
+ return nb;
46
+ }
47
+
48
+ // First bucket for this subject
49
+ const b = new Bucket(key, this.d);
50
+ this.buckets.set(key, b);
51
+ this.routing.set(key, [key]);
52
+ return b;
53
+ }
54
+
55
+ /** Get all buckets for a subject */
56
+ _subjectBuckets(subject) {
57
+ const ids = this.routing.get(subject.toLowerCase().trim()) || [];
58
+ return ids.map(id => this.buckets.get(id)).filter(Boolean);
59
+ }
60
+
61
+ // ── Store ──────────────────────────────────────────
62
+
63
+ /**
64
+ * Store a fact as a (subject, relation, object) triple.
65
+ * @param {string} subject - The entity (e.g., 'alice')
66
+ * @param {string} relation - The attribute (e.g., 'lives_in')
67
+ * @param {string} object - The value (e.g., 'paris')
68
+ * @returns {boolean} true if stored, false if duplicate
69
+ */
70
+ store(subject, relation, object) {
71
+ const triple = {
72
+ subject: subject.toLowerCase().trim(),
73
+ relation: relation.toLowerCase().trim(),
74
+ object: object.toLowerCase().trim(),
75
+ };
76
+
77
+ // Dedup across all subject buckets
78
+ for (const b of this._subjectBuckets(subject)) {
79
+ if (b.triples.some(t =>
80
+ t.subject === triple.subject &&
81
+ t.relation === triple.relation &&
82
+ t.object === triple.object
83
+ )) return false;
84
+ }
85
+
86
+ const s = this.symbols.get(subject);
87
+ const r = this.symbols.get(relation);
88
+ const o = this.symbols.get(object);
89
+ const association = bind(bind(s, r), o);
90
+
91
+ this._activeBucket(subject).storeVector(association, triple);
92
+ return true;
93
+ }
94
+
95
+ // ── Query ──────────────────────────────────────────
96
+
97
+ /**
98
+ * Query: given subject and relation, retrieve the object.
99
+ * @param {string} subject
100
+ * @param {string} relation
101
+ * @returns {{ match: string|null, score: number, confident: boolean, bucket: string|null }}
102
+ */
103
+ query(subject, relation) {
104
+ const buckets = this._subjectBuckets(subject);
105
+ if (buckets.length === 0) return { match: null, score: 0, confident: false, bucket: null };
106
+
107
+ const probe = bind(this.symbols.get(subject), this.symbols.get(relation));
108
+ let bestName = null, bestScore = -1, bestBucket = null;
109
+
110
+ for (const bucket of buckets) {
111
+ if (bucket.count === 0) continue;
112
+ const result = unbind(probe, bucket.memory);
113
+
114
+ // Optimized: only scan object symbols in this bucket
115
+ for (const t of bucket.triples) {
116
+ const score = similarity(result, this.symbols.get(t.object));
117
+ if (score > bestScore) {
118
+ bestScore = score;
119
+ bestName = t.object;
120
+ bestBucket = bucket.name;
121
+ }
122
+ }
123
+ }
124
+
125
+ return {
126
+ match: bestName,
127
+ score: Math.round(bestScore * 1000) / 1000,
128
+ confident: bestScore > 0.1,
129
+ bucket: bestBucket,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Get all known facts about a subject (symbolic, exact).
135
+ * @param {string} subject
136
+ * @returns {Array<{ relation: string, object: string }>}
137
+ */
138
+ querySubject(subject) {
139
+ const key = subject.toLowerCase().trim();
140
+ const facts = [];
141
+ for (const bucket of this._subjectBuckets(subject)) {
142
+ for (const t of bucket.triples) {
143
+ if (t.subject === key) facts.push({ relation: t.relation, object: t.object });
144
+ }
145
+ }
146
+ return facts;
147
+ }
148
+
149
+ /**
150
+ * Search across all buckets for triples matching a relation and/or object.
151
+ * @param {string|null} relation - Filter by relation (null = any)
152
+ * @param {string|null} object - Filter by object value (null = any)
153
+ * @returns {Array<{ subject: string, relation: string, object: string }>}
154
+ */
155
+ search(relation, object) {
156
+ const results = [];
157
+ const rel = relation ? relation.toLowerCase().trim() : null;
158
+ const obj = object ? object.toLowerCase().trim() : null;
159
+ for (const [_, bucket] of this.buckets) {
160
+ for (const t of bucket.triples) {
161
+ if (rel && t.relation !== rel) continue;
162
+ if (obj && t.object !== obj) continue;
163
+ results.push(t);
164
+ }
165
+ }
166
+ return results;
167
+ }
168
+
169
+ /**
170
+ * Free-form query: tries subject+relation, then subject lookup, then cross-bucket search.
171
+ * @param {string} question
172
+ */
173
+ ask(question) {
174
+ const parts = question.toLowerCase().trim().replace(/[?.,!]/g, '').split(/\s+/);
175
+
176
+ // Try consecutive word pairs as subject+relation
177
+ for (let i = 0; i < parts.length - 1; i++) {
178
+ const result = this.query(parts[i], parts[i + 1]);
179
+ if (result.confident) return { type: 'direct', ...result, subject: parts[i], relation: parts[i + 1] };
180
+ }
181
+
182
+ // Try each word as a subject
183
+ for (const word of parts) {
184
+ const facts = this.querySubject(word);
185
+ if (facts.length > 0) return { type: 'subject', subject: word, facts };
186
+ }
187
+
188
+ // Search across all buckets for any matching object
189
+ for (const word of parts) {
190
+ const results = this.search(null, word);
191
+ if (results.length > 0) return { type: 'search', term: word, results };
192
+ }
193
+
194
+ return { type: 'miss', query: question };
195
+ }
196
+
197
+ // ── Stats ──────────────────────────────────────────
198
+
199
+ /** Get memory statistics */
200
+ stats() {
201
+ let totalFacts = 0;
202
+ const bucketInfo = [];
203
+ for (const [_, b] of this.buckets) {
204
+ totalFacts += b.count;
205
+ bucketInfo.push({ name: b.name, facts: b.count, full: b.isFull });
206
+ }
207
+ const symBytes = this.symbols.size * this.d * 4;
208
+ const bktBytes = this.buckets.size * this.d * 4;
209
+ return {
210
+ dimensions: this.d,
211
+ maxBucketSize: MAX_BUCKET_SIZE,
212
+ symbols: this.symbols.size,
213
+ buckets: this.buckets.size,
214
+ subjects: this.routing.size,
215
+ totalFacts,
216
+ ramBytes: symBytes + bktBytes,
217
+ ramMB: Math.round((symBytes + bktBytes) / 1024 / 1024 * 10) / 10,
218
+ perBucket: bucketInfo,
219
+ };
220
+ }
221
+
222
+ // ── Persistence ────────────────────────────────────
223
+
224
+ /** Serialize to JSON */
225
+ toJSON() {
226
+ const buckets = {};
227
+ for (const [k, v] of this.buckets) buckets[k] = v.toJSON();
228
+ const routing = {};
229
+ for (const [k, v] of this.routing) routing[k] = v;
230
+ return { version: 3, d: this.d, symbols: this.symbols.toJSON(), buckets, routing };
231
+ }
232
+
233
+ /** Deserialize from JSON */
234
+ static fromJSON(data) {
235
+ const d = data.d || 2048;
236
+ const mem = new HRRMemory(d);
237
+ mem.symbols = SymbolTable.fromJSON(data.symbols || {}, d);
238
+ for (const [k, v] of Object.entries(data.buckets || {})) {
239
+ mem.buckets.set(k, Bucket.fromJSON(v, d));
240
+ }
241
+ for (const [k, v] of Object.entries(data.routing || {})) {
242
+ mem.routing.set(k, Array.isArray(v) ? v : [v]);
243
+ }
244
+ return mem;
245
+ }
246
+
247
+ /** Save to a JSON file */
248
+ save(filePath) {
249
+ writeFileSync(filePath, JSON.stringify(this.toJSON()));
250
+ }
251
+
252
+ /** Load from a JSON file (returns new empty store if file doesn't exist) */
253
+ static load(filePath, d = 2048) {
254
+ if (!existsSync(filePath)) return new HRRMemory(d);
255
+ try { return HRRMemory.fromJSON(JSON.parse(readFileSync(filePath, 'utf8'))); }
256
+ catch { return new HRRMemory(d); }
257
+ }
258
+ }
package/src/ops.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Core HRR operations — circular convolution, correlation, similarity.
3
+ * All vectors are Float32Array. FFT computed in Float64 for precision.
4
+ */
5
+
6
+ /** Generate a random unit vector of dimension d (Gaussian, normalized) */
7
+ export function randomVector(d) {
8
+ const v = new Float32Array(d);
9
+ for (let i = 0; i < d; i += 2) {
10
+ const u1 = Math.random(), u2 = Math.random();
11
+ const r = Math.sqrt(-2 * Math.log(u1));
12
+ v[i] = r * Math.cos(2 * Math.PI * u2);
13
+ if (i + 1 < d) v[i + 1] = r * Math.sin(2 * Math.PI * u2);
14
+ }
15
+ return normalize(v);
16
+ }
17
+
18
+ /** Normalize vector to unit length */
19
+ export function normalize(v) {
20
+ let norm = 0;
21
+ for (let i = 0; i < v.length; i++) norm += v[i] * v[i];
22
+ norm = Math.sqrt(norm);
23
+ if (norm === 0) return v;
24
+ const out = new Float32Array(v.length);
25
+ for (let i = 0; i < v.length; i++) out[i] = v[i] / norm;
26
+ return out;
27
+ }
28
+
29
+ /** Cosine similarity between two vectors */
30
+ export function similarity(a, b) {
31
+ let dot = 0, na = 0, nb = 0;
32
+ for (let i = 0; i < a.length; i++) {
33
+ dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
34
+ }
35
+ return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-10);
36
+ }
37
+
38
+ // ── FFT (Cooley-Tukey radix-2) ──
39
+
40
+ function fft(re, im, inverse) {
41
+ const n = re.length;
42
+ for (let i = 1, j = 0; i < n; i++) {
43
+ let bit = n >> 1;
44
+ for (; j & bit; bit >>= 1) j ^= bit;
45
+ j ^= bit;
46
+ if (i < j) {
47
+ [re[i], re[j]] = [re[j], re[i]];
48
+ [im[i], im[j]] = [im[j], im[i]];
49
+ }
50
+ }
51
+ for (let len = 2; len <= n; len <<= 1) {
52
+ const ang = (inverse ? -1 : 1) * 2 * Math.PI / len;
53
+ const wRe = Math.cos(ang), wIm = Math.sin(ang);
54
+ for (let i = 0; i < n; i += len) {
55
+ let curRe = 1, curIm = 0;
56
+ for (let j = 0; j < len / 2; j++) {
57
+ const uRe = re[i+j], uIm = im[i+j];
58
+ const vRe = re[i+j+len/2]*curRe - im[i+j+len/2]*curIm;
59
+ const vIm = re[i+j+len/2]*curIm + im[i+j+len/2]*curRe;
60
+ re[i+j] = uRe+vRe; im[i+j] = uIm+vIm;
61
+ re[i+j+len/2] = uRe-vRe; im[i+j+len/2] = uIm-vIm;
62
+ const nr = curRe*wRe - curIm*wIm;
63
+ curIm = curRe*wIm + curIm*wRe;
64
+ curRe = nr;
65
+ }
66
+ }
67
+ }
68
+ if (inverse) {
69
+ for (let i = 0; i < n; i++) { re[i] /= n; im[i] /= n; }
70
+ }
71
+ }
72
+
73
+ function nextPow2(n) { let p = 1; while (p < n) p <<= 1; return p; }
74
+
75
+ /**
76
+ * Circular convolution (binding): a ⊛ b
77
+ * Creates an association between two vectors.
78
+ * Computed in Float64, returned as Float32.
79
+ */
80
+ export function bind(a, b) {
81
+ const d = a.length, n = nextPow2(d);
82
+ const aR = new Float64Array(n), aI = new Float64Array(n);
83
+ const bR = new Float64Array(n), bI = new Float64Array(n);
84
+ for (let i = 0; i < d; i++) { aR[i] = a[i]; bR[i] = b[i]; }
85
+ fft(aR, aI, false); fft(bR, bI, false);
86
+ const cR = new Float64Array(n), cI = new Float64Array(n);
87
+ for (let i = 0; i < n; i++) {
88
+ cR[i] = aR[i]*bR[i] - aI[i]*bI[i];
89
+ cI[i] = aR[i]*bI[i] + aI[i]*bR[i];
90
+ }
91
+ fft(cR, cI, true);
92
+ const out = new Float32Array(d);
93
+ for (let i = 0; i < d; i++) out[i] = cR[i];
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * Circular correlation (unbinding): retrieve from association.
99
+ * unbind(key, memory) ≈ the value that was bound with key.
100
+ */
101
+ export function unbind(key, memory) {
102
+ const d = key.length;
103
+ const inv = new Float32Array(d);
104
+ inv[0] = key[0];
105
+ for (let i = 1; i < d; i++) inv[i] = key[d - i];
106
+ return bind(inv, memory);
107
+ }
package/src/symbols.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared symbol table — maps string names to random vectors.
3
+ * Shared across all buckets to save memory.
4
+ */
5
+
6
+ import { randomVector, similarity } from './ops.js';
7
+
8
+ export class SymbolTable {
9
+ constructor(d = 2048) {
10
+ this.d = d;
11
+ this.symbols = new Map();
12
+ }
13
+
14
+ /** Get or create a vector for a symbol name */
15
+ get(name) {
16
+ const key = name.toLowerCase().trim();
17
+ if (!this.symbols.has(key)) this.symbols.set(key, randomVector(this.d));
18
+ return this.symbols.get(key);
19
+ }
20
+
21
+ /** Check if a symbol exists */
22
+ has(name) { return this.symbols.has(name.toLowerCase().trim()); }
23
+
24
+ /** Number of symbols */
25
+ get size() { return this.symbols.size; }
26
+
27
+ /** Memory footprint in bytes */
28
+ get bytes() { return this.symbols.size * this.d * 4; }
29
+
30
+ /** Find the nearest symbol(s) to a result vector */
31
+ nearest(vec, candidates = null, topK = 1) {
32
+ const source = candidates || this.symbols;
33
+ const results = [];
34
+ for (const [name, svec] of source) {
35
+ results.push({ name, score: similarity(vec, svec) });
36
+ }
37
+ results.sort((a, b) => b.score - a.score);
38
+ return topK === 1 ? results[0] || null : results.slice(0, topK);
39
+ }
40
+
41
+ /** Serialize to plain object */
42
+ toJSON() {
43
+ const out = {};
44
+ for (const [k, v] of this.symbols) out[k] = Array.from(v);
45
+ return out;
46
+ }
47
+
48
+ /** Deserialize from plain object */
49
+ static fromJSON(data, d) {
50
+ const st = new SymbolTable(d);
51
+ for (const [k, v] of Object.entries(data)) {
52
+ st.symbols.set(k, new Float32Array(v));
53
+ }
54
+ return st;
55
+ }
56
+ }