agent-working-memory 0.7.0 → 0.7.2

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.
Files changed (40) hide show
  1. package/README.md +20 -5
  2. package/dist/adapters/common.d.ts.map +1 -1
  3. package/dist/adapters/common.js +9 -1
  4. package/dist/adapters/common.js.map +1 -1
  5. package/dist/api/routes.d.ts.map +1 -1
  6. package/dist/api/routes.js +107 -10
  7. package/dist/api/routes.js.map +1 -1
  8. package/dist/cli.js +103 -103
  9. package/dist/core/auto-tagger.d.ts +29 -0
  10. package/dist/core/auto-tagger.d.ts.map +1 -0
  11. package/dist/core/auto-tagger.js +139 -0
  12. package/dist/core/auto-tagger.js.map +1 -0
  13. package/dist/core/query-expander.d.ts.map +1 -1
  14. package/dist/core/query-expander.js.map +1 -1
  15. package/dist/core/reranker.d.ts.map +1 -1
  16. package/dist/core/reranker.js.map +1 -1
  17. package/dist/engine/consolidation.d.ts +1 -0
  18. package/dist/engine/consolidation.d.ts.map +1 -1
  19. package/dist/engine/consolidation.js +149 -9
  20. package/dist/engine/consolidation.js.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/mcp.js +114 -83
  23. package/dist/mcp.js.map +1 -1
  24. package/dist/storage/sqlite.d.ts.map +1 -1
  25. package/dist/storage/sqlite.js +6 -5
  26. package/dist/storage/sqlite.js.map +1 -1
  27. package/dist/types/engram.d.ts +1 -0
  28. package/dist/types/engram.d.ts.map +1 -1
  29. package/package.json +57 -57
  30. package/src/adapters/common.ts +9 -1
  31. package/src/api/routes.ts +723 -602
  32. package/src/cli.ts +719 -719
  33. package/src/core/auto-tagger.ts +168 -0
  34. package/src/core/query-expander.ts +0 -1
  35. package/src/core/reranker.ts +0 -1
  36. package/src/engine/consolidation.ts +165 -9
  37. package/src/index.ts +199 -199
  38. package/src/mcp.ts +1192 -1166
  39. package/src/storage/sqlite.ts +6 -5
  40. package/src/types/engram.ts +1 -0
