fastmemory 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Richard Anaya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ # Memory Research: How We Built a Zero-Cost Memory Judge
2
+
3
+ ## The Problem
4
+
5
+ When an AI agent talks to a user, thousands of messages fly by. Most are throwaway -- "thanks!", "how's it going?", "the build is failing." But some are gold: "I hate modals", "my API key is sk-abc123", "always use TypeScript."
6
+
7
+ We needed a way to automatically decide: **should this sentence be saved as a permanent memory?** And we wanted to do it with zero API calls, zero cost, fully offline.
8
+
9
+ ## The Approach: Embeddings as a Judge
10
+
11
+ We used a local embedding model (BGE-large, 1024 dimensions) that converts text into arrays of numbers. Similar sentences end up as similar arrays. The idea: compare incoming text against "prototype" sentences that represent what memorable content looks like, and see how close they are.
12
+
13
+ Think of it like sorting mail. You have example letters for "important" and "junk." When a new letter arrives, you check which pile it looks more like.
14
+
15
+ ## Round 1: Single-Sided Prototypes (Failed)
16
+
17
+ We started with 5 prototype sentences describing memorable content:
18
+
19
+ - "strong user preference or explicit dislike"
20
+ - "important long-term fact the user wants remembered"
21
+ - etc.
22
+
23
+ We scored each incoming sentence by how similar it was to the closest prototype. If the score was above a threshold, memorize it.
24
+
25
+ **Result: 61% accuracy.** Basically random.
26
+
27
+ **Why it failed:** The positive and negative examples had *completely overlapping* scores. "I like this song" scored 0.773 (should skip) while "User prefers bullet points" scored 0.783 (should memorize). The model sees both as "preferences" -- it can't tell that one is fleeting and the other is permanent. The average score for things we should memorize (0.692) was actually *lower* than the average for things we should skip (0.696).
28
+
29
+ ## Round 2: Better Prototypes (Helped Some)
30
+
31
+ We tried making prototypes more concrete -- instead of abstract descriptions, we used example-like sentences:
32
+
33
+ - "remember this forever: user always wants TypeScript and hates modals"
34
+ - "critical secret: API key sk-abc123 must never be shared"
35
+
36
+ **Result: 71.4% accuracy, F1=0.746.** Better, but still 12 false positives. Casual sentences like "I like this song" and "This chat is going well" kept slipping through because they're topically similar to real preferences.
37
+
38
+ ## Round 3: The Breakthrough -- Dual Prototypes
39
+
40
+ The key insight: instead of asking "does this look memorable?", ask **"does this look MORE memorable than throwaway?"**
41
+
42
+ We added a second set of *negative* prototypes describing junk content:
43
+
44
+ - "casual small talk about weather food feelings and daily life"
45
+ - "momentary reaction: laughing agreeing feeling tired having lunch"
46
+
47
+ For each sentence, we compute:
48
+ ```
49
+ gap = (similarity to best positive prototype) - (similarity to best negative prototype)
50
+ ```
51
+
52
+ If the gap is positive, the sentence is closer to "memorable" than "throwaway." If negative, it's closer to junk.
53
+
54
+ **Result on original 49 examples: 95.9% accuracy, F1=0.960.** Only 1 miss and 1 false positive. The gap metric completely eliminated the overlap problem.
55
+
56
+ ## Round 4: Reality Check -- Expanded Testing
57
+
58
+ We generated 160 new test sentences the model had never seen, covering:
59
+ - Preferences, personal facts, security, rules, lessons, config (should memorize)
60
+ - Chitchat, ephemeral events, general knowledge, questions, narration, opinions, emotions (should skip)
61
+ - Tricky edge cases in both directions
62
+
63
+ **Result with original prototypes: 51.2% accuracy.** The original 4 negative prototypes only covered "casual/feelings/weather." They didn't cover questions, general tech knowledge, status updates, or opinions -- which are the bulk of real agent conversations.
64
+
65
+ ## Round 5: Scaling Up Prototypes
66
+
67
+ We expanded to 8 positive and 11 negative prototypes, covering all the failure categories:
68
+
69
+ **Positive (8 prototypes):**
70
+ - Permanent tool/framework preferences
71
+ - Personal identity (name, birthday, disability, pronouns)
72
+ - Permanent project rules (always/never)
73
+ - Lessons from real experience
74
+ - Persistent project config
75
+ - Explicit "remember this" requests
76
+ - Work schedule and accessibility needs
77
+ - Casually mentioned personal facts
78
+
79
+ **Negative (11 prototypes):**
80
+ - Casual chat and greetings
81
+ - Ephemeral events (builds, deploys, current bugs)
82
+ - General tech knowledge (what React is, how HTTP works)
83
+ - Questions and help requests
84
+ - Status narration (meetings, sprints, progress)
85
+ - Opinions about external tech
86
+ - Emotional reactions
87
+ - Plus 4 concrete example-style negatives for extra coverage
88
+
89
+ We also switched from `max(neg)` to `avg(top-2 neg)` for the negative score, which improved precision because a single outlier negative match is less likely to wrongly suppress a real memory.
90
+
91
+ **Final result on 209 examples: 79.4% accuracy, F1=0.757, Precision=0.84, Recall=0.69.**
92
+
93
+ ## What We Tried That Didn't Help
94
+
95
+ - **Using actual positive examples as prototypes** -- Counterintuitively, this was terrible (69.4% accuracy). The prototypes need to be *general archetypes*, not specific examples, because specific examples are too close to specific negatives.
96
+ - **More scoring strategies** (avg top-2 pos, avg top-3 neg, etc.) -- All landed in the same ~76-80% band. The ceiling is set by the prototypes and the embedding model, not the math.
97
+ - **Very long, kitchen-sink prototypes** -- Stuffing everything into one mega-prototype diluted the signal. 5-8 focused prototypes per side worked best.
98
+
99
+ ## The Fundamental Limitation
100
+
101
+ Embeddings measure **topical similarity**, not **intent to persist**. These two sentences are nearly identical in embedding space:
102
+
103
+ - "Always add error boundaries around React components" (should memorize -- it's a rule)
104
+ - "React is a library for building user interfaces" (should skip -- it's general knowledge)
105
+
106
+ Both are about React. Both are technical. The difference is whether it's *a rule this specific user set* vs. *a fact anyone could Google*. That distinction requires understanding intent, which is beyond what cosine similarity can capture.
107
+
108
+ ## Final Takeaways
109
+
110
+ 1. **Dual prototypes (positive + negative) are far superior to single-sided.** The gap metric eliminates the score overlap problem that makes single-sided approaches nearly random.
111
+
112
+ 2. **~80% accuracy is the practical ceiling for embedding-only judges on diverse content.** Good enough as a zero-cost first filter. For the remaining ~20% ambiguous cases, you'd need a small LLM or fine-tuned classifier.
113
+
114
+ 3. **Prototypes should be archetypal descriptions, not literal examples.** "permanent project rule: always do X and never do Y" works better than "never use any in TypeScript."
115
+
116
+ 4. **Negative prototypes need to cover your actual content distribution.** The original 4 negatives (weather, feelings, news, opinions) missed entire categories (questions, general knowledge, narration) that dominate real conversations.
117
+
118
+ 5. **Precision vs. recall is tunable via a single threshold.** At gap >= 0.009, you get 84% precision / 69% recall. Lower the threshold for more recall (catch more, tolerate more noise). Raise it for precision (less noise, miss more).
119
+
120
+ 6. **The approach costs literally nothing at runtime.** No API calls, no tokens, no network. One local embedding model handles both storage and judging. Sub-10ms per decision after model warmup.
121
+
122
+ ## Configuration
123
+
124
+ ```typescript
125
+ // In agent-memory.ts, the final tuned values:
126
+ gapThreshold = 0.009 // memory importance cutoff
127
+ noveltyThreshold = 0.87 // deduplication cutoff
128
+
129
+ // 8 positive prototypes + 11 negative prototypes
130
+ // Scoring: max(pos_similarity) - avg(top_2_neg_similarities)
131
+ ```
package/README.md ADDED
@@ -0,0 +1,369 @@
1
+ # FastMemory
2
+
3
+ Zero-cost local semantic memory for AI agents. Hybrid search (BM25 + vector + fusion), importance detection, and deduplication -- all running 100% offline with Bun's native SQLite.
4
+
5
+ ## The Problem
6
+
7
+ Every AI agent conversation generates thousands of messages. The vast majority are ephemeral:
8
+ - "thanks!"
9
+ - "the build is failing on CI"
10
+ - "let me check something real quick"
11
+ - "React is a library for building UIs" (general knowledge)
12
+
13
+ But buried in the noise are permanent, valuable facts:
14
+ - "I hate modals, always use dark mode"
15
+ - "My API key is sk-abc123, never share it"
16
+ - "Always validate user input before processing"
17
+
18
+ **The challenge:** How do we automatically distinguish "remember forever" from "ignore immediately"? And how do we do it without:
19
+ - Costly API calls to judge every message
20
+ - Complex LLM chains that add latency
21
+ - Cloud dependencies that break offline usage
22
+
23
+ ## The Solution: Embeddings as Judge
24
+
25
+ We use a local embedding model (BGE-large, 1024 dimensions) that converts text into arrays of numbers. Similar sentences end up as similar arrays.
26
+
27
+ But here's the key innovation: **we don't just ask "is this memorable?" -- we ask "is this MORE memorable than throwaway?"**
28
+
29
+ ### Dual-Prototype Approach
30
+
31
+ We maintain two sets of prototype sentences:
32
+
33
+ **Positive prototypes** (what memorable content looks like):
34
+ - Permanent preferences ("hates modals, prefers TypeScript")
35
+ - Personal facts (name, birthday, disabilities)
36
+ - Project rules ("always validate, never deploy Fridays")
37
+ - Lessons learned ("SQLite VACUUM locks the database")
38
+ - Persistent config (ports, registries, CI pipelines)
39
+
40
+ **Negative prototypes** (what junk looks like):
41
+ - Casual chat ("thanks!", "got it", "let me think")
42
+ - Ephemeral events ("build failing", "tests passing", "deploying now")
43
+ - General knowledge ("React is a library", "HTTP 404 means not found")
44
+ - Questions ("how do I set up nginx?")
45
+ - Status narration ("working on payment feature", "had sprint planning")
46
+ - Opinions ("Rust is overhyped", "that talk was great")
47
+
48
+ For each incoming sentence, we compute:
49
+ ```
50
+ gap = max_similarity(positive) - avg(top_2_similarities(negative))
51
+ ```
52
+
53
+ If `gap > threshold`, the sentence is closer to "memorable" than "throwaway" -- save it. Otherwise, ignore.
54
+
55
+ ## Why Not a Micro LLM?
56
+
57
+ We considered using a tiny LLM (like Phi-3 or Gemma 2B) as the judge. Why didn't we?
58
+
59
+ 1. **Latency:** Even small LLMs take 50-200ms per inference on CPU. Embeddings take <10ms after warmup.
60
+
61
+ 2. **Resource usage:** Running an LLM constantly in the background consumes 2-4GB RAM. FastMemory uses ~1.2GB for the embedding model once, then operates in <100MB.
62
+
63
+ 3. **Consistency:** LLMs can be flaky -- same prompt, different answers. Cosine similarity is deterministic.
64
+
65
+ 4. **Cost:** Zero. No tokens, no API calls, no rate limits.
66
+
67
+ The tradeoff is accuracy. An LLM judge would likely hit 90-95% accuracy. FastMemory hits ~80%. For most agent use cases, 80% precision (only 20% noise) with zero ongoing cost is the right tradeoff.
68
+
69
+ ## Research Findings
70
+
71
+ See [MEMORY_RESEARCH.md](./MEMORY_RESEARCH.md) for the full technical deep-dive.
72
+
73
+ **Key findings:**
74
+
75
+ 1. **Single-sided prototypes fail:** Just scoring against "memorable" prototypes gives ~61% accuracy (random). The positive and negative distributions completely overlap in embedding space.
76
+
77
+ 2. **Dual prototypes work:** The gap metric (pos - neg) eliminates the overlap problem, achieving 95.9% on curated examples and ~80% on diverse real-world content.
78
+
79
+ 3. **~80% is the practical ceiling for embeddings:** The fundamental limitation is that embeddings measure *topical similarity*, not *intent to persist*. These look nearly identical to the embedding model:
80
+ - "Always add error boundaries" (should memorize -- it's a rule)
81
+ - "React is a library" (should skip -- it's general knowledge)
82
+
83
+ 4. **Prototypes should be archetypal, not literal:** "permanent project rule: always do X" works better than "never use any in TypeScript" because it's more general.
84
+
85
+ 5. **Negative prototypes must cover your actual distribution:** The original 4 negatives (weather, feelings) missed entire categories (questions, narration, opinions) that dominate real conversations.
86
+
87
+ ## Installation
88
+
89
+ Works with Bun (recommended, native SQLite) or Node.js:
90
+
91
+ ```bash
92
+ # Bun (recommended, faster)
93
+ bun add fastmemory
94
+
95
+ # Node.js
96
+ npm install fastmemory
97
+ ```
98
+
99
+ ## Quick Start
100
+
101
+ ```typescript
102
+ import { createAgentMemory } from 'fastmemory';
103
+
104
+ // Initialize (downloads BGE-large model on first run ~1.2GB)
105
+ const store = await createAgentMemory({ dbPath: './memory.db' });
106
+
107
+ // Get the importance judge
108
+ const shouldMemorize = await store.shouldCreateMemory();
109
+
110
+ // Test if content should be saved
111
+ const worthy = await shouldMemorize("User hates modals, prefers dark mode");
112
+ console.log(worthy ? 'Save it!' : 'Ignore it.');
113
+
114
+ // Add memory with metadata
115
+ const id = await store.add(
116
+ "User hates modals, prefers dark mode",
117
+ { type: "preference", topic: "ui" }
118
+ );
119
+
120
+ // Search memories
121
+ const bm25Results = store.searchBM25("modal", 5); // Keyword search
122
+ const vectorResults = await store.searchVector("dark theme", 5); // Semantic search
123
+ const hybridResults = await store.searchHybrid("avoid popup windows", 5); // Best of both
124
+
125
+ // Check stats
126
+ console.log(store.getStats()); // { total: 42 }
127
+
128
+ // Cleanup
129
+ store.close();
130
+ ```
131
+
132
+ ## How It Works
133
+
134
+ ### Storage Layer
135
+ - **Bun native SQLite** with WAL mode for performance
136
+ - **FTS5** for BM25 keyword search
137
+ - **1024-dimension vectors** from BGE-large-en-v1.5
138
+
139
+ ### Importance Detection
140
+ 1. Embed the incoming text
141
+ 2. Compute cosine similarity to all 8 positive prototypes, take max
142
+ 3. Compute cosine similarity to all 11 negative prototypes, take top-2, average
143
+ 4. Calculate gap: `pos_max - avg(top_2_neg)`
144
+ 5. If gap > 0.009 (tuned threshold) AND no similar memory exists (novelty check), save it
145
+
146
+ ### Search
147
+ - **BM25:** Pure keyword matching via SQLite FTS5 (blazing fast)
148
+ - **Vector:** Cosine similarity against stored embeddings
149
+ - **Hybrid:** Reciprocal Rank Fusion (RRF) of both scores
150
+
151
+ ## Configuration
152
+
153
+ ```typescript
154
+ const shouldMemorize = await store.shouldCreateMemory(
155
+ gapThreshold = 0.009, // Importance cutoff (tuned default)
156
+ noveltyThreshold = 0.87 // Deduplication cutoff
157
+ );
158
+ ```
159
+
160
+ **Tuning the threshold:**
161
+ - **Lower threshold** (e.g., -0.018): Catch more memories, tolerate more noise (higher recall)
162
+ - **Higher threshold** (e.g., 0.025): Less noise, miss more memories (higher precision)
163
+
164
+ ## Examples
165
+
166
+ ### Complete Agent Integration
167
+
168
+ ```typescript
169
+ import { createAgentMemory } from 'fastmemory';
170
+
171
+ class Agent {
172
+ private memory: Awaited<ReturnType<typeof createAgentMemory>>;
173
+ private judge: Awaited<ReturnType<ReturnType<typeof createAgentMemory>['shouldCreateMemory']>>;
174
+
175
+ async init() {
176
+ this.memory = await createAgentMemory({ dbPath: './agent.db' });
177
+ this.judge = await this.memory.shouldCreateMemory();
178
+ }
179
+
180
+ async handleMessage(userMessage: string, assistantResponse: string) {
181
+ // Check if user's message contains memorable content
182
+ if (await this.judge(userMessage)) {
183
+ await this.memory.add(userMessage, {
184
+ type: 'user_fact',
185
+ timestamp: Date.now()
186
+ });
187
+ console.log('💾 Saved user fact to memory');
188
+ }
189
+
190
+ // Check if assistant's response contains a lesson worth remembering
191
+ const insight = this.extractInsight(assistantResponse);
192
+ if (insight && await this.judge(insight)) {
193
+ await this.memory.add(insight, {
194
+ type: 'lesson',
195
+ context: 'assistant_response'
196
+ });
197
+ }
198
+
199
+ // Retrieve relevant memories for context
200
+ const relevant = await this.memory.searchHybrid(userMessage, 3);
201
+ return this.buildPrompt(userMessage, relevant);
202
+ }
203
+
204
+ private extractInsight(response: string): string | null {
205
+ // Extract key lessons or facts from response
206
+ // This is app-specific - maybe use a regex or simple heuristic
207
+ const match = response.match(/Key (?:lesson|takeaway):\s*(.+)/i);
208
+ return match ? match[1] : null;
209
+ }
210
+
211
+ private buildPrompt(message: string, memories: any[]) {
212
+ const context = memories.map(m => `- ${m.content}`).join('\n');
213
+ return `Relevant memories:\n${context}\n\nUser: ${message}`;
214
+ }
215
+ }
216
+
217
+ // Usage
218
+ const agent = new Agent();
219
+ await agent.init();
220
+
221
+ await agent.handleMessage(
222
+ "I hate modals and always use dark mode",
223
+ "I'll remember that. Key lesson: users prefer inline UI elements over modal interruptions."
224
+ );
225
+ ```
226
+
227
+ ### Judge Examples: What Gets Saved vs Skipped
228
+
229
+ ```typescript
230
+ const judge = await store.shouldCreateMemory();
231
+
232
+ // ✅ These will be SAVED (high importance gap)
233
+ await judge("User prefers tabs over spaces in all codebases"); // ✓ Saved
234
+ await judge("My name is Sarah, please use it in all responses"); // ✓ Saved
235
+ await judge("Never share the API key sk-abc123 with anyone"); // ✓ Saved
236
+ await judge("Always validate user input before processing"); // ✓ Saved
237
+ await judge("Learned that batch inserts are 50x faster than individual"); // ✓ Saved
238
+ await judge("User hates popups and prefers dark mode forever"); // ✓ Saved
239
+
240
+ // ❌ These will be SKIPPED (low gap, close to throwaway)
241
+ await judge("Thanks for the help!"); // ✗ Skipped
242
+ await judge("The build is currently failing on CI"); // ✗ Skipped (ephemeral)
243
+ await judge("React is a JavaScript library for building UIs"); // ✗ Skipped (general knowledge)
244
+ await judge("How do I set up nginx as a reverse proxy?"); // ✗ Skipped (question)
245
+ await judge("I think Rust is overhyped for web dev"); // ✗ Skipped (opinion)
246
+ await judge("Working on the payment integration feature today"); // ✗ Skipped (status)
247
+
248
+ // ⚠️ These might go either way (near the threshold)
249
+ await judge("User prefers bullet points in responses"); // Sometimes saved, sometimes not
250
+ ```
251
+
252
+ ### Working with Search Results
253
+
254
+ ```typescript
255
+ // Add some sample data
256
+ await store.add("User hates modals and prefers dark mode", { type: 'preference' });
257
+ await store.add("My API key is sk-abc12345", { type: 'secret', critical: true });
258
+ await store.add("Always use TypeScript, never JavaScript", { type: 'rule' });
259
+
260
+ // BM25 search (exact keyword matching)
261
+ const bm25 = store.searchBM25("modal", 5);
262
+ // Returns: [{ content: "User hates modals...", score: -0.78, ... }]
263
+
264
+ // Vector search (semantic similarity)
265
+ const vector = await store.searchVector("avoid popup windows", 5);
266
+ // Returns: [{ content: "User hates modals...", score: 0.73, ... }]
267
+
268
+ // Hybrid search (combines both - usually best)
269
+ const hybrid = await store.searchHybrid("security best practices", 5);
270
+ // Returns combined results ranked by RRF fusion
271
+
272
+ // Working with results
273
+ for (const memory of hybrid) {
274
+ console.log(`[${memory.score?.toFixed(3)}] ${memory.content}`);
275
+ console.log(` Metadata: ${JSON.stringify(memory.metadata)}`);
276
+ }
277
+ ```
278
+
279
+ ### Testing Judge Accuracy
280
+
281
+ ```typescript
282
+ import { tuningExamples } from 'fastmemory';
283
+
284
+ async function testJudgeAccuracy() {
285
+ const judge = await store.shouldCreateMemory();
286
+
287
+ let correct = 0;
288
+ for (const example of tuningExamples) {
289
+ const result = await judge(example.content);
290
+ const isCorrect = result === example.shouldMemorize;
291
+
292
+ console.log(
293
+ `${isCorrect ? '✅' : '❌'} ${example.shouldMemorize ? 'MEMORIZE' : 'SKIP '} | ` +
294
+ `"${example.content.slice(0, 50)}..."`
295
+ );
296
+
297
+ if (isCorrect) correct++;
298
+ }
299
+
300
+ console.log(`\nAccuracy: ${(correct / tuningExamples.length * 100).toFixed(1)}%`);
301
+ // Expected: ~88% on the built-in test set
302
+ }
303
+
304
+ testJudgeAccuracy();
305
+ ```
306
+
307
+ ### Custom Threshold for Different Use Cases
308
+
309
+ ```typescript
310
+ // High-precision mode: Less noise, miss some memories
311
+ // Good for production where memory quality matters more than quantity
312
+ const strictJudge = await store.shouldCreateMemory(0.025, 0.90);
313
+
314
+ // High-recall mode: Catch more, tolerate more noise
315
+ // Good for early development or when you can't afford to miss anything
316
+ const looseJudge = await store.shouldCreateMemory(-0.018, 0.80);
317
+
318
+ // Default balanced mode
319
+ const balancedJudge = await store.shouldCreateMemory(0.009, 0.87);
320
+ ```
321
+
322
+ ## Performance
323
+
324
+ On a modern CPU after model warmup:
325
+ - Judge decision: <10ms
326
+ - BM25 search: <1ms
327
+ - Vector search (1k memories): <5ms
328
+ - Hybrid search: <10ms
329
+
330
+ ## Limitations
331
+
332
+ 1. **~80% accuracy on diverse content:** The embedding-only approach has a ceiling. For critical applications, consider a two-stage filter: FastMemory as first pass, small LLM for ambiguous cases near the threshold.
333
+
334
+ 2. **Runtime support:** Works with both Bun (native SQLite) and Node.js (better-sqlite3).
335
+
336
+ 3. **Model download:** First run downloads ~1.2GB. Cached after that.
337
+
338
+ 4. **English-optimized:** BGE-large is English-focused. Performance on other languages will vary.
339
+
340
+ 5. **Brute-force vector search:** Currently linear scan. Fast for <10k memories, but will need indexing (like sqlite-vss) for larger datasets.
341
+
342
+ ## The 80% Problem
343
+
344
+ Here's a concrete example of where FastMemory struggles:
345
+
346
+ ```
347
+ ✓ "User always adds error boundaries" (memorize -- it's a rule)
348
+ ✗ "React is a library for building UIs" (skip -- general knowledge)
349
+ ```
350
+
351
+ Both score ~0.75 similarity to positive prototypes (they're about React patterns). Both score ~0.70 to negative prototypes (they're technical). The gap is small. One is correct, one is noise. An LLM would easily distinguish them. FastMemory might flip a coin.
352
+
353
+ **Why we accept this:** In practice, the false positive rate is ~16% (84% precision). That means your memory stays 84% high-signal. That's dramatically better than dumping everything (5% signal) and costs nothing versus an LLM judge (more accurate, but costs latency, memory, and money).
354
+
355
+ ## Roadmap
356
+
357
+ - [ ] sqlite-vss integration for faster vector search at scale
358
+ - [ ] Session-based filtering ("only search last 30 days")
359
+ - [ ] Configurable prototypes for domain-specific tuning
360
+ - [ ] Optional LLM second-pass for near-threshold cases
361
+ - [ ] Export/import for backup and migration
362
+
363
+ ## License
364
+
365
+ MIT. Full source, zero dependencies on external APIs.
366
+
367
+ ---
368
+
369
+ **Built with:** Bun, Node.js, fastembed, better ideas than LangChain.
@@ -0,0 +1,26 @@
1
+ export interface MemoryEntry {
2
+ id: string;
3
+ content: string;
4
+ metadata: Record<string, any>;
5
+ createdAt: string;
6
+ score?: number;
7
+ }
8
+ export interface AgentMemoryConfig {
9
+ dbPath: string;
10
+ }
11
+ export declare function createAgentMemory(config: AgentMemoryConfig): Promise<{
12
+ add: (content: string, metadata?: Record<string, any>) => Promise<`${string}-${string}-${string}-${string}-${string}`>;
13
+ searchBM25: (query: string, limit?: number) => MemoryEntry[];
14
+ searchVector: (query: string, limit?: number) => Promise<MemoryEntry[]>;
15
+ searchHybrid: (query: string, limit?: number) => Promise<MemoryEntry[]>;
16
+ getStats: () => {
17
+ total: number;
18
+ };
19
+ close: () => void;
20
+ shouldCreateMemory: (gapThreshold?: number, noveltyThreshold?: number) => Promise<(content: string) => Promise<boolean>>;
21
+ }>;
22
+ export declare const tuningExamples: {
23
+ content: string;
24
+ shouldMemorize: boolean;
25
+ }[];
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;CAChB;AA0ID,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB;mBAqDnC,MAAM,aAAY,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;wBAqBtC,MAAM,qBAAe,WAAW,EAAE;0BAa1B,MAAM,qBAAe,OAAO,CAAC,WAAW,EAAE,CAAC;0BAkB3C,MAAM,qBAAe,OAAO,CAAC,WAAW,EAAE,CAAC;;;;;8EAkD3E,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;GAyBlD;AAGD,eAAO,MAAM,cAAc;;;GAkD1B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { FlagEmbedding, EmbeddingModel } from 'fastembed';
3
+ // Detect runtime and load appropriate SQLite implementation
4
+ async function createDatabase(dbPath) {
5
+ const isBun = typeof Bun !== 'undefined';
6
+ if (isBun) {
7
+ // Use Bun's native SQLite
8
+ const { Database } = await import('bun:sqlite');
9
+ const db = new Database(dbPath, { create: true, readwrite: true });
10
+ db.run('PRAGMA journal_mode = WAL');
11
+ return {
12
+ run: (sql, params) => db.run(sql, params || []),
13
+ query: (sql) => {
14
+ const stmt = db.query(sql);
15
+ return {
16
+ all: (...params) => stmt.all(...params),
17
+ get: (...params) => stmt.get(...params)
18
+ };
19
+ },
20
+ close: () => db.close()
21
+ };
22
+ }
23
+ else {
24
+ // Use better-sqlite3 for Node.js
25
+ const Database = (await import('better-sqlite3')).default;
26
+ const db = new Database(dbPath);
27
+ db.pragma('journal_mode = WAL');
28
+ return {
29
+ run: (sql, params) => {
30
+ const stmt = db.prepare(sql);
31
+ return params ? stmt.run(...params) : stmt.run();
32
+ },
33
+ query: (sql) => {
34
+ const stmt = db.prepare(sql);
35
+ return {
36
+ all: (...params) => stmt.all(...params),
37
+ get: (...params) => stmt.get(...params)
38
+ };
39
+ },
40
+ close: () => db.close()
41
+ };
42
+ }
43
+ }
44
+ let embedder;
45
+ async function initEmbedder() {
46
+ if (!embedder) {
47
+ embedder = await FlagEmbedding.init({
48
+ model: EmbeddingModel.BGEBaseENV15,
49
+ });
50
+ }
51
+ }
52
+ // --- Dual-prototype memory judge ---
53
+ const POSITIVE_PROTOTYPES = [
54
+ "user permanently prefers specific tools languages frameworks themes and hates specific alternatives for all future work",
55
+ "user personal identity: name birthday allergy disability pronouns timezone contact email credential",
56
+ "permanent project rule: always do X and never do Y when building deploying testing or configuring",
57
+ "lesson learned from real experience: this specific approach solved a problem that another approach caused",
58
+ "persistent project config: branch names ports registries CI pipelines that must stay consistent",
59
+ "user explicitly asked to remember this fact for all future sessions and interactions",
60
+ "user's personal work schedule availability and accessibility needs that affect every interaction",
61
+ "user casually mentioned a permanent personal fact: language fluency work hours disability diet"
62
+ ];
63
+ const NEGATIVE_PROTOTYPES = [
64
+ "casual chat: greetings thanks acknowledgments reactions feelings okay sounds good",
65
+ "ephemeral event happening right now: build failing deploying fixing pushing committing running tests",
66
+ "general tech knowledge: what a framework library protocol or language is and how it generally works",
67
+ "question asking for help: how do I, can you help, what does this mean, should I use X or Y",
68
+ "status narration: working on feature, had a meeting, team is doing X, spent yesterday on, client wants",
69
+ "opinion about external tech: looks nice, is overhyped, talk was great, article is interesting, ecosystem moves fast",
70
+ "emotional reaction to current work: frustrated, excited, love it, hate it, finally done, best code ever",
71
+ "React is a library, TypeScript adds types, Docker is portable, Node runs JS outside browser",
72
+ "the build is failing, just pushed a fix, tests passing locally, linter complaining, deploying now",
73
+ "how do I set up nginx, can you debug this, what does this error mean, should I use Map or Object",
74
+ "working on payment feature, had sprint planning, code review took long, using Figma for designs"
75
+ ];
76
+ let posProtoEmbs = null;
77
+ let negProtoEmbs = null;
78
+ async function initPrototypes() {
79
+ if (posProtoEmbs)
80
+ return;
81
+ posProtoEmbs = [];
82
+ for (const proto of POSITIVE_PROTOTYPES) {
83
+ const embs = embedder.passageEmbed([proto]);
84
+ for await (const batch of embs) {
85
+ posProtoEmbs.push(batch[0]);
86
+ break;
87
+ }
88
+ }
89
+ negProtoEmbs = [];
90
+ for (const proto of NEGATIVE_PROTOTYPES) {
91
+ const embs = embedder.passageEmbed([proto]);
92
+ for await (const batch of embs) {
93
+ negProtoEmbs.push(batch[0]);
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ function cosineSim(a, b) {
99
+ let dot = 0, na = 0, nb = 0;
100
+ for (let i = 0; i < a.length; i++) {
101
+ dot += a[i] * b[i];
102
+ na += a[i] ** 2;
103
+ nb += b[i] ** 2;
104
+ }
105
+ return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
106
+ }
107
+ /**
108
+ * Computes the importance gap: max similarity to positive prototypes
109
+ * minus average of top-2 negative prototype similarities.
110
+ *
111
+ * Using avg top-2 neg instead of max neg improves precision (fewer false
112
+ * positives) because a single high neg match can be an outlier -- averaging
113
+ * the top 2 gives a more stable estimate of "throwaway-ness".
114
+ */
115
+ async function getImportanceGap(content) {
116
+ await initPrototypes();
117
+ const qEmb = await embedder.queryEmbed(content);
118
+ const posSim = Math.max(...posProtoEmbs.map(p => cosineSim(qEmb, p)));
119
+ const negSims = negProtoEmbs.map(p => cosineSim(qEmb, p)).sort((a, b) => b - a);
120
+ const avgTop2Neg = (negSims[0] + negSims[1]) / 2;
121
+ return posSim - avgTop2Neg;
122
+ }
123
+ export async function createAgentMemory(config) {
124
+ await initEmbedder();
125
+ // Create database using appropriate adapter (Bun or Node.js)
126
+ const db = await createDatabase(config.dbPath);
127
+ // Schema + FTS5 BM25 + triggers
128
+ db.run(`
129
+ CREATE TABLE IF NOT EXISTS memories (
130
+ id TEXT PRIMARY KEY,
131
+ content TEXT NOT NULL,
132
+ metadata TEXT,
133
+ embedding TEXT,
134
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
135
+ );
136
+ `);
137
+ db.run(`
138
+ CREATE VIRTUAL TABLE IF NOT EXISTS fts_memories USING fts5(
139
+ content,
140
+ id UNINDEXED
141
+ );
142
+ `);
143
+ db.run(`
144
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
145
+ INSERT INTO fts_memories (id, content) VALUES (new.id, new.content);
146
+ END;
147
+ `);
148
+ db.run(`
149
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
150
+ DELETE FROM fts_memories WHERE id = old.id;
151
+ END;
152
+ `);
153
+ db.run(`
154
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
155
+ DELETE FROM fts_memories WHERE id = old.id;
156
+ INSERT INTO fts_memories (id, content) VALUES (new.id, new.content);
157
+ END;
158
+ `);
159
+ function hydrate(row) {
160
+ return {
161
+ id: row.id,
162
+ content: row.content,
163
+ metadata: row.metadata ? JSON.parse(row.metadata) : {},
164
+ createdAt: row.created_at,
165
+ score: row.score
166
+ };
167
+ }
168
+ async function add(content, metadata = {}) {
169
+ const id = randomUUID();
170
+ const embeddings = embedder.passageEmbed([content]);
171
+ let embedding;
172
+ for await (const batch of embeddings) {
173
+ embedding = batch[0];
174
+ break;
175
+ }
176
+ if (!embedding) {
177
+ throw new Error('Failed to generate embedding');
178
+ }
179
+ db.run(`INSERT INTO memories (id, content, metadata, embedding, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)`, [id, content, JSON.stringify(metadata), JSON.stringify(embedding)]);
180
+ return id;
181
+ }
182
+ function searchBM25(query, limit = 10) {
183
+ const stmt = db.query(`
184
+ SELECT m.*, bm25(fts_memories) AS score
185
+ FROM fts_memories
186
+ JOIN memories m ON fts_memories.id = m.id
187
+ WHERE fts_memories MATCH ?
188
+ ORDER BY score DESC
189
+ LIMIT ?
190
+ `);
191
+ const rows = stmt.all(query, limit);
192
+ return rows.map(hydrate);
193
+ }
194
+ async function searchVector(query, limit = 10) {
195
+ const qEmb = await embedder.queryEmbed(query);
196
+ const stmt = db.query('SELECT * FROM memories');
197
+ const rows = stmt.all();
198
+ const scored = rows
199
+ .map(row => {
200
+ const emb = row.embedding ? JSON.parse(row.embedding) : null;
201
+ if (!emb)
202
+ return { ...hydrate(row), score: 0 };
203
+ return { ...hydrate(row), score: cosineSim(qEmb, emb) };
204
+ })
205
+ .sort((a, b) => (b.score || 0) - (a.score || 0))
206
+ .slice(0, limit);
207
+ return scored;
208
+ }
209
+ async function searchHybrid(query, limit = 10) {
210
+ const bm25s = searchBM25(query, 30);
211
+ const vecs = await searchVector(query, 30);
212
+ const k = 60;
213
+ const scores = new Map();
214
+ bm25s.forEach((r, i) => scores.set(r.id, (scores.get(r.id) || 0) + 1 / (k + i)));
215
+ vecs.forEach((r, i) => scores.set(r.id, (scores.get(r.id) || 0) + 1 / (k + i)));
216
+ const merged = [...new Set([...bm25s, ...vecs].map(r => r.id))]
217
+ .map(id => {
218
+ const item = bm25s.find(r => r.id === id) || vecs.find(r => r.id === id);
219
+ return { ...item, score: scores.get(id) };
220
+ })
221
+ .sort((a, b) => b.score - a.score)
222
+ .slice(0, limit);
223
+ return merged;
224
+ }
225
+ function getStats() {
226
+ const stmt = db.query('SELECT COUNT(*) as total FROM memories');
227
+ const result = stmt.get();
228
+ return { total: result.total };
229
+ }
230
+ function close() {
231
+ db.close();
232
+ }
233
+ /**
234
+ * Returns a function that determines whether content should become a memory.
235
+ *
236
+ * Uses dual-prototype gap scoring: computes max cosine similarity to
237
+ * "memorable" prototypes minus max cosine similarity to "throwaway" prototypes.
238
+ * Content is memorable only if the gap exceeds the threshold AND no similar
239
+ * memory already exists (novelty check).
240
+ *
241
+ * Tuned on 209 diverse examples (97 positive, 112 negative):
242
+ * F1=0.757, Accuracy=79.4%, Precision=0.84, Recall=0.69
243
+ * gapThreshold: 0.009
244
+ * noveltyThreshold: 0.87
245
+ *
246
+ * For higher recall (catch more, tolerate more noise): gapThreshold = -0.018
247
+ * For higher precision (less noise, miss more): gapThreshold = 0.025
248
+ */
249
+ async function shouldCreateMemory(gapThreshold = 0.009, noveltyThreshold = 0.87) {
250
+ await initPrototypes();
251
+ return async (content) => {
252
+ if (content.length < 20 || content.length > 800)
253
+ return false;
254
+ const gap = await getImportanceGap(content);
255
+ if (gap < gapThreshold)
256
+ return false;
257
+ const similars = await searchVector(content, 1);
258
+ const maxSim = similars[0]?.score ?? 0;
259
+ return maxSim < noveltyThreshold;
260
+ };
261
+ }
262
+ return {
263
+ add,
264
+ searchBM25,
265
+ searchVector,
266
+ searchHybrid,
267
+ getStats,
268
+ close,
269
+ shouldCreateMemory
270
+ };
271
+ }
272
+ // Tuning dataset
273
+ export const tuningExamples = [
274
+ { content: "User explicitly hates modal popups and prefers dark mode always", shouldMemorize: true },
275
+ { content: "The sky is blue today", shouldMemorize: false },
276
+ { content: "My name is Richard Anaya, always use it in responses", shouldMemorize: true },
277
+ { content: "Meeting is at 3pm tomorrow", shouldMemorize: false },
278
+ { content: "Never share the API key sk-abc12345 with anyone", shouldMemorize: true },
279
+ { content: "LOL that joke was hilarious", shouldMemorize: false },
280
+ { content: "User is allergic to nuts, remember for all food orders", shouldMemorize: true },
281
+ { content: "What time is it right now?", shouldMemorize: false },
282
+ { content: "Always validate user input before processing in this app", shouldMemorize: true },
283
+ { content: "The new iPhone looks pretty cool", shouldMemorize: false },
284
+ { content: "User wants all code examples in TypeScript only", shouldMemorize: true },
285
+ { content: "It's raining outside in Vancouver", shouldMemorize: false },
286
+ { content: "Key lesson: use WAL mode on SQLite for this agent", shouldMemorize: true },
287
+ { content: "Haha yeah same here", shouldMemorize: false },
288
+ { content: "User prefers bullet points in every response", shouldMemorize: true },
289
+ { content: "The stock market is up 2% today", shouldMemorize: false },
290
+ { content: "Never use Tailwind for future UI projects", shouldMemorize: true },
291
+ { content: "I had coffee this morning", shouldMemorize: false },
292
+ { content: "User's birthday is June 15th, remind them", shouldMemorize: true },
293
+ { content: "This chat is going well so far", shouldMemorize: false },
294
+ { content: "Critical fact: database path must be ./agent-memory.db", shouldMemorize: true },
295
+ { content: "The cat video you sent was cute", shouldMemorize: false },
296
+ { content: "User always wants dark theme enabled by default", shouldMemorize: true },
297
+ { content: "I'm feeling tired right now", shouldMemorize: false },
298
+ { content: "Lesson learned: BM25 beats vector-only for exact keywords", shouldMemorize: true },
299
+ { content: "Pizza sounds good for lunch", shouldMemorize: false },
300
+ { content: "User hates popups and modals forever", shouldMemorize: true },
301
+ { content: "Weather forecast says sun tomorrow", shouldMemorize: false },
302
+ { content: "Remember to use BGE-large for all embeddings", shouldMemorize: true },
303
+ { content: "Yeah I agree completely", shouldMemorize: false },
304
+ { content: "User's preferred language is English with British spelling", shouldMemorize: true },
305
+ { content: "Just finished reading that article", shouldMemorize: false },
306
+ { content: "Never expose embedding vectors in logs", shouldMemorize: true },
307
+ { content: "The game last night was amazing", shouldMemorize: false },
308
+ { content: "User wants session summaries at end of every chat", shouldMemorize: true },
309
+ { content: "Random thought: birds are cool", shouldMemorize: false },
310
+ { content: "Important: threshold for novelty is now 0.87", shouldMemorize: true },
311
+ { content: "How's your day going?", shouldMemorize: false },
312
+ { content: "User prefers fastembed over any cloud provider", shouldMemorize: true },
313
+ { content: "This code runs fine on my machine", shouldMemorize: false },
314
+ { content: "Fact: cosine similarity beats dot product here", shouldMemorize: true },
315
+ { content: "Traffic is bad this morning", shouldMemorize: false },
316
+ { content: "User's favorite IDE is VS Code with specific extensions", shouldMemorize: true },
317
+ { content: "I like this song", shouldMemorize: false },
318
+ { content: "Always close DB connection after use", shouldMemorize: true },
319
+ { content: "The movie was okay", shouldMemorize: false },
320
+ { content: "User never wants emojis in professional responses", shouldMemorize: true },
321
+ { content: "Just had lunch", shouldMemorize: false },
322
+ { content: "Critical preference: hybrid search only for recall", shouldMemorize: true }
323
+ ];
324
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "fastmemory",
3
+ "version": "0.1.0",
4
+ "description": "Zero-cost local semantic memory for AI agents with hybrid search and dual-prototype importance detection",
5
+ "keywords": [
6
+ "memory",
7
+ "agent",
8
+ "ai",
9
+ "embeddings",
10
+ "sqlite",
11
+ "semantic-search",
12
+ "bm25",
13
+ "fastembed",
14
+ "local-first"
15
+ ],
16
+ "homepage": "https://github.com/richardanaya/fastmemory",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/richardanaya/fastmemory.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/richardanaya/fastmemory/issues"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Richard Anaya",
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "dependencies": {
39
+ "fastembed": "^2.1.0"
40
+ },
41
+ "optionalDependencies": {
42
+ "better-sqlite3": "^11.0.0"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "MEMORY_RESEARCH.md",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "scripts": {
51
+ "build": "tsc",
52
+ "test": "npm run build && node test-runtime.mjs && bun test-runtime.mjs"
53
+ },
54
+ "devDependencies": {
55
+ "@types/better-sqlite3": "^7.6.13",
56
+ "@types/bun": "^1.3.9",
57
+ "@types/node": "^25.2.3",
58
+ "typescript": "^5.9.3"
59
+ }
60
+ }