@memstack/core 0.2.0 → 0.4.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 +144 -56
- package/dist/index.cjs +2212 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +496 -27
- package/dist/index.d.ts +496 -27
- package/dist/index.js +2203 -260
- package/dist/index.js.map +1 -1
- package/package.json +32 -32
- package/src/adapters/embedding/cohere.ts +0 -77
- package/src/adapters/embedding/openai.ts +0 -65
- package/src/adapters/llm/anthropic.ts +0 -69
- package/src/adapters/llm/groq.ts +0 -185
- package/src/adapters/llm/ollama.ts +0 -85
- package/src/adapters/llm/openai.ts +0 -178
- package/src/adapters/storage/disk.ts +0 -334
- package/src/adapters/storage/memory.ts +0 -147
- package/src/adapters/storage/postgres.ts +0 -310
- package/src/adapters/storage/redis.ts +0 -274
- package/src/client.ts +0 -236
- package/src/errors.ts +0 -47
- package/src/index.ts +0 -52
- package/src/interfaces.ts +0 -154
- package/src/memory/ContextCompiler.ts +0 -158
- package/src/memory/MemoryStore.ts +0 -266
- package/src/memory/Pruner.ts +0 -66
- package/src/memory/Summarizer.ts +0 -46
- package/src/types.ts +0 -50
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,10 @@ var Summarizer = class {
|
|
|
47
47
|
if (memories.length === 0) {
|
|
48
48
|
throw new MemStackError("VALIDATION_ERROR", "No memories to summarize");
|
|
49
49
|
}
|
|
50
|
+
const chunkSize = options.chunkSize ?? 50;
|
|
51
|
+
if (memories.length > chunkSize) {
|
|
52
|
+
return this._summarizeChunked(memories, options, promptOverride);
|
|
53
|
+
}
|
|
50
54
|
const sorted = [...memories].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
51
55
|
const memoryTexts = sorted.map((m) => `[${m.createdAt.toISOString()}] ${m.content}`);
|
|
52
56
|
const system = promptOverride ?? this.defaultPrompt;
|
|
@@ -66,6 +70,76 @@ var Summarizer = class {
|
|
|
66
70
|
);
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
async _summarizeChunked(memories, options, promptOverride) {
|
|
74
|
+
const chunkSize = options.chunkSize ?? 50;
|
|
75
|
+
const sorted = [...memories].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for (let i = 0; i < sorted.length; i += chunkSize) {
|
|
78
|
+
chunks.push(sorted.slice(i, i + chunkSize));
|
|
79
|
+
}
|
|
80
|
+
let totalTokens = 0;
|
|
81
|
+
const chunkSummaries = [];
|
|
82
|
+
for (const chunk of chunks) {
|
|
83
|
+
const memoryTexts = chunk.map((m) => `[${m.createdAt.toISOString()}] ${m.content}`);
|
|
84
|
+
const system = promptOverride ?? this.defaultPrompt;
|
|
85
|
+
const user = memoryTexts.join("\n\n");
|
|
86
|
+
try {
|
|
87
|
+
const response = await this.llm.complete({ system, user });
|
|
88
|
+
chunkSummaries.push(response.text.trim());
|
|
89
|
+
totalTokens += response.tokens.total;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof MemStackError) throw err;
|
|
92
|
+
throw new MemStackError(
|
|
93
|
+
"LLM_ERROR",
|
|
94
|
+
`Chunked summarization failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
{ retryable: true, details: { cause: err instanceof Error ? err.message : String(err) } }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (chunkSummaries.length === 1) {
|
|
100
|
+
return { summaryContent: chunkSummaries[0], tokenCount: totalTokens };
|
|
101
|
+
}
|
|
102
|
+
const summaryMemories = chunkSummaries.map((content, i) => ({
|
|
103
|
+
id: `_chunk_summary_${i}`,
|
|
104
|
+
actorId: sorted[0].actorId,
|
|
105
|
+
memoryType: "summary",
|
|
106
|
+
content,
|
|
107
|
+
importance: 0.5,
|
|
108
|
+
emotionalValence: 0,
|
|
109
|
+
tags: ["summary"],
|
|
110
|
+
createdAt: sorted[sorted.length - 1].createdAt
|
|
111
|
+
}));
|
|
112
|
+
const merged = await this._summarizeChunked(summaryMemories, options, promptOverride);
|
|
113
|
+
return {
|
|
114
|
+
summaryContent: merged.summaryContent,
|
|
115
|
+
tokenCount: totalTokens + merged.tokenCount
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async *summarizeStream(memories, options = {}, promptOverride) {
|
|
119
|
+
if (memories.length === 0) {
|
|
120
|
+
throw new MemStackError("VALIDATION_ERROR", "No memories to summarize");
|
|
121
|
+
}
|
|
122
|
+
if (!this.llm.completeStream) {
|
|
123
|
+
const result = await this.summarize(memories, options, promptOverride);
|
|
124
|
+
yield { chunk: result.summaryContent, text: result.summaryContent };
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const chunkSize = options.chunkSize ?? 50;
|
|
128
|
+
let toSummarize = memories;
|
|
129
|
+
if (memories.length > chunkSize) {
|
|
130
|
+
const chunked = await this._summarizeChunked(memories, options, promptOverride);
|
|
131
|
+
toSummarize = [{ id: "_chunked", actorId: "summarizer", memoryType: "summary", content: chunked.summaryContent, importance: 0.5, emotionalValence: 0, tags: [], createdAt: /* @__PURE__ */ new Date() }];
|
|
132
|
+
}
|
|
133
|
+
const sorted = [...toSummarize].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
134
|
+
const memoryTexts = sorted.map((m) => `[${m.createdAt.toISOString()}] ${m.content}`);
|
|
135
|
+
const system = promptOverride ?? this.defaultPrompt;
|
|
136
|
+
const user = memoryTexts.join("\n\n");
|
|
137
|
+
let fullText = "";
|
|
138
|
+
for await (const chunk of this.llm.completeStream({ system, user })) {
|
|
139
|
+
fullText += chunk.text;
|
|
140
|
+
yield { chunk: chunk.text, text: fullText };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
69
143
|
};
|
|
70
144
|
|
|
71
145
|
// src/memory/Pruner.ts
|
|
@@ -83,6 +157,8 @@ var Pruner = class {
|
|
|
83
157
|
return this.removeByType(memories, strategy);
|
|
84
158
|
case "custom":
|
|
85
159
|
return this.removeCustom(memories, strategy);
|
|
160
|
+
case "compose":
|
|
161
|
+
return this.keepCompose(memories, strategy);
|
|
86
162
|
default:
|
|
87
163
|
return memories;
|
|
88
164
|
}
|
|
@@ -124,21 +200,36 @@ var Pruner = class {
|
|
|
124
200
|
if (!strategy.shouldRemove) return memories;
|
|
125
201
|
return memories.filter((m) => !strategy.shouldRemove(m));
|
|
126
202
|
}
|
|
203
|
+
/** Apply sub-strategies cumulatively: kept memories must pass ALL. */
|
|
204
|
+
keepCompose(memories, strategy) {
|
|
205
|
+
if (!strategy.strategies || strategy.strategies.length === 0) {
|
|
206
|
+
return memories;
|
|
207
|
+
}
|
|
208
|
+
let kept = memories;
|
|
209
|
+
for (const sub of strategy.strategies) {
|
|
210
|
+
kept = this.execute(kept, sub);
|
|
211
|
+
}
|
|
212
|
+
return kept;
|
|
213
|
+
}
|
|
127
214
|
};
|
|
128
215
|
|
|
129
216
|
// src/memory/ContextCompiler.ts
|
|
130
217
|
var ContextCompiler = class {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
218
|
+
tokenCounter;
|
|
219
|
+
importantRatio;
|
|
220
|
+
constructor(tokenCounter, importantRatio) {
|
|
221
|
+
this.tokenCounter = tokenCounter ?? this._heuristicTokens.bind(this);
|
|
222
|
+
this.importantRatio = importantRatio ?? 0.6;
|
|
223
|
+
}
|
|
224
|
+
_heuristicTokens(text) {
|
|
137
225
|
const charEst = Math.ceil(text.length / 4);
|
|
138
226
|
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
139
227
|
const wordEst = Math.ceil(words.length * 1.3);
|
|
140
228
|
return Math.max(charEst, Math.max(1, wordEst));
|
|
141
229
|
}
|
|
230
|
+
estimateTokens(text) {
|
|
231
|
+
return this.tokenCounter(text);
|
|
232
|
+
}
|
|
142
233
|
/**
|
|
143
234
|
* Truncate text to fit within a token budget, preserving whole words.
|
|
144
235
|
* Appends "..." when content is cut. Falls back to character-based
|
|
@@ -175,6 +266,15 @@ var ContextCompiler = class {
|
|
|
175
266
|
);
|
|
176
267
|
const { recentMemories, importantMemories, systemPrompt } = this.assembleTokenBudgeted(sortedRecent, sortedImportant, maxTokens);
|
|
177
268
|
const tokenEstimate = this.estimateTokens(systemPrompt);
|
|
269
|
+
if (options.format === "messages") {
|
|
270
|
+
return {
|
|
271
|
+
systemPrompt,
|
|
272
|
+
messages: [{ role: "system", content: systemPrompt }],
|
|
273
|
+
recentMemories,
|
|
274
|
+
importantMemories,
|
|
275
|
+
tokenEstimate
|
|
276
|
+
};
|
|
277
|
+
}
|
|
178
278
|
return { systemPrompt, recentMemories, importantMemories, tokenEstimate };
|
|
179
279
|
}
|
|
180
280
|
assembleTokenBudgeted(recent, important, maxTokens) {
|
|
@@ -189,7 +289,7 @@ var ContextCompiler = class {
|
|
|
189
289
|
`;
|
|
190
290
|
const overhead = this.estimateTokens(prefix + suffix);
|
|
191
291
|
let tokens = this.estimateTokens(m.content) + overhead;
|
|
192
|
-
const remaining = Math.floor(maxTokens *
|
|
292
|
+
const remaining = Math.floor(maxTokens * this.importantRatio) - usedTokens;
|
|
193
293
|
if (remaining <= 0) break;
|
|
194
294
|
if (tokens > remaining) {
|
|
195
295
|
const contentBudget = Math.max(0, remaining - overhead);
|
|
@@ -268,6 +368,15 @@ var MemoryStore = class {
|
|
|
268
368
|
pruner;
|
|
269
369
|
compiler;
|
|
270
370
|
embedOnStore;
|
|
371
|
+
limits;
|
|
372
|
+
contentHashIndex = null;
|
|
373
|
+
initialized = false;
|
|
374
|
+
async _ensureInit() {
|
|
375
|
+
if (!this.initialized) {
|
|
376
|
+
await this.storage.initialize();
|
|
377
|
+
this.initialized = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
271
380
|
constructor(config) {
|
|
272
381
|
this.storage = config.storage;
|
|
273
382
|
this.embedding = config.embedding;
|
|
@@ -276,31 +385,43 @@ var MemoryStore = class {
|
|
|
276
385
|
this.summarizer = new Summarizer(config.llm, config.summarizationPrompt);
|
|
277
386
|
}
|
|
278
387
|
this.pruner = new Pruner();
|
|
279
|
-
this.compiler = new ContextCompiler();
|
|
388
|
+
this.compiler = new ContextCompiler(config.tokenCounter, config.contextImportantRatio);
|
|
389
|
+
this.limits = {
|
|
390
|
+
dedupScan: 1e3,
|
|
391
|
+
contextScan: 50,
|
|
392
|
+
summarizeScan: 1e3,
|
|
393
|
+
pruneScan: 1e4,
|
|
394
|
+
exportScan: 1e5,
|
|
395
|
+
purgeScan: 1e5,
|
|
396
|
+
...config.limits
|
|
397
|
+
};
|
|
280
398
|
}
|
|
281
399
|
async store(input) {
|
|
400
|
+
await this._ensureInit();
|
|
282
401
|
if (input.onConflict === "append") {
|
|
402
|
+
await this._ensureHashIndex();
|
|
283
403
|
const hash = this._hashContent(input.actorId, input.content);
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
404
|
+
const existingId = this.contentHashIndex.get(hash);
|
|
405
|
+
if (existingId) {
|
|
406
|
+
const match = await this.storage.get(existingId);
|
|
407
|
+
if (match) {
|
|
408
|
+
const mergedTags = [.../* @__PURE__ */ new Set([...match.tags ?? [], ...input.tags ?? []])];
|
|
409
|
+
const updated = await this.storage.store({
|
|
410
|
+
id: match.id,
|
|
411
|
+
actorId: match.actorId,
|
|
412
|
+
content: match.content,
|
|
413
|
+
memoryType: match.memoryType,
|
|
414
|
+
importance: input.importance ?? match.importance,
|
|
415
|
+
emotionalValence: input.emotionalValence ?? match.emotionalValence,
|
|
416
|
+
tags: mergedTags,
|
|
417
|
+
embedding: match.embedding,
|
|
418
|
+
sourceId: input.sourceId ?? match.sourceId,
|
|
419
|
+
metadata: { ...match.metadata, ...input.metadata },
|
|
420
|
+
expiresAt: input.expiresAt ?? match.expiresAt
|
|
421
|
+
});
|
|
422
|
+
this.contentHashIndex.set(hash, updated.id);
|
|
423
|
+
return updated;
|
|
424
|
+
}
|
|
304
425
|
}
|
|
305
426
|
}
|
|
306
427
|
let withEmbedding = input;
|
|
@@ -309,9 +430,15 @@ var MemoryStore = class {
|
|
|
309
430
|
withEmbedding = { ...input, embedding: embeddings[0] };
|
|
310
431
|
}
|
|
311
432
|
const { onConflict: _, ...clean } = withEmbedding;
|
|
312
|
-
|
|
433
|
+
const memory = await this.storage.store(clean);
|
|
434
|
+
if (this.contentHashIndex !== null) {
|
|
435
|
+
const hash = this._hashContent(memory.actorId, memory.content);
|
|
436
|
+
this.contentHashIndex.set(hash, memory.id);
|
|
437
|
+
}
|
|
438
|
+
return memory;
|
|
313
439
|
}
|
|
314
440
|
async storeBatch(inputs) {
|
|
441
|
+
await this._ensureInit();
|
|
315
442
|
const appendInputs = inputs.filter((i) => i.onConflict === "append");
|
|
316
443
|
const normalInputs = inputs.filter((i) => i.onConflict !== "append");
|
|
317
444
|
const results = [];
|
|
@@ -319,74 +446,89 @@ var MemoryStore = class {
|
|
|
319
446
|
results.push(await this.store(input));
|
|
320
447
|
}
|
|
321
448
|
if (normalInputs.length === 0) return results;
|
|
449
|
+
let stored;
|
|
322
450
|
if (this.embedding && this.embedOnStore) {
|
|
451
|
+
const enrichedResults = [];
|
|
323
452
|
for (let i = 0; i < normalInputs.length; i += EMBEDDING_BATCH_SIZE) {
|
|
324
453
|
const batch = normalInputs.slice(i, i + EMBEDDING_BATCH_SIZE);
|
|
325
454
|
const texts = batch.map((inp) => inp.content);
|
|
326
455
|
const embeddings = await this.embedding.embed(texts);
|
|
327
456
|
const enriched = batch.map((input, j) => ({ ...input, embedding: embeddings[j] }));
|
|
328
|
-
const
|
|
329
|
-
|
|
457
|
+
const batchResults = await this.storage.storeBatch(enriched);
|
|
458
|
+
enrichedResults.push(...batchResults);
|
|
330
459
|
}
|
|
460
|
+
stored = enrichedResults;
|
|
331
461
|
} else {
|
|
332
|
-
|
|
333
|
-
|
|
462
|
+
stored = await this.storage.storeBatch(normalInputs);
|
|
463
|
+
}
|
|
464
|
+
if (this.contentHashIndex !== null) {
|
|
465
|
+
for (const mem of stored) {
|
|
466
|
+
const hash = this._hashContent(mem.actorId, mem.content);
|
|
467
|
+
this.contentHashIndex.set(hash, mem.id);
|
|
468
|
+
}
|
|
334
469
|
}
|
|
470
|
+
results.push(...stored);
|
|
335
471
|
return results;
|
|
336
472
|
}
|
|
337
473
|
async get(id) {
|
|
474
|
+
await this._ensureInit();
|
|
338
475
|
return this.storage.get(id);
|
|
339
476
|
}
|
|
340
477
|
async delete(id) {
|
|
478
|
+
await this._ensureInit();
|
|
341
479
|
return this.storage.delete(id);
|
|
342
480
|
}
|
|
343
481
|
async deleteMany(ids) {
|
|
482
|
+
await this._ensureInit();
|
|
344
483
|
return this.storage.deleteMany(ids);
|
|
345
484
|
}
|
|
346
485
|
async touch(id) {
|
|
486
|
+
await this._ensureInit();
|
|
347
487
|
const memory = await this.storage.get(id);
|
|
348
488
|
if (!memory) throw notFound("Memory", id);
|
|
349
489
|
if (this.storage.touch) {
|
|
350
490
|
return this.storage.touch(id);
|
|
351
491
|
}
|
|
352
|
-
await this.storage.delete(id);
|
|
353
|
-
await this.storage.store({
|
|
354
|
-
actorId: memory.actorId,
|
|
355
|
-
content: memory.content,
|
|
356
|
-
memoryType: memory.memoryType,
|
|
357
|
-
importance: memory.importance,
|
|
358
|
-
emotionalValence: memory.emotionalValence,
|
|
359
|
-
tags: memory.tags,
|
|
360
|
-
embedding: memory.embedding,
|
|
361
|
-
sourceId: memory.sourceId,
|
|
362
|
-
metadata: memory.metadata,
|
|
363
|
-
expiresAt: memory.expiresAt
|
|
364
|
-
});
|
|
365
492
|
}
|
|
366
493
|
async count(filter) {
|
|
494
|
+
await this._ensureInit();
|
|
367
495
|
return this.storage.count(filter);
|
|
368
496
|
}
|
|
369
497
|
async retrieve(query) {
|
|
498
|
+
await this._ensureInit();
|
|
370
499
|
let embedding;
|
|
371
500
|
if (this.embedding && query.query && (query.strategy === "semantic" || query.strategy === "hybrid")) {
|
|
372
501
|
const vecs = await this.embedding.embed([query.query]);
|
|
373
502
|
embedding = vecs[0];
|
|
374
503
|
}
|
|
375
|
-
|
|
504
|
+
let results = await this.storage.retrieve(query, embedding);
|
|
505
|
+
if (query.createdAfter) {
|
|
506
|
+
results = results.filter((m) => m.createdAt >= query.createdAfter);
|
|
507
|
+
}
|
|
508
|
+
if (query.createdBefore) {
|
|
509
|
+
results = results.filter((m) => m.createdAt <= query.createdBefore);
|
|
510
|
+
}
|
|
511
|
+
return results;
|
|
376
512
|
}
|
|
377
513
|
async compileContext(options) {
|
|
514
|
+
await this._ensureInit();
|
|
378
515
|
const maxTokens = options.maxTokens ?? 2e3;
|
|
379
|
-
const memories = await this.storage.retrieve({
|
|
516
|
+
const memories = await this.storage.retrieve({
|
|
517
|
+
actorId: options.actorId,
|
|
518
|
+
limit: this.limits.contextScan,
|
|
519
|
+
strategy: options.retrieveStrategy
|
|
520
|
+
});
|
|
380
521
|
return this.compiler.compile(memories, { ...options, maxTokens });
|
|
381
522
|
}
|
|
382
523
|
async summarize(options, onError) {
|
|
524
|
+
await this._ensureInit();
|
|
383
525
|
if (!this.summarizer) {
|
|
384
526
|
throw validationError("No LLM configured for summarization");
|
|
385
527
|
}
|
|
386
528
|
const allMemories = await this.storage.retrieve({
|
|
387
529
|
actorId: options.actorId,
|
|
388
530
|
memoryTypes: options.memoryTypes ?? ["interaction"],
|
|
389
|
-
limit:
|
|
531
|
+
limit: this.limits.summarizeScan,
|
|
390
532
|
strategy: "recent"
|
|
391
533
|
});
|
|
392
534
|
let toSummarize = allMemories;
|
|
@@ -406,7 +548,7 @@ var MemoryStore = class {
|
|
|
406
548
|
}
|
|
407
549
|
let summaryContent;
|
|
408
550
|
try {
|
|
409
|
-
const result = await this.summarizer.summarize(toSummarize, options);
|
|
551
|
+
const result = await this.summarizer.summarize(toSummarize, options, options.prompt);
|
|
410
552
|
summaryContent = result.summaryContent;
|
|
411
553
|
} catch (err) {
|
|
412
554
|
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -428,8 +570,37 @@ var MemoryStore = class {
|
|
|
428
570
|
}
|
|
429
571
|
return { summary, deletedCount };
|
|
430
572
|
}
|
|
573
|
+
async *summarizeStream(options) {
|
|
574
|
+
await this._ensureInit();
|
|
575
|
+
if (!this.summarizer) {
|
|
576
|
+
throw validationError("No LLM configured for summarization");
|
|
577
|
+
}
|
|
578
|
+
const allMemories = await this.storage.retrieve({
|
|
579
|
+
actorId: options.actorId,
|
|
580
|
+
memoryTypes: options.memoryTypes ?? ["interaction"],
|
|
581
|
+
limit: this.limits.summarizeScan,
|
|
582
|
+
strategy: "recent"
|
|
583
|
+
});
|
|
584
|
+
let toSummarize = allMemories;
|
|
585
|
+
if (options.olderThan) {
|
|
586
|
+
toSummarize = toSummarize.filter((m) => m.createdAt < options.olderThan);
|
|
587
|
+
}
|
|
588
|
+
if (options.skipMostRecent) {
|
|
589
|
+
const sorted = [...toSummarize].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
590
|
+
toSummarize = sorted.slice(0, Math.max(0, sorted.length - options.skipMostRecent));
|
|
591
|
+
}
|
|
592
|
+
if (options.targetCount && toSummarize.length > options.targetCount) {
|
|
593
|
+
const sorted = [...toSummarize].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
594
|
+
toSummarize = sorted.slice(0, options.targetCount);
|
|
595
|
+
}
|
|
596
|
+
if (toSummarize.length === 0) {
|
|
597
|
+
throw validationError("No memories to summarize");
|
|
598
|
+
}
|
|
599
|
+
yield* this.summarizer.summarizeStream(toSummarize, options, options.prompt);
|
|
600
|
+
}
|
|
431
601
|
async prune(strategy) {
|
|
432
|
-
|
|
602
|
+
await this._ensureInit();
|
|
603
|
+
const allMemories = await this.storage.retrieve({ limit: this.limits.pruneScan });
|
|
433
604
|
const kept = this.pruner.execute(allMemories, strategy);
|
|
434
605
|
const keptIds = new Set(kept.map((k) => k.id));
|
|
435
606
|
const prunedIds = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
|
|
@@ -440,14 +611,107 @@ var MemoryStore = class {
|
|
|
440
611
|
return { pruned: prunedIds, count };
|
|
441
612
|
}
|
|
442
613
|
async dryRunPrune(strategy) {
|
|
443
|
-
|
|
614
|
+
await this._ensureInit();
|
|
615
|
+
const allMemories = await this.storage.retrieve({ limit: this.limits.pruneScan });
|
|
444
616
|
const kept = this.pruner.execute(allMemories, strategy);
|
|
445
617
|
const keptIds = new Set(kept.map((k) => k.id));
|
|
446
618
|
const wouldPrune = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
|
|
447
619
|
return { wouldPrune, count: wouldPrune.length };
|
|
448
620
|
}
|
|
449
|
-
async export() {
|
|
450
|
-
|
|
621
|
+
async export(actorId) {
|
|
622
|
+
await this._ensureInit();
|
|
623
|
+
return this.storage.retrieve({ actorId, limit: this.limits.exportScan });
|
|
624
|
+
}
|
|
625
|
+
async stats(actorId) {
|
|
626
|
+
await this._ensureInit();
|
|
627
|
+
const memories = await this.storage.retrieve({ actorId, limit: this.limits.exportScan });
|
|
628
|
+
const now = /* @__PURE__ */ new Date();
|
|
629
|
+
const byType = {};
|
|
630
|
+
const byActor = {};
|
|
631
|
+
let totalImportance = 0;
|
|
632
|
+
let expired = 0;
|
|
633
|
+
let oldest = null;
|
|
634
|
+
let newest = null;
|
|
635
|
+
for (const m of memories) {
|
|
636
|
+
totalImportance += m.importance;
|
|
637
|
+
byType[m.memoryType] = (byType[m.memoryType] ?? 0) + 1;
|
|
638
|
+
const actor = byActor[m.actorId] ?? { count: 0, oldest: m.createdAt, newest: m.createdAt, totalImportance: 0 };
|
|
639
|
+
actor.count++;
|
|
640
|
+
actor.totalImportance += m.importance;
|
|
641
|
+
if (m.createdAt < actor.oldest) actor.oldest = m.createdAt;
|
|
642
|
+
if (m.createdAt > actor.newest) actor.newest = m.createdAt;
|
|
643
|
+
byActor[m.actorId] = actor;
|
|
644
|
+
if (m.expiresAt && m.expiresAt <= now) expired++;
|
|
645
|
+
if (!oldest || m.createdAt < oldest) oldest = m.createdAt;
|
|
646
|
+
if (!newest || m.createdAt > newest) newest = m.createdAt;
|
|
647
|
+
}
|
|
648
|
+
const byActorFinal = {};
|
|
649
|
+
for (const [id, a] of Object.entries(byActor)) {
|
|
650
|
+
byActorFinal[id] = { count: a.count, oldest: a.oldest, newest: a.newest, avgImportance: a.totalImportance / a.count };
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
total: memories.length,
|
|
654
|
+
expired,
|
|
655
|
+
oldest,
|
|
656
|
+
newest,
|
|
657
|
+
avgImportance: memories.length > 0 ? totalImportance / memories.length : 0,
|
|
658
|
+
byType,
|
|
659
|
+
byActor: byActorFinal
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async purgeActor(actorId) {
|
|
663
|
+
await this._ensureInit();
|
|
664
|
+
const memories = await this.storage.retrieve({ actorId, limit: this.limits.purgeScan ?? 1e5 });
|
|
665
|
+
if (memories.length === 0) return 0;
|
|
666
|
+
return this.storage.deleteMany(memories.map((m) => m.id));
|
|
667
|
+
}
|
|
668
|
+
async merge(ids) {
|
|
669
|
+
await this._ensureInit();
|
|
670
|
+
if (ids.length < 2) throw validationError("merge requires at least 2 memory IDs");
|
|
671
|
+
const memories = [];
|
|
672
|
+
for (const id of ids) {
|
|
673
|
+
const m = await this.storage.get(id);
|
|
674
|
+
if (m) memories.push(m);
|
|
675
|
+
}
|
|
676
|
+
if (memories.length < 2) throw validationError("At least 2 of the provided IDs must exist");
|
|
677
|
+
const sorted = memories.sort((a, b) => b.importance - a.importance);
|
|
678
|
+
const winner = sorted[0];
|
|
679
|
+
const rest = sorted.slice(1);
|
|
680
|
+
const mergedContent = [
|
|
681
|
+
winner.content,
|
|
682
|
+
"",
|
|
683
|
+
"--- merged from " + rest.length + " related memories ---",
|
|
684
|
+
...rest.map((m) => "- " + m.content)
|
|
685
|
+
].join("\n");
|
|
686
|
+
const mergedTags = [...new Set(memories.flatMap((m) => m.tags))];
|
|
687
|
+
const mergedMetadata = {
|
|
688
|
+
...winner.metadata,
|
|
689
|
+
mergedFrom: rest.map((m) => m.id)
|
|
690
|
+
};
|
|
691
|
+
const updated = await this.storage.store({
|
|
692
|
+
id: winner.id,
|
|
693
|
+
actorId: winner.actorId,
|
|
694
|
+
content: mergedContent,
|
|
695
|
+
memoryType: winner.memoryType,
|
|
696
|
+
importance: winner.importance,
|
|
697
|
+
emotionalValence: winner.emotionalValence,
|
|
698
|
+
tags: mergedTags,
|
|
699
|
+
embedding: winner.embedding,
|
|
700
|
+
sourceId: winner.sourceId,
|
|
701
|
+
metadata: mergedMetadata,
|
|
702
|
+
expiresAt: winner.expiresAt
|
|
703
|
+
});
|
|
704
|
+
await this.storage.deleteMany(rest.map((m) => m.id));
|
|
705
|
+
return updated;
|
|
706
|
+
}
|
|
707
|
+
async _ensureHashIndex() {
|
|
708
|
+
if (this.contentHashIndex !== null) return;
|
|
709
|
+
this.contentHashIndex = /* @__PURE__ */ new Map();
|
|
710
|
+
const existing = await this.storage.retrieve({ limit: this.limits.dedupScan });
|
|
711
|
+
for (const m of existing) {
|
|
712
|
+
const hash = this._hashContent(m.actorId, m.content);
|
|
713
|
+
this.contentHashIndex.set(hash, m.id);
|
|
714
|
+
}
|
|
451
715
|
}
|
|
452
716
|
_hashContent(actorId, content) {
|
|
453
717
|
const normalized = `${actorId}::${content.toLowerCase().trim()}`;
|
|
@@ -461,7 +725,7 @@ var MemoryStore = class {
|
|
|
461
725
|
};
|
|
462
726
|
|
|
463
727
|
// src/adapters/storage/memory.ts
|
|
464
|
-
var
|
|
728
|
+
var InMemoryStorageAdapter = class {
|
|
465
729
|
memories = /* @__PURE__ */ new Map();
|
|
466
730
|
async initialize() {
|
|
467
731
|
}
|
|
@@ -535,6 +799,12 @@ var InMemoryStorage = class {
|
|
|
535
799
|
const q = query.query.toLowerCase();
|
|
536
800
|
results = results.filter((m) => m.content.toLowerCase().includes(q));
|
|
537
801
|
}
|
|
802
|
+
if (query.createdAfter) {
|
|
803
|
+
results = results.filter((m) => m.createdAt >= query.createdAfter);
|
|
804
|
+
}
|
|
805
|
+
if (query.createdBefore) {
|
|
806
|
+
results = results.filter((m) => m.createdAt <= query.createdBefore);
|
|
807
|
+
}
|
|
538
808
|
switch (query.strategy) {
|
|
539
809
|
case "recent":
|
|
540
810
|
results.sort((a, b) => b._touchedAt.getTime() - a._touchedAt.getTime());
|
|
@@ -586,13 +856,14 @@ var MemStack = class {
|
|
|
586
856
|
memory;
|
|
587
857
|
config;
|
|
588
858
|
storage;
|
|
589
|
-
|
|
859
|
+
processCounts = /* @__PURE__ */ new Map();
|
|
860
|
+
pruneCheckCount = 0;
|
|
590
861
|
constructor(config) {
|
|
591
862
|
if (!config.llm) {
|
|
592
863
|
throw configError("LLM provider is required");
|
|
593
864
|
}
|
|
594
865
|
this.config = config;
|
|
595
|
-
this.storage = config.storage ?? new
|
|
866
|
+
this.storage = config.storage ?? new InMemoryStorageAdapter();
|
|
596
867
|
this.memory = new MemoryStore({
|
|
597
868
|
storage: this.storage,
|
|
598
869
|
embedding: config.embedding,
|
|
@@ -658,35 +929,34 @@ var MemStack = class {
|
|
|
658
929
|
};
|
|
659
930
|
const memory = await this.memory.store(memoryInput);
|
|
660
931
|
this.config.hooks?.onMemoryStored?.(memory);
|
|
661
|
-
this.
|
|
932
|
+
const count = (this.processCounts.get(input.actorId) ?? 0) + 1;
|
|
933
|
+
this.processCounts.set(input.actorId, count);
|
|
662
934
|
let summaryCreated;
|
|
663
935
|
const threshold = this.config.defaults?.summarizationThreshold ?? 100;
|
|
664
|
-
if (threshold > 0) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
"summarize"
|
|
683
|
-
);
|
|
684
|
-
}
|
|
936
|
+
if (threshold > 0 && count % threshold === 0) {
|
|
937
|
+
try {
|
|
938
|
+
const result = await this.memory.summarize(
|
|
939
|
+
{
|
|
940
|
+
actorId: input.actorId,
|
|
941
|
+
memoryTypes: ["interaction"],
|
|
942
|
+
skipMostRecent: 5,
|
|
943
|
+
targetCount: Math.min(15, count)
|
|
944
|
+
},
|
|
945
|
+
this.config.hooks?.onError ? (err) => this.config.hooks.onError(err, "summarize") : void 0
|
|
946
|
+
);
|
|
947
|
+
summaryCreated = result.summary;
|
|
948
|
+
this.config.hooks?.onSummaryCreated?.(result.summary, result.deletedCount);
|
|
949
|
+
} catch (err) {
|
|
950
|
+
this.config.hooks?.onError?.(
|
|
951
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
952
|
+
"summarize"
|
|
953
|
+
);
|
|
685
954
|
}
|
|
686
955
|
}
|
|
956
|
+
this.pruneCheckCount++;
|
|
687
957
|
const pruneStrategy = this.config.defaults?.pruneStrategy;
|
|
688
958
|
const pruneInterval = this.config.defaults?.pruneInterval ?? 100;
|
|
689
|
-
if (pruneStrategy && this.
|
|
959
|
+
if (pruneStrategy && this.pruneCheckCount % pruneInterval === 0) {
|
|
690
960
|
try {
|
|
691
961
|
const pruneResult = await this.memory.prune(pruneStrategy);
|
|
692
962
|
if (pruneResult.count > 0) {
|
|
@@ -701,8 +971,8 @@ var MemStack = class {
|
|
|
701
971
|
}
|
|
702
972
|
return { memory, summaryCreated };
|
|
703
973
|
}
|
|
704
|
-
async export() {
|
|
705
|
-
const memories = await this.memory.export();
|
|
974
|
+
async export(actorId) {
|
|
975
|
+
const memories = await this.memory.export(actorId);
|
|
706
976
|
return {
|
|
707
977
|
version: 1,
|
|
708
978
|
memories,
|
|
@@ -772,14 +1042,16 @@ var MemStack = class {
|
|
|
772
1042
|
return cleaned;
|
|
773
1043
|
}
|
|
774
1044
|
};
|
|
775
|
-
var
|
|
1045
|
+
var DiskStorageAdapter = class {
|
|
776
1046
|
dir;
|
|
777
1047
|
_writeLocks = /* @__PURE__ */ new Map();
|
|
1048
|
+
_idIndex = /* @__PURE__ */ new Map();
|
|
778
1049
|
constructor(config = {}) {
|
|
779
1050
|
this.dir = config.storageDir ?? "./memstack-data";
|
|
780
1051
|
}
|
|
781
1052
|
async initialize() {
|
|
782
1053
|
await promises.mkdir(this.dir, { recursive: true });
|
|
1054
|
+
await this._buildIndex();
|
|
783
1055
|
}
|
|
784
1056
|
generateId() {
|
|
785
1057
|
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -809,6 +1081,7 @@ var DiskStorage = class {
|
|
|
809
1081
|
} else {
|
|
810
1082
|
records.push(memory);
|
|
811
1083
|
}
|
|
1084
|
+
this._idIndex.set(memory.id, input.actorId);
|
|
812
1085
|
await this._writeFile(input.actorId, records);
|
|
813
1086
|
});
|
|
814
1087
|
return this._toExternal(memory);
|
|
@@ -848,6 +1121,7 @@ var DiskStorage = class {
|
|
|
848
1121
|
} else {
|
|
849
1122
|
existing.push(mem);
|
|
850
1123
|
}
|
|
1124
|
+
this._idIndex.set(mem.id, actorId);
|
|
851
1125
|
}
|
|
852
1126
|
await this._writeFile(actorId, existing);
|
|
853
1127
|
});
|
|
@@ -855,8 +1129,8 @@ var DiskStorage = class {
|
|
|
855
1129
|
return all.map((m) => this._toExternal(m));
|
|
856
1130
|
}
|
|
857
1131
|
async get(id) {
|
|
858
|
-
const
|
|
859
|
-
|
|
1132
|
+
const actorId = this._idIndex.get(id);
|
|
1133
|
+
if (actorId) {
|
|
860
1134
|
const records = await this._readFile(actorId);
|
|
861
1135
|
const record = records.find((r) => r.id === id);
|
|
862
1136
|
if (record) {
|
|
@@ -864,26 +1138,43 @@ var DiskStorage = class {
|
|
|
864
1138
|
return this._toExternal(record);
|
|
865
1139
|
}
|
|
866
1140
|
}
|
|
1141
|
+
const actorIds = await this._listActors();
|
|
1142
|
+
for (const aid of actorIds) {
|
|
1143
|
+
const records = await this._readFile(aid);
|
|
1144
|
+
const record = records.find((r) => r.id === id);
|
|
1145
|
+
if (record) {
|
|
1146
|
+
this._idIndex.set(id, aid);
|
|
1147
|
+
if (record.expiresAt && new Date(record.expiresAt) <= /* @__PURE__ */ new Date()) return null;
|
|
1148
|
+
return this._toExternal(record);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
867
1151
|
return null;
|
|
868
1152
|
}
|
|
869
1153
|
async delete(id) {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
fresh.splice(i, 1);
|
|
880
|
-
await this._writeFile(actorId, fresh);
|
|
881
|
-
}
|
|
882
|
-
});
|
|
883
|
-
return;
|
|
1154
|
+
let actorId = this._idIndex.get(id);
|
|
1155
|
+
if (!actorId) {
|
|
1156
|
+
const actorIds = await this._listActors();
|
|
1157
|
+
for (const aid of actorIds) {
|
|
1158
|
+
const records = await this._readFile(aid);
|
|
1159
|
+
if (records.some((r) => r.id === id)) {
|
|
1160
|
+
actorId = aid;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
884
1163
|
}
|
|
885
1164
|
}
|
|
886
|
-
throw notFound("Memory", id);
|
|
1165
|
+
if (!actorId) throw notFound("Memory", id);
|
|
1166
|
+
let deleted = false;
|
|
1167
|
+
await this._withWriteLock(actorId, async () => {
|
|
1168
|
+
const fresh = await this._readFile(actorId);
|
|
1169
|
+
const i = fresh.findIndex((r) => r.id === id);
|
|
1170
|
+
if (i !== -1) {
|
|
1171
|
+
fresh.splice(i, 1);
|
|
1172
|
+
this._idIndex.delete(id);
|
|
1173
|
+
await this._writeFile(actorId, fresh);
|
|
1174
|
+
deleted = true;
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
if (!deleted) throw notFound("Memory", id);
|
|
887
1178
|
}
|
|
888
1179
|
async deleteMany(ids) {
|
|
889
1180
|
const idSet = new Set(ids);
|
|
@@ -896,6 +1187,9 @@ var DiskStorage = class {
|
|
|
896
1187
|
const filtered = records.filter((r) => !idSet.has(r.id));
|
|
897
1188
|
count += before - filtered.length;
|
|
898
1189
|
if (before !== filtered.length) {
|
|
1190
|
+
for (const r of records) {
|
|
1191
|
+
if (idSet.has(r.id)) this._idIndex.delete(r.id);
|
|
1192
|
+
}
|
|
899
1193
|
await this._writeFile(actorId, filtered);
|
|
900
1194
|
}
|
|
901
1195
|
});
|
|
@@ -978,24 +1272,16 @@ var DiskStorage = class {
|
|
|
978
1272
|
return total;
|
|
979
1273
|
}
|
|
980
1274
|
async touch(id) {
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
match._touchedAt = nowStr;
|
|
992
|
-
await this._writeFile(actorId, fresh);
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
throw notFound("Memory", id);
|
|
1275
|
+
const actorId = this._idIndex.get(id);
|
|
1276
|
+
if (!actorId) throw notFound("Memory", id);
|
|
1277
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
1278
|
+
await this._withWriteLock(actorId, async () => {
|
|
1279
|
+
const fresh = await this._readFile(actorId);
|
|
1280
|
+
const match = fresh.find((r) => r.id === id);
|
|
1281
|
+
if (!match) throw notFound("Memory", id);
|
|
1282
|
+
match._touchedAt = nowStr;
|
|
1283
|
+
await this._writeFile(actorId, fresh);
|
|
1284
|
+
});
|
|
999
1285
|
}
|
|
1000
1286
|
async close() {
|
|
1001
1287
|
this._writeLocks.clear();
|
|
@@ -1048,6 +1334,15 @@ var DiskStorage = class {
|
|
|
1048
1334
|
throw storageError(`Failed to list actors: ${err.message}`);
|
|
1049
1335
|
}
|
|
1050
1336
|
}
|
|
1337
|
+
async _buildIndex() {
|
|
1338
|
+
const actorIds = await this._listActors();
|
|
1339
|
+
for (const actorId of actorIds) {
|
|
1340
|
+
const records = await this._readFile(actorId);
|
|
1341
|
+
for (const r of records) {
|
|
1342
|
+
this._idIndex.set(r.id, actorId);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1051
1346
|
_toExternal(record) {
|
|
1052
1347
|
const { _touchedAt: _, ...rest } = record;
|
|
1053
1348
|
return {
|
|
@@ -1057,16 +1352,18 @@ var DiskStorage = class {
|
|
|
1057
1352
|
};
|
|
1058
1353
|
}
|
|
1059
1354
|
};
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1355
|
+
var MarkdownStorageAdapter = class {
|
|
1356
|
+
dir;
|
|
1357
|
+
oneFilePerActor;
|
|
1358
|
+
_cache = null;
|
|
1359
|
+
_deletedIds = /* @__PURE__ */ new Set();
|
|
1360
|
+
_cacheDirty = true;
|
|
1065
1361
|
constructor(config) {
|
|
1066
|
-
this.
|
|
1067
|
-
this.
|
|
1362
|
+
this.dir = config.dir;
|
|
1363
|
+
this.oneFilePerActor = config.oneFilePerActor ?? true;
|
|
1068
1364
|
}
|
|
1069
1365
|
async initialize() {
|
|
1366
|
+
await promises.mkdir(this.dir, { recursive: true });
|
|
1070
1367
|
}
|
|
1071
1368
|
generateId() {
|
|
1072
1369
|
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
@@ -1085,99 +1382,89 @@ var RedisStorage = class {
|
|
|
1085
1382
|
sourceId: input.sourceId,
|
|
1086
1383
|
metadata: input.metadata ?? {},
|
|
1087
1384
|
expiresAt: input.expiresAt,
|
|
1088
|
-
createdAt: now
|
|
1089
|
-
_touchedAt: now.toISOString()
|
|
1385
|
+
createdAt: now
|
|
1090
1386
|
};
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
await
|
|
1094
|
-
|
|
1387
|
+
const filePath = this._filePath(input.actorId);
|
|
1388
|
+
const block = this._formatBlock(memory);
|
|
1389
|
+
await promises.appendFile(filePath, block, "utf-8");
|
|
1390
|
+
this._cacheDirty = true;
|
|
1391
|
+
return memory;
|
|
1095
1392
|
}
|
|
1096
1393
|
async storeBatch(inputs) {
|
|
1097
|
-
|
|
1098
|
-
const now = /* @__PURE__ */ new Date();
|
|
1394
|
+
const results = [];
|
|
1099
1395
|
for (const input of inputs) {
|
|
1100
|
-
|
|
1101
|
-
id: input.id ?? this.generateId(),
|
|
1102
|
-
actorId: input.actorId,
|
|
1103
|
-
memoryType: input.memoryType ?? "interaction",
|
|
1104
|
-
content: input.content,
|
|
1105
|
-
importance: input.importance ?? 0.5,
|
|
1106
|
-
emotionalValence: input.emotionalValence ?? 0,
|
|
1107
|
-
tags: input.tags ?? [],
|
|
1108
|
-
embedding: input.embedding,
|
|
1109
|
-
sourceId: input.sourceId,
|
|
1110
|
-
metadata: input.metadata ?? {},
|
|
1111
|
-
expiresAt: input.expiresAt,
|
|
1112
|
-
createdAt: now,
|
|
1113
|
-
_touchedAt: now.toISOString()
|
|
1114
|
-
};
|
|
1115
|
-
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
1116
|
-
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
1117
|
-
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
1118
|
-
results.push(this._toExternal(memory));
|
|
1396
|
+
results.push(await this.store(input));
|
|
1119
1397
|
}
|
|
1120
1398
|
return results;
|
|
1121
1399
|
}
|
|
1122
1400
|
async get(id) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if (
|
|
1127
|
-
|
|
1401
|
+
await this._ensureCache();
|
|
1402
|
+
const memory = this._cache.get(id);
|
|
1403
|
+
if (!memory) return null;
|
|
1404
|
+
if (memory.expiresAt && memory.expiresAt <= /* @__PURE__ */ new Date()) return null;
|
|
1405
|
+
if (this._deletedIds.has(id)) return null;
|
|
1406
|
+
return memory;
|
|
1128
1407
|
}
|
|
1129
1408
|
async delete(id) {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1409
|
+
await this._ensureCache();
|
|
1410
|
+
const memory = this._cache.get(id);
|
|
1411
|
+
if (!memory || this._deletedIds.has(id)) {
|
|
1412
|
+
throw notFound("Memory", id);
|
|
1413
|
+
}
|
|
1414
|
+
if (memory.expiresAt && memory.expiresAt <= /* @__PURE__ */ new Date()) {
|
|
1415
|
+
throw notFound("Memory", id);
|
|
1416
|
+
}
|
|
1417
|
+
const filePath = this._filePath(memory.actorId);
|
|
1418
|
+
const tombstone = this._formatTombstone(id);
|
|
1419
|
+
await promises.appendFile(filePath, tombstone, "utf-8");
|
|
1420
|
+
this._cacheDirty = true;
|
|
1135
1421
|
}
|
|
1136
1422
|
async deleteMany(ids) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1423
|
+
await this._ensureCache();
|
|
1424
|
+
new Set(ids);
|
|
1425
|
+
const toDelete = [];
|
|
1139
1426
|
for (const id of ids) {
|
|
1140
|
-
const
|
|
1141
|
-
if (
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
byActor.set(record.actorId, list);
|
|
1427
|
+
const memory = this._cache.get(id);
|
|
1428
|
+
if (memory && !this._deletedIds.has(id)) {
|
|
1429
|
+
if (!memory.expiresAt || memory.expiresAt > /* @__PURE__ */ new Date()) {
|
|
1430
|
+
toDelete.push({ id, actorId: memory.actorId });
|
|
1431
|
+
}
|
|
1146
1432
|
}
|
|
1147
1433
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1434
|
+
const byActor = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const { id, actorId } of toDelete) {
|
|
1436
|
+
const group = byActor.get(actorId) ?? [];
|
|
1437
|
+
group.push(id);
|
|
1438
|
+
byActor.set(actorId, group);
|
|
1152
1439
|
}
|
|
1153
|
-
|
|
1440
|
+
for (const [actorId, groupIds] of byActor) {
|
|
1441
|
+
const filePath = this._filePath(actorId);
|
|
1442
|
+
const blocks = groupIds.map((gid) => this._formatTombstone(gid)).join("");
|
|
1443
|
+
await promises.appendFile(filePath, blocks, "utf-8");
|
|
1444
|
+
}
|
|
1445
|
+
this._cacheDirty = true;
|
|
1446
|
+
return toDelete.length;
|
|
1154
1447
|
}
|
|
1155
1448
|
async retrieve(query, _embedding) {
|
|
1156
|
-
|
|
1449
|
+
await this._ensureCache();
|
|
1450
|
+
const now = /* @__PURE__ */ new Date();
|
|
1451
|
+
let results = Array.from(this._cache.values()).filter((m) => !this._deletedIds.has(m.id)).filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
1157
1452
|
if (query.actorId) {
|
|
1158
|
-
|
|
1159
|
-
} else {
|
|
1160
|
-
const actorIds = await this._listActors();
|
|
1161
|
-
for (const actorId of actorIds) {
|
|
1162
|
-
const records = await this._getActorRecords(actorId);
|
|
1163
|
-
allRecords.push(...records);
|
|
1164
|
-
}
|
|
1453
|
+
results = results.filter((m) => m.actorId === query.actorId);
|
|
1165
1454
|
}
|
|
1166
|
-
const now = /* @__PURE__ */ new Date();
|
|
1167
|
-
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
1168
1455
|
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
1169
|
-
results = results.filter((
|
|
1456
|
+
results = results.filter((m) => query.memoryTypes.includes(m.memoryType));
|
|
1170
1457
|
}
|
|
1171
1458
|
if (query.tags && query.tags.length > 0) {
|
|
1172
|
-
results = results.filter((
|
|
1459
|
+
results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
|
|
1173
1460
|
}
|
|
1174
1461
|
if (query.query) {
|
|
1175
1462
|
const q = query.query.toLowerCase();
|
|
1176
|
-
results = results.filter((
|
|
1463
|
+
results = results.filter((m) => m.content.toLowerCase().includes(q));
|
|
1177
1464
|
}
|
|
1178
1465
|
switch (query.strategy) {
|
|
1179
1466
|
case "recent":
|
|
1180
|
-
results.sort((a, b) =>
|
|
1467
|
+
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1181
1468
|
break;
|
|
1182
1469
|
case "important":
|
|
1183
1470
|
results.sort((a, b) => b.importance - a.importance);
|
|
@@ -1186,97 +1473,472 @@ var RedisStorage = class {
|
|
|
1186
1473
|
case "semantic":
|
|
1187
1474
|
default:
|
|
1188
1475
|
results.sort(
|
|
1189
|
-
(a, b) => b.importance - a.importance ||
|
|
1476
|
+
(a, b) => b.importance - a.importance || b.createdAt.getTime() - a.createdAt.getTime()
|
|
1190
1477
|
);
|
|
1191
1478
|
break;
|
|
1192
1479
|
}
|
|
1193
|
-
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
1194
|
-
const toTouch = results.slice(0, query.limit ?? 10);
|
|
1195
|
-
for (const r of toTouch) {
|
|
1196
|
-
r._touchedAt = nowStr;
|
|
1197
|
-
await this.redis.set(this._memKey(r.id), JSON.stringify(r));
|
|
1198
|
-
}
|
|
1199
1480
|
const limit = query.limit ?? 10;
|
|
1200
|
-
return results.slice(0, limit)
|
|
1481
|
+
return results.slice(0, limit);
|
|
1201
1482
|
}
|
|
1202
1483
|
async count(filter) {
|
|
1203
|
-
|
|
1484
|
+
await this._ensureCache();
|
|
1485
|
+
const now = /* @__PURE__ */ new Date();
|
|
1486
|
+
let results = Array.from(this._cache.values()).filter((m) => !this._deletedIds.has(m.id)).filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
1204
1487
|
if (filter?.actorId) {
|
|
1205
|
-
|
|
1206
|
-
} else {
|
|
1207
|
-
const actorIds = await this._listActors();
|
|
1208
|
-
for (const actorId of actorIds) {
|
|
1209
|
-
const records = await this._getActorRecords(actorId);
|
|
1210
|
-
allRecords.push(...records);
|
|
1211
|
-
}
|
|
1488
|
+
results = results.filter((m) => m.actorId === filter.actorId);
|
|
1212
1489
|
}
|
|
1213
|
-
const now = /* @__PURE__ */ new Date();
|
|
1214
|
-
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
1215
1490
|
if (filter?.memoryType) {
|
|
1216
|
-
results = results.filter((
|
|
1491
|
+
results = results.filter((m) => m.memoryType === filter.memoryType);
|
|
1217
1492
|
}
|
|
1218
1493
|
if (filter?.minImportance !== void 0) {
|
|
1219
|
-
results = results.filter((
|
|
1494
|
+
results = results.filter((m) => m.importance >= filter.minImportance);
|
|
1220
1495
|
}
|
|
1221
1496
|
return results.length;
|
|
1222
1497
|
}
|
|
1223
1498
|
async close() {
|
|
1499
|
+
this._cache = null;
|
|
1500
|
+
this._deletedIds.clear();
|
|
1501
|
+
this._cacheDirty = true;
|
|
1224
1502
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1503
|
+
async vacuum() {
|
|
1504
|
+
if (this.oneFilePerActor) {
|
|
1505
|
+
const actorIds = await this._listActors();
|
|
1506
|
+
for (const actorId of actorIds) {
|
|
1507
|
+
await this._vacuumFile(this._filePath(actorId));
|
|
1508
|
+
}
|
|
1509
|
+
} else {
|
|
1510
|
+
await this._vacuumFile(this._filePath(""));
|
|
1511
|
+
}
|
|
1512
|
+
this._cacheDirty = true;
|
|
1228
1513
|
}
|
|
1229
|
-
|
|
1514
|
+
// ── Internal ──
|
|
1515
|
+
_filePath(actorId) {
|
|
1516
|
+
if (!this.oneFilePerActor) {
|
|
1517
|
+
return path.join(this.dir, "memories.md");
|
|
1518
|
+
}
|
|
1230
1519
|
const safe = actorId.replace(/[^a-zA-Z0-9_\-.,@]/g, "_");
|
|
1231
|
-
return
|
|
1520
|
+
return path.join(this.dir, `${safe}.md`);
|
|
1232
1521
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1522
|
+
_formatBlock(memory) {
|
|
1523
|
+
const iso = memory.createdAt.toISOString();
|
|
1524
|
+
const meta = JSON.stringify({
|
|
1525
|
+
id: memory.id,
|
|
1526
|
+
actorId: memory.actorId,
|
|
1527
|
+
memoryType: memory.memoryType,
|
|
1528
|
+
importance: memory.importance,
|
|
1529
|
+
emotionalValence: memory.emotionalValence,
|
|
1530
|
+
tags: memory.tags,
|
|
1531
|
+
embedding: memory.embedding,
|
|
1532
|
+
sourceId: memory.sourceId ?? null,
|
|
1533
|
+
metadata: memory.metadata ?? {},
|
|
1534
|
+
createdAt: memory.createdAt.toISOString(),
|
|
1535
|
+
expiresAt: memory.expiresAt?.toISOString() ?? null
|
|
1536
|
+
});
|
|
1537
|
+
return `## ${iso}
|
|
1538
|
+
|
|
1539
|
+
~~~json
|
|
1540
|
+
${meta}
|
|
1541
|
+
~~~
|
|
1542
|
+
|
|
1543
|
+
${memory.content}
|
|
1544
|
+
|
|
1545
|
+
---
|
|
1546
|
+
`;
|
|
1235
1547
|
}
|
|
1236
|
-
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1548
|
+
_formatTombstone(id) {
|
|
1549
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1550
|
+
return `## ${iso}
|
|
1551
|
+
|
|
1552
|
+
<!-- deleted: ${id} -->
|
|
1553
|
+
|
|
1554
|
+
---
|
|
1555
|
+
`;
|
|
1556
|
+
}
|
|
1557
|
+
async _ensureCache() {
|
|
1558
|
+
if (!this._cacheDirty) return;
|
|
1559
|
+
const allMemories = /* @__PURE__ */ new Map();
|
|
1560
|
+
const allDeletedIds = /* @__PURE__ */ new Set();
|
|
1561
|
+
if (this.oneFilePerActor) {
|
|
1562
|
+
const actorIds = await this._listActors();
|
|
1563
|
+
for (const actorId of actorIds) {
|
|
1564
|
+
const { memories, deletedIds } = await this._parseFile(this._filePath(actorId));
|
|
1565
|
+
for (const mem of memories) {
|
|
1566
|
+
allMemories.set(mem.id, mem);
|
|
1567
|
+
}
|
|
1568
|
+
for (const did of deletedIds) {
|
|
1569
|
+
allDeletedIds.add(did);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} else {
|
|
1573
|
+
const { memories, deletedIds } = await this._parseFile(this._filePath(""));
|
|
1574
|
+
for (const mem of memories) {
|
|
1575
|
+
allMemories.set(mem.id, mem);
|
|
1576
|
+
}
|
|
1577
|
+
for (const did of deletedIds) {
|
|
1578
|
+
allDeletedIds.add(did);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
this._cache = allMemories;
|
|
1582
|
+
this._deletedIds = allDeletedIds;
|
|
1583
|
+
this._cacheDirty = false;
|
|
1584
|
+
}
|
|
1585
|
+
async _parseFile(filePath) {
|
|
1586
|
+
let raw;
|
|
1587
|
+
try {
|
|
1588
|
+
raw = await promises.readFile(filePath, "utf-8");
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
if (err.code === "ENOENT") {
|
|
1591
|
+
return { memories: [], deletedIds: [] };
|
|
1592
|
+
}
|
|
1593
|
+
throw storageError(`Failed to read file ${filePath}: ${err.message}`);
|
|
1594
|
+
}
|
|
1595
|
+
const memories = [];
|
|
1596
|
+
const deletedIds = [];
|
|
1597
|
+
const sections = raw.split("\n---\n");
|
|
1598
|
+
for (const section of sections) {
|
|
1599
|
+
const trimmed = section.trim();
|
|
1600
|
+
if (!trimmed) continue;
|
|
1601
|
+
const cleanBlock = trimmed.replace(/\n---$/, "");
|
|
1602
|
+
const lines = cleanBlock.split("\n");
|
|
1603
|
+
let i = 0;
|
|
1604
|
+
if (!lines[i]?.startsWith("## ")) continue;
|
|
1605
|
+
i++;
|
|
1606
|
+
while (i < lines.length && lines[i].trim() === "") i++;
|
|
1607
|
+
if (i >= lines.length) continue;
|
|
1608
|
+
const tombstoneMatch = lines[i].trim().match(/^<!-- deleted:\s*(\S+)\s*-->/);
|
|
1609
|
+
if (tombstoneMatch) {
|
|
1610
|
+
deletedIds.push(tombstoneMatch[1]);
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (lines[i].trim() !== "~~~json") continue;
|
|
1614
|
+
i++;
|
|
1615
|
+
const jsonLines = [];
|
|
1616
|
+
while (i < lines.length && lines[i].trim() !== "~~~") {
|
|
1617
|
+
jsonLines.push(lines[i]);
|
|
1618
|
+
i++;
|
|
1619
|
+
}
|
|
1620
|
+
i++;
|
|
1621
|
+
let metadata = null;
|
|
1622
|
+
try {
|
|
1623
|
+
metadata = JSON.parse(jsonLines.join("\n"));
|
|
1624
|
+
} catch {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
while (i < lines.length && lines[i].trim() === "") i++;
|
|
1628
|
+
const contentLines = [];
|
|
1629
|
+
while (i < lines.length) {
|
|
1630
|
+
contentLines.push(lines[i]);
|
|
1631
|
+
i++;
|
|
1632
|
+
}
|
|
1633
|
+
const content = contentLines.join("\n").trim();
|
|
1634
|
+
if (metadata && typeof metadata.id === "string") {
|
|
1635
|
+
memories.push({
|
|
1636
|
+
id: metadata.id,
|
|
1637
|
+
actorId: metadata.actorId,
|
|
1638
|
+
memoryType: metadata.memoryType ?? "interaction",
|
|
1639
|
+
content,
|
|
1640
|
+
importance: metadata.importance ?? 0.5,
|
|
1641
|
+
emotionalValence: metadata.emotionalValence ?? 0,
|
|
1642
|
+
tags: metadata.tags ?? [],
|
|
1643
|
+
embedding: metadata.embedding,
|
|
1644
|
+
sourceId: metadata.sourceId ?? void 0,
|
|
1645
|
+
metadata: metadata.metadata ?? {},
|
|
1646
|
+
createdAt: new Date(metadata.createdAt),
|
|
1647
|
+
expiresAt: metadata.expiresAt ? new Date(metadata.expiresAt) : void 0
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return { memories, deletedIds };
|
|
1652
|
+
}
|
|
1653
|
+
async _vacuumFile(filePath) {
|
|
1654
|
+
await this._ensureCache();
|
|
1655
|
+
let raw;
|
|
1656
|
+
try {
|
|
1657
|
+
raw = await promises.readFile(filePath, "utf-8");
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
if (err.code === "ENOENT") return;
|
|
1660
|
+
throw storageError(`Failed to read file for vacuum: ${err.message}`);
|
|
1661
|
+
}
|
|
1662
|
+
const sections = raw.split("\n---\n");
|
|
1663
|
+
const kept = [];
|
|
1664
|
+
for (const section of sections) {
|
|
1665
|
+
const trimmed = section.trim();
|
|
1666
|
+
if (!trimmed) continue;
|
|
1667
|
+
const cleanBlock = trimmed.replace(/\n---$/, "");
|
|
1668
|
+
const lines = cleanBlock.split("\n");
|
|
1669
|
+
let i = 0;
|
|
1670
|
+
if (!lines[i]?.startsWith("## ")) {
|
|
1671
|
+
kept.push(trimmed);
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
i++;
|
|
1675
|
+
while (i < lines.length && lines[i].trim() === "") i++;
|
|
1676
|
+
if (i >= lines.length) {
|
|
1677
|
+
kept.push(trimmed);
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
const tombstoneMatch = lines[i].trim().match(/^<!-- deleted:\s*(\S+)\s*-->/);
|
|
1681
|
+
if (tombstoneMatch) continue;
|
|
1682
|
+
if (lines[i].trim() !== "~~~json") {
|
|
1683
|
+
kept.push(trimmed);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
i++;
|
|
1687
|
+
while (i < lines.length && lines[i].trim() !== "~~~") i++;
|
|
1688
|
+
i++;
|
|
1689
|
+
while (i < lines.length && lines[i].trim() === "") i++;
|
|
1690
|
+
const contentLines = [];
|
|
1691
|
+
while (i < lines.length) {
|
|
1692
|
+
contentLines.push(lines[i]);
|
|
1693
|
+
i++;
|
|
1694
|
+
}
|
|
1695
|
+
try {
|
|
1696
|
+
const jsonStartIdx = lines.findIndex((l) => l.trim() === "~~~json");
|
|
1697
|
+
const jsonEndIdx = lines.findIndex((l, idx) => idx > jsonStartIdx && l.trim() === "~~~");
|
|
1698
|
+
const jsonText = lines.slice(jsonStartIdx + 1, jsonEndIdx).join("\n");
|
|
1699
|
+
const meta = JSON.parse(jsonText);
|
|
1700
|
+
if (this._deletedIds.has(meta.id)) continue;
|
|
1701
|
+
} catch {
|
|
1702
|
+
}
|
|
1703
|
+
const content = contentLines.join("\n").trim();
|
|
1704
|
+
if (content || trimmed.includes("~~~json")) {
|
|
1705
|
+
kept.push(trimmed);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const output = kept.length > 0 ? kept.join("\n---\n") + "\n---\n" : "";
|
|
1709
|
+
await promises.writeFile(filePath, output, "utf-8");
|
|
1710
|
+
}
|
|
1711
|
+
async _listActors() {
|
|
1712
|
+
try {
|
|
1713
|
+
const entries = await promises.readdir(this.dir);
|
|
1714
|
+
return entries.filter((e) => e.endsWith(".md")).map((e) => e.slice(0, -3));
|
|
1715
|
+
} catch (err) {
|
|
1716
|
+
if (err.code === "ENOENT") return [];
|
|
1717
|
+
throw storageError(`Failed to list actors: ${err.message}`);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// src/adapters/storage/hybrid.ts
|
|
1723
|
+
var HybridStorageAdapter = class {
|
|
1724
|
+
cache;
|
|
1725
|
+
durable;
|
|
1726
|
+
writeBoth;
|
|
1727
|
+
readFromCache;
|
|
1728
|
+
syncOnInit;
|
|
1729
|
+
constructor(config) {
|
|
1730
|
+
this.cache = config.cache;
|
|
1731
|
+
this.durable = config.durable;
|
|
1732
|
+
this.writeBoth = config.writeBoth ?? true;
|
|
1733
|
+
this.readFromCache = config.readFromCache ?? true;
|
|
1734
|
+
this.syncOnInit = config.syncOnInit ?? false;
|
|
1735
|
+
}
|
|
1736
|
+
async initialize() {
|
|
1737
|
+
await Promise.all([this.cache.initialize(), this.durable.initialize()]);
|
|
1738
|
+
if (this.syncOnInit) {
|
|
1739
|
+
const results = await this.durable.retrieve({ limit: 1e5 });
|
|
1740
|
+
if (results.length > 0) {
|
|
1741
|
+
await this.cache.storeBatch(results.map((m) => ({
|
|
1742
|
+
actorId: m.actorId,
|
|
1743
|
+
content: m.content,
|
|
1744
|
+
memoryType: m.memoryType,
|
|
1745
|
+
importance: m.importance,
|
|
1746
|
+
emotionalValence: m.emotionalValence,
|
|
1747
|
+
tags: m.tags,
|
|
1748
|
+
embedding: m.embedding,
|
|
1749
|
+
sourceId: m.sourceId,
|
|
1750
|
+
metadata: m.metadata,
|
|
1751
|
+
expiresAt: m.expiresAt,
|
|
1752
|
+
id: m.id
|
|
1753
|
+
}))).catch(() => {
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async store(input) {
|
|
1759
|
+
const result = await this.durable.store(input);
|
|
1760
|
+
if (this.writeBoth) {
|
|
1761
|
+
try {
|
|
1762
|
+
await this.cache.store({ ...input, id: result.id });
|
|
1763
|
+
} catch {
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return result;
|
|
1767
|
+
}
|
|
1768
|
+
async storeBatch(inputs) {
|
|
1769
|
+
const results = await this.durable.storeBatch(inputs);
|
|
1770
|
+
if (this.writeBoth) {
|
|
1771
|
+
const cacheInputs = results.map((r, i) => ({
|
|
1772
|
+
...inputs[i],
|
|
1773
|
+
id: r.id
|
|
1774
|
+
}));
|
|
1775
|
+
try {
|
|
1776
|
+
await this.cache.storeBatch(cacheInputs);
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return results;
|
|
1781
|
+
}
|
|
1782
|
+
async get(id) {
|
|
1783
|
+
if (this.readFromCache) {
|
|
1784
|
+
const cached = await this.cache.get(id);
|
|
1785
|
+
if (cached) return cached;
|
|
1786
|
+
const result = await this.durable.get(id);
|
|
1787
|
+
if (result) {
|
|
1244
1788
|
try {
|
|
1245
|
-
|
|
1789
|
+
await this.cache.store({
|
|
1790
|
+
actorId: result.actorId,
|
|
1791
|
+
content: result.content,
|
|
1792
|
+
memoryType: result.memoryType,
|
|
1793
|
+
importance: result.importance,
|
|
1794
|
+
emotionalValence: result.emotionalValence,
|
|
1795
|
+
tags: result.tags,
|
|
1796
|
+
embedding: result.embedding,
|
|
1797
|
+
sourceId: result.sourceId,
|
|
1798
|
+
metadata: result.metadata,
|
|
1799
|
+
expiresAt: result.expiresAt,
|
|
1800
|
+
id: result.id
|
|
1801
|
+
});
|
|
1246
1802
|
} catch {
|
|
1247
1803
|
}
|
|
1248
1804
|
}
|
|
1805
|
+
return result;
|
|
1249
1806
|
}
|
|
1250
|
-
return
|
|
1807
|
+
return this.durable.get(id);
|
|
1251
1808
|
}
|
|
1252
|
-
async
|
|
1253
|
-
|
|
1809
|
+
async retrieve(query, embedding) {
|
|
1810
|
+
if (this.readFromCache) {
|
|
1811
|
+
const cached = await this.cache.retrieve(query, embedding);
|
|
1812
|
+
if (cached.length > 0) return cached;
|
|
1813
|
+
const results = await this.durable.retrieve(query, embedding);
|
|
1814
|
+
if (results.length > 0) {
|
|
1815
|
+
const inputs = results.map((r) => ({
|
|
1816
|
+
actorId: r.actorId,
|
|
1817
|
+
content: r.content,
|
|
1818
|
+
memoryType: r.memoryType,
|
|
1819
|
+
importance: r.importance,
|
|
1820
|
+
emotionalValence: r.emotionalValence,
|
|
1821
|
+
tags: r.tags,
|
|
1822
|
+
embedding: r.embedding,
|
|
1823
|
+
sourceId: r.sourceId,
|
|
1824
|
+
metadata: r.metadata,
|
|
1825
|
+
expiresAt: r.expiresAt,
|
|
1826
|
+
id: r.id
|
|
1827
|
+
}));
|
|
1828
|
+
try {
|
|
1829
|
+
await this.cache.storeBatch(inputs);
|
|
1830
|
+
} catch {
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return results;
|
|
1834
|
+
}
|
|
1835
|
+
return this.durable.retrieve(query, embedding);
|
|
1254
1836
|
}
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1837
|
+
async delete(id) {
|
|
1838
|
+
const results = await Promise.allSettled([
|
|
1839
|
+
this.cache.delete(id),
|
|
1840
|
+
this.durable.delete(id)
|
|
1841
|
+
]);
|
|
1842
|
+
const durableResult = results[1];
|
|
1843
|
+
if (durableResult.status === "rejected") {
|
|
1844
|
+
throw durableResult.reason;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
async deleteMany(ids) {
|
|
1848
|
+
const [cacheResult, durableCount] = await Promise.all([
|
|
1849
|
+
this.cache.deleteMany(ids).catch(() => 0),
|
|
1850
|
+
this.durable.deleteMany(ids)
|
|
1851
|
+
]);
|
|
1852
|
+
return durableCount;
|
|
1853
|
+
}
|
|
1854
|
+
async count(filter) {
|
|
1855
|
+
return this.durable.count(filter);
|
|
1856
|
+
}
|
|
1857
|
+
async touch(id) {
|
|
1858
|
+
if (this.durable.touch) {
|
|
1859
|
+
await this.durable.touch(id);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async close() {
|
|
1863
|
+
await Promise.all([this.cache.close(), this.durable.close()]);
|
|
1262
1864
|
}
|
|
1263
1865
|
};
|
|
1264
1866
|
|
|
1265
1867
|
// src/adapters/storage/postgres.ts
|
|
1266
|
-
var
|
|
1868
|
+
var PostgresStorageAdapter = class {
|
|
1267
1869
|
pool;
|
|
1268
1870
|
table;
|
|
1269
|
-
|
|
1871
|
+
vectorDimensions;
|
|
1872
|
+
_ownsPool = false;
|
|
1270
1873
|
constructor(config) {
|
|
1271
|
-
this.pool = config.pool;
|
|
1272
1874
|
this.table = config.tableName ?? "memstack_memories";
|
|
1273
1875
|
if (!/^[a-zA-Z0-9_]+$/.test(this.table)) {
|
|
1274
|
-
throw
|
|
1876
|
+
throw configError(`Invalid table name: "${this.table}". Use only alphanumeric characters and underscores.`);
|
|
1877
|
+
}
|
|
1878
|
+
this.vectorDimensions = config.vectorDimensions ?? 1536;
|
|
1879
|
+
if (config.pool) {
|
|
1880
|
+
this.pool = config.pool;
|
|
1881
|
+
} else if (config.connectionString) {
|
|
1882
|
+
this._connectionString = config.connectionString;
|
|
1883
|
+
this._ssl = config.ssl;
|
|
1884
|
+
} else if (config.host || config.database || config.user) {
|
|
1885
|
+
this._connParts = { host: config.host, port: config.port, database: config.database, user: config.user, password: config.password, ssl: config.ssl };
|
|
1886
|
+
} else {
|
|
1887
|
+
throw configError("PostgresStorageAdapter requires a pool, connectionString, or connection details");
|
|
1275
1888
|
}
|
|
1276
|
-
this.autoMigrate = config.autoMigrate ?? true;
|
|
1277
1889
|
}
|
|
1890
|
+
_connectionString;
|
|
1891
|
+
_connParts;
|
|
1892
|
+
_ssl;
|
|
1278
1893
|
async initialize() {
|
|
1279
|
-
if (
|
|
1894
|
+
if (this._connectionString || this._connParts) {
|
|
1895
|
+
this.pool = await this._createPool();
|
|
1896
|
+
this._ownsPool = true;
|
|
1897
|
+
}
|
|
1898
|
+
await this._runMigration();
|
|
1899
|
+
}
|
|
1900
|
+
async _createPool() {
|
|
1901
|
+
const connStr = this._connectionString ?? this._buildConnString();
|
|
1902
|
+
try {
|
|
1903
|
+
const { default: createPostgres } = await import('postgres');
|
|
1904
|
+
const options = {};
|
|
1905
|
+
if (this._ssl) {
|
|
1906
|
+
options.ssl = typeof this._ssl === "object" ? this._ssl : "require";
|
|
1907
|
+
}
|
|
1908
|
+
const sql = createPostgres(connStr, options);
|
|
1909
|
+
return {
|
|
1910
|
+
query: (text, params) => sql.unsafe(text, params).then((r) => ({ rows: r })),
|
|
1911
|
+
end: () => sql.end()
|
|
1912
|
+
};
|
|
1913
|
+
} catch {
|
|
1914
|
+
try {
|
|
1915
|
+
const { Pool } = await import('pg');
|
|
1916
|
+
const pool = new Pool({ connectionString: connStr, ssl: this._ssl });
|
|
1917
|
+
return { query: (text, params) => pool.query(text, params), end: () => pool.end() };
|
|
1918
|
+
} catch {
|
|
1919
|
+
throw configError("connectionString requires 'postgres' (postgres.js) or 'pg' (node-postgres) to be installed");
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
_buildConnString() {
|
|
1924
|
+
const p = this._connParts;
|
|
1925
|
+
const host = p.host ?? "localhost";
|
|
1926
|
+
const port = p.port ?? 5432;
|
|
1927
|
+
const db = p.database ?? "postgres";
|
|
1928
|
+
const user = encodeURIComponent(p.user ?? "postgres");
|
|
1929
|
+
const pw = p.password ? `:${encodeURIComponent(p.password)}` : "";
|
|
1930
|
+
let connStr = `postgres://${user}${pw}@${host}:${port}/${db}`;
|
|
1931
|
+
if (this._ssl) {
|
|
1932
|
+
const sslmode = typeof this._ssl === "object" ? "no-verify" : "require";
|
|
1933
|
+
connStr += `?sslmode=${sslmode}`;
|
|
1934
|
+
}
|
|
1935
|
+
return connStr;
|
|
1936
|
+
}
|
|
1937
|
+
async _runMigration() {
|
|
1938
|
+
try {
|
|
1939
|
+
await this.pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
1940
|
+
} catch {
|
|
1941
|
+
}
|
|
1280
1942
|
await this.pool.query(`
|
|
1281
1943
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
1282
1944
|
id TEXT PRIMARY KEY,
|
|
@@ -1286,7 +1948,7 @@ var PostgresStorage = class {
|
|
|
1286
1948
|
importance REAL NOT NULL DEFAULT 0.5,
|
|
1287
1949
|
emotional_valence REAL NOT NULL DEFAULT 0,
|
|
1288
1950
|
tags JSONB NOT NULL DEFAULT '[]',
|
|
1289
|
-
embedding vector(
|
|
1951
|
+
embedding vector(${this.vectorDimensions}),
|
|
1290
1952
|
source_id TEXT,
|
|
1291
1953
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
1292
1954
|
expires_at TIMESTAMPTZ,
|
|
@@ -1296,7 +1958,9 @@ var PostgresStorage = class {
|
|
|
1296
1958
|
`);
|
|
1297
1959
|
try {
|
|
1298
1960
|
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_actor ON ${this.table} (actor_id)`);
|
|
1299
|
-
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}
|
|
1961
|
+
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_created ON ${this.table} (created_at DESC)`);
|
|
1962
|
+
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_importance ON ${this.table} (importance DESC)`);
|
|
1963
|
+
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding ON ${this.table} USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64)`);
|
|
1300
1964
|
} catch {
|
|
1301
1965
|
}
|
|
1302
1966
|
}
|
|
@@ -1378,6 +2042,11 @@ var PostgresStorage = class {
|
|
|
1378
2042
|
);
|
|
1379
2043
|
return rows.length;
|
|
1380
2044
|
}
|
|
2045
|
+
async touch(id) {
|
|
2046
|
+
const { rows } = await this.pool.query(`SELECT id FROM ${this.table} WHERE id = $1`, [id]);
|
|
2047
|
+
if (rows.length === 0) throw notFound("Memory", id);
|
|
2048
|
+
await this.pool.query(`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`, [id]);
|
|
2049
|
+
}
|
|
1381
2050
|
async retrieve(query, embedding) {
|
|
1382
2051
|
const conditions = ["(expires_at IS NULL OR expires_at > NOW())"];
|
|
1383
2052
|
const params = [];
|
|
@@ -1398,6 +2067,14 @@ var PostgresStorage = class {
|
|
|
1398
2067
|
conditions.push(`content ILIKE $${paramIdx++}`);
|
|
1399
2068
|
params.push(`%${query.query}%`);
|
|
1400
2069
|
}
|
|
2070
|
+
if (query.createdAfter) {
|
|
2071
|
+
conditions.push(`created_at >= $${paramIdx++}`);
|
|
2072
|
+
params.push(query.createdAfter.toISOString());
|
|
2073
|
+
}
|
|
2074
|
+
if (query.createdBefore) {
|
|
2075
|
+
conditions.push(`created_at <= $${paramIdx++}`);
|
|
2076
|
+
params.push(query.createdBefore.toISOString());
|
|
2077
|
+
}
|
|
1401
2078
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1402
2079
|
let orderBy;
|
|
1403
2080
|
switch (query.strategy) {
|
|
@@ -1410,8 +2087,8 @@ var PostgresStorage = class {
|
|
|
1410
2087
|
case "hybrid":
|
|
1411
2088
|
case "semantic":
|
|
1412
2089
|
default:
|
|
1413
|
-
if (embedding
|
|
1414
|
-
return this._semanticRetrieve(embedding, query, params, where);
|
|
2090
|
+
if (embedding) {
|
|
2091
|
+
return this._semanticRetrieve(embedding, query, params, where, paramIdx);
|
|
1415
2092
|
}
|
|
1416
2093
|
orderBy = "importance DESC, touched_at DESC";
|
|
1417
2094
|
break;
|
|
@@ -1422,10 +2099,7 @@ var PostgresStorage = class {
|
|
|
1422
2099
|
const { rows } = await this.pool.query(sql, params);
|
|
1423
2100
|
for (const row of rows.slice(0, limit)) {
|
|
1424
2101
|
const r = row;
|
|
1425
|
-
await this.pool.query(
|
|
1426
|
-
`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`,
|
|
1427
|
-
[r.id]
|
|
1428
|
-
);
|
|
2102
|
+
await this.pool.query(`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`, [r.id]);
|
|
1429
2103
|
}
|
|
1430
2104
|
return rows.map((r) => this._rowToMemory(r));
|
|
1431
2105
|
}
|
|
@@ -1445,6 +2119,14 @@ var PostgresStorage = class {
|
|
|
1445
2119
|
conditions.push(`importance >= $${paramIdx++}`);
|
|
1446
2120
|
params.push(filter.minImportance);
|
|
1447
2121
|
}
|
|
2122
|
+
if (filter?.createdAfter) {
|
|
2123
|
+
conditions.push(`created_at >= $${paramIdx++}`);
|
|
2124
|
+
params.push(filter.createdAfter.toISOString());
|
|
2125
|
+
}
|
|
2126
|
+
if (filter?.createdBefore) {
|
|
2127
|
+
conditions.push(`created_at <= $${paramIdx++}`);
|
|
2128
|
+
params.push(filter.createdBefore.toISOString());
|
|
2129
|
+
}
|
|
1448
2130
|
const where = conditions.join(" AND ");
|
|
1449
2131
|
const { rows } = await this.pool.query(
|
|
1450
2132
|
`SELECT COUNT(*) as count FROM ${this.table} WHERE ${where}`,
|
|
@@ -1453,22 +2135,32 @@ var PostgresStorage = class {
|
|
|
1453
2135
|
return Number(rows[0].count);
|
|
1454
2136
|
}
|
|
1455
2137
|
async close() {
|
|
2138
|
+
if (this._ownsPool && this.pool?.end) {
|
|
2139
|
+
await this.pool.end();
|
|
2140
|
+
}
|
|
1456
2141
|
}
|
|
1457
|
-
|
|
1458
|
-
async _semanticRetrieve(embedding, query, params, baseWhere) {
|
|
1459
|
-
const vecParam = `{${embedding.join(",")}}`;
|
|
2142
|
+
async _semanticRetrieve(embedding, query, params, baseWhere, paramIdx) {
|
|
1460
2143
|
const limit = query.limit ?? 10;
|
|
1461
2144
|
const conditions = baseWhere.replace(/^WHERE\s+/i, "");
|
|
1462
2145
|
const whereClause = conditions ? `WHERE ${conditions} AND embedding IS NOT NULL` : `WHERE embedding IS NOT NULL`;
|
|
1463
|
-
const
|
|
2146
|
+
const isHybrid = query.strategy === "hybrid";
|
|
2147
|
+
const sql = isHybrid ? `
|
|
2148
|
+
SELECT *,
|
|
2149
|
+
(embedding <-> $${paramIdx}::float8[]::vector) AS distance,
|
|
2150
|
+
(1.0 - (embedding <-> $${paramIdx}::float8[]::vector) / 2.0 + importance) AS score
|
|
2151
|
+
FROM ${this.table}
|
|
2152
|
+
${whereClause}
|
|
2153
|
+
ORDER BY score DESC
|
|
2154
|
+
LIMIT $${paramIdx + 1}
|
|
2155
|
+
` : `
|
|
1464
2156
|
SELECT *,
|
|
1465
|
-
(embedding <-> $${
|
|
2157
|
+
(embedding <-> $${paramIdx}::float8[]::vector) AS distance
|
|
1466
2158
|
FROM ${this.table}
|
|
1467
2159
|
${whereClause}
|
|
1468
2160
|
ORDER BY distance ASC
|
|
1469
|
-
LIMIT $${
|
|
2161
|
+
LIMIT $${paramIdx + 1}
|
|
1470
2162
|
`;
|
|
1471
|
-
params.push(
|
|
2163
|
+
params.push(embedding);
|
|
1472
2164
|
params.push(limit);
|
|
1473
2165
|
const { rows } = await this.pool.query(sql, params);
|
|
1474
2166
|
for (const row of rows.slice(0, limit)) {
|
|
@@ -1495,6 +2187,1257 @@ var PostgresStorage = class {
|
|
|
1495
2187
|
}
|
|
1496
2188
|
};
|
|
1497
2189
|
|
|
2190
|
+
// src/adapters/storage/redis.ts
|
|
2191
|
+
var RedisStorageAdapter = class {
|
|
2192
|
+
redis;
|
|
2193
|
+
prefix;
|
|
2194
|
+
constructor(config) {
|
|
2195
|
+
this.redis = config.redis;
|
|
2196
|
+
this.prefix = config.keyPrefix ?? "memstack";
|
|
2197
|
+
}
|
|
2198
|
+
async initialize() {
|
|
2199
|
+
}
|
|
2200
|
+
generateId() {
|
|
2201
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
2202
|
+
}
|
|
2203
|
+
async store(input) {
|
|
2204
|
+
const now = /* @__PURE__ */ new Date();
|
|
2205
|
+
const memory = {
|
|
2206
|
+
id: input.id ?? this.generateId(),
|
|
2207
|
+
actorId: input.actorId,
|
|
2208
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2209
|
+
content: input.content,
|
|
2210
|
+
importance: input.importance ?? 0.5,
|
|
2211
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2212
|
+
tags: input.tags ?? [],
|
|
2213
|
+
embedding: input.embedding,
|
|
2214
|
+
sourceId: input.sourceId,
|
|
2215
|
+
metadata: input.metadata ?? {},
|
|
2216
|
+
expiresAt: input.expiresAt,
|
|
2217
|
+
createdAt: now,
|
|
2218
|
+
_touchedAt: now.toISOString()
|
|
2219
|
+
};
|
|
2220
|
+
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
2221
|
+
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
2222
|
+
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
2223
|
+
return this._toExternal(memory);
|
|
2224
|
+
}
|
|
2225
|
+
async storeBatch(inputs) {
|
|
2226
|
+
const results = [];
|
|
2227
|
+
const now = /* @__PURE__ */ new Date();
|
|
2228
|
+
for (const input of inputs) {
|
|
2229
|
+
const memory = {
|
|
2230
|
+
id: input.id ?? this.generateId(),
|
|
2231
|
+
actorId: input.actorId,
|
|
2232
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2233
|
+
content: input.content,
|
|
2234
|
+
importance: input.importance ?? 0.5,
|
|
2235
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2236
|
+
tags: input.tags ?? [],
|
|
2237
|
+
embedding: input.embedding,
|
|
2238
|
+
sourceId: input.sourceId,
|
|
2239
|
+
metadata: input.metadata ?? {},
|
|
2240
|
+
expiresAt: input.expiresAt,
|
|
2241
|
+
createdAt: now,
|
|
2242
|
+
_touchedAt: now.toISOString()
|
|
2243
|
+
};
|
|
2244
|
+
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
2245
|
+
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
2246
|
+
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
2247
|
+
results.push(this._toExternal(memory));
|
|
2248
|
+
}
|
|
2249
|
+
return results;
|
|
2250
|
+
}
|
|
2251
|
+
async get(id) {
|
|
2252
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
2253
|
+
if (!raw) return null;
|
|
2254
|
+
const record = JSON.parse(raw);
|
|
2255
|
+
if (record.expiresAt && new Date(record.expiresAt) <= /* @__PURE__ */ new Date()) return null;
|
|
2256
|
+
return this._toExternal(record);
|
|
2257
|
+
}
|
|
2258
|
+
async delete(id) {
|
|
2259
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
2260
|
+
if (!raw) throw notFound("Memory", id);
|
|
2261
|
+
const record = JSON.parse(raw);
|
|
2262
|
+
await this.redis.del(this._memKey(id));
|
|
2263
|
+
await this.redis.srem(this._actorKey(record.actorId), id);
|
|
2264
|
+
}
|
|
2265
|
+
async deleteMany(ids) {
|
|
2266
|
+
let count = 0;
|
|
2267
|
+
const byActor = /* @__PURE__ */ new Map();
|
|
2268
|
+
for (const id of ids) {
|
|
2269
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
2270
|
+
if (raw) {
|
|
2271
|
+
const record = JSON.parse(raw);
|
|
2272
|
+
const list = byActor.get(record.actorId) ?? [];
|
|
2273
|
+
list.push(id);
|
|
2274
|
+
byActor.set(record.actorId, list);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
for (const [actorId, memIds] of byActor) {
|
|
2278
|
+
await this.redis.del(memIds.map((id) => this._memKey(id)));
|
|
2279
|
+
await this.redis.srem(this._actorKey(actorId), ...memIds);
|
|
2280
|
+
count += memIds.length;
|
|
2281
|
+
}
|
|
2282
|
+
return count;
|
|
2283
|
+
}
|
|
2284
|
+
async touch(id) {
|
|
2285
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
2286
|
+
if (!raw) throw notFound("Memory", id);
|
|
2287
|
+
const record = JSON.parse(raw);
|
|
2288
|
+
record._touchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2289
|
+
await this.redis.set(this._memKey(id), JSON.stringify(record));
|
|
2290
|
+
}
|
|
2291
|
+
async retrieve(query, _embedding) {
|
|
2292
|
+
let allRecords = [];
|
|
2293
|
+
if (query.actorId) {
|
|
2294
|
+
allRecords = await this._getActorRecords(query.actorId);
|
|
2295
|
+
} else {
|
|
2296
|
+
const actorIds = await this._listActors();
|
|
2297
|
+
for (const actorId of actorIds) {
|
|
2298
|
+
const records = await this._getActorRecords(actorId);
|
|
2299
|
+
allRecords.push(...records);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
const now = /* @__PURE__ */ new Date();
|
|
2303
|
+
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
2304
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
2305
|
+
results = results.filter((r) => query.memoryTypes.includes(r.memoryType));
|
|
2306
|
+
}
|
|
2307
|
+
if (query.tags && query.tags.length > 0) {
|
|
2308
|
+
results = results.filter((r) => r.tags?.some((t) => query.tags.includes(t)));
|
|
2309
|
+
}
|
|
2310
|
+
if (query.query) {
|
|
2311
|
+
const q = query.query.toLowerCase();
|
|
2312
|
+
results = results.filter((r) => r.content.toLowerCase().includes(q));
|
|
2313
|
+
}
|
|
2314
|
+
switch (query.strategy) {
|
|
2315
|
+
case "recent":
|
|
2316
|
+
results.sort((a, b) => new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime());
|
|
2317
|
+
break;
|
|
2318
|
+
case "important":
|
|
2319
|
+
results.sort((a, b) => b.importance - a.importance);
|
|
2320
|
+
break;
|
|
2321
|
+
case "hybrid":
|
|
2322
|
+
case "semantic":
|
|
2323
|
+
default:
|
|
2324
|
+
results.sort(
|
|
2325
|
+
(a, b) => b.importance - a.importance || new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime()
|
|
2326
|
+
);
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
2330
|
+
const toTouch = results.slice(0, query.limit ?? 10);
|
|
2331
|
+
for (const r of toTouch) {
|
|
2332
|
+
r._touchedAt = nowStr;
|
|
2333
|
+
await this.redis.set(this._memKey(r.id), JSON.stringify(r));
|
|
2334
|
+
}
|
|
2335
|
+
const limit = query.limit ?? 10;
|
|
2336
|
+
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
2337
|
+
}
|
|
2338
|
+
async count(filter) {
|
|
2339
|
+
let allRecords = [];
|
|
2340
|
+
if (filter?.actorId) {
|
|
2341
|
+
allRecords = await this._getActorRecords(filter.actorId);
|
|
2342
|
+
} else {
|
|
2343
|
+
const actorIds = await this._listActors();
|
|
2344
|
+
for (const actorId of actorIds) {
|
|
2345
|
+
const records = await this._getActorRecords(actorId);
|
|
2346
|
+
allRecords.push(...records);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
const now = /* @__PURE__ */ new Date();
|
|
2350
|
+
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
2351
|
+
if (filter?.memoryType) {
|
|
2352
|
+
results = results.filter((r) => r.memoryType === filter.memoryType);
|
|
2353
|
+
}
|
|
2354
|
+
if (filter?.minImportance !== void 0) {
|
|
2355
|
+
results = results.filter((r) => r.importance >= filter.minImportance);
|
|
2356
|
+
}
|
|
2357
|
+
return results.length;
|
|
2358
|
+
}
|
|
2359
|
+
async close() {
|
|
2360
|
+
}
|
|
2361
|
+
_actorsSetKey() {
|
|
2362
|
+
return `${this.prefix}:actors`;
|
|
2363
|
+
}
|
|
2364
|
+
_actorKey(actorId) {
|
|
2365
|
+
const safe = actorId.replace(/[^a-zA-Z0-9_\-.,@]/g, "_");
|
|
2366
|
+
return `${this.prefix}:a:${safe}`;
|
|
2367
|
+
}
|
|
2368
|
+
_memKey(id) {
|
|
2369
|
+
return `${this.prefix}:m:${id}`;
|
|
2370
|
+
}
|
|
2371
|
+
async _getActorRecords(actorId) {
|
|
2372
|
+
const ids = await this.redis.smembers(this._actorKey(actorId));
|
|
2373
|
+
if (ids.length === 0) return [];
|
|
2374
|
+
const keys = ids.map((id) => this._memKey(id));
|
|
2375
|
+
const raws = await this.redis.mget(...keys);
|
|
2376
|
+
const records = [];
|
|
2377
|
+
for (const raw of raws) {
|
|
2378
|
+
if (raw) {
|
|
2379
|
+
try {
|
|
2380
|
+
records.push(JSON.parse(raw));
|
|
2381
|
+
} catch {
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return records;
|
|
2386
|
+
}
|
|
2387
|
+
async _listActors() {
|
|
2388
|
+
return this.redis.smembers(this._actorsSetKey());
|
|
2389
|
+
}
|
|
2390
|
+
_toExternal(record) {
|
|
2391
|
+
const { _touchedAt: _, ...rest } = record;
|
|
2392
|
+
return {
|
|
2393
|
+
...rest,
|
|
2394
|
+
createdAt: new Date(record.createdAt),
|
|
2395
|
+
expiresAt: record.expiresAt ? new Date(record.expiresAt) : void 0
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2400
|
+
// src/adapters/storage/qdrant.ts
|
|
2401
|
+
var QdrantStorageAdapter = class {
|
|
2402
|
+
client;
|
|
2403
|
+
collectionName;
|
|
2404
|
+
vectorSize;
|
|
2405
|
+
_idToQdrant = /* @__PURE__ */ new Map();
|
|
2406
|
+
constructor(config) {
|
|
2407
|
+
this.client = config.client;
|
|
2408
|
+
this.collectionName = config.collectionName ?? "memstack";
|
|
2409
|
+
this.vectorSize = config.vectorSize ?? 1536;
|
|
2410
|
+
}
|
|
2411
|
+
async initialize() {
|
|
2412
|
+
try {
|
|
2413
|
+
await this.client.getCollection(this.collectionName);
|
|
2414
|
+
} catch {
|
|
2415
|
+
await this.client.createCollection(this.collectionName, {
|
|
2416
|
+
vectors: { size: this.vectorSize, distance: "Cosine" }
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
generateId() {
|
|
2421
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
2422
|
+
}
|
|
2423
|
+
async store(input) {
|
|
2424
|
+
const now = /* @__PURE__ */ new Date();
|
|
2425
|
+
const memory = {
|
|
2426
|
+
id: input.id ?? this.generateId(),
|
|
2427
|
+
actorId: input.actorId,
|
|
2428
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2429
|
+
content: input.content,
|
|
2430
|
+
importance: input.importance ?? 0.5,
|
|
2431
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2432
|
+
tags: input.tags ?? [],
|
|
2433
|
+
embedding: input.embedding,
|
|
2434
|
+
sourceId: input.sourceId,
|
|
2435
|
+
metadata: input.metadata ?? {},
|
|
2436
|
+
expiresAt: input.expiresAt,
|
|
2437
|
+
createdAt: now
|
|
2438
|
+
};
|
|
2439
|
+
const qdrantId = crypto.randomUUID();
|
|
2440
|
+
await this.client.upsert(this.collectionName, {
|
|
2441
|
+
points: [
|
|
2442
|
+
{
|
|
2443
|
+
id: qdrantId,
|
|
2444
|
+
vector: input.embedding ?? new Array(this.vectorSize).fill(0),
|
|
2445
|
+
payload: this._toPayload(memory)
|
|
2446
|
+
}
|
|
2447
|
+
]
|
|
2448
|
+
});
|
|
2449
|
+
this._idToQdrant.set(memory.id, qdrantId);
|
|
2450
|
+
return memory;
|
|
2451
|
+
}
|
|
2452
|
+
async storeBatch(inputs) {
|
|
2453
|
+
const results = [];
|
|
2454
|
+
const now = /* @__PURE__ */ new Date();
|
|
2455
|
+
const points = [];
|
|
2456
|
+
for (const input of inputs) {
|
|
2457
|
+
const memory = {
|
|
2458
|
+
id: input.id ?? this.generateId(),
|
|
2459
|
+
actorId: input.actorId,
|
|
2460
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2461
|
+
content: input.content,
|
|
2462
|
+
importance: input.importance ?? 0.5,
|
|
2463
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2464
|
+
tags: input.tags ?? [],
|
|
2465
|
+
embedding: input.embedding,
|
|
2466
|
+
sourceId: input.sourceId,
|
|
2467
|
+
metadata: input.metadata ?? {},
|
|
2468
|
+
expiresAt: input.expiresAt,
|
|
2469
|
+
createdAt: now
|
|
2470
|
+
};
|
|
2471
|
+
results.push(memory);
|
|
2472
|
+
const qid = crypto.randomUUID();
|
|
2473
|
+
points.push({
|
|
2474
|
+
id: qid,
|
|
2475
|
+
vector: input.embedding ?? new Array(this.vectorSize).fill(0),
|
|
2476
|
+
payload: this._toPayload(memory)
|
|
2477
|
+
});
|
|
2478
|
+
this._idToQdrant.set(memory.id, qid);
|
|
2479
|
+
}
|
|
2480
|
+
await this.client.upsert(this.collectionName, { points });
|
|
2481
|
+
return results;
|
|
2482
|
+
}
|
|
2483
|
+
async get(id) {
|
|
2484
|
+
const qdrantId = this._idToQdrant.get(id);
|
|
2485
|
+
if (qdrantId) {
|
|
2486
|
+
const points = await this.client.retrieve(this.collectionName, { ids: [qdrantId] });
|
|
2487
|
+
if (points && points.length > 0) {
|
|
2488
|
+
const point = points[0];
|
|
2489
|
+
if (point.payload.expiresAt && new Date(point.payload.expiresAt) <= /* @__PURE__ */ new Date()) return null;
|
|
2490
|
+
return this._fromPoint(point);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
async delete(id) {
|
|
2496
|
+
const qdrantId = this._idToQdrant.get(id);
|
|
2497
|
+
if (!qdrantId) throw notFound("Memory", id);
|
|
2498
|
+
await this.client.delete(this.collectionName, { points: [qdrantId] });
|
|
2499
|
+
this._idToQdrant.delete(id);
|
|
2500
|
+
}
|
|
2501
|
+
async deleteMany(ids) {
|
|
2502
|
+
const qdrantIds = [];
|
|
2503
|
+
for (const id of ids) {
|
|
2504
|
+
const qid = this._idToQdrant.get(id);
|
|
2505
|
+
if (qid) {
|
|
2506
|
+
qdrantIds.push(qid);
|
|
2507
|
+
this._idToQdrant.delete(id);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if (qdrantIds.length === 0) return 0;
|
|
2511
|
+
await this.client.delete(this.collectionName, { points: qdrantIds });
|
|
2512
|
+
return qdrantIds.length;
|
|
2513
|
+
}
|
|
2514
|
+
async retrieve(query, embedding) {
|
|
2515
|
+
const must = [];
|
|
2516
|
+
if (query.actorId) {
|
|
2517
|
+
must.push({ key: "actorId", match: { value: query.actorId } });
|
|
2518
|
+
}
|
|
2519
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
2520
|
+
must.push({ key: "memoryType", match: { any: query.memoryTypes } });
|
|
2521
|
+
}
|
|
2522
|
+
if (query.tags && query.tags.length > 0) {
|
|
2523
|
+
must.push({ key: "tags", match: { any: query.tags } });
|
|
2524
|
+
}
|
|
2525
|
+
const filter = must.length > 0 ? { must } : void 0;
|
|
2526
|
+
const limit = query.limit ?? 10;
|
|
2527
|
+
if (query.strategy === "semantic" || query.strategy === "hybrid") {
|
|
2528
|
+
const vector2 = embedding ?? new Array(this.vectorSize).fill(0);
|
|
2529
|
+
const results2 = await this.client.search(this.collectionName, {
|
|
2530
|
+
vector: vector2,
|
|
2531
|
+
limit,
|
|
2532
|
+
filter
|
|
2533
|
+
});
|
|
2534
|
+
const points2 = results2;
|
|
2535
|
+
return points2.map((p) => this._fromPoint(p));
|
|
2536
|
+
}
|
|
2537
|
+
const vector = new Array(this.vectorSize).fill(0);
|
|
2538
|
+
const results = await this.client.search(this.collectionName, {
|
|
2539
|
+
vector,
|
|
2540
|
+
limit: 1e4,
|
|
2541
|
+
filter
|
|
2542
|
+
});
|
|
2543
|
+
let points = results;
|
|
2544
|
+
switch (query.strategy) {
|
|
2545
|
+
case "recent":
|
|
2546
|
+
points.sort((a, b) => {
|
|
2547
|
+
const aTime = new Date(a.payload.createdAt).getTime();
|
|
2548
|
+
const bTime = new Date(b.payload.createdAt).getTime();
|
|
2549
|
+
return bTime - aTime;
|
|
2550
|
+
});
|
|
2551
|
+
break;
|
|
2552
|
+
case "important":
|
|
2553
|
+
points.sort((a, b) => b.payload.importance - a.payload.importance);
|
|
2554
|
+
break;
|
|
2555
|
+
}
|
|
2556
|
+
return points.slice(0, limit).map((p) => this._fromPoint(p));
|
|
2557
|
+
}
|
|
2558
|
+
async count(filter) {
|
|
2559
|
+
const must = [];
|
|
2560
|
+
if (filter?.actorId) {
|
|
2561
|
+
must.push({ key: "actorId", match: { value: filter.actorId } });
|
|
2562
|
+
}
|
|
2563
|
+
if (filter?.memoryType) {
|
|
2564
|
+
must.push({ key: "memoryType", match: { value: filter.memoryType } });
|
|
2565
|
+
}
|
|
2566
|
+
if (filter?.minImportance !== void 0) {
|
|
2567
|
+
must.push({ key: "importance", range: { gte: filter.minImportance } });
|
|
2568
|
+
}
|
|
2569
|
+
const filterObj = must.length > 0 ? { must } : void 0;
|
|
2570
|
+
const result = await this.client.count(this.collectionName, filterObj ? { filter: filterObj } : void 0);
|
|
2571
|
+
return result.count;
|
|
2572
|
+
}
|
|
2573
|
+
async close() {
|
|
2574
|
+
}
|
|
2575
|
+
_toPayload(memory) {
|
|
2576
|
+
return {
|
|
2577
|
+
_id: memory.id,
|
|
2578
|
+
actorId: memory.actorId,
|
|
2579
|
+
memoryType: memory.memoryType,
|
|
2580
|
+
importance: memory.importance,
|
|
2581
|
+
emotionalValence: memory.emotionalValence,
|
|
2582
|
+
tags: memory.tags,
|
|
2583
|
+
content: memory.content,
|
|
2584
|
+
metadata: memory.metadata ?? {},
|
|
2585
|
+
createdAt: memory.createdAt.toISOString(),
|
|
2586
|
+
sourceId: memory.sourceId ?? null,
|
|
2587
|
+
expiresAt: memory.expiresAt?.toISOString() ?? null
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
_fromPoint(point) {
|
|
2591
|
+
const p = point.payload;
|
|
2592
|
+
return {
|
|
2593
|
+
id: p._id ?? point.id,
|
|
2594
|
+
actorId: p.actorId,
|
|
2595
|
+
memoryType: p.memoryType,
|
|
2596
|
+
content: p.content,
|
|
2597
|
+
importance: p.importance,
|
|
2598
|
+
emotionalValence: p.emotionalValence ?? 0,
|
|
2599
|
+
tags: p.tags ?? [],
|
|
2600
|
+
embedding: point.vector,
|
|
2601
|
+
sourceId: p.sourceId ?? void 0,
|
|
2602
|
+
metadata: p.metadata ?? {},
|
|
2603
|
+
expiresAt: p.expiresAt ? new Date(p.expiresAt) : void 0,
|
|
2604
|
+
createdAt: new Date(p.createdAt)
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
// src/adapters/storage/neo4j.ts
|
|
2610
|
+
var Neo4jStorageAdapter = class {
|
|
2611
|
+
driver;
|
|
2612
|
+
database;
|
|
2613
|
+
constructor(config) {
|
|
2614
|
+
this.driver = config.driver;
|
|
2615
|
+
this.database = config.database ?? "neo4j";
|
|
2616
|
+
}
|
|
2617
|
+
async initialize() {
|
|
2618
|
+
const session = this.driver.session({ database: this.database });
|
|
2619
|
+
try {
|
|
2620
|
+
await session.run(
|
|
2621
|
+
"CREATE CONSTRAINT memory_id IF NOT EXISTS FOR (m:Memory) REQUIRE m.id IS UNIQUE"
|
|
2622
|
+
);
|
|
2623
|
+
await session.run(
|
|
2624
|
+
`CREATE VECTOR INDEX memory_embedding IF NOT EXISTS FOR (m:Memory) ON m.embedding
|
|
2625
|
+
OPTIONS { indexConfig: { \`vector.dimensions\`: 1536, \`vector.similarity_function\`: "cosine" } }`
|
|
2626
|
+
);
|
|
2627
|
+
} finally {
|
|
2628
|
+
await session.close();
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
generateId() {
|
|
2632
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
2633
|
+
}
|
|
2634
|
+
async store(input) {
|
|
2635
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2636
|
+
const id = input.id ?? this.generateId();
|
|
2637
|
+
const props = {
|
|
2638
|
+
actorId: input.actorId,
|
|
2639
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2640
|
+
content: input.content,
|
|
2641
|
+
importance: input.importance ?? 0.5,
|
|
2642
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2643
|
+
tags: input.tags ?? [],
|
|
2644
|
+
sourceId: input.sourceId ?? null,
|
|
2645
|
+
metadata: JSON.stringify(input.metadata ?? {}),
|
|
2646
|
+
expiresAt: input.expiresAt?.toISOString() ?? null,
|
|
2647
|
+
createdAt: now,
|
|
2648
|
+
embedding: input.embedding ?? null
|
|
2649
|
+
};
|
|
2650
|
+
const { createdAt: _, ...updateProps } = props;
|
|
2651
|
+
const session = this.driver.session({ database: this.database });
|
|
2652
|
+
try {
|
|
2653
|
+
await session.run(
|
|
2654
|
+
"MERGE (m:Memory {id: $id}) ON CREATE SET m += $props ON MATCH SET m += $updateProps",
|
|
2655
|
+
{ id, props, updateProps }
|
|
2656
|
+
);
|
|
2657
|
+
} finally {
|
|
2658
|
+
await session.close();
|
|
2659
|
+
}
|
|
2660
|
+
return {
|
|
2661
|
+
id,
|
|
2662
|
+
actorId: input.actorId,
|
|
2663
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2664
|
+
content: input.content,
|
|
2665
|
+
importance: input.importance ?? 0.5,
|
|
2666
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2667
|
+
tags: input.tags ?? [],
|
|
2668
|
+
embedding: input.embedding,
|
|
2669
|
+
sourceId: input.sourceId,
|
|
2670
|
+
metadata: input.metadata ?? {},
|
|
2671
|
+
expiresAt: input.expiresAt,
|
|
2672
|
+
createdAt: new Date(now)
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
async storeBatch(inputs) {
|
|
2676
|
+
const results = [];
|
|
2677
|
+
for (const input of inputs) {
|
|
2678
|
+
results.push(await this.store(input));
|
|
2679
|
+
}
|
|
2680
|
+
return results;
|
|
2681
|
+
}
|
|
2682
|
+
async get(id) {
|
|
2683
|
+
const session = this.driver.session({ database: this.database });
|
|
2684
|
+
try {
|
|
2685
|
+
const result = await session.run(
|
|
2686
|
+
"MATCH (m:Memory {id: $id}) WHERE m.expiresAt IS NULL OR m.expiresAt > datetime() RETURN m",
|
|
2687
|
+
{ id }
|
|
2688
|
+
);
|
|
2689
|
+
if (result.records.length === 0) return null;
|
|
2690
|
+
const raw = result.records[0].get("m");
|
|
2691
|
+
const node = raw.properties ?? raw;
|
|
2692
|
+
return this._nodeToMemory(node);
|
|
2693
|
+
} finally {
|
|
2694
|
+
await session.close();
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
async delete(id) {
|
|
2698
|
+
const session = this.driver.session({ database: this.database });
|
|
2699
|
+
try {
|
|
2700
|
+
const check = await session.run("MATCH (m:Memory {id: $id}) RETURN m.id as id", { id });
|
|
2701
|
+
if (check.records.length === 0) throw notFound("Memory", id);
|
|
2702
|
+
await session.run("MATCH (m:Memory {id: $id}) DETACH DELETE m", { id });
|
|
2703
|
+
} finally {
|
|
2704
|
+
await session.close();
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
async deleteMany(ids) {
|
|
2708
|
+
const session = this.driver.session({ database: this.database });
|
|
2709
|
+
try {
|
|
2710
|
+
const result = await session.run(
|
|
2711
|
+
"UNWIND $ids AS mid MATCH (m:Memory {id: mid}) DETACH DELETE m RETURN count(m) as deleted",
|
|
2712
|
+
{ ids }
|
|
2713
|
+
);
|
|
2714
|
+
const record = result.records[0];
|
|
2715
|
+
if (!record) return 0;
|
|
2716
|
+
return record.get("deleted");
|
|
2717
|
+
} finally {
|
|
2718
|
+
await session.close();
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
async retrieve(query, embedding) {
|
|
2722
|
+
const session = this.driver.session({ database: this.database });
|
|
2723
|
+
try {
|
|
2724
|
+
const limit = Math.floor(query.limit ?? 10);
|
|
2725
|
+
const strategy = query.strategy ?? "hybrid";
|
|
2726
|
+
if (strategy === "semantic" && embedding) {
|
|
2727
|
+
return this._semanticRetrieve(session, embedding, query, limit);
|
|
2728
|
+
}
|
|
2729
|
+
const strLimit = String(limit);
|
|
2730
|
+
let cypher;
|
|
2731
|
+
const params = {};
|
|
2732
|
+
const hasActor = query.actorId !== void 0;
|
|
2733
|
+
switch (strategy) {
|
|
2734
|
+
case "recent":
|
|
2735
|
+
if (hasActor) {
|
|
2736
|
+
cypher = "MATCH (m:Memory) WHERE m.actorId = $actorId RETURN m ORDER BY m.createdAt DESC LIMIT " + strLimit;
|
|
2737
|
+
params.actorId = query.actorId;
|
|
2738
|
+
} else {
|
|
2739
|
+
cypher = "MATCH (m:Memory) RETURN m ORDER BY m.createdAt DESC LIMIT " + strLimit;
|
|
2740
|
+
}
|
|
2741
|
+
break;
|
|
2742
|
+
case "important":
|
|
2743
|
+
if (hasActor) {
|
|
2744
|
+
cypher = "MATCH (m:Memory) WHERE m.actorId = $actorId RETURN m ORDER BY m.importance DESC LIMIT " + strLimit;
|
|
2745
|
+
params.actorId = query.actorId;
|
|
2746
|
+
} else {
|
|
2747
|
+
cypher = "MATCH (m:Memory) RETURN m ORDER BY m.importance DESC LIMIT " + strLimit;
|
|
2748
|
+
}
|
|
2749
|
+
break;
|
|
2750
|
+
case "hybrid":
|
|
2751
|
+
default:
|
|
2752
|
+
if (embedding) {
|
|
2753
|
+
return this._semanticRetrieve(session, embedding, query, limit);
|
|
2754
|
+
}
|
|
2755
|
+
if (hasActor) {
|
|
2756
|
+
cypher = "MATCH (m:Memory) WHERE m.actorId = $actorId RETURN m ORDER BY m.importance DESC, m.createdAt DESC LIMIT " + strLimit;
|
|
2757
|
+
params.actorId = query.actorId;
|
|
2758
|
+
} else {
|
|
2759
|
+
cypher = "MATCH (m:Memory) RETURN m ORDER BY m.importance DESC, m.createdAt DESC LIMIT " + strLimit;
|
|
2760
|
+
}
|
|
2761
|
+
break;
|
|
2762
|
+
}
|
|
2763
|
+
const result = await session.run(cypher, params);
|
|
2764
|
+
const memories = result.records.map((r) => {
|
|
2765
|
+
const raw = r.get("m");
|
|
2766
|
+
const node = raw.properties ?? raw;
|
|
2767
|
+
return this._nodeToMemory(node);
|
|
2768
|
+
});
|
|
2769
|
+
return this._postFilter(memories, query);
|
|
2770
|
+
} finally {
|
|
2771
|
+
await session.close();
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
async count(filter) {
|
|
2775
|
+
const session = this.driver.session({ database: this.database });
|
|
2776
|
+
try {
|
|
2777
|
+
const conditions = [];
|
|
2778
|
+
const params = {};
|
|
2779
|
+
if (filter?.actorId) {
|
|
2780
|
+
conditions.push("m.actorId = $actorId");
|
|
2781
|
+
params.actorId = filter.actorId;
|
|
2782
|
+
}
|
|
2783
|
+
if (filter?.memoryType) {
|
|
2784
|
+
conditions.push("m.memoryType = $memoryType");
|
|
2785
|
+
params.memoryType = filter.memoryType;
|
|
2786
|
+
}
|
|
2787
|
+
if (filter?.minImportance !== void 0) {
|
|
2788
|
+
conditions.push("m.importance >= $minImportance");
|
|
2789
|
+
params.minImportance = filter.minImportance;
|
|
2790
|
+
}
|
|
2791
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2792
|
+
const result = await session.run(
|
|
2793
|
+
`MATCH (m:Memory) ${where} RETURN count(m) as count`,
|
|
2794
|
+
params
|
|
2795
|
+
);
|
|
2796
|
+
const rawCount = result.records[0].get("count");
|
|
2797
|
+
return typeof rawCount === "number" ? rawCount : Number(rawCount);
|
|
2798
|
+
} finally {
|
|
2799
|
+
await session.close();
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
async graphQuery(cypher, params) {
|
|
2803
|
+
const session = this.driver.session({ database: this.database });
|
|
2804
|
+
try {
|
|
2805
|
+
const result = await session.run(cypher, params);
|
|
2806
|
+
return result.records.map((r) => {
|
|
2807
|
+
const obj = r.toObject();
|
|
2808
|
+
const mKey = Object.keys(obj).find((k) => obj[k] && typeof obj[k] === "object" && "properties" in obj[k]);
|
|
2809
|
+
if (mKey) {
|
|
2810
|
+
const raw = obj[mKey];
|
|
2811
|
+
const node = raw.properties ?? raw;
|
|
2812
|
+
return this._nodeToMemory(node);
|
|
2813
|
+
}
|
|
2814
|
+
return this._nodeToMemory(obj);
|
|
2815
|
+
});
|
|
2816
|
+
} finally {
|
|
2817
|
+
await session.close();
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
async close() {
|
|
2821
|
+
await this.driver.close();
|
|
2822
|
+
}
|
|
2823
|
+
async _semanticRetrieve(session, embedding, query, limit) {
|
|
2824
|
+
const hasActor = query.actorId !== void 0;
|
|
2825
|
+
const strLimit = String(limit);
|
|
2826
|
+
const cypher = hasActor ? `CALL db.index.vector.queryNodes('memory_embedding', ${strLimit}, $embedding) YIELD node, score
|
|
2827
|
+
MATCH (node) WHERE node.actorId = $actorId
|
|
2828
|
+
RETURN node ORDER BY score LIMIT ${strLimit}` : `CALL db.index.vector.queryNodes('memory_embedding', ${strLimit}, $embedding) YIELD node, score
|
|
2829
|
+
RETURN node ORDER BY score LIMIT ${strLimit}`;
|
|
2830
|
+
const params = { embedding };
|
|
2831
|
+
if (hasActor) params.actorId = query.actorId;
|
|
2832
|
+
const result = await session.run(cypher, params);
|
|
2833
|
+
const memories = result.records.map((r) => {
|
|
2834
|
+
const raw = r.get("node");
|
|
2835
|
+
const node = raw.properties ?? raw;
|
|
2836
|
+
return this._nodeToMemory(node);
|
|
2837
|
+
});
|
|
2838
|
+
return this._postFilter(memories, query);
|
|
2839
|
+
}
|
|
2840
|
+
_postFilter(memories, query) {
|
|
2841
|
+
const now = /* @__PURE__ */ new Date();
|
|
2842
|
+
let results = memories.filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
2843
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
2844
|
+
results = results.filter((m) => query.memoryTypes.includes(m.memoryType));
|
|
2845
|
+
}
|
|
2846
|
+
if (query.tags && query.tags.length > 0) {
|
|
2847
|
+
results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
|
|
2848
|
+
}
|
|
2849
|
+
if (query.query) {
|
|
2850
|
+
const q = query.query.toLowerCase();
|
|
2851
|
+
results = results.filter((m) => m.content.toLowerCase().includes(q));
|
|
2852
|
+
}
|
|
2853
|
+
return results;
|
|
2854
|
+
}
|
|
2855
|
+
_nodeToMemory(node) {
|
|
2856
|
+
return {
|
|
2857
|
+
id: node.id,
|
|
2858
|
+
actorId: node.actorId,
|
|
2859
|
+
memoryType: node.memoryType ?? "interaction",
|
|
2860
|
+
content: node.content,
|
|
2861
|
+
importance: node.importance ?? 0.5,
|
|
2862
|
+
emotionalValence: node.emotionalValence ?? 0,
|
|
2863
|
+
tags: Array.isArray(node.tags) ? node.tags : [],
|
|
2864
|
+
embedding: node.embedding ?? void 0,
|
|
2865
|
+
sourceId: node.sourceId ?? void 0,
|
|
2866
|
+
metadata: typeof node.metadata === "string" ? JSON.parse(node.metadata) : node.metadata ?? {},
|
|
2867
|
+
expiresAt: node.expiresAt ? new Date(node.expiresAt) : void 0,
|
|
2868
|
+
createdAt: new Date(node.createdAt)
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
// src/adapters/storage/weaviate.ts
|
|
2874
|
+
var WeaviateStorageAdapter = class {
|
|
2875
|
+
client;
|
|
2876
|
+
className;
|
|
2877
|
+
_collection = null;
|
|
2878
|
+
_idToWv = /* @__PURE__ */ new Map();
|
|
2879
|
+
constructor(config) {
|
|
2880
|
+
this.client = config.client;
|
|
2881
|
+
this.className = config.className ?? "MemstackMemory";
|
|
2882
|
+
}
|
|
2883
|
+
async initialize() {
|
|
2884
|
+
this._collection = this.client.collections.get(this.className);
|
|
2885
|
+
}
|
|
2886
|
+
collection() {
|
|
2887
|
+
if (!this._collection) {
|
|
2888
|
+
this._collection = this.client.collections.get(this.className);
|
|
2889
|
+
}
|
|
2890
|
+
return this._collection;
|
|
2891
|
+
}
|
|
2892
|
+
generateId() {
|
|
2893
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
2894
|
+
}
|
|
2895
|
+
async store(input) {
|
|
2896
|
+
const now = /* @__PURE__ */ new Date();
|
|
2897
|
+
const memory = {
|
|
2898
|
+
id: input.id ?? this.generateId(),
|
|
2899
|
+
actorId: input.actorId,
|
|
2900
|
+
memoryType: input.memoryType ?? "interaction",
|
|
2901
|
+
content: input.content,
|
|
2902
|
+
importance: input.importance ?? 0.5,
|
|
2903
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
2904
|
+
tags: input.tags ?? [],
|
|
2905
|
+
embedding: input.embedding,
|
|
2906
|
+
sourceId: input.sourceId,
|
|
2907
|
+
metadata: input.metadata ?? {},
|
|
2908
|
+
expiresAt: input.expiresAt,
|
|
2909
|
+
createdAt: now
|
|
2910
|
+
};
|
|
2911
|
+
const wvId = await this.collection().data.insert({
|
|
2912
|
+
id: crypto.randomUUID(),
|
|
2913
|
+
properties: this._toProperties(memory),
|
|
2914
|
+
vectors: input.embedding ?? new Array(1536).fill(0)
|
|
2915
|
+
});
|
|
2916
|
+
this._idToWv.set(memory.id, wvId);
|
|
2917
|
+
return memory;
|
|
2918
|
+
}
|
|
2919
|
+
async storeBatch(inputs) {
|
|
2920
|
+
const results = [];
|
|
2921
|
+
for (const input of inputs) {
|
|
2922
|
+
results.push(await this.store(input));
|
|
2923
|
+
}
|
|
2924
|
+
return results;
|
|
2925
|
+
}
|
|
2926
|
+
async get(id) {
|
|
2927
|
+
const wvId = this._idToWv.get(id);
|
|
2928
|
+
if (!wvId) return null;
|
|
2929
|
+
const obj = await this.collection().query.fetchObjectById(wvId, { includeVector: true });
|
|
2930
|
+
if (!obj) return null;
|
|
2931
|
+
const memory = this._fromProperties(obj.properties, obj.vectors?.default ?? void 0);
|
|
2932
|
+
if (memory.expiresAt && memory.expiresAt <= /* @__PURE__ */ new Date()) return null;
|
|
2933
|
+
return memory;
|
|
2934
|
+
}
|
|
2935
|
+
async delete(id) {
|
|
2936
|
+
const wvId = this._idToWv.get(id);
|
|
2937
|
+
if (!wvId) throw notFound("Memory", id);
|
|
2938
|
+
await this.collection().data.deleteById(wvId);
|
|
2939
|
+
this._idToWv.delete(id);
|
|
2940
|
+
}
|
|
2941
|
+
async deleteMany(ids) {
|
|
2942
|
+
let count = 0;
|
|
2943
|
+
for (const id of ids) {
|
|
2944
|
+
try {
|
|
2945
|
+
await this.delete(id);
|
|
2946
|
+
count++;
|
|
2947
|
+
} catch {
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
return count;
|
|
2951
|
+
}
|
|
2952
|
+
async retrieve(query, embedding) {
|
|
2953
|
+
const strategy = query.strategy ?? "recent";
|
|
2954
|
+
const limit = query.limit ?? 10;
|
|
2955
|
+
if ((strategy === "semantic" || strategy === "hybrid") && embedding) {
|
|
2956
|
+
const filters2 = this._buildFilter(query);
|
|
2957
|
+
const result2 = await this.collection().query.nearVector(embedding, {
|
|
2958
|
+
limit: 100,
|
|
2959
|
+
filters: filters2,
|
|
2960
|
+
includeVector: true
|
|
2961
|
+
});
|
|
2962
|
+
let objects2 = result2.objects.map((o) => ({
|
|
2963
|
+
...this._fromProperties(o.properties, o.vectors?.default),
|
|
2964
|
+
_score: o.metadata?.creationTimeUnix ?? 0
|
|
2965
|
+
}));
|
|
2966
|
+
objects2 = objects2.filter((o) => !o.expiresAt || o.expiresAt > /* @__PURE__ */ new Date());
|
|
2967
|
+
if (query.actorId) {
|
|
2968
|
+
objects2 = objects2.filter((o) => o.actorId === query.actorId);
|
|
2969
|
+
}
|
|
2970
|
+
if (strategy === "hybrid") {
|
|
2971
|
+
objects2.sort((a, b) => b._score + b.importance - (a._score + a.importance));
|
|
2972
|
+
}
|
|
2973
|
+
return objects2.slice(0, limit);
|
|
2974
|
+
}
|
|
2975
|
+
const filters = this._buildFilter(query);
|
|
2976
|
+
const result = await this.collection().query.fetchObjects({
|
|
2977
|
+
limit: 1e4,
|
|
2978
|
+
filters
|
|
2979
|
+
});
|
|
2980
|
+
let objects = result.objects.map((o) => ({
|
|
2981
|
+
...this._fromProperties(o.properties, o.vectors?.default),
|
|
2982
|
+
_createdAt: o.properties.createdAt,
|
|
2983
|
+
_importance: o.properties.importance ?? 0.5
|
|
2984
|
+
}));
|
|
2985
|
+
objects = objects.filter((o) => !o.expiresAt || o.expiresAt > /* @__PURE__ */ new Date());
|
|
2986
|
+
if (query.actorId) {
|
|
2987
|
+
objects = objects.filter((o) => o.actorId === query.actorId);
|
|
2988
|
+
}
|
|
2989
|
+
switch (strategy) {
|
|
2990
|
+
case "recent":
|
|
2991
|
+
objects.sort((a, b) => new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime());
|
|
2992
|
+
break;
|
|
2993
|
+
case "important":
|
|
2994
|
+
objects.sort((a, b) => b._importance - a._importance);
|
|
2995
|
+
break;
|
|
2996
|
+
case "hybrid":
|
|
2997
|
+
case "semantic":
|
|
2998
|
+
default:
|
|
2999
|
+
objects.sort((a, b) => b._importance - a._importance || new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime());
|
|
3000
|
+
break;
|
|
3001
|
+
}
|
|
3002
|
+
return objects.slice(0, limit);
|
|
3003
|
+
}
|
|
3004
|
+
async count(filter) {
|
|
3005
|
+
const and = [];
|
|
3006
|
+
if (filter?.actorId) {
|
|
3007
|
+
and.push({ operator: "Equal", target: { property: "actorId" }, value: filter.actorId });
|
|
3008
|
+
}
|
|
3009
|
+
if (filter?.memoryType) {
|
|
3010
|
+
and.push({ operator: "Equal", target: { property: "memoryType" }, value: filter.memoryType });
|
|
3011
|
+
}
|
|
3012
|
+
if (filter?.minImportance !== void 0) {
|
|
3013
|
+
and.push({ operator: "GreaterThanEqual", target: { property: "importance" }, value: filter.minImportance });
|
|
3014
|
+
}
|
|
3015
|
+
const finalFilter = and.length === 0 ? void 0 : and.length === 1 ? and[0] : { operator: "And", filters: and };
|
|
3016
|
+
const result = await this.collection().query.fetchObjects({
|
|
3017
|
+
limit: 1e4,
|
|
3018
|
+
filters: finalFilter
|
|
3019
|
+
});
|
|
3020
|
+
return result.objects.length;
|
|
3021
|
+
}
|
|
3022
|
+
async close() {
|
|
3023
|
+
}
|
|
3024
|
+
_buildFilter(query) {
|
|
3025
|
+
const and = [];
|
|
3026
|
+
if (query.actorId) {
|
|
3027
|
+
and.push({ operator: "Equal", target: { property: "actorId" }, value: query.actorId });
|
|
3028
|
+
}
|
|
3029
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
3030
|
+
if (query.memoryTypes.length === 1) {
|
|
3031
|
+
and.push({ operator: "Equal", target: { property: "memoryType" }, value: query.memoryTypes[0] });
|
|
3032
|
+
} else {
|
|
3033
|
+
and.push({
|
|
3034
|
+
operator: "Or",
|
|
3035
|
+
filters: query.memoryTypes.map((t) => ({ operator: "Equal", target: { property: "memoryType" }, value: t }))
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (query.tags && query.tags.length > 0) {
|
|
3040
|
+
if (query.tags.length === 1) {
|
|
3041
|
+
and.push({ operator: "ContainsAny", target: { property: "tags" }, values: query.tags });
|
|
3042
|
+
} else {
|
|
3043
|
+
and.push({
|
|
3044
|
+
operator: "Or",
|
|
3045
|
+
filters: query.tags.map((t) => ({ operator: "ContainsAny", target: { property: "tags" }, values: [t] }))
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (query.query) {
|
|
3050
|
+
and.push({ operator: "Like", target: { property: "content" }, value: `*${query.query}*` });
|
|
3051
|
+
}
|
|
3052
|
+
return and.length === 0 ? void 0 : and.length === 1 ? and[0] : { operator: "And", filters: and };
|
|
3053
|
+
}
|
|
3054
|
+
_toProperties(memory) {
|
|
3055
|
+
return {
|
|
3056
|
+
_memstackId: memory.id,
|
|
3057
|
+
actorId: memory.actorId,
|
|
3058
|
+
memoryType: memory.memoryType,
|
|
3059
|
+
content: memory.content,
|
|
3060
|
+
importance: memory.importance,
|
|
3061
|
+
emotionalValence: memory.emotionalValence,
|
|
3062
|
+
tags: memory.tags,
|
|
3063
|
+
metadata: JSON.stringify(memory.metadata ?? {}),
|
|
3064
|
+
sourceId: memory.sourceId ?? null,
|
|
3065
|
+
expiresAt: memory.expiresAt?.toISOString() ?? null,
|
|
3066
|
+
createdAt: memory.createdAt.toISOString()
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
_fromProperties(props, vectors) {
|
|
3070
|
+
return {
|
|
3071
|
+
id: props._memstackId ?? "",
|
|
3072
|
+
actorId: props.actorId ?? "",
|
|
3073
|
+
memoryType: props.memoryType ?? "interaction",
|
|
3074
|
+
content: props.content ?? "",
|
|
3075
|
+
importance: props.importance ?? 0.5,
|
|
3076
|
+
emotionalValence: props.emotionalValence ?? 0,
|
|
3077
|
+
tags: Array.isArray(props.tags) ? props.tags : [],
|
|
3078
|
+
embedding: vectors,
|
|
3079
|
+
sourceId: props.sourceId ?? void 0,
|
|
3080
|
+
metadata: typeof props.metadata === "string" ? JSON.parse(props.metadata) : props.metadata ?? {},
|
|
3081
|
+
expiresAt: props.expiresAt ? new Date(props.expiresAt) : void 0,
|
|
3082
|
+
createdAt: new Date(props.createdAt ?? (/* @__PURE__ */ new Date()).toISOString())
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
|
|
3087
|
+
// src/adapters/storage/lancedb.ts
|
|
3088
|
+
var LanceDBStorageAdapter = class {
|
|
3089
|
+
connection;
|
|
3090
|
+
table;
|
|
3091
|
+
tableName;
|
|
3092
|
+
_zeroVector;
|
|
3093
|
+
constructor(config) {
|
|
3094
|
+
if (!config.connection) {
|
|
3095
|
+
throw configError("LanceDBStorageAdapter requires a LanceDB connection instance");
|
|
3096
|
+
}
|
|
3097
|
+
this.connection = config.connection;
|
|
3098
|
+
this.tableName = config.tableName ?? "memstack";
|
|
3099
|
+
this._zeroVector = new Array(1536).fill(0);
|
|
3100
|
+
}
|
|
3101
|
+
async initialize() {
|
|
3102
|
+
const tables = await this.connection.tableNames();
|
|
3103
|
+
if (!tables.includes(this.tableName)) {
|
|
3104
|
+
this.table = await this.connection.createTable(this.tableName, []);
|
|
3105
|
+
} else {
|
|
3106
|
+
this.table = await this.connection.openTable(this.tableName);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
generateId() {
|
|
3110
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
3111
|
+
}
|
|
3112
|
+
async store(input) {
|
|
3113
|
+
const now = /* @__PURE__ */ new Date();
|
|
3114
|
+
const id = input.id ?? this.generateId();
|
|
3115
|
+
const row = {
|
|
3116
|
+
id,
|
|
3117
|
+
vector: input.embedding ?? this._zeroVector,
|
|
3118
|
+
actor_id: input.actorId,
|
|
3119
|
+
memory_type: input.memoryType ?? "interaction",
|
|
3120
|
+
content: input.content,
|
|
3121
|
+
importance: input.importance ?? 0.5,
|
|
3122
|
+
emotional_valence: input.emotionalValence ?? 0,
|
|
3123
|
+
tags: JSON.stringify(input.tags ?? []),
|
|
3124
|
+
source_id: input.sourceId ?? "",
|
|
3125
|
+
metadata: JSON.stringify(input.metadata ?? {}),
|
|
3126
|
+
expires_at: input.expiresAt?.toISOString() ?? "",
|
|
3127
|
+
created_at: now.toISOString()
|
|
3128
|
+
};
|
|
3129
|
+
await this.table.add([row]);
|
|
3130
|
+
return {
|
|
3131
|
+
id,
|
|
3132
|
+
actorId: input.actorId,
|
|
3133
|
+
memoryType: input.memoryType ?? "interaction",
|
|
3134
|
+
content: input.content,
|
|
3135
|
+
importance: input.importance ?? 0.5,
|
|
3136
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
3137
|
+
tags: input.tags ?? [],
|
|
3138
|
+
embedding: input.embedding,
|
|
3139
|
+
sourceId: input.sourceId,
|
|
3140
|
+
metadata: input.metadata ?? {},
|
|
3141
|
+
expiresAt: input.expiresAt,
|
|
3142
|
+
createdAt: now
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
async storeBatch(inputs) {
|
|
3146
|
+
const results = [];
|
|
3147
|
+
for (const input of inputs) {
|
|
3148
|
+
results.push(await this.store(input));
|
|
3149
|
+
}
|
|
3150
|
+
return results;
|
|
3151
|
+
}
|
|
3152
|
+
async get(id) {
|
|
3153
|
+
const results = await this.table.search(this._zeroVector).limit(1).where(`id = "${id}"`).execute();
|
|
3154
|
+
if (results.length === 0) return null;
|
|
3155
|
+
const row = results[0];
|
|
3156
|
+
if (row.expires_at) {
|
|
3157
|
+
const expiresAt = new Date(row.expires_at);
|
|
3158
|
+
if (expiresAt <= /* @__PURE__ */ new Date()) {
|
|
3159
|
+
await this.table.delete(`id = "${id}"`);
|
|
3160
|
+
return null;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
return this._rowToMemory(row);
|
|
3164
|
+
}
|
|
3165
|
+
async delete(id) {
|
|
3166
|
+
const existing = await this.get(id);
|
|
3167
|
+
if (!existing) throw notFound("Memory", id);
|
|
3168
|
+
await this.table.delete(`id = "${id}"`);
|
|
3169
|
+
}
|
|
3170
|
+
async deleteMany(ids) {
|
|
3171
|
+
const before = await this.table.countRows();
|
|
3172
|
+
const inClause = ids.map((id) => `"${id}"`).join(", ");
|
|
3173
|
+
await this.table.delete(`id IN (${inClause})`);
|
|
3174
|
+
const after = await this.table.countRows();
|
|
3175
|
+
return before - after;
|
|
3176
|
+
}
|
|
3177
|
+
async retrieve(query, embedding) {
|
|
3178
|
+
const conditions = [];
|
|
3179
|
+
if (query.actorId) {
|
|
3180
|
+
conditions.push(`actor_id = "${query.actorId}"`);
|
|
3181
|
+
}
|
|
3182
|
+
const where = conditions.length > 0 ? conditions.join(" AND ") : "";
|
|
3183
|
+
const isSemantic = (query.strategy === "semantic" || query.strategy === "hybrid") && embedding && embedding.length > 0;
|
|
3184
|
+
const searchVec = isSemantic ? embedding : this._zeroVector;
|
|
3185
|
+
const scanLimit = isSemantic ? query.limit ?? 10 : 1e4;
|
|
3186
|
+
const results = await this.table.search(searchVec).limit(scanLimit).where(where).execute();
|
|
3187
|
+
let rows = results;
|
|
3188
|
+
const now = /* @__PURE__ */ new Date();
|
|
3189
|
+
rows = rows.filter((r) => !r.expires_at || new Date(r.expires_at) > now);
|
|
3190
|
+
if (query.actorId) {
|
|
3191
|
+
rows = rows.filter((r) => r.actor_id === query.actorId);
|
|
3192
|
+
}
|
|
3193
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
3194
|
+
rows = rows.filter((r) => query.memoryTypes.includes(r.memory_type));
|
|
3195
|
+
}
|
|
3196
|
+
if (query.tags && query.tags.length > 0) {
|
|
3197
|
+
rows = rows.filter((r) => {
|
|
3198
|
+
const tags = JSON.parse(r.tags);
|
|
3199
|
+
return tags.some((t) => query.tags.includes(t));
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
if (query.query) {
|
|
3203
|
+
const q = query.query.toLowerCase();
|
|
3204
|
+
rows = rows.filter((r) => r.content.toLowerCase().includes(q));
|
|
3205
|
+
}
|
|
3206
|
+
const limit = query.limit ?? 10;
|
|
3207
|
+
if (isSemantic && query.strategy === "hybrid") {
|
|
3208
|
+
rows.sort((a, b) => {
|
|
3209
|
+
const scoreA = (a._distance ?? 0) + a.importance;
|
|
3210
|
+
const scoreB = (b._distance ?? 0) + b.importance;
|
|
3211
|
+
return scoreB - scoreA;
|
|
3212
|
+
});
|
|
3213
|
+
} else if (query.strategy === "recent") {
|
|
3214
|
+
rows.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
3215
|
+
} else if (query.strategy === "important") {
|
|
3216
|
+
rows.sort((a, b) => b.importance - a.importance);
|
|
3217
|
+
} else if (!isSemantic) {
|
|
3218
|
+
rows.sort((a, b) => b.importance - a.importance || new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
3219
|
+
}
|
|
3220
|
+
return rows.slice(0, limit).map((r) => this._rowToMemory(r));
|
|
3221
|
+
}
|
|
3222
|
+
async count(filter) {
|
|
3223
|
+
const conditions = [];
|
|
3224
|
+
if (filter?.actorId) {
|
|
3225
|
+
conditions.push(`actor_id = "${filter.actorId}"`);
|
|
3226
|
+
}
|
|
3227
|
+
const where = conditions.length > 0 ? conditions.join(" AND ") : "";
|
|
3228
|
+
const results = await this.table.search(this._zeroVector).limit(1e4).where(where).execute();
|
|
3229
|
+
const now = /* @__PURE__ */ new Date();
|
|
3230
|
+
let rows = results.filter((r) => !r.expires_at || new Date(r.expires_at) > now);
|
|
3231
|
+
if (filter?.actorId) {
|
|
3232
|
+
rows = rows.filter((r) => r.actor_id === filter.actorId);
|
|
3233
|
+
}
|
|
3234
|
+
if (filter?.memoryType) {
|
|
3235
|
+
rows = rows.filter((r) => r.memory_type === filter.memoryType);
|
|
3236
|
+
}
|
|
3237
|
+
if (filter?.minImportance !== void 0) {
|
|
3238
|
+
rows = rows.filter((r) => r.importance >= filter.minImportance);
|
|
3239
|
+
}
|
|
3240
|
+
return rows.length;
|
|
3241
|
+
}
|
|
3242
|
+
async close() {
|
|
3243
|
+
}
|
|
3244
|
+
_rowToMemory(row) {
|
|
3245
|
+
return {
|
|
3246
|
+
id: row.id,
|
|
3247
|
+
actorId: row.actor_id,
|
|
3248
|
+
memoryType: row.memory_type,
|
|
3249
|
+
content: row.content,
|
|
3250
|
+
importance: row.importance,
|
|
3251
|
+
emotionalValence: row.emotional_valence,
|
|
3252
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
3253
|
+
embedding: row.vector && row.vector.length > 0 ? row.vector : void 0,
|
|
3254
|
+
sourceId: row.source_id || void 0,
|
|
3255
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
3256
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : void 0,
|
|
3257
|
+
createdAt: new Date(row.created_at)
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
};
|
|
3261
|
+
|
|
3262
|
+
// src/adapters/storage/mongodb.ts
|
|
3263
|
+
var MongoDBStorageAdapter = class {
|
|
3264
|
+
collection;
|
|
3265
|
+
vectorDimensions;
|
|
3266
|
+
constructor(config) {
|
|
3267
|
+
this.collection = config.collection;
|
|
3268
|
+
this.vectorDimensions = config.vectorDimensions ?? 1536;
|
|
3269
|
+
}
|
|
3270
|
+
async initialize() {
|
|
3271
|
+
try {
|
|
3272
|
+
const col = this.collection;
|
|
3273
|
+
if (col.createSearchIndex) {
|
|
3274
|
+
await col.createSearchIndex({
|
|
3275
|
+
name: "memstack_vector",
|
|
3276
|
+
definition: {
|
|
3277
|
+
mappings: {
|
|
3278
|
+
dynamic: false,
|
|
3279
|
+
fields: {
|
|
3280
|
+
embedding: {
|
|
3281
|
+
type: "knnVector",
|
|
3282
|
+
dimensions: this.vectorDimensions,
|
|
3283
|
+
similarity: "cosine"
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
} catch {
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
generateId() {
|
|
3294
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
3295
|
+
}
|
|
3296
|
+
async store(input) {
|
|
3297
|
+
const now = /* @__PURE__ */ new Date();
|
|
3298
|
+
const memory = {
|
|
3299
|
+
id: input.id ?? this.generateId(),
|
|
3300
|
+
actorId: input.actorId,
|
|
3301
|
+
memoryType: input.memoryType ?? "interaction",
|
|
3302
|
+
content: input.content,
|
|
3303
|
+
importance: input.importance ?? 0.5,
|
|
3304
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
3305
|
+
tags: input.tags ?? [],
|
|
3306
|
+
embedding: input.embedding,
|
|
3307
|
+
sourceId: input.sourceId,
|
|
3308
|
+
metadata: input.metadata ?? {},
|
|
3309
|
+
expiresAt: input.expiresAt,
|
|
3310
|
+
createdAt: now
|
|
3311
|
+
};
|
|
3312
|
+
await this.collection.insertOne(this.toDoc(memory));
|
|
3313
|
+
return memory;
|
|
3314
|
+
}
|
|
3315
|
+
async storeBatch(inputs) {
|
|
3316
|
+
const results = [];
|
|
3317
|
+
for (const input of inputs) {
|
|
3318
|
+
results.push(await this.store(input));
|
|
3319
|
+
}
|
|
3320
|
+
return results;
|
|
3321
|
+
}
|
|
3322
|
+
async get(id) {
|
|
3323
|
+
const doc = await this.collection.findOne({ _id: id });
|
|
3324
|
+
if (!doc) return null;
|
|
3325
|
+
const memory = this.toMemory(doc);
|
|
3326
|
+
if (memory.expiresAt && memory.expiresAt <= /* @__PURE__ */ new Date()) return null;
|
|
3327
|
+
return memory;
|
|
3328
|
+
}
|
|
3329
|
+
async delete(id) {
|
|
3330
|
+
const result = await this.collection.deleteOne({ _id: id });
|
|
3331
|
+
if (result.deletedCount === 0) throw notFound("Memory", id);
|
|
3332
|
+
}
|
|
3333
|
+
async deleteMany(ids) {
|
|
3334
|
+
const result = await this.collection.deleteMany({ _id: { $in: ids } });
|
|
3335
|
+
return result.deletedCount;
|
|
3336
|
+
}
|
|
3337
|
+
async retrieve(query, embedding) {
|
|
3338
|
+
if (query.strategy === "semantic" && embedding) {
|
|
3339
|
+
return this.retrieveSemantic(query, embedding);
|
|
3340
|
+
}
|
|
3341
|
+
return this.retrieveStandard(query);
|
|
3342
|
+
}
|
|
3343
|
+
async count(filter) {
|
|
3344
|
+
const mongoFilter = {};
|
|
3345
|
+
if (filter?.actorId) mongoFilter.actorId = filter.actorId;
|
|
3346
|
+
if (filter?.memoryType) mongoFilter.memoryType = filter.memoryType;
|
|
3347
|
+
if (filter?.minImportance !== void 0) {
|
|
3348
|
+
mongoFilter.importance = { $gte: filter.minImportance };
|
|
3349
|
+
}
|
|
3350
|
+
return this.collection.countDocuments(mongoFilter);
|
|
3351
|
+
}
|
|
3352
|
+
async close() {
|
|
3353
|
+
}
|
|
3354
|
+
async retrieveStandard(query) {
|
|
3355
|
+
const filter = {};
|
|
3356
|
+
if (query.actorId) filter.actorId = query.actorId;
|
|
3357
|
+
if (query.tags && query.tags.length > 0) {
|
|
3358
|
+
filter.tags = { $in: query.tags };
|
|
3359
|
+
}
|
|
3360
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
3361
|
+
filter.memoryType = { $in: query.memoryTypes };
|
|
3362
|
+
}
|
|
3363
|
+
let sortObj;
|
|
3364
|
+
switch (query.strategy) {
|
|
3365
|
+
case "recent":
|
|
3366
|
+
sortObj = { createdAt: -1 };
|
|
3367
|
+
break;
|
|
3368
|
+
case "important":
|
|
3369
|
+
sortObj = { importance: -1 };
|
|
3370
|
+
break;
|
|
3371
|
+
default:
|
|
3372
|
+
sortObj = { importance: -1, createdAt: -1 };
|
|
3373
|
+
break;
|
|
3374
|
+
}
|
|
3375
|
+
const docs = await this.collection.find(filter).sort(sortObj).limit(1e4).toArray();
|
|
3376
|
+
const now = /* @__PURE__ */ new Date();
|
|
3377
|
+
let results = docs.map((doc) => this.toMemory(doc)).filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
3378
|
+
if (query.query) {
|
|
3379
|
+
const q = query.query.toLowerCase();
|
|
3380
|
+
results = results.filter((m) => m.content.toLowerCase().includes(q));
|
|
3381
|
+
}
|
|
3382
|
+
return results.slice(0, query.limit ?? 10);
|
|
3383
|
+
}
|
|
3384
|
+
async retrieveSemantic(query, embedding) {
|
|
3385
|
+
try {
|
|
3386
|
+
const filter = {};
|
|
3387
|
+
if (query.actorId) filter.actorId = query.actorId;
|
|
3388
|
+
const pipeline = [
|
|
3389
|
+
{
|
|
3390
|
+
$vectorSearch: {
|
|
3391
|
+
index: "memstack_vector",
|
|
3392
|
+
path: "embedding",
|
|
3393
|
+
queryVector: embedding,
|
|
3394
|
+
numCandidates: 100,
|
|
3395
|
+
limit: query.limit ?? 10,
|
|
3396
|
+
filter
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
];
|
|
3400
|
+
const docs = await this.collection.aggregate(pipeline).toArray();
|
|
3401
|
+
const now = /* @__PURE__ */ new Date();
|
|
3402
|
+
return docs.map((doc) => this.toMemory(doc)).filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
3403
|
+
} catch {
|
|
3404
|
+
return this.retrieveStandard(query);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
toMemory(doc) {
|
|
3408
|
+
return {
|
|
3409
|
+
id: doc._id,
|
|
3410
|
+
actorId: doc.actorId,
|
|
3411
|
+
memoryType: doc.memoryType,
|
|
3412
|
+
content: doc.content,
|
|
3413
|
+
importance: doc.importance,
|
|
3414
|
+
emotionalValence: doc.emotionalValence,
|
|
3415
|
+
tags: doc.tags,
|
|
3416
|
+
embedding: doc.embedding,
|
|
3417
|
+
sourceId: doc.sourceId,
|
|
3418
|
+
metadata: doc.metadata,
|
|
3419
|
+
expiresAt: doc.expiresAt,
|
|
3420
|
+
createdAt: doc.createdAt
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
toDoc(memory) {
|
|
3424
|
+
return {
|
|
3425
|
+
_id: memory.id,
|
|
3426
|
+
actorId: memory.actorId,
|
|
3427
|
+
memoryType: memory.memoryType,
|
|
3428
|
+
content: memory.content,
|
|
3429
|
+
importance: memory.importance,
|
|
3430
|
+
emotionalValence: memory.emotionalValence,
|
|
3431
|
+
tags: memory.tags,
|
|
3432
|
+
embedding: memory.embedding,
|
|
3433
|
+
sourceId: memory.sourceId,
|
|
3434
|
+
metadata: memory.metadata,
|
|
3435
|
+
expiresAt: memory.expiresAt,
|
|
3436
|
+
createdAt: memory.createdAt
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
};
|
|
3440
|
+
|
|
1498
3441
|
// src/adapters/llm/openai.ts
|
|
1499
3442
|
var OpenAILLMAdapter = class {
|
|
1500
3443
|
apiKey;
|
|
@@ -1634,7 +3577,6 @@ var AnthropicLLMAdapter = class {
|
|
|
1634
3577
|
baseURL;
|
|
1635
3578
|
defaultModel;
|
|
1636
3579
|
defaultMaxTokens;
|
|
1637
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1638
3580
|
sdk;
|
|
1639
3581
|
constructor(config) {
|
|
1640
3582
|
this.apiKey = config.apiKey;
|
|
@@ -1650,7 +3592,8 @@ var AnthropicLLMAdapter = class {
|
|
|
1650
3592
|
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
1651
3593
|
this.sdk = new Anthropic({ apiKey: this.apiKey, baseURL: this.baseURL });
|
|
1652
3594
|
}
|
|
1653
|
-
const
|
|
3595
|
+
const sdk = this.sdk;
|
|
3596
|
+
const response = await sdk.messages.create({
|
|
1654
3597
|
model,
|
|
1655
3598
|
system: request.system,
|
|
1656
3599
|
messages: [{ role: "user", content: request.user }],
|
|
@@ -1973,16 +3916,23 @@ var GroqLLMAdapter = class {
|
|
|
1973
3916
|
|
|
1974
3917
|
exports.AnthropicLLMAdapter = AnthropicLLMAdapter;
|
|
1975
3918
|
exports.CohereEmbeddingAdapter = CohereEmbeddingAdapter;
|
|
1976
|
-
exports.
|
|
3919
|
+
exports.DiskStorageAdapter = DiskStorageAdapter;
|
|
1977
3920
|
exports.GroqLLMAdapter = GroqLLMAdapter;
|
|
1978
|
-
exports.
|
|
3921
|
+
exports.HybridStorageAdapter = HybridStorageAdapter;
|
|
3922
|
+
exports.InMemoryStorageAdapter = InMemoryStorageAdapter;
|
|
3923
|
+
exports.LanceDBStorageAdapter = LanceDBStorageAdapter;
|
|
3924
|
+
exports.MarkdownStorageAdapter = MarkdownStorageAdapter;
|
|
1979
3925
|
exports.MemStack = MemStack;
|
|
1980
3926
|
exports.MemStackError = MemStackError;
|
|
3927
|
+
exports.MongoDBStorageAdapter = MongoDBStorageAdapter;
|
|
3928
|
+
exports.Neo4jStorageAdapter = Neo4jStorageAdapter;
|
|
1981
3929
|
exports.OllamaLLMAdapter = OllamaLLMAdapter;
|
|
1982
3930
|
exports.OpenAIEmbeddingAdapter = OpenAIEmbeddingAdapter;
|
|
1983
3931
|
exports.OpenAILLMAdapter = OpenAILLMAdapter;
|
|
1984
|
-
exports.
|
|
1985
|
-
exports.
|
|
3932
|
+
exports.PostgresStorageAdapter = PostgresStorageAdapter;
|
|
3933
|
+
exports.QdrantStorageAdapter = QdrantStorageAdapter;
|
|
3934
|
+
exports.RedisStorageAdapter = RedisStorageAdapter;
|
|
3935
|
+
exports.WeaviateStorageAdapter = WeaviateStorageAdapter;
|
|
1986
3936
|
exports.configError = configError;
|
|
1987
3937
|
exports.notFound = notFound;
|
|
1988
3938
|
exports.storageError = storageError;
|