@memstack/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +774 -0
- package/dist/index.cjs +1991 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +536 -0
- package/dist/index.d.ts +536 -0
- package/dist/index.js +1974 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/adapters/embedding/cohere.ts +77 -0
- package/src/adapters/embedding/openai.ts +65 -0
- package/src/adapters/llm/anthropic.ts +69 -0
- package/src/adapters/llm/groq.ts +185 -0
- package/src/adapters/llm/ollama.ts +85 -0
- package/src/adapters/llm/openai.ts +178 -0
- package/src/adapters/storage/disk.ts +334 -0
- package/src/adapters/storage/memory.ts +147 -0
- package/src/adapters/storage/postgres.ts +310 -0
- package/src/adapters/storage/redis.ts +274 -0
- package/src/client.ts +236 -0
- package/src/errors.ts +47 -0
- package/src/index.ts +52 -0
- package/src/interfaces.ts +154 -0
- package/src/memory/ContextCompiler.ts +158 -0
- package/src/memory/MemoryStore.ts +266 -0
- package/src/memory/Pruner.ts +66 -0
- package/src/memory/Summarizer.ts +46 -0
- package/src/types.ts +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1974 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, readdir } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var MemStackError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
retryable;
|
|
8
|
+
details;
|
|
9
|
+
constructor(code, message, options = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "MemStackError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.retryable = options.retryable ?? false;
|
|
14
|
+
this.details = options.details;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function notFound(entity, id) {
|
|
18
|
+
return new MemStackError("NOT_FOUND", `${entity} not found: ${id}`, {
|
|
19
|
+
retryable: false,
|
|
20
|
+
details: { entity, id }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function validationError(message, details) {
|
|
24
|
+
return new MemStackError("VALIDATION_ERROR", message, { details });
|
|
25
|
+
}
|
|
26
|
+
function storageError(message, cause) {
|
|
27
|
+
return new MemStackError("STORAGE_ERROR", message, {
|
|
28
|
+
retryable: true,
|
|
29
|
+
details: { cause: cause instanceof Error ? cause.message : String(cause) }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function configError(message) {
|
|
33
|
+
return new MemStackError("CONFIG_ERROR", message, { retryable: false });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/memory/Summarizer.ts
|
|
37
|
+
var Summarizer = class {
|
|
38
|
+
llm;
|
|
39
|
+
defaultPrompt;
|
|
40
|
+
constructor(llm, defaultPrompt) {
|
|
41
|
+
this.llm = llm;
|
|
42
|
+
this.defaultPrompt = defaultPrompt ?? "You are a memory summarizer. Condense the following list of memories into a single, concise summary paragraph that captures the key events, emotional tone, and important details. Write in third person past tense.";
|
|
43
|
+
}
|
|
44
|
+
async summarize(memories, options = {}, promptOverride) {
|
|
45
|
+
if (memories.length === 0) {
|
|
46
|
+
throw new MemStackError("VALIDATION_ERROR", "No memories to summarize");
|
|
47
|
+
}
|
|
48
|
+
const sorted = [...memories].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
49
|
+
const memoryTexts = sorted.map((m) => `[${m.createdAt.toISOString()}] ${m.content}`);
|
|
50
|
+
const system = promptOverride ?? this.defaultPrompt;
|
|
51
|
+
const user = memoryTexts.join("\n\n");
|
|
52
|
+
try {
|
|
53
|
+
const response = await this.llm.complete({ system, user });
|
|
54
|
+
return {
|
|
55
|
+
summaryContent: response.text.trim(),
|
|
56
|
+
tokenCount: response.tokens.total
|
|
57
|
+
};
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err instanceof MemStackError) throw err;
|
|
60
|
+
throw new MemStackError(
|
|
61
|
+
"LLM_ERROR",
|
|
62
|
+
`Summarization failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
{ retryable: true, details: { cause: err instanceof Error ? err.message : String(err) } }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/memory/Pruner.ts
|
|
70
|
+
var Pruner = class {
|
|
71
|
+
/** Returns memories to KEEP after applying the strategy. */
|
|
72
|
+
execute(memories, strategy) {
|
|
73
|
+
switch (strategy.type) {
|
|
74
|
+
case "byAge":
|
|
75
|
+
return this.keepByAge(memories, strategy);
|
|
76
|
+
case "byImportance":
|
|
77
|
+
return this.keepByImportance(memories, strategy);
|
|
78
|
+
case "byCount":
|
|
79
|
+
return this.keepByCount(memories, strategy);
|
|
80
|
+
case "byType":
|
|
81
|
+
return this.removeByType(memories, strategy);
|
|
82
|
+
case "custom":
|
|
83
|
+
return this.removeCustom(memories, strategy);
|
|
84
|
+
default:
|
|
85
|
+
return memories;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
dryRun(memories, strategy) {
|
|
89
|
+
return this.execute(memories, strategy);
|
|
90
|
+
}
|
|
91
|
+
keepByAge(memories, strategy) {
|
|
92
|
+
if (!strategy.maxAge) return memories;
|
|
93
|
+
const cutoff = Date.now() - strategy.maxAge;
|
|
94
|
+
return memories.filter((m) => m.createdAt.getTime() >= cutoff);
|
|
95
|
+
}
|
|
96
|
+
keepByImportance(memories, strategy) {
|
|
97
|
+
const threshold = strategy.minImportance ?? 0.5;
|
|
98
|
+
return memories.filter((m) => m.importance >= threshold);
|
|
99
|
+
}
|
|
100
|
+
keepByCount(memories, strategy) {
|
|
101
|
+
const maxPerActor = strategy.maxPerActor ?? 100;
|
|
102
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const m of memories) {
|
|
104
|
+
const list = grouped.get(m.actorId) || [];
|
|
105
|
+
list.push(m);
|
|
106
|
+
grouped.set(m.actorId, list);
|
|
107
|
+
}
|
|
108
|
+
const kept = [];
|
|
109
|
+
for (const [, actorMemories] of grouped) {
|
|
110
|
+
const sorted = [...actorMemories].sort((a, b) => b.importance - a.importance);
|
|
111
|
+
kept.push(...sorted.slice(0, maxPerActor));
|
|
112
|
+
}
|
|
113
|
+
return kept;
|
|
114
|
+
}
|
|
115
|
+
/** Remove memories of specified types. Opposite of filtering — removes the matches. */
|
|
116
|
+
removeByType(memories, strategy) {
|
|
117
|
+
if (!strategy.memoryTypes || strategy.memoryTypes.length === 0) return memories;
|
|
118
|
+
return memories.filter((m) => !strategy.memoryTypes.includes(m.memoryType));
|
|
119
|
+
}
|
|
120
|
+
/** Remove memories matching the predicate. Predicate returns true = should be removed. */
|
|
121
|
+
removeCustom(memories, strategy) {
|
|
122
|
+
if (!strategy.shouldRemove) return memories;
|
|
123
|
+
return memories.filter((m) => !strategy.shouldRemove(m));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/memory/ContextCompiler.ts
|
|
128
|
+
var ContextCompiler = class {
|
|
129
|
+
/**
|
|
130
|
+
* Estimate token count from text using a blended heuristic.
|
|
131
|
+
* Combines word-based (prose) and character-based (code/json) estimates,
|
|
132
|
+
* taking the max to handle arbitrary content types.
|
|
133
|
+
*/
|
|
134
|
+
estimateTokens(text) {
|
|
135
|
+
const charEst = Math.ceil(text.length / 4);
|
|
136
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
137
|
+
const wordEst = Math.ceil(words.length * 1.3);
|
|
138
|
+
return Math.max(charEst, Math.max(1, wordEst));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Truncate text to fit within a token budget, preserving whole words.
|
|
142
|
+
* Appends "..." when content is cut. Falls back to character-based
|
|
143
|
+
* truncation for text with few/no word boundaries.
|
|
144
|
+
*/
|
|
145
|
+
truncateToTokens(text, maxTokens) {
|
|
146
|
+
if (this.estimateTokens(text) <= maxTokens) return text;
|
|
147
|
+
const words = text.split(/\s+/);
|
|
148
|
+
const targetWords = Math.max(1, Math.floor(maxTokens / 1.3));
|
|
149
|
+
let currentTokens = 0;
|
|
150
|
+
let i = 0;
|
|
151
|
+
for (; i < words.length && i < targetWords; i++) {
|
|
152
|
+
currentTokens += this.estimateTokens(words[i]);
|
|
153
|
+
if (currentTokens > maxTokens) break;
|
|
154
|
+
}
|
|
155
|
+
if (i >= words.length) return text;
|
|
156
|
+
if (i > 0) return words.slice(0, i).join(" ") + "...";
|
|
157
|
+
const charLimit = maxTokens * 4;
|
|
158
|
+
return text.slice(0, charLimit) + "...";
|
|
159
|
+
}
|
|
160
|
+
compile(memories, options) {
|
|
161
|
+
const maxTokens = options.maxTokens ?? 2e3;
|
|
162
|
+
const relevant = memories.filter((m) => {
|
|
163
|
+
if (options.memoryTypes && options.memoryTypes.length > 0) {
|
|
164
|
+
return options.memoryTypes.includes(m.memoryType);
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
const sortedRecent = [...relevant].sort(
|
|
169
|
+
(a, b) => b.createdAt.getTime() - a.createdAt.getTime()
|
|
170
|
+
);
|
|
171
|
+
const sortedImportant = this.deduplicate(
|
|
172
|
+
[...relevant].filter((m) => m.importance >= 0.5).sort((a, b) => b.importance - a.importance)
|
|
173
|
+
);
|
|
174
|
+
const { recentMemories, importantMemories, systemPrompt } = this.assembleTokenBudgeted(sortedRecent, sortedImportant, maxTokens);
|
|
175
|
+
const tokenEstimate = this.estimateTokens(systemPrompt);
|
|
176
|
+
return { systemPrompt, recentMemories, importantMemories, tokenEstimate };
|
|
177
|
+
}
|
|
178
|
+
assembleTokenBudgeted(recent, important, maxTokens) {
|
|
179
|
+
const includedImportant = [];
|
|
180
|
+
const includedRecent = [];
|
|
181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
182
|
+
let usedTokens = 0;
|
|
183
|
+
for (const m of important) {
|
|
184
|
+
if (seen.has(m.id)) continue;
|
|
185
|
+
const prefix = `- `;
|
|
186
|
+
const suffix = ` (${m.memoryType}, importance: ${m.importance.toFixed(2)})
|
|
187
|
+
`;
|
|
188
|
+
const overhead = this.estimateTokens(prefix + suffix);
|
|
189
|
+
let tokens = this.estimateTokens(m.content) + overhead;
|
|
190
|
+
const remaining = Math.floor(maxTokens * 0.6) - usedTokens;
|
|
191
|
+
if (remaining <= 0) break;
|
|
192
|
+
if (tokens > remaining) {
|
|
193
|
+
const contentBudget = Math.max(0, remaining - overhead);
|
|
194
|
+
if (contentBudget > 0) {
|
|
195
|
+
const truncatedContent = this.truncateToTokens(m.content, contentBudget);
|
|
196
|
+
includedImportant.push({ ...m, content: truncatedContent });
|
|
197
|
+
tokens = this.estimateTokens(truncatedContent) + overhead;
|
|
198
|
+
usedTokens += tokens;
|
|
199
|
+
seen.add(m.id);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
includedImportant.push(m);
|
|
203
|
+
usedTokens += tokens;
|
|
204
|
+
seen.add(m.id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const m of recent) {
|
|
208
|
+
if (seen.has(m.id)) continue;
|
|
209
|
+
const prefix = `- `;
|
|
210
|
+
const suffix = `
|
|
211
|
+
`;
|
|
212
|
+
const overhead = this.estimateTokens(prefix + suffix);
|
|
213
|
+
let tokens = this.estimateTokens(m.content) + overhead;
|
|
214
|
+
const remaining = maxTokens - usedTokens;
|
|
215
|
+
if (remaining <= 0) break;
|
|
216
|
+
if (tokens > remaining) {
|
|
217
|
+
const contentBudget = Math.max(0, remaining - overhead);
|
|
218
|
+
if (contentBudget > 0) {
|
|
219
|
+
const truncatedContent = this.truncateToTokens(m.content, contentBudget);
|
|
220
|
+
includedRecent.push({ ...m, content: truncatedContent });
|
|
221
|
+
tokens = this.estimateTokens(truncatedContent) + overhead;
|
|
222
|
+
usedTokens += tokens;
|
|
223
|
+
seen.add(m.id);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
includedRecent.push(m);
|
|
227
|
+
usedTokens += tokens;
|
|
228
|
+
seen.add(m.id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const parts = [];
|
|
232
|
+
if (includedImportant.length > 0) {
|
|
233
|
+
parts.push("## Important Memories");
|
|
234
|
+
for (const m of includedImportant) {
|
|
235
|
+
parts.push(`- ${m.content} (${m.memoryType}, importance: ${m.importance.toFixed(2)})`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (includedRecent.length > 0) {
|
|
239
|
+
parts.push("\n## Recent Interactions");
|
|
240
|
+
for (const m of includedRecent) {
|
|
241
|
+
parts.push(`- ${m.content}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { recentMemories: includedRecent, importantMemories: includedImportant, systemPrompt: parts.join("\n") };
|
|
245
|
+
}
|
|
246
|
+
deduplicate(memories) {
|
|
247
|
+
const seen = /* @__PURE__ */ new Set();
|
|
248
|
+
const result = [];
|
|
249
|
+
for (const m of memories) {
|
|
250
|
+
const key = m.content.toLowerCase().trim();
|
|
251
|
+
if (!seen.has(key)) {
|
|
252
|
+
seen.add(key);
|
|
253
|
+
result.push(m);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/memory/MemoryStore.ts
|
|
261
|
+
var EMBEDDING_BATCH_SIZE = 2048;
|
|
262
|
+
var MemoryStore = class {
|
|
263
|
+
storage;
|
|
264
|
+
embedding;
|
|
265
|
+
summarizer;
|
|
266
|
+
pruner;
|
|
267
|
+
compiler;
|
|
268
|
+
embedOnStore;
|
|
269
|
+
constructor(config) {
|
|
270
|
+
this.storage = config.storage;
|
|
271
|
+
this.embedding = config.embedding;
|
|
272
|
+
this.embedOnStore = config.embedOnStore ?? true;
|
|
273
|
+
if (config.llm) {
|
|
274
|
+
this.summarizer = new Summarizer(config.llm, config.summarizationPrompt);
|
|
275
|
+
}
|
|
276
|
+
this.pruner = new Pruner();
|
|
277
|
+
this.compiler = new ContextCompiler();
|
|
278
|
+
}
|
|
279
|
+
async store(input) {
|
|
280
|
+
if (input.onConflict === "append") {
|
|
281
|
+
const hash = this._hashContent(input.actorId, input.content);
|
|
282
|
+
const existing = await this.storage.retrieve({ actorId: input.actorId, limit: 1e3 });
|
|
283
|
+
const match = existing.find(
|
|
284
|
+
(m) => this._hashContent(m.actorId, m.content) === hash
|
|
285
|
+
);
|
|
286
|
+
if (match) {
|
|
287
|
+
const mergedTags = [.../* @__PURE__ */ new Set([...match.tags ?? [], ...input.tags ?? []])];
|
|
288
|
+
const updated = await this.storage.store({
|
|
289
|
+
id: match.id,
|
|
290
|
+
actorId: match.actorId,
|
|
291
|
+
content: match.content,
|
|
292
|
+
memoryType: match.memoryType,
|
|
293
|
+
importance: input.importance ?? match.importance,
|
|
294
|
+
emotionalValence: input.emotionalValence ?? match.emotionalValence,
|
|
295
|
+
tags: mergedTags,
|
|
296
|
+
embedding: match.embedding,
|
|
297
|
+
sourceId: input.sourceId ?? match.sourceId,
|
|
298
|
+
metadata: { ...match.metadata, ...input.metadata },
|
|
299
|
+
expiresAt: input.expiresAt ?? match.expiresAt
|
|
300
|
+
});
|
|
301
|
+
return updated;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let withEmbedding = input;
|
|
305
|
+
if (this.embedding && this.embedOnStore) {
|
|
306
|
+
const embeddings = await this.embedding.embed([input.content]);
|
|
307
|
+
withEmbedding = { ...input, embedding: embeddings[0] };
|
|
308
|
+
}
|
|
309
|
+
const { onConflict: _, ...clean } = withEmbedding;
|
|
310
|
+
return this.storage.store(clean);
|
|
311
|
+
}
|
|
312
|
+
async storeBatch(inputs) {
|
|
313
|
+
const appendInputs = inputs.filter((i) => i.onConflict === "append");
|
|
314
|
+
const normalInputs = inputs.filter((i) => i.onConflict !== "append");
|
|
315
|
+
const results = [];
|
|
316
|
+
for (const input of appendInputs) {
|
|
317
|
+
results.push(await this.store(input));
|
|
318
|
+
}
|
|
319
|
+
if (normalInputs.length === 0) return results;
|
|
320
|
+
if (this.embedding && this.embedOnStore) {
|
|
321
|
+
for (let i = 0; i < normalInputs.length; i += EMBEDDING_BATCH_SIZE) {
|
|
322
|
+
const batch = normalInputs.slice(i, i + EMBEDDING_BATCH_SIZE);
|
|
323
|
+
const texts = batch.map((inp) => inp.content);
|
|
324
|
+
const embeddings = await this.embedding.embed(texts);
|
|
325
|
+
const enriched = batch.map((input, j) => ({ ...input, embedding: embeddings[j] }));
|
|
326
|
+
const stored = await this.storage.storeBatch(enriched);
|
|
327
|
+
results.push(...stored);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const stored = await this.storage.storeBatch(normalInputs);
|
|
331
|
+
results.push(...stored);
|
|
332
|
+
}
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
async get(id) {
|
|
336
|
+
return this.storage.get(id);
|
|
337
|
+
}
|
|
338
|
+
async delete(id) {
|
|
339
|
+
return this.storage.delete(id);
|
|
340
|
+
}
|
|
341
|
+
async deleteMany(ids) {
|
|
342
|
+
return this.storage.deleteMany(ids);
|
|
343
|
+
}
|
|
344
|
+
async touch(id) {
|
|
345
|
+
const memory = await this.storage.get(id);
|
|
346
|
+
if (!memory) throw notFound("Memory", id);
|
|
347
|
+
if (this.storage.touch) {
|
|
348
|
+
return this.storage.touch(id);
|
|
349
|
+
}
|
|
350
|
+
await this.storage.delete(id);
|
|
351
|
+
await this.storage.store({
|
|
352
|
+
actorId: memory.actorId,
|
|
353
|
+
content: memory.content,
|
|
354
|
+
memoryType: memory.memoryType,
|
|
355
|
+
importance: memory.importance,
|
|
356
|
+
emotionalValence: memory.emotionalValence,
|
|
357
|
+
tags: memory.tags,
|
|
358
|
+
embedding: memory.embedding,
|
|
359
|
+
sourceId: memory.sourceId,
|
|
360
|
+
metadata: memory.metadata,
|
|
361
|
+
expiresAt: memory.expiresAt
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
async count(filter) {
|
|
365
|
+
return this.storage.count(filter);
|
|
366
|
+
}
|
|
367
|
+
async retrieve(query) {
|
|
368
|
+
let embedding;
|
|
369
|
+
if (this.embedding && query.query && (query.strategy === "semantic" || query.strategy === "hybrid")) {
|
|
370
|
+
const vecs = await this.embedding.embed([query.query]);
|
|
371
|
+
embedding = vecs[0];
|
|
372
|
+
}
|
|
373
|
+
return this.storage.retrieve(query, embedding);
|
|
374
|
+
}
|
|
375
|
+
async compileContext(options) {
|
|
376
|
+
const maxTokens = options.maxTokens ?? 2e3;
|
|
377
|
+
const memories = await this.storage.retrieve({ actorId: options.actorId, limit: 50 });
|
|
378
|
+
return this.compiler.compile(memories, { ...options, maxTokens });
|
|
379
|
+
}
|
|
380
|
+
async summarize(options, onError) {
|
|
381
|
+
if (!this.summarizer) {
|
|
382
|
+
throw validationError("No LLM configured for summarization");
|
|
383
|
+
}
|
|
384
|
+
const allMemories = await this.storage.retrieve({
|
|
385
|
+
actorId: options.actorId,
|
|
386
|
+
memoryTypes: options.memoryTypes ?? ["interaction"],
|
|
387
|
+
limit: 1e3,
|
|
388
|
+
strategy: "recent"
|
|
389
|
+
});
|
|
390
|
+
let toSummarize = allMemories;
|
|
391
|
+
if (options.olderThan) {
|
|
392
|
+
toSummarize = toSummarize.filter((m) => m.createdAt < options.olderThan);
|
|
393
|
+
}
|
|
394
|
+
if (options.skipMostRecent) {
|
|
395
|
+
const sorted = [...toSummarize].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
396
|
+
toSummarize = sorted.slice(0, Math.max(0, sorted.length - options.skipMostRecent));
|
|
397
|
+
}
|
|
398
|
+
if (options.targetCount && toSummarize.length > options.targetCount) {
|
|
399
|
+
const sorted = [...toSummarize].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
400
|
+
toSummarize = sorted.slice(0, options.targetCount);
|
|
401
|
+
}
|
|
402
|
+
if (toSummarize.length === 0) {
|
|
403
|
+
throw validationError("No memories to summarize");
|
|
404
|
+
}
|
|
405
|
+
let summaryContent;
|
|
406
|
+
try {
|
|
407
|
+
const result = await this.summarizer.summarize(toSummarize, options);
|
|
408
|
+
summaryContent = result.summaryContent;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
const summary = await this.storage.store({
|
|
414
|
+
actorId: options.actorId ?? toSummarize[0].actorId,
|
|
415
|
+
content: summaryContent,
|
|
416
|
+
memoryType: "summary",
|
|
417
|
+
importance: Math.max(...toSummarize.map((m) => m.importance)),
|
|
418
|
+
emotionalValence: toSummarize.reduce((sum, m) => sum + m.emotionalValence, 0) / toSummarize.length,
|
|
419
|
+
tags: ["summary"],
|
|
420
|
+
sourceId: toSummarize[0].id
|
|
421
|
+
});
|
|
422
|
+
let deletedCount = 0;
|
|
423
|
+
if (!options.keepOriginals) {
|
|
424
|
+
const ids = toSummarize.map((m) => m.id);
|
|
425
|
+
deletedCount = await this.storage.deleteMany(ids);
|
|
426
|
+
}
|
|
427
|
+
return { summary, deletedCount };
|
|
428
|
+
}
|
|
429
|
+
async prune(strategy) {
|
|
430
|
+
const allMemories = await this.storage.retrieve({ limit: 1e4 });
|
|
431
|
+
const kept = this.pruner.execute(allMemories, strategy);
|
|
432
|
+
const keptIds = new Set(kept.map((k) => k.id));
|
|
433
|
+
const prunedIds = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
|
|
434
|
+
let count = 0;
|
|
435
|
+
if (prunedIds.length > 0) {
|
|
436
|
+
count = await this.storage.deleteMany(prunedIds);
|
|
437
|
+
}
|
|
438
|
+
return { pruned: prunedIds, count };
|
|
439
|
+
}
|
|
440
|
+
async dryRunPrune(strategy) {
|
|
441
|
+
const allMemories = await this.storage.retrieve({ limit: 1e4 });
|
|
442
|
+
const kept = this.pruner.execute(allMemories, strategy);
|
|
443
|
+
const keptIds = new Set(kept.map((k) => k.id));
|
|
444
|
+
const wouldPrune = allMemories.filter((m) => !keptIds.has(m.id)).map((m) => m.id);
|
|
445
|
+
return { wouldPrune, count: wouldPrune.length };
|
|
446
|
+
}
|
|
447
|
+
async export() {
|
|
448
|
+
return this.storage.retrieve({ limit: 1e5 });
|
|
449
|
+
}
|
|
450
|
+
_hashContent(actorId, content) {
|
|
451
|
+
const normalized = `${actorId}::${content.toLowerCase().trim()}`;
|
|
452
|
+
let hash = 14695981039346656037n;
|
|
453
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
454
|
+
hash ^= BigInt(normalized.charCodeAt(i));
|
|
455
|
+
hash *= 1099511628211n;
|
|
456
|
+
}
|
|
457
|
+
return hash.toString(36);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// src/adapters/storage/memory.ts
|
|
462
|
+
var InMemoryStorage = class {
|
|
463
|
+
memories = /* @__PURE__ */ new Map();
|
|
464
|
+
async initialize() {
|
|
465
|
+
}
|
|
466
|
+
generateId() {
|
|
467
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
468
|
+
}
|
|
469
|
+
async store(input) {
|
|
470
|
+
const now = /* @__PURE__ */ new Date();
|
|
471
|
+
const memory = {
|
|
472
|
+
id: input.id ?? this.generateId(),
|
|
473
|
+
actorId: input.actorId,
|
|
474
|
+
memoryType: input.memoryType ?? "interaction",
|
|
475
|
+
content: input.content,
|
|
476
|
+
importance: input.importance ?? 0.5,
|
|
477
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
478
|
+
tags: input.tags ?? [],
|
|
479
|
+
embedding: input.embedding,
|
|
480
|
+
sourceId: input.sourceId,
|
|
481
|
+
metadata: input.metadata ?? {},
|
|
482
|
+
expiresAt: input.expiresAt,
|
|
483
|
+
createdAt: now,
|
|
484
|
+
_touchedAt: now
|
|
485
|
+
};
|
|
486
|
+
this.memories.set(memory.id, memory);
|
|
487
|
+
return memory;
|
|
488
|
+
}
|
|
489
|
+
async storeBatch(inputs) {
|
|
490
|
+
const results = [];
|
|
491
|
+
for (const input of inputs) {
|
|
492
|
+
results.push(await this.store(input));
|
|
493
|
+
}
|
|
494
|
+
return results;
|
|
495
|
+
}
|
|
496
|
+
async get(id) {
|
|
497
|
+
const memory = this.memories.get(id);
|
|
498
|
+
if (!memory) return null;
|
|
499
|
+
if (memory.expiresAt && memory.expiresAt <= /* @__PURE__ */ new Date()) {
|
|
500
|
+
this.memories.delete(id);
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return memory;
|
|
504
|
+
}
|
|
505
|
+
async delete(id) {
|
|
506
|
+
if (!this.memories.has(id)) throw notFound("Memory", id);
|
|
507
|
+
this.memories.delete(id);
|
|
508
|
+
}
|
|
509
|
+
async deleteMany(ids) {
|
|
510
|
+
let count = 0;
|
|
511
|
+
for (const id of ids) {
|
|
512
|
+
if (this.memories.has(id)) {
|
|
513
|
+
this.memories.delete(id);
|
|
514
|
+
count++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return count;
|
|
518
|
+
}
|
|
519
|
+
async retrieve(query, _embedding) {
|
|
520
|
+
let results = Array.from(this.memories.values());
|
|
521
|
+
const now = /* @__PURE__ */ new Date();
|
|
522
|
+
results = results.filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
523
|
+
if (query.actorId) {
|
|
524
|
+
results = results.filter((m) => m.actorId === query.actorId);
|
|
525
|
+
}
|
|
526
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
527
|
+
results = results.filter((m) => query.memoryTypes.includes(m.memoryType));
|
|
528
|
+
}
|
|
529
|
+
if (query.tags && query.tags.length > 0) {
|
|
530
|
+
results = results.filter((m) => m.tags?.some((t) => query.tags.includes(t)));
|
|
531
|
+
}
|
|
532
|
+
if (query.query) {
|
|
533
|
+
const q = query.query.toLowerCase();
|
|
534
|
+
results = results.filter((m) => m.content.toLowerCase().includes(q));
|
|
535
|
+
}
|
|
536
|
+
switch (query.strategy) {
|
|
537
|
+
case "recent":
|
|
538
|
+
results.sort((a, b) => b._touchedAt.getTime() - a._touchedAt.getTime());
|
|
539
|
+
break;
|
|
540
|
+
case "important":
|
|
541
|
+
results.sort((a, b) => b.importance - a.importance);
|
|
542
|
+
break;
|
|
543
|
+
case "hybrid":
|
|
544
|
+
case "semantic":
|
|
545
|
+
default:
|
|
546
|
+
results.sort((a, b) => b.importance - a.importance || b._touchedAt.getTime() - a._touchedAt.getTime());
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
for (const r of results.slice(0, query.limit ?? 10)) {
|
|
550
|
+
r._touchedAt = /* @__PURE__ */ new Date();
|
|
551
|
+
}
|
|
552
|
+
const limit = query.limit ?? 10;
|
|
553
|
+
return results.slice(0, limit);
|
|
554
|
+
}
|
|
555
|
+
async count(filter) {
|
|
556
|
+
const now = /* @__PURE__ */ new Date();
|
|
557
|
+
let results = Array.from(this.memories.values()).filter((m) => !m.expiresAt || m.expiresAt > now);
|
|
558
|
+
if (filter?.actorId) {
|
|
559
|
+
results = results.filter((m) => m.actorId === filter.actorId);
|
|
560
|
+
}
|
|
561
|
+
if (filter?.memoryType) {
|
|
562
|
+
results = results.filter((m) => m.memoryType === filter.memoryType);
|
|
563
|
+
}
|
|
564
|
+
if (filter?.minImportance !== void 0) {
|
|
565
|
+
results = results.filter((m) => m.importance >= filter.minImportance);
|
|
566
|
+
}
|
|
567
|
+
return results.length;
|
|
568
|
+
}
|
|
569
|
+
async touch(id) {
|
|
570
|
+
const memory = this.memories.get(id);
|
|
571
|
+
if (!memory) throw notFound("Memory", id);
|
|
572
|
+
memory._touchedAt = /* @__PURE__ */ new Date();
|
|
573
|
+
}
|
|
574
|
+
async close() {
|
|
575
|
+
this.memories.clear();
|
|
576
|
+
}
|
|
577
|
+
getAllMemories() {
|
|
578
|
+
return Array.from(this.memories.values());
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/client.ts
|
|
583
|
+
var MemStack = class {
|
|
584
|
+
memory;
|
|
585
|
+
config;
|
|
586
|
+
storage;
|
|
587
|
+
processCount = 0;
|
|
588
|
+
constructor(config) {
|
|
589
|
+
if (!config.llm) {
|
|
590
|
+
throw configError("LLM provider is required");
|
|
591
|
+
}
|
|
592
|
+
this.config = config;
|
|
593
|
+
this.storage = config.storage ?? new InMemoryStorage();
|
|
594
|
+
this.memory = new MemoryStore({
|
|
595
|
+
storage: this.storage,
|
|
596
|
+
embedding: config.embedding,
|
|
597
|
+
llm: config.llm,
|
|
598
|
+
embedOnStore: config.defaults?.embedOnStore ?? true,
|
|
599
|
+
summarizationPrompt: config.defaults?.summarizationPrompt
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
async process(input) {
|
|
603
|
+
let importance = input.importance;
|
|
604
|
+
let tags = input.tags;
|
|
605
|
+
const needImportance = importance === void 0 && this.config.defaults?.autoImportance;
|
|
606
|
+
const needTags = (!tags || tags.length === 0) && this.config.defaults?.autoTags;
|
|
607
|
+
if (needImportance || needTags) {
|
|
608
|
+
try {
|
|
609
|
+
if (needImportance && needTags) {
|
|
610
|
+
const result = await this.config.llm.complete({
|
|
611
|
+
system: 'Analyze the memory and respond with a JSON object: {"importance": <0.0-1.0>, "tags": [<array of 1-5 lowercase single-word tags>]}. Return ONLY the JSON, nothing else.',
|
|
612
|
+
user: input.content,
|
|
613
|
+
maxTokens: 100,
|
|
614
|
+
temperature: 0
|
|
615
|
+
});
|
|
616
|
+
const parsed = this._parseEnrichmentJson(result.text);
|
|
617
|
+
if (parsed.importance !== void 0) importance = parsed.importance;
|
|
618
|
+
if (parsed.tags) tags = parsed.tags;
|
|
619
|
+
} else if (needImportance) {
|
|
620
|
+
const result = await this.config.llm.complete({
|
|
621
|
+
system: "Rate the importance of the following memory for an AI agent on a scale of 0.0 to 1.0, where 0.0 is trivial and 1.0 is critical. Return ONLY a single float number, nothing else.",
|
|
622
|
+
user: input.content,
|
|
623
|
+
maxTokens: 10,
|
|
624
|
+
temperature: 0
|
|
625
|
+
});
|
|
626
|
+
const parsed = parseFloat(result.text.trim());
|
|
627
|
+
importance = Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : 0.5;
|
|
628
|
+
} else if (needTags) {
|
|
629
|
+
const result = await this.config.llm.complete({
|
|
630
|
+
system: 'Extract 1-5 concise, lowercase, single-word tags from the following memory. Return ONLY a JSON array of strings, nothing else. Example: ["combat", "goblin", "forest"]',
|
|
631
|
+
user: input.content,
|
|
632
|
+
maxTokens: 80,
|
|
633
|
+
temperature: 0
|
|
634
|
+
});
|
|
635
|
+
tags = this._parseTagsJson(result.text);
|
|
636
|
+
}
|
|
637
|
+
} catch (err) {
|
|
638
|
+
this.config.hooks?.onError?.(
|
|
639
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
640
|
+
"auto-enrich"
|
|
641
|
+
);
|
|
642
|
+
if (importance === void 0) importance = 0.5;
|
|
643
|
+
if (!tags) tags = [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const memoryInput = {
|
|
647
|
+
actorId: input.actorId,
|
|
648
|
+
content: input.content,
|
|
649
|
+
memoryType: input.memoryType ?? "interaction",
|
|
650
|
+
importance: importance ?? 0.5,
|
|
651
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
652
|
+
tags: tags ?? [],
|
|
653
|
+
metadata: input.metadata ?? {},
|
|
654
|
+
expiresAt: input.expiresAt,
|
|
655
|
+
onConflict: input.onConflict
|
|
656
|
+
};
|
|
657
|
+
const memory = await this.memory.store(memoryInput);
|
|
658
|
+
this.config.hooks?.onMemoryStored?.(memory);
|
|
659
|
+
this.processCount++;
|
|
660
|
+
let summaryCreated;
|
|
661
|
+
const threshold = this.config.defaults?.summarizationThreshold ?? 100;
|
|
662
|
+
if (threshold > 0) {
|
|
663
|
+
const count = await this.memory.count({ actorId: input.actorId, memoryType: "interaction" });
|
|
664
|
+
if (count % threshold === 0 && count > 0) {
|
|
665
|
+
try {
|
|
666
|
+
const result = await this.memory.summarize(
|
|
667
|
+
{
|
|
668
|
+
actorId: input.actorId,
|
|
669
|
+
memoryTypes: ["interaction"],
|
|
670
|
+
skipMostRecent: 5,
|
|
671
|
+
targetCount: Math.min(15, count)
|
|
672
|
+
},
|
|
673
|
+
this.config.hooks?.onError ? (err) => this.config.hooks.onError(err, "summarize") : void 0
|
|
674
|
+
);
|
|
675
|
+
summaryCreated = result.summary;
|
|
676
|
+
this.config.hooks?.onSummaryCreated?.(result.summary, result.deletedCount);
|
|
677
|
+
} catch (err) {
|
|
678
|
+
this.config.hooks?.onError?.(
|
|
679
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
680
|
+
"summarize"
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const pruneStrategy = this.config.defaults?.pruneStrategy;
|
|
686
|
+
const pruneInterval = this.config.defaults?.pruneInterval ?? 100;
|
|
687
|
+
if (pruneStrategy && this.processCount % pruneInterval === 0) {
|
|
688
|
+
try {
|
|
689
|
+
const pruneResult = await this.memory.prune(pruneStrategy);
|
|
690
|
+
if (pruneResult.count > 0) {
|
|
691
|
+
this.config.hooks?.onMemoryPruned?.(pruneResult.pruned);
|
|
692
|
+
}
|
|
693
|
+
} catch (err) {
|
|
694
|
+
this.config.hooks?.onError?.(
|
|
695
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
696
|
+
"prune"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return { memory, summaryCreated };
|
|
701
|
+
}
|
|
702
|
+
async export() {
|
|
703
|
+
const memories = await this.memory.export();
|
|
704
|
+
return {
|
|
705
|
+
version: 1,
|
|
706
|
+
memories,
|
|
707
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async import(snapshot) {
|
|
711
|
+
if (snapshot.version !== 1) {
|
|
712
|
+
throw validationError(`Unsupported snapshot version: ${snapshot.version}`);
|
|
713
|
+
}
|
|
714
|
+
await this.memory.storeBatch(snapshot.memories);
|
|
715
|
+
}
|
|
716
|
+
async health() {
|
|
717
|
+
const status = { storage: false, llm: false, embedding: false };
|
|
718
|
+
try {
|
|
719
|
+
await this.memory.count();
|
|
720
|
+
status.storage = true;
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
await this.config.llm.complete({ system: "ok", user: "health", maxTokens: 1 });
|
|
725
|
+
status.llm = true;
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
if (this.config.embedding) {
|
|
729
|
+
try {
|
|
730
|
+
await this.config.embedding.embed(["health check"]);
|
|
731
|
+
status.embedding = true;
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return status;
|
|
736
|
+
}
|
|
737
|
+
async close() {
|
|
738
|
+
await this.storage.close();
|
|
739
|
+
}
|
|
740
|
+
_parseEnrichmentJson(text) {
|
|
741
|
+
try {
|
|
742
|
+
const json = JSON.parse(this._extractJson(text));
|
|
743
|
+
const result = {};
|
|
744
|
+
if (typeof json.importance === "number" && Number.isFinite(json.importance)) {
|
|
745
|
+
result.importance = Math.max(0, Math.min(1, json.importance));
|
|
746
|
+
}
|
|
747
|
+
if (Array.isArray(json.tags) && json.tags.every((t) => typeof t === "string")) {
|
|
748
|
+
result.tags = json.tags.map((t) => t.toLowerCase().trim()).slice(0, 5);
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
} catch {
|
|
752
|
+
return {};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
_parseTagsJson(text) {
|
|
756
|
+
try {
|
|
757
|
+
const parsed = JSON.parse(this._extractJson(text));
|
|
758
|
+
if (Array.isArray(parsed) && parsed.every((t) => typeof t === "string")) {
|
|
759
|
+
return parsed.map((t) => t.toLowerCase().trim()).slice(0, 5);
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
return text.split(",").map((t) => t.replace(/[\[\]"]/g, "").trim().toLowerCase()).filter((t) => t.length > 0).slice(0, 5);
|
|
764
|
+
}
|
|
765
|
+
/** Strip markdown code fences and extract the JSON payload from LLM output. */
|
|
766
|
+
_extractJson(text) {
|
|
767
|
+
let cleaned = text.trim();
|
|
768
|
+
const fenceMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
769
|
+
if (fenceMatch) cleaned = fenceMatch[1].trim();
|
|
770
|
+
return cleaned;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
var DiskStorage = class {
|
|
774
|
+
dir;
|
|
775
|
+
_writeLocks = /* @__PURE__ */ new Map();
|
|
776
|
+
constructor(config = {}) {
|
|
777
|
+
this.dir = config.storageDir ?? "./memstack-data";
|
|
778
|
+
}
|
|
779
|
+
async initialize() {
|
|
780
|
+
await mkdir(this.dir, { recursive: true });
|
|
781
|
+
}
|
|
782
|
+
generateId() {
|
|
783
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
784
|
+
}
|
|
785
|
+
async store(input) {
|
|
786
|
+
const now = /* @__PURE__ */ new Date();
|
|
787
|
+
const memory = {
|
|
788
|
+
id: input.id ?? this.generateId(),
|
|
789
|
+
actorId: input.actorId,
|
|
790
|
+
memoryType: input.memoryType ?? "interaction",
|
|
791
|
+
content: input.content,
|
|
792
|
+
importance: input.importance ?? 0.5,
|
|
793
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
794
|
+
tags: input.tags ?? [],
|
|
795
|
+
embedding: input.embedding,
|
|
796
|
+
sourceId: input.sourceId,
|
|
797
|
+
metadata: input.metadata ?? {},
|
|
798
|
+
expiresAt: input.expiresAt,
|
|
799
|
+
createdAt: now,
|
|
800
|
+
_touchedAt: now.toISOString()
|
|
801
|
+
};
|
|
802
|
+
await this._withWriteLock(input.actorId, async () => {
|
|
803
|
+
const records = await this._readFile(input.actorId);
|
|
804
|
+
const existingIdx = records.findIndex((r) => r.id === memory.id);
|
|
805
|
+
if (existingIdx !== -1) {
|
|
806
|
+
records[existingIdx] = memory;
|
|
807
|
+
} else {
|
|
808
|
+
records.push(memory);
|
|
809
|
+
}
|
|
810
|
+
await this._writeFile(input.actorId, records);
|
|
811
|
+
});
|
|
812
|
+
return this._toExternal(memory);
|
|
813
|
+
}
|
|
814
|
+
async storeBatch(inputs) {
|
|
815
|
+
const byActor = /* @__PURE__ */ new Map();
|
|
816
|
+
const now = /* @__PURE__ */ new Date();
|
|
817
|
+
const all = [];
|
|
818
|
+
for (const input of inputs) {
|
|
819
|
+
const memory = {
|
|
820
|
+
id: input.id ?? this.generateId(),
|
|
821
|
+
actorId: input.actorId,
|
|
822
|
+
memoryType: input.memoryType ?? "interaction",
|
|
823
|
+
content: input.content,
|
|
824
|
+
importance: input.importance ?? 0.5,
|
|
825
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
826
|
+
tags: input.tags ?? [],
|
|
827
|
+
embedding: input.embedding,
|
|
828
|
+
sourceId: input.sourceId,
|
|
829
|
+
metadata: input.metadata ?? {},
|
|
830
|
+
expiresAt: input.expiresAt,
|
|
831
|
+
createdAt: now,
|
|
832
|
+
_touchedAt: now.toISOString()
|
|
833
|
+
};
|
|
834
|
+
all.push(memory);
|
|
835
|
+
const group = byActor.get(input.actorId) ?? [];
|
|
836
|
+
group.push(memory);
|
|
837
|
+
byActor.set(input.actorId, group);
|
|
838
|
+
}
|
|
839
|
+
for (const [actorId, group] of byActor) {
|
|
840
|
+
await this._withWriteLock(actorId, async () => {
|
|
841
|
+
const existing = await this._readFile(actorId);
|
|
842
|
+
for (const mem of group) {
|
|
843
|
+
const existingIdx = existing.findIndex((r) => r.id === mem.id);
|
|
844
|
+
if (existingIdx !== -1) {
|
|
845
|
+
existing[existingIdx] = mem;
|
|
846
|
+
} else {
|
|
847
|
+
existing.push(mem);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
await this._writeFile(actorId, existing);
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return all.map((m) => this._toExternal(m));
|
|
854
|
+
}
|
|
855
|
+
async get(id) {
|
|
856
|
+
const actorIds = await this._listActors();
|
|
857
|
+
for (const actorId of actorIds) {
|
|
858
|
+
const records = await this._readFile(actorId);
|
|
859
|
+
const record = records.find((r) => r.id === id);
|
|
860
|
+
if (record) {
|
|
861
|
+
if (record.expiresAt && new Date(record.expiresAt) <= /* @__PURE__ */ new Date()) return null;
|
|
862
|
+
return this._toExternal(record);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
async delete(id) {
|
|
868
|
+
const actorIds = await this._listActors();
|
|
869
|
+
for (const actorId of actorIds) {
|
|
870
|
+
const records = await this._readFile(actorId);
|
|
871
|
+
const index = records.findIndex((r) => r.id === id);
|
|
872
|
+
if (index !== -1) {
|
|
873
|
+
await this._withWriteLock(actorId, async () => {
|
|
874
|
+
const fresh = await this._readFile(actorId);
|
|
875
|
+
const i = fresh.findIndex((r) => r.id === id);
|
|
876
|
+
if (i !== -1) {
|
|
877
|
+
fresh.splice(i, 1);
|
|
878
|
+
await this._writeFile(actorId, fresh);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
throw notFound("Memory", id);
|
|
885
|
+
}
|
|
886
|
+
async deleteMany(ids) {
|
|
887
|
+
const idSet = new Set(ids);
|
|
888
|
+
let count = 0;
|
|
889
|
+
const actorIds = await this._listActors();
|
|
890
|
+
for (const actorId of actorIds) {
|
|
891
|
+
await this._withWriteLock(actorId, async () => {
|
|
892
|
+
const records = await this._readFile(actorId);
|
|
893
|
+
const before = records.length;
|
|
894
|
+
const filtered = records.filter((r) => !idSet.has(r.id));
|
|
895
|
+
count += before - filtered.length;
|
|
896
|
+
if (before !== filtered.length) {
|
|
897
|
+
await this._writeFile(actorId, filtered);
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return count;
|
|
902
|
+
}
|
|
903
|
+
async retrieve(query, _embedding) {
|
|
904
|
+
const actorIds = query.actorId ? [query.actorId] : await this._listActors();
|
|
905
|
+
let results = [];
|
|
906
|
+
for (const actorId of actorIds) {
|
|
907
|
+
let records = await this._readFile(actorId);
|
|
908
|
+
const now = /* @__PURE__ */ new Date();
|
|
909
|
+
records = records.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
910
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
911
|
+
records = records.filter((r) => query.memoryTypes.includes(r.memoryType));
|
|
912
|
+
}
|
|
913
|
+
if (query.tags && query.tags.length > 0) {
|
|
914
|
+
records = records.filter((r) => r.tags?.some((t) => query.tags.includes(t)));
|
|
915
|
+
}
|
|
916
|
+
if (query.query) {
|
|
917
|
+
const q = query.query.toLowerCase();
|
|
918
|
+
records = records.filter((r) => r.content.toLowerCase().includes(q));
|
|
919
|
+
}
|
|
920
|
+
results.push(...records);
|
|
921
|
+
}
|
|
922
|
+
switch (query.strategy) {
|
|
923
|
+
case "recent":
|
|
924
|
+
results.sort((a, b) => new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime());
|
|
925
|
+
break;
|
|
926
|
+
case "important":
|
|
927
|
+
results.sort((a, b) => b.importance - a.importance);
|
|
928
|
+
break;
|
|
929
|
+
case "hybrid":
|
|
930
|
+
case "semantic":
|
|
931
|
+
default:
|
|
932
|
+
results.sort(
|
|
933
|
+
(a, b) => b.importance - a.importance || new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime()
|
|
934
|
+
);
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
938
|
+
const toTouch = results.slice(0, query.limit ?? 10);
|
|
939
|
+
const touchedActors = /* @__PURE__ */ new Set();
|
|
940
|
+
for (const r of toTouch) {
|
|
941
|
+
r._touchedAt = nowStr;
|
|
942
|
+
touchedActors.add(r.actorId);
|
|
943
|
+
}
|
|
944
|
+
for (const actorId of touchedActors) {
|
|
945
|
+
await this._withWriteLock(actorId, async () => {
|
|
946
|
+
const records = await this._readFile(actorId);
|
|
947
|
+
let changed = false;
|
|
948
|
+
for (const r of records) {
|
|
949
|
+
const touched = toTouch.find((t) => t.id === r.id);
|
|
950
|
+
if (touched) {
|
|
951
|
+
r._touchedAt = nowStr;
|
|
952
|
+
changed = true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (changed) await this._writeFile(actorId, records);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
const limit = query.limit ?? 10;
|
|
959
|
+
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
960
|
+
}
|
|
961
|
+
async count(filter) {
|
|
962
|
+
const actorIds = filter?.actorId ? [filter.actorId] : await this._listActors();
|
|
963
|
+
let total = 0;
|
|
964
|
+
const now = /* @__PURE__ */ new Date();
|
|
965
|
+
for (const actorId of actorIds) {
|
|
966
|
+
let records = await this._readFile(actorId);
|
|
967
|
+
records = records.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
968
|
+
if (filter?.memoryType) {
|
|
969
|
+
records = records.filter((r) => r.memoryType === filter.memoryType);
|
|
970
|
+
}
|
|
971
|
+
if (filter?.minImportance !== void 0) {
|
|
972
|
+
records = records.filter((r) => r.importance >= filter.minImportance);
|
|
973
|
+
}
|
|
974
|
+
total += records.length;
|
|
975
|
+
}
|
|
976
|
+
return total;
|
|
977
|
+
}
|
|
978
|
+
async touch(id) {
|
|
979
|
+
const actorIds = await this._listActors();
|
|
980
|
+
for (const actorId of actorIds) {
|
|
981
|
+
const records = await this._readFile(actorId);
|
|
982
|
+
const found = records.find((r) => r.id === id);
|
|
983
|
+
if (found) {
|
|
984
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
985
|
+
await this._withWriteLock(actorId, async () => {
|
|
986
|
+
const fresh = await this._readFile(actorId);
|
|
987
|
+
const match = fresh.find((r) => r.id === id);
|
|
988
|
+
if (match) {
|
|
989
|
+
match._touchedAt = nowStr;
|
|
990
|
+
await this._writeFile(actorId, fresh);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
throw notFound("Memory", id);
|
|
997
|
+
}
|
|
998
|
+
async close() {
|
|
999
|
+
this._writeLocks.clear();
|
|
1000
|
+
}
|
|
1001
|
+
// ── Internal ──
|
|
1002
|
+
_filePath(actorId) {
|
|
1003
|
+
const safe = actorId.replace(/[^a-zA-Z0-9_\-.,@]/g, "_");
|
|
1004
|
+
return join(this.dir, `${safe}.json`);
|
|
1005
|
+
}
|
|
1006
|
+
async _readFile(actorId) {
|
|
1007
|
+
try {
|
|
1008
|
+
const raw = await readFile(this._filePath(actorId), "utf-8");
|
|
1009
|
+
const parsed = JSON.parse(raw);
|
|
1010
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
if (err.code === "ENOENT") return [];
|
|
1013
|
+
throw storageError(`Failed to read memories for actor ${actorId}: ${err.message}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
async _writeFile(actorId, records) {
|
|
1017
|
+
try {
|
|
1018
|
+
await writeFile(this._filePath(actorId), JSON.stringify(records, null, 2), "utf-8");
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
throw storageError(`Failed to write memories for actor ${actorId}: ${err.message}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
async _withWriteLock(actorId, fn) {
|
|
1024
|
+
const prev = this._writeLocks.get(actorId) ?? Promise.resolve();
|
|
1025
|
+
let resolve;
|
|
1026
|
+
const next = new Promise((r) => {
|
|
1027
|
+
resolve = r;
|
|
1028
|
+
});
|
|
1029
|
+
this._writeLocks.set(actorId, next);
|
|
1030
|
+
await prev;
|
|
1031
|
+
try {
|
|
1032
|
+
await fn();
|
|
1033
|
+
} finally {
|
|
1034
|
+
resolve();
|
|
1035
|
+
if (this._writeLocks.get(actorId) === next) {
|
|
1036
|
+
this._writeLocks.delete(actorId);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async _listActors() {
|
|
1041
|
+
try {
|
|
1042
|
+
const entries = await readdir(this.dir);
|
|
1043
|
+
return entries.filter((e) => e.endsWith(".json")).map((e) => e.slice(0, -5));
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (err.code === "ENOENT") return [];
|
|
1046
|
+
throw storageError(`Failed to list actors: ${err.message}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
_toExternal(record) {
|
|
1050
|
+
const { _touchedAt: _, ...rest } = record;
|
|
1051
|
+
return {
|
|
1052
|
+
...rest,
|
|
1053
|
+
createdAt: new Date(record.createdAt),
|
|
1054
|
+
expiresAt: record.expiresAt ? new Date(record.expiresAt) : void 0
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// src/adapters/storage/redis.ts
|
|
1060
|
+
var RedisStorage = class {
|
|
1061
|
+
redis;
|
|
1062
|
+
prefix;
|
|
1063
|
+
constructor(config) {
|
|
1064
|
+
this.redis = config.redis;
|
|
1065
|
+
this.prefix = config.keyPrefix ?? "memstack";
|
|
1066
|
+
}
|
|
1067
|
+
async initialize() {
|
|
1068
|
+
}
|
|
1069
|
+
generateId() {
|
|
1070
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
1071
|
+
}
|
|
1072
|
+
async store(input) {
|
|
1073
|
+
const now = /* @__PURE__ */ new Date();
|
|
1074
|
+
const memory = {
|
|
1075
|
+
id: input.id ?? this.generateId(),
|
|
1076
|
+
actorId: input.actorId,
|
|
1077
|
+
memoryType: input.memoryType ?? "interaction",
|
|
1078
|
+
content: input.content,
|
|
1079
|
+
importance: input.importance ?? 0.5,
|
|
1080
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
1081
|
+
tags: input.tags ?? [],
|
|
1082
|
+
embedding: input.embedding,
|
|
1083
|
+
sourceId: input.sourceId,
|
|
1084
|
+
metadata: input.metadata ?? {},
|
|
1085
|
+
expiresAt: input.expiresAt,
|
|
1086
|
+
createdAt: now,
|
|
1087
|
+
_touchedAt: now.toISOString()
|
|
1088
|
+
};
|
|
1089
|
+
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
1090
|
+
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
1091
|
+
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
1092
|
+
return this._toExternal(memory);
|
|
1093
|
+
}
|
|
1094
|
+
async storeBatch(inputs) {
|
|
1095
|
+
let results = [];
|
|
1096
|
+
const now = /* @__PURE__ */ new Date();
|
|
1097
|
+
for (const input of inputs) {
|
|
1098
|
+
const memory = {
|
|
1099
|
+
id: input.id ?? this.generateId(),
|
|
1100
|
+
actorId: input.actorId,
|
|
1101
|
+
memoryType: input.memoryType ?? "interaction",
|
|
1102
|
+
content: input.content,
|
|
1103
|
+
importance: input.importance ?? 0.5,
|
|
1104
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
1105
|
+
tags: input.tags ?? [],
|
|
1106
|
+
embedding: input.embedding,
|
|
1107
|
+
sourceId: input.sourceId,
|
|
1108
|
+
metadata: input.metadata ?? {},
|
|
1109
|
+
expiresAt: input.expiresAt,
|
|
1110
|
+
createdAt: now,
|
|
1111
|
+
_touchedAt: now.toISOString()
|
|
1112
|
+
};
|
|
1113
|
+
await this.redis.set(this._memKey(memory.id), JSON.stringify(memory));
|
|
1114
|
+
await this.redis.sadd(this._actorKey(memory.actorId), memory.id);
|
|
1115
|
+
await this.redis.sadd(this._actorsSetKey(), memory.actorId);
|
|
1116
|
+
results.push(this._toExternal(memory));
|
|
1117
|
+
}
|
|
1118
|
+
return results;
|
|
1119
|
+
}
|
|
1120
|
+
async get(id) {
|
|
1121
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
1122
|
+
if (!raw) return null;
|
|
1123
|
+
const record = JSON.parse(raw);
|
|
1124
|
+
if (record.expiresAt && new Date(record.expiresAt) <= /* @__PURE__ */ new Date()) return null;
|
|
1125
|
+
return this._toExternal(record);
|
|
1126
|
+
}
|
|
1127
|
+
async delete(id) {
|
|
1128
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
1129
|
+
if (!raw) throw notFound("Memory", id);
|
|
1130
|
+
const record = JSON.parse(raw);
|
|
1131
|
+
await this.redis.del(this._memKey(id));
|
|
1132
|
+
await this.redis.srem(this._actorKey(record.actorId), id);
|
|
1133
|
+
}
|
|
1134
|
+
async deleteMany(ids) {
|
|
1135
|
+
let count = 0;
|
|
1136
|
+
const byActor = /* @__PURE__ */ new Map();
|
|
1137
|
+
for (const id of ids) {
|
|
1138
|
+
const raw = await this.redis.get(this._memKey(id));
|
|
1139
|
+
if (raw) {
|
|
1140
|
+
const record = JSON.parse(raw);
|
|
1141
|
+
const list = byActor.get(record.actorId) ?? [];
|
|
1142
|
+
list.push(id);
|
|
1143
|
+
byActor.set(record.actorId, list);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
for (const [actorId, memIds] of byActor) {
|
|
1147
|
+
await this.redis.del(memIds.map((id) => this._memKey(id)));
|
|
1148
|
+
await this.redis.srem(this._actorKey(actorId), ...memIds);
|
|
1149
|
+
count += memIds.length;
|
|
1150
|
+
}
|
|
1151
|
+
return count;
|
|
1152
|
+
}
|
|
1153
|
+
async retrieve(query, _embedding) {
|
|
1154
|
+
let allRecords = [];
|
|
1155
|
+
if (query.actorId) {
|
|
1156
|
+
allRecords = await this._getActorRecords(query.actorId);
|
|
1157
|
+
} else {
|
|
1158
|
+
const actorIds = await this._listActors();
|
|
1159
|
+
for (const actorId of actorIds) {
|
|
1160
|
+
const records = await this._getActorRecords(actorId);
|
|
1161
|
+
allRecords.push(...records);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const now = /* @__PURE__ */ new Date();
|
|
1165
|
+
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
1166
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
1167
|
+
results = results.filter((r) => query.memoryTypes.includes(r.memoryType));
|
|
1168
|
+
}
|
|
1169
|
+
if (query.tags && query.tags.length > 0) {
|
|
1170
|
+
results = results.filter((r) => r.tags?.some((t) => query.tags.includes(t)));
|
|
1171
|
+
}
|
|
1172
|
+
if (query.query) {
|
|
1173
|
+
const q = query.query.toLowerCase();
|
|
1174
|
+
results = results.filter((r) => r.content.toLowerCase().includes(q));
|
|
1175
|
+
}
|
|
1176
|
+
switch (query.strategy) {
|
|
1177
|
+
case "recent":
|
|
1178
|
+
results.sort((a, b) => new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime());
|
|
1179
|
+
break;
|
|
1180
|
+
case "important":
|
|
1181
|
+
results.sort((a, b) => b.importance - a.importance);
|
|
1182
|
+
break;
|
|
1183
|
+
case "hybrid":
|
|
1184
|
+
case "semantic":
|
|
1185
|
+
default:
|
|
1186
|
+
results.sort(
|
|
1187
|
+
(a, b) => b.importance - a.importance || new Date(b._touchedAt).getTime() - new Date(a._touchedAt).getTime()
|
|
1188
|
+
);
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
1192
|
+
const toTouch = results.slice(0, query.limit ?? 10);
|
|
1193
|
+
for (const r of toTouch) {
|
|
1194
|
+
r._touchedAt = nowStr;
|
|
1195
|
+
await this.redis.set(this._memKey(r.id), JSON.stringify(r));
|
|
1196
|
+
}
|
|
1197
|
+
const limit = query.limit ?? 10;
|
|
1198
|
+
return results.slice(0, limit).map((r) => this._toExternal(r));
|
|
1199
|
+
}
|
|
1200
|
+
async count(filter) {
|
|
1201
|
+
let allRecords = [];
|
|
1202
|
+
if (filter?.actorId) {
|
|
1203
|
+
allRecords = await this._getActorRecords(filter.actorId);
|
|
1204
|
+
} else {
|
|
1205
|
+
const actorIds = await this._listActors();
|
|
1206
|
+
for (const actorId of actorIds) {
|
|
1207
|
+
const records = await this._getActorRecords(actorId);
|
|
1208
|
+
allRecords.push(...records);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const now = /* @__PURE__ */ new Date();
|
|
1212
|
+
let results = allRecords.filter((r) => !r.expiresAt || new Date(r.expiresAt) > now);
|
|
1213
|
+
if (filter?.memoryType) {
|
|
1214
|
+
results = results.filter((r) => r.memoryType === filter.memoryType);
|
|
1215
|
+
}
|
|
1216
|
+
if (filter?.minImportance !== void 0) {
|
|
1217
|
+
results = results.filter((r) => r.importance >= filter.minImportance);
|
|
1218
|
+
}
|
|
1219
|
+
return results.length;
|
|
1220
|
+
}
|
|
1221
|
+
async close() {
|
|
1222
|
+
}
|
|
1223
|
+
// ── Internal ──
|
|
1224
|
+
_actorsSetKey() {
|
|
1225
|
+
return `${this.prefix}:actors`;
|
|
1226
|
+
}
|
|
1227
|
+
_actorKey(actorId) {
|
|
1228
|
+
const safe = actorId.replace(/[^a-zA-Z0-9_\-.,@]/g, "_");
|
|
1229
|
+
return `${this.prefix}:a:${safe}`;
|
|
1230
|
+
}
|
|
1231
|
+
_memKey(id) {
|
|
1232
|
+
return `${this.prefix}:m:${id}`;
|
|
1233
|
+
}
|
|
1234
|
+
async _getActorRecords(actorId) {
|
|
1235
|
+
const ids = await this.redis.smembers(this._actorKey(actorId));
|
|
1236
|
+
if (ids.length === 0) return [];
|
|
1237
|
+
const keys = ids.map((id) => this._memKey(id));
|
|
1238
|
+
const raws = await this.redis.mget(...keys);
|
|
1239
|
+
const records = [];
|
|
1240
|
+
for (const raw of raws) {
|
|
1241
|
+
if (raw) {
|
|
1242
|
+
try {
|
|
1243
|
+
records.push(JSON.parse(raw));
|
|
1244
|
+
} catch {
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return records;
|
|
1249
|
+
}
|
|
1250
|
+
async _listActors() {
|
|
1251
|
+
return this.redis.smembers(this._actorsSetKey());
|
|
1252
|
+
}
|
|
1253
|
+
_toExternal(record) {
|
|
1254
|
+
const { _touchedAt: _, ...rest } = record;
|
|
1255
|
+
return {
|
|
1256
|
+
...rest,
|
|
1257
|
+
createdAt: new Date(record.createdAt),
|
|
1258
|
+
expiresAt: record.expiresAt ? new Date(record.expiresAt) : void 0
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
// src/adapters/storage/postgres.ts
|
|
1264
|
+
var PostgresStorage = class {
|
|
1265
|
+
pool;
|
|
1266
|
+
table;
|
|
1267
|
+
autoMigrate;
|
|
1268
|
+
constructor(config) {
|
|
1269
|
+
this.pool = config.pool;
|
|
1270
|
+
this.table = config.tableName ?? "memstack_memories";
|
|
1271
|
+
if (!/^[a-zA-Z0-9_]+$/.test(this.table)) {
|
|
1272
|
+
throw new Error(`Invalid table name: "${this.table}". Use only alphanumeric characters and underscores.`);
|
|
1273
|
+
}
|
|
1274
|
+
this.autoMigrate = config.autoMigrate ?? true;
|
|
1275
|
+
}
|
|
1276
|
+
async initialize() {
|
|
1277
|
+
if (!this.autoMigrate) return;
|
|
1278
|
+
await this.pool.query(`
|
|
1279
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
1280
|
+
id TEXT PRIMARY KEY,
|
|
1281
|
+
actor_id TEXT NOT NULL,
|
|
1282
|
+
memory_type TEXT NOT NULL DEFAULT 'interaction',
|
|
1283
|
+
content TEXT NOT NULL,
|
|
1284
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
1285
|
+
emotional_valence REAL NOT NULL DEFAULT 0,
|
|
1286
|
+
tags JSONB NOT NULL DEFAULT '[]',
|
|
1287
|
+
embedding vector(3072),
|
|
1288
|
+
source_id TEXT,
|
|
1289
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
1290
|
+
expires_at TIMESTAMPTZ,
|
|
1291
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
1292
|
+
touched_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
1293
|
+
)
|
|
1294
|
+
`);
|
|
1295
|
+
try {
|
|
1296
|
+
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_actor ON ${this.table} (actor_id)`);
|
|
1297
|
+
await this.pool.query(`CREATE INDEX IF NOT EXISTS idx_${this.table}_type ON ${this.table} (memory_type)`);
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
generateId() {
|
|
1302
|
+
return `mem_${Date.now().toString(36)}_${crypto.randomUUID().slice(0, 8)}`;
|
|
1303
|
+
}
|
|
1304
|
+
async store(input) {
|
|
1305
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1306
|
+
const id = input.id ?? this.generateId();
|
|
1307
|
+
await this.pool.query(
|
|
1308
|
+
`INSERT INTO ${this.table} (id, actor_id, memory_type, content, importance, emotional_valence, tags, embedding, source_id, metadata, expires_at, created_at, touched_at)
|
|
1309
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::float8[]::vector, $9, $10, $11, $12, $13)
|
|
1310
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
1311
|
+
content = EXCLUDED.content,
|
|
1312
|
+
importance = EXCLUDED.importance,
|
|
1313
|
+
emotional_valence = EXCLUDED.emotional_valence,
|
|
1314
|
+
tags = EXCLUDED.tags,
|
|
1315
|
+
embedding = EXCLUDED.embedding,
|
|
1316
|
+
source_id = EXCLUDED.source_id,
|
|
1317
|
+
metadata = EXCLUDED.metadata,
|
|
1318
|
+
expires_at = EXCLUDED.expires_at,
|
|
1319
|
+
touched_at = EXCLUDED.touched_at`,
|
|
1320
|
+
[
|
|
1321
|
+
id,
|
|
1322
|
+
input.actorId,
|
|
1323
|
+
input.memoryType ?? "interaction",
|
|
1324
|
+
input.content,
|
|
1325
|
+
input.importance ?? 0.5,
|
|
1326
|
+
input.emotionalValence ?? 0,
|
|
1327
|
+
JSON.stringify(input.tags ?? []),
|
|
1328
|
+
input.embedding ? `{${input.embedding.join(",")}}` : null,
|
|
1329
|
+
input.sourceId ?? null,
|
|
1330
|
+
JSON.stringify(input.metadata ?? {}),
|
|
1331
|
+
input.expiresAt?.toISOString() ?? null,
|
|
1332
|
+
now,
|
|
1333
|
+
now
|
|
1334
|
+
]
|
|
1335
|
+
);
|
|
1336
|
+
return {
|
|
1337
|
+
id,
|
|
1338
|
+
actorId: input.actorId,
|
|
1339
|
+
memoryType: input.memoryType ?? "interaction",
|
|
1340
|
+
content: input.content,
|
|
1341
|
+
importance: input.importance ?? 0.5,
|
|
1342
|
+
emotionalValence: input.emotionalValence ?? 0,
|
|
1343
|
+
tags: input.tags ?? [],
|
|
1344
|
+
embedding: input.embedding,
|
|
1345
|
+
sourceId: input.sourceId,
|
|
1346
|
+
metadata: input.metadata ?? {},
|
|
1347
|
+
expiresAt: input.expiresAt,
|
|
1348
|
+
createdAt: new Date(now)
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
async storeBatch(inputs) {
|
|
1352
|
+
const results = [];
|
|
1353
|
+
for (const input of inputs) {
|
|
1354
|
+
results.push(await this.store(input));
|
|
1355
|
+
}
|
|
1356
|
+
return results;
|
|
1357
|
+
}
|
|
1358
|
+
async get(id) {
|
|
1359
|
+
const { rows } = await this.pool.query(
|
|
1360
|
+
`SELECT * FROM ${this.table} WHERE id = $1 AND (expires_at IS NULL OR expires_at > NOW())`,
|
|
1361
|
+
[id]
|
|
1362
|
+
);
|
|
1363
|
+
if (rows.length === 0) return null;
|
|
1364
|
+
return this._rowToMemory(rows[0]);
|
|
1365
|
+
}
|
|
1366
|
+
async delete(id) {
|
|
1367
|
+
const { rows } = await this.pool.query(`SELECT id FROM ${this.table} WHERE id = $1`, [id]);
|
|
1368
|
+
if (rows.length === 0) throw notFound("Memory", id);
|
|
1369
|
+
await this.pool.query(`DELETE FROM ${this.table} WHERE id = $1`, [id]);
|
|
1370
|
+
}
|
|
1371
|
+
async deleteMany(ids) {
|
|
1372
|
+
const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
|
|
1373
|
+
const { rows } = await this.pool.query(
|
|
1374
|
+
`DELETE FROM ${this.table} WHERE id IN (${placeholders}) RETURNING id`,
|
|
1375
|
+
ids
|
|
1376
|
+
);
|
|
1377
|
+
return rows.length;
|
|
1378
|
+
}
|
|
1379
|
+
async retrieve(query, embedding) {
|
|
1380
|
+
const conditions = ["(expires_at IS NULL OR expires_at > NOW())"];
|
|
1381
|
+
const params = [];
|
|
1382
|
+
let paramIdx = 1;
|
|
1383
|
+
if (query.actorId) {
|
|
1384
|
+
conditions.push(`actor_id = $${paramIdx++}`);
|
|
1385
|
+
params.push(query.actorId);
|
|
1386
|
+
}
|
|
1387
|
+
if (query.memoryTypes && query.memoryTypes.length > 0) {
|
|
1388
|
+
conditions.push(`memory_type = ANY($${paramIdx++})`);
|
|
1389
|
+
params.push(query.memoryTypes);
|
|
1390
|
+
}
|
|
1391
|
+
if (query.tags && query.tags.length > 0) {
|
|
1392
|
+
conditions.push(`tags ?| $${paramIdx++}`);
|
|
1393
|
+
params.push(query.tags);
|
|
1394
|
+
}
|
|
1395
|
+
if (query.query) {
|
|
1396
|
+
conditions.push(`content ILIKE $${paramIdx++}`);
|
|
1397
|
+
params.push(`%${query.query}%`);
|
|
1398
|
+
}
|
|
1399
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1400
|
+
let orderBy;
|
|
1401
|
+
switch (query.strategy) {
|
|
1402
|
+
case "recent":
|
|
1403
|
+
orderBy = "touched_at DESC";
|
|
1404
|
+
break;
|
|
1405
|
+
case "important":
|
|
1406
|
+
orderBy = "importance DESC";
|
|
1407
|
+
break;
|
|
1408
|
+
case "hybrid":
|
|
1409
|
+
case "semantic":
|
|
1410
|
+
default:
|
|
1411
|
+
if (embedding && query.query) {
|
|
1412
|
+
return this._semanticRetrieve(embedding, query, params, where);
|
|
1413
|
+
}
|
|
1414
|
+
orderBy = "importance DESC, touched_at DESC";
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
const limit = query.limit ?? 10;
|
|
1418
|
+
const sql = `SELECT * FROM ${this.table} ${where} ORDER BY ${orderBy} LIMIT $${paramIdx}`;
|
|
1419
|
+
params.push(limit);
|
|
1420
|
+
const { rows } = await this.pool.query(sql, params);
|
|
1421
|
+
for (const row of rows.slice(0, limit)) {
|
|
1422
|
+
const r = row;
|
|
1423
|
+
await this.pool.query(
|
|
1424
|
+
`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`,
|
|
1425
|
+
[r.id]
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
return rows.map((r) => this._rowToMemory(r));
|
|
1429
|
+
}
|
|
1430
|
+
async count(filter) {
|
|
1431
|
+
const conditions = ["(expires_at IS NULL OR expires_at > NOW())"];
|
|
1432
|
+
const params = [];
|
|
1433
|
+
let paramIdx = 1;
|
|
1434
|
+
if (filter?.actorId) {
|
|
1435
|
+
conditions.push(`actor_id = $${paramIdx++}`);
|
|
1436
|
+
params.push(filter.actorId);
|
|
1437
|
+
}
|
|
1438
|
+
if (filter?.memoryType) {
|
|
1439
|
+
conditions.push(`memory_type = $${paramIdx++}`);
|
|
1440
|
+
params.push(filter.memoryType);
|
|
1441
|
+
}
|
|
1442
|
+
if (filter?.minImportance !== void 0) {
|
|
1443
|
+
conditions.push(`importance >= $${paramIdx++}`);
|
|
1444
|
+
params.push(filter.minImportance);
|
|
1445
|
+
}
|
|
1446
|
+
const where = conditions.join(" AND ");
|
|
1447
|
+
const { rows } = await this.pool.query(
|
|
1448
|
+
`SELECT COUNT(*) as count FROM ${this.table} WHERE ${where}`,
|
|
1449
|
+
params
|
|
1450
|
+
);
|
|
1451
|
+
return Number(rows[0].count);
|
|
1452
|
+
}
|
|
1453
|
+
async close() {
|
|
1454
|
+
}
|
|
1455
|
+
// ── Internal ──
|
|
1456
|
+
async _semanticRetrieve(embedding, query, params, baseWhere) {
|
|
1457
|
+
const vecParam = `{${embedding.join(",")}}`;
|
|
1458
|
+
const limit = query.limit ?? 10;
|
|
1459
|
+
const conditions = baseWhere.replace(/^WHERE\s+/i, "");
|
|
1460
|
+
const whereClause = conditions ? `WHERE ${conditions} AND embedding IS NOT NULL` : `WHERE embedding IS NOT NULL`;
|
|
1461
|
+
const sql = `
|
|
1462
|
+
SELECT *,
|
|
1463
|
+
(embedding <-> $${params.length + 1}::float8[]::vector) AS distance
|
|
1464
|
+
FROM ${this.table}
|
|
1465
|
+
${whereClause}
|
|
1466
|
+
ORDER BY distance ASC
|
|
1467
|
+
LIMIT $${params.length + 2}
|
|
1468
|
+
`;
|
|
1469
|
+
params.push(vecParam);
|
|
1470
|
+
params.push(limit);
|
|
1471
|
+
const { rows } = await this.pool.query(sql, params);
|
|
1472
|
+
for (const row of rows.slice(0, limit)) {
|
|
1473
|
+
const r = row;
|
|
1474
|
+
await this.pool.query(`UPDATE ${this.table} SET touched_at = NOW() WHERE id = $1`, [r.id]);
|
|
1475
|
+
}
|
|
1476
|
+
return rows.map((r) => this._rowToMemory(r));
|
|
1477
|
+
}
|
|
1478
|
+
_rowToMemory(row) {
|
|
1479
|
+
return {
|
|
1480
|
+
id: row.id,
|
|
1481
|
+
actorId: row.actor_id,
|
|
1482
|
+
memoryType: row.memory_type,
|
|
1483
|
+
content: row.content,
|
|
1484
|
+
importance: row.importance,
|
|
1485
|
+
emotionalValence: row.emotional_valence,
|
|
1486
|
+
tags: Array.isArray(row.tags) ? row.tags : [],
|
|
1487
|
+
embedding: row.embedding ?? void 0,
|
|
1488
|
+
sourceId: row.source_id ?? void 0,
|
|
1489
|
+
metadata: row.metadata ?? {},
|
|
1490
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : void 0,
|
|
1491
|
+
createdAt: new Date(row.created_at)
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
|
|
1496
|
+
// src/adapters/llm/openai.ts
|
|
1497
|
+
var OpenAILLMAdapter = class {
|
|
1498
|
+
apiKey;
|
|
1499
|
+
baseURL;
|
|
1500
|
+
defaultModel;
|
|
1501
|
+
defaultTemperature;
|
|
1502
|
+
defaultMaxTokens;
|
|
1503
|
+
constructor(config) {
|
|
1504
|
+
this.apiKey = config.apiKey;
|
|
1505
|
+
this.baseURL = config.baseURL ?? "https://api.openai.com/v1";
|
|
1506
|
+
this.defaultModel = config.defaultModel ?? "gpt-4o-mini";
|
|
1507
|
+
this.defaultTemperature = config.defaultTemperature ?? 0.7;
|
|
1508
|
+
this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
|
|
1509
|
+
}
|
|
1510
|
+
async complete(request) {
|
|
1511
|
+
const model = request.model ?? this.defaultModel;
|
|
1512
|
+
const temperature = request.temperature ?? this.defaultTemperature;
|
|
1513
|
+
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
1514
|
+
try {
|
|
1515
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
1516
|
+
method: "POST",
|
|
1517
|
+
headers: {
|
|
1518
|
+
"Content-Type": "application/json",
|
|
1519
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1520
|
+
},
|
|
1521
|
+
body: JSON.stringify({
|
|
1522
|
+
model,
|
|
1523
|
+
messages: [
|
|
1524
|
+
{ role: "system", content: request.system },
|
|
1525
|
+
{ role: "user", content: request.user }
|
|
1526
|
+
],
|
|
1527
|
+
temperature,
|
|
1528
|
+
max_tokens: maxTokens
|
|
1529
|
+
})
|
|
1530
|
+
});
|
|
1531
|
+
if (!response.ok) {
|
|
1532
|
+
const err = await response.text();
|
|
1533
|
+
throw new MemStackError("LLM_ERROR", `OpenAI API error: ${response.status} ${err}`, {
|
|
1534
|
+
retryable: response.status >= 500
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
const data = await response.json();
|
|
1538
|
+
return {
|
|
1539
|
+
text: data.choices[0]?.message?.content ?? "",
|
|
1540
|
+
tokens: {
|
|
1541
|
+
prompt: data.usage?.prompt_tokens ?? 0,
|
|
1542
|
+
completion: data.usage?.completion_tokens ?? 0,
|
|
1543
|
+
total: data.usage?.total_tokens ?? 0
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
if (err instanceof MemStackError) throw err;
|
|
1548
|
+
throw new MemStackError("LLM_ERROR", `LLM request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1549
|
+
retryable: true
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async *completeStream(request) {
|
|
1554
|
+
const model = request.model ?? this.defaultModel;
|
|
1555
|
+
const temperature = request.temperature ?? this.defaultTemperature;
|
|
1556
|
+
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
1557
|
+
let response;
|
|
1558
|
+
try {
|
|
1559
|
+
response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
1560
|
+
method: "POST",
|
|
1561
|
+
headers: {
|
|
1562
|
+
"Content-Type": "application/json",
|
|
1563
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1564
|
+
},
|
|
1565
|
+
body: JSON.stringify({
|
|
1566
|
+
model,
|
|
1567
|
+
messages: [
|
|
1568
|
+
{ role: "system", content: request.system },
|
|
1569
|
+
{ role: "user", content: request.user }
|
|
1570
|
+
],
|
|
1571
|
+
temperature,
|
|
1572
|
+
max_tokens: maxTokens,
|
|
1573
|
+
stream: true,
|
|
1574
|
+
stream_options: { include_usage: true }
|
|
1575
|
+
})
|
|
1576
|
+
});
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
throw new MemStackError("LLM_ERROR", `OpenAI streaming request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1579
|
+
retryable: true
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
if (!response.ok) {
|
|
1583
|
+
const err = await response.text();
|
|
1584
|
+
throw new MemStackError("LLM_ERROR", `OpenAI streaming error: ${response.status} ${err}`, {
|
|
1585
|
+
retryable: response.status >= 500
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
const reader = response.body?.getReader();
|
|
1589
|
+
if (!reader) throw new MemStackError("LLM_ERROR", "No response body for streaming");
|
|
1590
|
+
const decoder = new TextDecoder();
|
|
1591
|
+
let buffer = "";
|
|
1592
|
+
let promptTokens = 0;
|
|
1593
|
+
let completionTokens = 0;
|
|
1594
|
+
try {
|
|
1595
|
+
while (true) {
|
|
1596
|
+
const { done, value } = await reader.read();
|
|
1597
|
+
if (done) break;
|
|
1598
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1599
|
+
const lines = buffer.split("\n");
|
|
1600
|
+
buffer = lines.pop() ?? "";
|
|
1601
|
+
for (const line of lines) {
|
|
1602
|
+
const trimmed = line.trim();
|
|
1603
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
1604
|
+
const data = trimmed.slice(6);
|
|
1605
|
+
if (data === "[DONE]") continue;
|
|
1606
|
+
try {
|
|
1607
|
+
const parsed = JSON.parse(data);
|
|
1608
|
+
const content = parsed.choices?.[0]?.delta?.content ?? "";
|
|
1609
|
+
if (parsed.usage) {
|
|
1610
|
+
promptTokens = parsed.usage.prompt_tokens;
|
|
1611
|
+
completionTokens = parsed.usage.completion_tokens;
|
|
1612
|
+
}
|
|
1613
|
+
if (content) {
|
|
1614
|
+
yield {
|
|
1615
|
+
text: content,
|
|
1616
|
+
tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens }
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} finally {
|
|
1624
|
+
reader.releaseLock();
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
// src/adapters/llm/anthropic.ts
|
|
1630
|
+
var AnthropicLLMAdapter = class {
|
|
1631
|
+
apiKey;
|
|
1632
|
+
baseURL;
|
|
1633
|
+
defaultModel;
|
|
1634
|
+
defaultMaxTokens;
|
|
1635
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1636
|
+
sdk;
|
|
1637
|
+
constructor(config) {
|
|
1638
|
+
this.apiKey = config.apiKey;
|
|
1639
|
+
this.baseURL = config.baseURL ?? "https://api.anthropic.com";
|
|
1640
|
+
this.defaultModel = config.defaultModel ?? "claude-sonnet-4-5-20250929";
|
|
1641
|
+
this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
|
|
1642
|
+
}
|
|
1643
|
+
async complete(request) {
|
|
1644
|
+
const model = request.model ?? this.defaultModel;
|
|
1645
|
+
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
1646
|
+
try {
|
|
1647
|
+
if (!this.sdk) {
|
|
1648
|
+
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
1649
|
+
this.sdk = new Anthropic({ apiKey: this.apiKey, baseURL: this.baseURL });
|
|
1650
|
+
}
|
|
1651
|
+
const response = await this.sdk.messages.create({
|
|
1652
|
+
model,
|
|
1653
|
+
system: request.system,
|
|
1654
|
+
messages: [{ role: "user", content: request.user }],
|
|
1655
|
+
max_tokens: maxTokens,
|
|
1656
|
+
...request.temperature !== void 0 ? { temperature: request.temperature } : {}
|
|
1657
|
+
});
|
|
1658
|
+
const text = response.content.length > 0 && "text" in response.content[0] ? response.content[0].text : "";
|
|
1659
|
+
return {
|
|
1660
|
+
text,
|
|
1661
|
+
tokens: {
|
|
1662
|
+
prompt: response.usage?.input_tokens ?? 0,
|
|
1663
|
+
completion: response.usage?.output_tokens ?? 0,
|
|
1664
|
+
total: (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0)
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
} catch (err) {
|
|
1668
|
+
if (err instanceof MemStackError) throw err;
|
|
1669
|
+
throw new MemStackError("LLM_ERROR", `Anthropic request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1670
|
+
retryable: true
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// src/adapters/embedding/openai.ts
|
|
1677
|
+
var OpenAIEmbeddingAdapter = class {
|
|
1678
|
+
apiKey;
|
|
1679
|
+
baseURL;
|
|
1680
|
+
model;
|
|
1681
|
+
dimensions;
|
|
1682
|
+
constructor(config) {
|
|
1683
|
+
this.apiKey = config.apiKey;
|
|
1684
|
+
this.baseURL = config.baseURL ?? "https://api.openai.com/v1";
|
|
1685
|
+
this.model = config.model ?? "text-embedding-3-small";
|
|
1686
|
+
this.dimensions = this.model === "text-embedding-3-large" ? 3072 : 1536;
|
|
1687
|
+
}
|
|
1688
|
+
async embed(texts) {
|
|
1689
|
+
if (texts.length === 0) return [];
|
|
1690
|
+
if (texts.length > 2048) {
|
|
1691
|
+
throw new MemStackError("VALIDATION_ERROR", `Max 2048 texts per batch, got ${texts.length}`);
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
const response = await fetch(`${this.baseURL}/embeddings`, {
|
|
1695
|
+
method: "POST",
|
|
1696
|
+
headers: {
|
|
1697
|
+
"Content-Type": "application/json",
|
|
1698
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1699
|
+
},
|
|
1700
|
+
body: JSON.stringify({
|
|
1701
|
+
model: this.model,
|
|
1702
|
+
input: texts
|
|
1703
|
+
})
|
|
1704
|
+
});
|
|
1705
|
+
if (!response.ok) {
|
|
1706
|
+
const err = await response.text();
|
|
1707
|
+
throw new MemStackError("EMBEDDING_ERROR", `OpenAI Embedding error: ${response.status} ${err}`, {
|
|
1708
|
+
retryable: response.status >= 500
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
const data = await response.json();
|
|
1712
|
+
return data.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
if (err instanceof MemStackError) throw err;
|
|
1715
|
+
throw new MemStackError(
|
|
1716
|
+
"EMBEDDING_ERROR",
|
|
1717
|
+
`Embedding request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1718
|
+
{ retryable: true }
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/adapters/embedding/cohere.ts
|
|
1725
|
+
var DIMENSIONS = {
|
|
1726
|
+
"embed-english-v3.0": 1024,
|
|
1727
|
+
"embed-multilingual-v3.0": 1024,
|
|
1728
|
+
"embed-english-light-v3.0": 384,
|
|
1729
|
+
"embed-multilingual-light-v3.0": 384,
|
|
1730
|
+
"embed-english-v2.0": 4096,
|
|
1731
|
+
"embed-multilingual-v2.0": 768
|
|
1732
|
+
};
|
|
1733
|
+
var CohereEmbeddingAdapter = class {
|
|
1734
|
+
apiKey;
|
|
1735
|
+
baseURL;
|
|
1736
|
+
model;
|
|
1737
|
+
dimensions;
|
|
1738
|
+
constructor(config) {
|
|
1739
|
+
this.apiKey = config.apiKey;
|
|
1740
|
+
this.baseURL = config.baseURL ?? "https://api.cohere.ai/v1";
|
|
1741
|
+
this.model = config.model ?? "embed-english-v3.0";
|
|
1742
|
+
this.dimensions = DIMENSIONS[this.model] ?? 1024;
|
|
1743
|
+
}
|
|
1744
|
+
async embed(texts) {
|
|
1745
|
+
if (texts.length === 0) return [];
|
|
1746
|
+
if (texts.length > 96) {
|
|
1747
|
+
throw new MemStackError("VALIDATION_ERROR", `Cohere max 96 texts per batch, got ${texts.length}`);
|
|
1748
|
+
}
|
|
1749
|
+
try {
|
|
1750
|
+
const response = await fetch(`${this.baseURL}/embed`, {
|
|
1751
|
+
method: "POST",
|
|
1752
|
+
headers: {
|
|
1753
|
+
"Content-Type": "application/json",
|
|
1754
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1755
|
+
},
|
|
1756
|
+
body: JSON.stringify({
|
|
1757
|
+
model: this.model,
|
|
1758
|
+
texts,
|
|
1759
|
+
input_type: "search_document",
|
|
1760
|
+
embedding_types: ["float"]
|
|
1761
|
+
})
|
|
1762
|
+
});
|
|
1763
|
+
if (!response.ok) {
|
|
1764
|
+
const err = await response.text();
|
|
1765
|
+
throw new MemStackError("EMBEDDING_ERROR", `Cohere Embedding error: ${response.status} ${err}`, {
|
|
1766
|
+
retryable: response.status >= 500
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
const data = await response.json();
|
|
1770
|
+
return data.embeddings?.float ?? [];
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
if (err instanceof MemStackError) throw err;
|
|
1773
|
+
throw new MemStackError(
|
|
1774
|
+
"EMBEDDING_ERROR",
|
|
1775
|
+
`Cohere embedding request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1776
|
+
{ retryable: true }
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
// src/adapters/llm/ollama.ts
|
|
1783
|
+
var OllamaLLMAdapter = class {
|
|
1784
|
+
baseURL;
|
|
1785
|
+
defaultModel;
|
|
1786
|
+
defaultTemperature;
|
|
1787
|
+
defaultMaxTokens;
|
|
1788
|
+
constructor(config = {}) {
|
|
1789
|
+
this.baseURL = config.baseURL ?? "http://localhost:11434";
|
|
1790
|
+
this.defaultModel = config.defaultModel ?? "llama3.2";
|
|
1791
|
+
this.defaultTemperature = config.defaultTemperature ?? 0.7;
|
|
1792
|
+
this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
|
|
1793
|
+
}
|
|
1794
|
+
async complete(request) {
|
|
1795
|
+
const model = request.model ?? this.defaultModel;
|
|
1796
|
+
const temperature = request.temperature ?? this.defaultTemperature;
|
|
1797
|
+
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
1798
|
+
try {
|
|
1799
|
+
const response = await fetch(`${this.baseURL}/api/chat`, {
|
|
1800
|
+
method: "POST",
|
|
1801
|
+
headers: { "Content-Type": "application/json" },
|
|
1802
|
+
body: JSON.stringify({
|
|
1803
|
+
model,
|
|
1804
|
+
messages: [
|
|
1805
|
+
{ role: "system", content: request.system },
|
|
1806
|
+
{ role: "user", content: request.user }
|
|
1807
|
+
],
|
|
1808
|
+
options: { temperature, num_predict: maxTokens },
|
|
1809
|
+
stream: false
|
|
1810
|
+
})
|
|
1811
|
+
});
|
|
1812
|
+
if (!response.ok) {
|
|
1813
|
+
const err = await response.text();
|
|
1814
|
+
throw new MemStackError("LLM_ERROR", `Ollama API error: ${response.status} ${err}`, {
|
|
1815
|
+
retryable: response.status >= 500
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
const data = await response.json();
|
|
1819
|
+
const promptTokens = data.prompt_eval_count ?? 0;
|
|
1820
|
+
const completionTokens = data.eval_count ?? 0;
|
|
1821
|
+
return {
|
|
1822
|
+
text: data.message?.content ?? "",
|
|
1823
|
+
tokens: {
|
|
1824
|
+
prompt: promptTokens,
|
|
1825
|
+
completion: completionTokens,
|
|
1826
|
+
total: promptTokens + completionTokens
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
} catch (err) {
|
|
1830
|
+
if (err instanceof MemStackError) throw err;
|
|
1831
|
+
throw new MemStackError("LLM_ERROR", `Ollama request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1832
|
+
retryable: true
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
// src/adapters/llm/groq.ts
|
|
1839
|
+
var GroqLLMAdapter = class {
|
|
1840
|
+
apiKey;
|
|
1841
|
+
baseURL;
|
|
1842
|
+
defaultModel;
|
|
1843
|
+
defaultTemperature;
|
|
1844
|
+
defaultMaxTokens;
|
|
1845
|
+
constructor(config) {
|
|
1846
|
+
this.apiKey = config.apiKey;
|
|
1847
|
+
this.baseURL = config.baseURL ?? "https://api.groq.com/openai/v1";
|
|
1848
|
+
this.defaultModel = config.defaultModel ?? "llama-3.3-70b-versatile";
|
|
1849
|
+
this.defaultTemperature = config.defaultTemperature ?? 0.7;
|
|
1850
|
+
this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
|
|
1851
|
+
}
|
|
1852
|
+
async complete(request) {
|
|
1853
|
+
const model = request.model ?? this.defaultModel;
|
|
1854
|
+
const temperature = request.temperature ?? this.defaultTemperature;
|
|
1855
|
+
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
1856
|
+
try {
|
|
1857
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
1858
|
+
method: "POST",
|
|
1859
|
+
headers: {
|
|
1860
|
+
"Content-Type": "application/json",
|
|
1861
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1862
|
+
},
|
|
1863
|
+
body: JSON.stringify({
|
|
1864
|
+
model,
|
|
1865
|
+
messages: [
|
|
1866
|
+
{ role: "system", content: request.system },
|
|
1867
|
+
{ role: "user", content: request.user }
|
|
1868
|
+
],
|
|
1869
|
+
temperature,
|
|
1870
|
+
max_tokens: maxTokens
|
|
1871
|
+
})
|
|
1872
|
+
});
|
|
1873
|
+
if (!response.ok) {
|
|
1874
|
+
const err = await response.text();
|
|
1875
|
+
throw new MemStackError("LLM_ERROR", `Groq API error: ${response.status} ${err}`, {
|
|
1876
|
+
retryable: response.status >= 500
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
const data = await response.json();
|
|
1880
|
+
return {
|
|
1881
|
+
text: data.choices[0]?.message?.content ?? "",
|
|
1882
|
+
tokens: {
|
|
1883
|
+
prompt: data.usage?.prompt_tokens ?? 0,
|
|
1884
|
+
completion: data.usage?.completion_tokens ?? 0,
|
|
1885
|
+
total: data.usage?.total_tokens ?? 0
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
if (err instanceof MemStackError) throw err;
|
|
1890
|
+
throw new MemStackError("LLM_ERROR", `Groq request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1891
|
+
retryable: true
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
async *completeStream(request) {
|
|
1896
|
+
const model = request.model ?? this.defaultModel;
|
|
1897
|
+
const temperature = request.temperature ?? this.defaultTemperature;
|
|
1898
|
+
const maxTokens = request.maxTokens ?? this.defaultMaxTokens;
|
|
1899
|
+
let response;
|
|
1900
|
+
try {
|
|
1901
|
+
response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
1902
|
+
method: "POST",
|
|
1903
|
+
headers: {
|
|
1904
|
+
"Content-Type": "application/json",
|
|
1905
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1906
|
+
},
|
|
1907
|
+
body: JSON.stringify({
|
|
1908
|
+
model,
|
|
1909
|
+
messages: [
|
|
1910
|
+
{ role: "system", content: request.system },
|
|
1911
|
+
{ role: "user", content: request.user }
|
|
1912
|
+
],
|
|
1913
|
+
temperature,
|
|
1914
|
+
max_tokens: maxTokens,
|
|
1915
|
+
stream: true,
|
|
1916
|
+
stream_options: { include_usage: true }
|
|
1917
|
+
})
|
|
1918
|
+
});
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
throw new MemStackError("LLM_ERROR", `Groq streaming request failed: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1921
|
+
retryable: true
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
if (!response.ok) {
|
|
1925
|
+
const err = await response.text();
|
|
1926
|
+
throw new MemStackError("LLM_ERROR", `Groq streaming error: ${response.status} ${err}`, {
|
|
1927
|
+
retryable: response.status >= 500
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
const reader = response.body?.getReader();
|
|
1931
|
+
if (!reader) throw new MemStackError("LLM_ERROR", "No response body for streaming");
|
|
1932
|
+
const decoder = new TextDecoder();
|
|
1933
|
+
let buffer = "";
|
|
1934
|
+
let promptTokens = 0;
|
|
1935
|
+
let completionTokens = 0;
|
|
1936
|
+
try {
|
|
1937
|
+
while (true) {
|
|
1938
|
+
const { done, value } = await reader.read();
|
|
1939
|
+
if (done) break;
|
|
1940
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1941
|
+
const lines = buffer.split("\n");
|
|
1942
|
+
buffer = lines.pop() ?? "";
|
|
1943
|
+
for (const line of lines) {
|
|
1944
|
+
const trimmed = line.trim();
|
|
1945
|
+
if (!trimmed || !trimmed.startsWith("data: ")) continue;
|
|
1946
|
+
const data = trimmed.slice(6);
|
|
1947
|
+
if (data === "[DONE]") continue;
|
|
1948
|
+
try {
|
|
1949
|
+
const parsed = JSON.parse(data);
|
|
1950
|
+
const content = parsed.choices?.[0]?.delta?.content ?? "";
|
|
1951
|
+
const usage = parsed.usage ?? parsed.x_groq?.usage;
|
|
1952
|
+
if (usage) {
|
|
1953
|
+
promptTokens = usage.prompt_tokens;
|
|
1954
|
+
completionTokens = usage.completion_tokens;
|
|
1955
|
+
}
|
|
1956
|
+
if (content) {
|
|
1957
|
+
yield {
|
|
1958
|
+
text: content,
|
|
1959
|
+
tokens: { prompt: promptTokens, completion: completionTokens, total: promptTokens + completionTokens }
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
} catch {
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
} finally {
|
|
1967
|
+
reader.releaseLock();
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
export { AnthropicLLMAdapter, CohereEmbeddingAdapter, DiskStorage, GroqLLMAdapter, InMemoryStorage, MemStack, MemStackError, OllamaLLMAdapter, OpenAIEmbeddingAdapter, OpenAILLMAdapter, PostgresStorage, RedisStorage, configError, notFound, storageError, validationError };
|
|
1973
|
+
//# sourceMappingURL=index.js.map
|
|
1974
|
+
//# sourceMappingURL=index.js.map
|