@@ -0,0 +1,168 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Auto-Tagger — generates meta-tags for memories at write time.
5
+ *
6
+ * Meta-tags serve as categorical signals that boost BM25 recall.
7
+ * They're indexed in FTS5 alongside concept/content/tags, so queries
8
+ * that match a category tag get better BM25 scores.
9
+ *
10
+ * Three sources of tags:
11
+ * 1. Content analysis — extract topics, entities, categories from the text
12
+ * 2. Context propagation — inherit relevant tags from related memories
13
+ * 3. Type classification — fact/experience/belief/entity markers
14
+ *
15
+ * Design: lightweight heuristics, no LLM calls. Tags are additive —
16
+ * they enrich the existing tag set without replacing user-provided tags.
17
+ */
18
+
19
+ /**
20
+ * Extract meta-tags from memory content using keyword patterns.
21
+ * Returns tags prefixed with 'cat:' to distinguish from user tags.
22
+ */
23
+ export function extractMetaTags(concept: string, content: string): string[] {
24
+ const tags: string[] = [];
25
+ const text = `${concept} ${content}`.toLowerCase();
26
+
27
+ // --- Category tags (broad topic classification) ---
28
+
29
+ // People / personal
30
+ if (/\b(i |my |i'm |i've |we |our |me )\b/.test(text)) {
31
+ tags.push('cat:personal');
32
+ }
33
+
34
+ // Work / professional
35
+ if (/\b(work|job|office|meeting|project|team|manager|colleague|career|salary|hired)\b/.test(text)) {
36
+ tags.push('cat:work');
37
+ }
38
+
39
+ // Technology / computing
40
+ if (/\b(code|programming|software|database|api|server|deploy|bug|git|typescript|python|react|node)\b/.test(text)) {
41
+ tags.push('cat:tech');
42
+ }
43
+
44
+ // Health / wellness
45
+ if (/\b(health|doctor|exercise|yoga|gym|diet|sleep|meditation|therapy|medicine|symptom)\b/.test(text)) {
46
+ tags.push('cat:health');
47
+ }
48
+
49
+ // Finance / money
50
+ if (/\b(money|budget|savings|invest|salary|cost|price|payment|bank|credit|expense|coupon|store|bought|purchased)\b/.test(text)) {
51
+ tags.push('cat:finance');
52
+ }
53
+
54
+ // Home / living
55
+ if (/\b(home|house|apartment|room|kitchen|bedroom|furniture|garden|repair|renovation|neighbor|moved)\b/.test(text)) {
56
+ tags.push('cat:home');
57
+ }
58
+
59
+ // Travel / location
60
+ if (/\b(travel|trip|vacation|flight|hotel|restaurant|city|country|visited|downtown|park)\b/.test(text)) {
61
+ tags.push('cat:location');
62
+ }
63
+
64
+ // Education / learning
65
+ if (/\b(school|university|college|degree|course|class|study|learn|graduate|student|teacher|exam)\b/.test(text)) {
66
+ tags.push('cat:education');
67
+ }
68
+
69
+ // Social / relationships
70
+ if (/\b(friend|family|partner|spouse|child|parent|sibling|birthday|party|dinner|wedding|date)\b/.test(text)) {
71
+ tags.push('cat:social');
72
+ }
73
+
74
+ // Hobbies / entertainment
75
+ if (/\b(music|movie|book|game|sport|hobby|play|concert|theater|playlist|podcast|guitar|tennis|yoga|painting)\b/.test(text)) {
76
+ tags.push('cat:hobby');
77
+ }
78
+
79
+ // Shopping / consumer
80
+ if (/\b(bought|purchased|ordered|shop|store|amazon|target|walmart|coupon|sale|discount|delivery)\b/.test(text)) {
81
+ tags.push('cat:shopping');
82
+ }
83
+
84
+ // Food / cooking
85
+ if (/\b(cook|recipe|restaurant|meal|food|dinner|lunch|breakfast|coffee|tea|bake|kitchen)\b/.test(text)) {
86
+ tags.push('cat:food');
87
+ }
88
+
89
+ // Pets / animals
90
+ if (/\b(pet|dog|cat|animal|vet|shelter|walk|breed)\b/.test(text)) {
91
+ tags.push('cat:pets');
92
+ }
93
+
94
+ // Time markers
95
+ if (/\b(yesterday|today|last week|last month|tomorrow|next week|this morning|this evening|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/.test(text)) {
96
+ tags.push('cat:temporal');
97
+ }
98
+
99
+ // --- Entity extraction (simple noun phrase patterns) ---
100
+
101
+ // Proper nouns (capitalized words that aren't sentence starters)
102
+ const properNouns = content.match(/(?:^|\.\s+)?([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/g);
103
+ if (properNouns) {
104
+ const unique = [...new Set(properNouns.map(n => n.trim()).filter(n => n.length > 2 && n.length < 30))];
105
+ for (const noun of unique.slice(0, 5)) {
106
+ tags.push(`entity:${noun}`);
107
+ }
108
+ }
109
+
110
+ // --- Knowledge type tags ---
111
+
112
+ // Preference (I like/prefer/enjoy/love/hate)
113
+ if (/\b(i like|i prefer|i enjoy|i love|i hate|my favorite|i don't like)\b/.test(text)) {
114
+ tags.push('cat:preference');
115
+ }
116
+
117
+ // Fact (declarative statements about identity/attributes)
118
+ if (/\b(my name is|i am a|i work at|i live in|i graduated|my birthday|i was born)\b/.test(text)) {
119
+ tags.push('cat:identity');
120
+ }
121
+
122
+ // Plan / intention
123
+ if (/\b(i plan to|i'm going to|i want to|i'm thinking of|i'm considering|next week i|planning to)\b/.test(text)) {
124
+ tags.push('cat:plan');
125
+ }
126
+
127
+ // Experience / event
128
+ if (/\b(i went|i visited|i attended|i tried|i saw|i heard|i found|i discovered)\b/.test(text)) {
129
+ tags.push('cat:experience');
130
+ }
131
+
132
+ return tags;
133
+ }
134
+
135
+ /**
136
+ * Propagate relevant tags from related memories to a new memory.
137
+ * Called after connection engine links the new memory to existing ones.
138
+ *
139
+ * Strategy: inherit meta-tags from strongly connected neighbors,
140
+ * but only tags that appear in 2+ neighbors (consensus filtering).
141
+ */
142
+ export function propagateTagsFromNeighbors(
143
+ existingTags: string[],
144
+ neighborTagSets: string[][],
145
+ ): string[] {
146
+ if (neighborTagSets.length === 0) return [];
147
+
148
+ // Count meta-tag occurrences across neighbors
149
+ const tagCounts = new Map<string, number>();
150
+ for (const tags of neighborTagSets) {
151
+ for (const tag of tags) {
152
+ if (tag.startsWith('cat:') || tag.startsWith('entity:')) {
153
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
154
+ }
155
+ }
156
+ }
157
+
158
+ // Only propagate tags that appear in 2+ neighbors (consensus)
159
+ const propagated: string[] = [];
160
+ const existingSet = new Set(existingTags);
161
+ for (const [tag, count] of tagCounts) {
162
+ if (count >= 2 && !existingSet.has(tag)) {
163
+ propagated.push(tag);
164
+ }
165
+ }
166
+
167
+ return propagated.slice(0, 5); // Cap to avoid tag bloat
168
+ }
@@ -15,7 +15,6 @@
15
15
  import { pipeline, type Text2TextGenerationPipeline } from '@huggingface/transformers';
16
16
 
17
17
  const MODEL_ID = 'Xenova/flan-t5-small';
18
-
19
18
  let instance: Text2TextGenerationPipeline | null = null;
20
19
  let initPromise: Promise<Text2TextGenerationPipeline> | null = null;
21
20
 
@@ -23,7 +23,6 @@ import {
23
23
 
24
24
  const DEFAULT_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
25
25
  const MODEL_ID = process.env.AWM_RERANKER_MODEL || DEFAULT_MODEL;
26
-
27
26
  let tokenizer: PreTrainedTokenizer | null = null;
28
27
  let model: PreTrainedModel | null = null;
29
28
  let initPromise: Promise<void> | null = null;
@@ -99,8 +99,19 @@ export interface ConsolidationResult {
99
99
  stagingPromoted: number;
100
100
  stagingDiscarded: number;
101
101
  engramsProcessed: number;
102
+ synthesesCreated: number;
102
103
  }
103
104
 
105
+ const MAX_SYNTHESES_PER_CYCLE = 5;
106
+ const MIN_CLUSTER_SIZE_FOR_SYNTHESIS = 3;
107
+
108
+ /** Shared stopwords for synthesis keyword extraction */
109
+ const SYNTH_STOPWORDS = new Set(['the', 'is', 'a', 'an', 'and', 'or', 'of', 'to', 'in', 'for',
110
+ 'on', 'with', 'that', 'this', 'it', 'was', 'are', 'be', 'has', 'had', 'but', 'not', 'from',
111
+ 'by', 'as', 'at', 'i', 'you', 'we', 'my', 'your', 'can', 'will', 'do', 'did', 'if', 'user',
112
+ 'assistant', 'would', 'like', 'just', 'also', 'about', 'really', 'think', 'know', 'want',
113
+ 'here', 'there', 'some', 'more', 'very', 'been', 'have', 'what', 'when', 'how', 'they']);
114
+
104
115
  export class ConsolidationEngine {
105
116
  private store: EngramStore;
106
117
 
@@ -136,6 +147,7 @@ export class ConsolidationEngine {
136
147
  stagingPromoted: 0,
137
148
  stagingDiscarded: 0,
138
149
  engramsProcessed: 0,
150
+ synthesesCreated: 0,
139
151
  };
140
152
 
141
153
  // --- Phase 1: Replay ---
@@ -208,6 +220,153 @@ export class ConsolidationEngine {
208
220
  }
209
221
  }
210
222
 
223
+ // --- Phase 2.5: Two types of synthesis ---
224
+ //
225
+ // Type A: SESSION SYNTHESIS (perfect recall)
226
+ // Groups by shared metadata tags (sid=, proj=, topic=).
227
+ // Summarizes what happened in a conversation/project session.
228
+ // Helps find specific facts by providing a topical anchor.
229
+ //
230
+ // Type B: PATTERN SYNTHESIS (novel recall)
231
+ // Uses the existing vector-similarity clusters.
232
+ // Finds structural patterns across disparate topics.
233
+ // "Debugging X by Y" + "Resolving A by B" → pattern: "conflict → ordering"
234
+ // Lower confidence — these are speculative connections, not facts.
235
+
236
+ let synthCount = 0;
237
+
238
+ // --- Type A: Session synthesis (tag-based grouping) ---
239
+ // Group engrams by shared session/project tags, NOT vector similarity
240
+ const tagGroups = new Map<string, Engram[]>();
241
+ for (const e of engrams) {
242
+ if (e.tags.includes('synth=true')) continue; // Skip existing syntheses
243
+ for (const tag of e.tags) {
244
+ if (tag.startsWith('sid=') || tag.startsWith('proj=') || tag.startsWith('topic=')) {
245
+ const group = tagGroups.get(tag) ?? [];
246
+ group.push(e);
247
+ tagGroups.set(tag, group);
248
+ }
249
+ }
250
+ }
251
+
252
+ for (const [tag, group] of tagGroups) {
253
+ if (group.length < MIN_CLUSTER_SIZE_FOR_SYNTHESIS) continue;
254
+ if (synthCount >= MAX_SYNTHESES_PER_CYCLE) break;
255
+
256
+ // Check if a synthesis for this tag already exists
257
+ const existing = engrams.find(e =>
258
+ e.tags.includes('synth=true') && e.tags.includes(tag)
259
+ );
260
+ if (existing) continue;
261
+
262
+ // Extract key terms from this group
263
+ const wordCounts = new Map<string, number>();
264
+ for (const e of group) {
265
+ const words = e.content.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/);
266
+ for (const w of words) {
267
+ if (w.length > 3 && !SYNTH_STOPWORDS.has(w)) {
268
+ wordCounts.set(w, (wordCounts.get(w) ?? 0) + 1);
269
+ }
270
+ }
271
+ }
272
+ const keyTerms = [...wordCounts.entries()]
273
+ .sort((a, b) => b[1] - a[1])
274
+ .slice(0, 15)
275
+ .map(([word]) => word);
276
+
277
+ // Extract unique concepts (deduplicated)
278
+ const concepts = [...new Set(group.map(e => e.concept))];
279
+
280
+ const synthContent = [
281
+ `Session summary (${tag}, ${group.length} turns).`,
282
+ `Key topics: ${keyTerms.slice(0, 8).join(', ')}.`,
283
+ `Discussed: ${keyTerms.slice(8).join(', ')}.`,
284
+ ].join(' ');
285
+
286
+ const synthEngram = this.store.createEngram({
287
+ agentId,
288
+ concept: `session: ${tag} (${keyTerms.slice(0, 3).join(', ')})`,
289
+ content: synthContent,
290
+ tags: [tag, 'synth=true', 'synth-type=session'],
291
+ salience: 0.6,
292
+ confidence: 0.55,
293
+ memoryType: 'semantic',
294
+ });
295
+
296
+ // Set embedding to group centroid
297
+ const centroid = this.computeCentroid(group);
298
+ if (centroid.length > 0) {
299
+ this.store.updateEmbedding(synthEngram.id, centroid);
300
+ }
301
+
302
+ // Link to sources
303
+ for (const source of group.slice(0, 10)) { // Cap links to prevent explosion
304
+ this.store.upsertAssociation(synthEngram.id, source.id, 0.4, 'causal');
305
+ }
306
+
307
+ synthCount++;
308
+ result.synthesesCreated++;
309
+ }
310
+
311
+ // --- Type B: Pattern synthesis (vector-similarity clusters, speculative) ---
312
+ // Only create these for clusters where members come from DIFFERENT sessions/projects.
313
+ // This finds cross-domain patterns: "debugging technique A" + "architecture pattern B"
314
+ for (const cluster of clusters) {
315
+ if (cluster.length < MIN_CLUSTER_SIZE_FOR_SYNTHESIS) continue;
316
+ if (synthCount >= MAX_SYNTHESES_PER_CYCLE) break;
317
+ if (cluster.some(e => e.tags.includes('synth=true'))) continue;
318
+
319
+ // Only create pattern synthesis if cluster spans multiple sessions
320
+ const sessionTags = new Set<string>();
321
+ for (const e of cluster) {
322
+ for (const tag of e.tags) {
323
+ if (tag.startsWith('sid=') || tag.startsWith('proj=')) sessionTags.add(tag);
324
+ }
325
+ }
326
+ if (sessionTags.size < 2) continue; // Same session → skip (Type A handles it)
327
+
328
+ const wordCounts = new Map<string, number>();
329
+ for (const e of cluster) {
330
+ const words = e.content.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/);
331
+ for (const w of words) {
332
+ if (w.length > 3 && !SYNTH_STOPWORDS.has(w)) {
333
+ wordCounts.set(w, (wordCounts.get(w) ?? 0) + 1);
334
+ }
335
+ }
336
+ }
337
+ const keyTerms = [...wordCounts.entries()]
338
+ .sort((a, b) => b[1] - a[1])
339
+ .slice(0, 10)
340
+ .map(([word]) => word);
341
+
342
+ const synthContent = [
343
+ `Pattern across ${sessionTags.size} sessions (${cluster.length} memories).`,
344
+ `Common themes: ${keyTerms.join(', ')}.`,
345
+ ].join(' ');
346
+
347
+ const synthEngram = this.store.createEngram({
348
+ agentId,
349
+ concept: `pattern: ${keyTerms.slice(0, 3).join(', ')}`,
350
+ content: synthContent,
351
+ tags: [...sessionTags, 'synth=true', 'synth-type=pattern'],
352
+ salience: 0.5, // Lower — speculative
353
+ confidence: 0.4, // Lower — these are hypotheses not facts
354
+ memoryType: 'semantic',
355
+ });
356
+
357
+ const centroid = this.computeCentroid(cluster);
358
+ if (centroid.length > 0) {
359
+ this.store.updateEmbedding(synthEngram.id, centroid);
360
+ }
361
+
362
+ for (const source of cluster.slice(0, 8)) {
363
+ this.store.upsertAssociation(synthEngram.id, source.id, 0.3, 'bridge');
364
+ }
365
+
366
+ synthCount++;
367
+ result.synthesesCreated++;
368
+ }
369
+
211
370
  // --- Phase 3: Direct cross-cluster bridging ---
212
371
  // Find the closest pair of memories between each cluster pair and bridge them.
213
372
  if (clusters.length >= 2) {
@@ -244,21 +403,18 @@ export class ConsolidationEngine {
244
403
  // that received positive feedback are more durable — just like how
245
404
  // practiced memories are more resistant to forgetting in the brain.
246
405
  // Base half-life: 7 days. High-confidence (0.8+) gets up to 30 days.
247
- const engramConfMap = new Map(engrams.map(e => [e.id, e.confidence]));
406
+ const engramMap = new Map(engrams.map(e => [e.id, e]));
248
407
  const associations = this.store.getAllAssociations(agentId);
249
408
  for (const assoc of associations) {
250
409
  const daysSince =
251
410
  (Date.now() - assoc.lastActivated.getTime()) / (1000 * 60 * 60 * 24);
252
- if (daysSince < 0.5) continue; // Skip recently activated
411
+ if (daysSince < 0.5) continue;
253
412
 
254
- // Confidence + access-count modulated half-life (synaptic tagging for edges)
255
- // Base: 7 days. High confidence (0.8+): up to 21 days.
256
- // High access count: further extends half-life (log-scaled, capped at 2x boost).
257
- const fromConf = engramConfMap.get(assoc.fromEngramId) ?? 0.5;
258
- const toConf = engramConfMap.get(assoc.toEngramId) ?? 0.5;
413
+ const fromEngram = engramMap.get(assoc.fromEngramId);
414
+ const toEngram = engramMap.get(assoc.toEngramId);
415
+ const fromConf = fromEngram?.confidence ?? 0.5;
416
+ const toConf = toEngram?.confidence ?? 0.5;
259
417
  const maxConf = Math.max(fromConf, toConf);
260
- const fromEngram = engrams.find(e => e.id === assoc.fromEngramId);
261
- const toEngram = engrams.find(e => e.id === assoc.toEngramId);
262
418
  const maxAccess = Math.max(fromEngram?.accessCount ?? 0, toEngram?.accessCount ?? 0);
263
419
  const accessBoost = Math.min(2.0, 1.0 + 0.5 * Math.log1p(maxAccess));
264
420
  const halfLifeDays = Math.min(