agentic-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/dist/adapters/openai.d.mts +33 -0
- package/dist/adapters/openai.d.ts +33 -0
- package/dist/adapters/openai.js +85 -0
- package/dist/adapters/openai.mjs +60 -0
- package/dist/adapters/voyageai.d.mts +30 -0
- package/dist/adapters/voyageai.d.ts +30 -0
- package/dist/adapters/voyageai.js +64 -0
- package/dist/adapters/voyageai.mjs +39 -0
- package/dist/index.d.mts +246 -0
- package/dist/index.d.ts +246 -0
- package/dist/index.js +724 -0
- package/dist/index.mjs +689 -0
- package/dist/types-CQ8Hcoqw.d.mts +131 -0
- package/dist/types-CQ8Hcoqw.d.ts +131 -0
- package/package.json +76 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
// src/core/utils.ts
|
|
2
|
+
function generateId() {
|
|
3
|
+
return `mem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
|
|
4
|
+
}
|
|
5
|
+
function now() {
|
|
6
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
7
|
+
}
|
|
8
|
+
function cosineSimilarity(a, b) {
|
|
9
|
+
if (a.length !== b.length) return 0;
|
|
10
|
+
let dotProduct = 0;
|
|
11
|
+
let normA = 0;
|
|
12
|
+
let normB = 0;
|
|
13
|
+
for (let i = 0; i < a.length; i++) {
|
|
14
|
+
dotProduct += a[i] * b[i];
|
|
15
|
+
normA += a[i] * a[i];
|
|
16
|
+
normB += b[i] * b[i];
|
|
17
|
+
}
|
|
18
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
19
|
+
return denom === 0 ? 0 : dotProduct / denom;
|
|
20
|
+
}
|
|
21
|
+
function daysSince(isoTimestamp) {
|
|
22
|
+
const diff = Date.now() - new Date(isoTimestamp).getTime();
|
|
23
|
+
return diff / (1e3 * 60 * 60 * 24);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/store/local.ts
|
|
27
|
+
var LocalStore = class {
|
|
28
|
+
entries = /* @__PURE__ */ new Map();
|
|
29
|
+
async get(id) {
|
|
30
|
+
return this.entries.get(id) ?? null;
|
|
31
|
+
}
|
|
32
|
+
async getAll(scope) {
|
|
33
|
+
const all = Array.from(this.entries.values());
|
|
34
|
+
if (!scope) return all;
|
|
35
|
+
return all.filter((e) => e.scope === scope);
|
|
36
|
+
}
|
|
37
|
+
async set(entry) {
|
|
38
|
+
this.entries.set(entry.id, { ...entry });
|
|
39
|
+
}
|
|
40
|
+
async delete(id) {
|
|
41
|
+
return this.entries.delete(id);
|
|
42
|
+
}
|
|
43
|
+
async clear(scope) {
|
|
44
|
+
if (!scope) {
|
|
45
|
+
this.entries.clear();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const [id, entry] of this.entries) {
|
|
49
|
+
if (entry.scope === scope) {
|
|
50
|
+
this.entries.delete(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async search(query) {
|
|
55
|
+
let results = Array.from(this.entries.values());
|
|
56
|
+
if (query.scope) {
|
|
57
|
+
results = results.filter((e) => e.scope === query.scope);
|
|
58
|
+
}
|
|
59
|
+
if (query.types && query.types.length > 0) {
|
|
60
|
+
results = results.filter((e) => query.types.includes(e.type));
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
/** Get the number of stored entries */
|
|
65
|
+
get size() {
|
|
66
|
+
return this.entries.size;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/retrieval/embedder.ts
|
|
71
|
+
var BuiltinEmbedder = class {
|
|
72
|
+
dim;
|
|
73
|
+
vocabulary = /* @__PURE__ */ new Map();
|
|
74
|
+
idfCache = /* @__PURE__ */ new Map();
|
|
75
|
+
documentCount = 0;
|
|
76
|
+
documentFreq = /* @__PURE__ */ new Map();
|
|
77
|
+
constructor(dimensions = 256) {
|
|
78
|
+
this.dim = dimensions;
|
|
79
|
+
}
|
|
80
|
+
dimensions() {
|
|
81
|
+
return this.dim;
|
|
82
|
+
}
|
|
83
|
+
async embed(text) {
|
|
84
|
+
const tokens = this.tokenize(text);
|
|
85
|
+
return this.tokensToVector(tokens);
|
|
86
|
+
}
|
|
87
|
+
async embedBatch(texts) {
|
|
88
|
+
return Promise.all(texts.map((t) => this.embed(t)));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Feed documents to build vocabulary and IDF stats.
|
|
92
|
+
* Call this when adding memories to improve embedding quality.
|
|
93
|
+
*/
|
|
94
|
+
train(documents) {
|
|
95
|
+
for (const doc of documents) {
|
|
96
|
+
this.documentCount++;
|
|
97
|
+
const uniqueTokens = new Set(this.tokenize(doc));
|
|
98
|
+
for (const token of uniqueTokens) {
|
|
99
|
+
this.documentFreq.set(token, (this.documentFreq.get(token) ?? 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.idfCache.clear();
|
|
103
|
+
for (const [token, freq] of this.documentFreq) {
|
|
104
|
+
this.idfCache.set(token, Math.log((this.documentCount + 1) / (freq + 1)) + 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
tokenize(text) {
|
|
108
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 1);
|
|
109
|
+
}
|
|
110
|
+
tokensToVector(tokens) {
|
|
111
|
+
const vector = new Array(this.dim).fill(0);
|
|
112
|
+
const tf = /* @__PURE__ */ new Map();
|
|
113
|
+
for (const token of tokens) {
|
|
114
|
+
tf.set(token, (tf.get(token) ?? 0) + 1);
|
|
115
|
+
}
|
|
116
|
+
for (const [token, freq] of tf) {
|
|
117
|
+
const idf = this.idfCache.get(token) ?? 1;
|
|
118
|
+
const weight = freq / tokens.length * idf;
|
|
119
|
+
const hash = this.hashToken(token);
|
|
120
|
+
const idx = Math.abs(hash) % this.dim;
|
|
121
|
+
const sign = this.hashToken(token + "_sign") > 0 ? 1 : -1;
|
|
122
|
+
vector[idx] += sign * weight;
|
|
123
|
+
}
|
|
124
|
+
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
|
125
|
+
if (norm > 0) {
|
|
126
|
+
for (let i = 0; i < vector.length; i++) {
|
|
127
|
+
vector[i] /= norm;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return vector;
|
|
131
|
+
}
|
|
132
|
+
hashToken(token) {
|
|
133
|
+
let hash = 0;
|
|
134
|
+
for (let i = 0; i < token.length; i++) {
|
|
135
|
+
const char = token.charCodeAt(i);
|
|
136
|
+
hash = (hash << 5) - hash + char;
|
|
137
|
+
hash |= 0;
|
|
138
|
+
}
|
|
139
|
+
return hash;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/retrieval/multi-signal.ts
|
|
144
|
+
var DEFAULT_WEIGHTS = {
|
|
145
|
+
similarity: 0.4,
|
|
146
|
+
recency: 0.25,
|
|
147
|
+
importance: 0.2,
|
|
148
|
+
taskRelevance: 0.15
|
|
149
|
+
};
|
|
150
|
+
var IMPORTANCE_SCORES = {
|
|
151
|
+
hard: 1,
|
|
152
|
+
soft: 0.5,
|
|
153
|
+
ephemeral: 0.2
|
|
154
|
+
};
|
|
155
|
+
var MultiSignalRetriever = class {
|
|
156
|
+
store;
|
|
157
|
+
embedder;
|
|
158
|
+
weights;
|
|
159
|
+
constructor(store, embedder, weights) {
|
|
160
|
+
this.store = store;
|
|
161
|
+
this.embedder = embedder;
|
|
162
|
+
this.weights = { ...DEFAULT_WEIGHTS, ...weights };
|
|
163
|
+
}
|
|
164
|
+
async retrieve(query) {
|
|
165
|
+
const signals = query.signals ?? ["similarity", "recency", "importance"];
|
|
166
|
+
const limit = query.limit ?? 10;
|
|
167
|
+
const threshold = query.threshold ?? 0;
|
|
168
|
+
const candidates = await this.store.search(query);
|
|
169
|
+
if (candidates.length === 0) return [];
|
|
170
|
+
let queryEmbedding = null;
|
|
171
|
+
if (signals.includes("similarity")) {
|
|
172
|
+
queryEmbedding = await this.embedder.embed(query.query);
|
|
173
|
+
}
|
|
174
|
+
let taskEmbedding = null;
|
|
175
|
+
if (signals.includes("taskRelevance") && query.taskContext) {
|
|
176
|
+
taskEmbedding = await this.embedder.embed(query.taskContext);
|
|
177
|
+
}
|
|
178
|
+
const scored = [];
|
|
179
|
+
for (const entry of candidates) {
|
|
180
|
+
const signalScores = {};
|
|
181
|
+
let totalScore = 0;
|
|
182
|
+
let totalWeight = 0;
|
|
183
|
+
let entryEmbedding = null;
|
|
184
|
+
const needsEmbedding = signals.includes("similarity") && queryEmbedding || signals.includes("taskRelevance") && taskEmbedding;
|
|
185
|
+
if (needsEmbedding) {
|
|
186
|
+
entryEmbedding = entry.embedding ?? await this.embedder.embed(entry.content);
|
|
187
|
+
}
|
|
188
|
+
if (signals.includes("similarity") && queryEmbedding && entryEmbedding) {
|
|
189
|
+
signalScores.similarity = Math.max(0, cosineSimilarity(queryEmbedding, entryEmbedding));
|
|
190
|
+
totalScore += signalScores.similarity * this.weights.similarity;
|
|
191
|
+
totalWeight += this.weights.similarity;
|
|
192
|
+
}
|
|
193
|
+
if (signals.includes("recency")) {
|
|
194
|
+
const age = Math.max(0, daysSince(entry.updatedAt));
|
|
195
|
+
signalScores.recency = Math.exp(-0.693 * age / 7);
|
|
196
|
+
totalScore += signalScores.recency * this.weights.recency;
|
|
197
|
+
totalWeight += this.weights.recency;
|
|
198
|
+
}
|
|
199
|
+
if (signals.includes("importance")) {
|
|
200
|
+
signalScores.importance = IMPORTANCE_SCORES[entry.importance] ?? 0.5;
|
|
201
|
+
totalScore += signalScores.importance * this.weights.importance;
|
|
202
|
+
totalWeight += this.weights.importance;
|
|
203
|
+
}
|
|
204
|
+
if (signals.includes("taskRelevance") && taskEmbedding && entryEmbedding) {
|
|
205
|
+
signalScores.taskRelevance = Math.max(0, cosineSimilarity(taskEmbedding, entryEmbedding));
|
|
206
|
+
totalScore += signalScores.taskRelevance * this.weights.taskRelevance;
|
|
207
|
+
totalWeight += this.weights.taskRelevance;
|
|
208
|
+
}
|
|
209
|
+
const normalizedScore = totalWeight > 0 ? totalScore / totalWeight : 0;
|
|
210
|
+
if (normalizedScore >= threshold) {
|
|
211
|
+
scored.push({ entry, score: normalizedScore, signalScores });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
scored.sort((a, b) => b.score - a.score);
|
|
215
|
+
return scored.slice(0, limit);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/retrieval/conflict.ts
|
|
220
|
+
var NEGATION_PATTERNS = [
|
|
221
|
+
/\bnot\b/i,
|
|
222
|
+
/\bnever\b/i,
|
|
223
|
+
/\bno\b/i,
|
|
224
|
+
/\bdon't\b/i,
|
|
225
|
+
/\bdoesn't\b/i,
|
|
226
|
+
/\bwon't\b/i,
|
|
227
|
+
/\bcan't\b/i,
|
|
228
|
+
/\bhate\b/i,
|
|
229
|
+
/\bdislike\b/i,
|
|
230
|
+
/\bavoid\b/i,
|
|
231
|
+
/\bstop\b/i,
|
|
232
|
+
/\bquit\b/i,
|
|
233
|
+
/\bremove\b/i,
|
|
234
|
+
/\bdelete\b/i,
|
|
235
|
+
/\binstead of\b/i,
|
|
236
|
+
/\brather than\b/i,
|
|
237
|
+
/\bno longer\b/i
|
|
238
|
+
];
|
|
239
|
+
var CHANGE_PATTERNS = [
|
|
240
|
+
/\bnow\b/i,
|
|
241
|
+
/\bactually\b/i,
|
|
242
|
+
/\bchanged?\b/i,
|
|
243
|
+
/\bswitch/i,
|
|
244
|
+
/\bprefer\b/i,
|
|
245
|
+
/\bwant\b/i,
|
|
246
|
+
/\bused to\b/i,
|
|
247
|
+
/\banymore\b/i
|
|
248
|
+
];
|
|
249
|
+
var ConflictDetector = class {
|
|
250
|
+
store;
|
|
251
|
+
embedder;
|
|
252
|
+
/** Similarity threshold to consider two entries as "same topic" */
|
|
253
|
+
topicThreshold;
|
|
254
|
+
constructor(store, embedder, topicThreshold = 0.6) {
|
|
255
|
+
this.store = store;
|
|
256
|
+
this.embedder = embedder;
|
|
257
|
+
this.topicThreshold = topicThreshold;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if new content conflicts with any stored memories.
|
|
261
|
+
* Returns conflicts sorted by confidence (highest first).
|
|
262
|
+
*/
|
|
263
|
+
async check(content, scope) {
|
|
264
|
+
const entries = await this.store.getAll(scope);
|
|
265
|
+
if (entries.length === 0) return [];
|
|
266
|
+
const contentEmbedding = await this.embedder.embed(content);
|
|
267
|
+
const conflicts = [];
|
|
268
|
+
for (const entry of entries) {
|
|
269
|
+
const entryEmbedding = entry.embedding ?? await this.embedder.embed(entry.content);
|
|
270
|
+
const similarity = cosineSimilarity(contentEmbedding, entryEmbedding);
|
|
271
|
+
if (similarity < this.topicThreshold) continue;
|
|
272
|
+
const contradictionScore = this.scoreContradiction(content, entry.content);
|
|
273
|
+
if (contradictionScore <= 0) continue;
|
|
274
|
+
const confidence = Math.min(1, similarity * 0.5 + contradictionScore * 0.5);
|
|
275
|
+
conflicts.push({
|
|
276
|
+
incoming: content,
|
|
277
|
+
stored: entry,
|
|
278
|
+
confidence,
|
|
279
|
+
action: this.suggestAction(entry, confidence),
|
|
280
|
+
reason: this.generateReason(content, entry, contradictionScore)
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
conflicts.sort((a, b) => b.confidence - a.confidence);
|
|
284
|
+
return conflicts;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Score how contradictory two pieces of text are.
|
|
288
|
+
* Returns 0 (no contradiction) to 1 (strong contradiction).
|
|
289
|
+
*/
|
|
290
|
+
scoreContradiction(incoming, stored) {
|
|
291
|
+
let score = 0;
|
|
292
|
+
const incomingHasNegation = NEGATION_PATTERNS.some((p) => p.test(incoming));
|
|
293
|
+
const storedHasNegation = NEGATION_PATTERNS.some((p) => p.test(stored));
|
|
294
|
+
if (incomingHasNegation !== storedHasNegation) {
|
|
295
|
+
score += 0.5;
|
|
296
|
+
}
|
|
297
|
+
const hasChangeLanguage = CHANGE_PATTERNS.some((p) => p.test(incoming));
|
|
298
|
+
if (hasChangeLanguage) {
|
|
299
|
+
score += 0.3;
|
|
300
|
+
}
|
|
301
|
+
const antonymScore = this.checkAntonyms(incoming, stored);
|
|
302
|
+
score += antonymScore * 0.4;
|
|
303
|
+
return Math.min(1, score);
|
|
304
|
+
}
|
|
305
|
+
/** Check for common antonym pairs */
|
|
306
|
+
checkAntonyms(a, b) {
|
|
307
|
+
const antonyms = [
|
|
308
|
+
[/\blike\b/i, /\bdislike\b/i],
|
|
309
|
+
[/\blove\b/i, /\bhate\b/i],
|
|
310
|
+
[/\byes\b/i, /\bno\b/i],
|
|
311
|
+
[/\btrue\b/i, /\bfalse\b/i],
|
|
312
|
+
[/\bvegetarian\b/i, /\bmeat\b/i],
|
|
313
|
+
[/\bvegan\b/i, /\bmeat\b|\bdairy\b/i],
|
|
314
|
+
[/\bmorning\b/i, /\bevening\b|\bnight\b/i],
|
|
315
|
+
[/\blight\b/i, /\bdark\b/i],
|
|
316
|
+
[/\bhot\b/i, /\bcold\b/i],
|
|
317
|
+
[/\bfast\b/i, /\bslow\b/i],
|
|
318
|
+
[/\benable\b/i, /\bdisable\b/i],
|
|
319
|
+
[/\ballow\b/i, /\bblock\b|\bdeny\b/i],
|
|
320
|
+
[/\baccept\b/i, /\breject\b/i]
|
|
321
|
+
];
|
|
322
|
+
let matches = 0;
|
|
323
|
+
for (const [word1, word2] of antonyms) {
|
|
324
|
+
if (word1.test(a) && word2.test(b) || word2.test(a) && word1.test(b)) {
|
|
325
|
+
matches++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return Math.min(1, matches * 0.5);
|
|
329
|
+
}
|
|
330
|
+
suggestAction(stored, confidence) {
|
|
331
|
+
if (stored.importance === "hard") return "clarify";
|
|
332
|
+
if (confidence > 0.7 && stored.importance === "soft") return "override";
|
|
333
|
+
if (stored.importance === "ephemeral") return "override";
|
|
334
|
+
return "clarify";
|
|
335
|
+
}
|
|
336
|
+
generateReason(incoming, stored, contradictionScore) {
|
|
337
|
+
if (stored.importance === "hard") {
|
|
338
|
+
return `Conflicts with a hard constraint: "${stored.content}". This memory was marked as non-negotiable - please confirm the change.`;
|
|
339
|
+
}
|
|
340
|
+
if (contradictionScore > 0.7) {
|
|
341
|
+
return `Directly contradicts stored memory: "${stored.content}". The new statement appears to reverse a previous preference.`;
|
|
342
|
+
}
|
|
343
|
+
return `Potentially conflicts with: "${stored.content}". The statements may be inconsistent.`;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/core/decay.ts
|
|
348
|
+
var DEFAULT_DECAY_CONFIG = {
|
|
349
|
+
policies: {
|
|
350
|
+
constraint: { policy: "none" },
|
|
351
|
+
preference: { policy: "exponential", halfLife: 30 * 24 * 60 * 60 * 1e3 },
|
|
352
|
+
// 30 days
|
|
353
|
+
fact: { policy: "exponential", halfLife: 90 * 24 * 60 * 60 * 1e3 },
|
|
354
|
+
// 90 days
|
|
355
|
+
task: { policy: "step", maxAge: 7 * 24 * 60 * 60 * 1e3 },
|
|
356
|
+
// 7 days then gone
|
|
357
|
+
episodic: { policy: "exponential", halfLife: 14 * 24 * 60 * 60 * 1e3 }
|
|
358
|
+
// 14 days
|
|
359
|
+
},
|
|
360
|
+
defaultPolicy: "exponential",
|
|
361
|
+
defaultHalfLife: 30 * 24 * 60 * 60 * 1e3
|
|
362
|
+
};
|
|
363
|
+
var DecayEngine = class {
|
|
364
|
+
config;
|
|
365
|
+
constructor(config) {
|
|
366
|
+
this.config = {
|
|
367
|
+
...DEFAULT_DECAY_CONFIG,
|
|
368
|
+
...config,
|
|
369
|
+
policies: { ...DEFAULT_DECAY_CONFIG.policies, ...config?.policies }
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Compute the current effective confidence of a memory entry
|
|
374
|
+
* after applying temporal decay.
|
|
375
|
+
* Returns a value between 0 and original confidence.
|
|
376
|
+
*/
|
|
377
|
+
computeDecayedConfidence(entry) {
|
|
378
|
+
if (entry.importance === "hard") return entry.confidence;
|
|
379
|
+
const typeConfig = this.config.policies[entry.type];
|
|
380
|
+
const policy = typeConfig?.policy ?? this.config.defaultPolicy;
|
|
381
|
+
const ageMs = Date.now() - new Date(entry.updatedAt).getTime();
|
|
382
|
+
switch (policy) {
|
|
383
|
+
case "none":
|
|
384
|
+
return entry.confidence;
|
|
385
|
+
case "linear": {
|
|
386
|
+
const ratePerDay = typeConfig?.ratePerDay ?? 0.01;
|
|
387
|
+
const ageDays = ageMs / (1e3 * 60 * 60 * 24);
|
|
388
|
+
return Math.max(0, entry.confidence - ratePerDay * ageDays);
|
|
389
|
+
}
|
|
390
|
+
case "exponential": {
|
|
391
|
+
const halfLife = typeConfig?.halfLife ?? this.config.defaultHalfLife;
|
|
392
|
+
const decayFactor = Math.exp(-0.693 * ageMs / halfLife);
|
|
393
|
+
return entry.confidence * decayFactor;
|
|
394
|
+
}
|
|
395
|
+
case "step": {
|
|
396
|
+
const maxAge = typeConfig?.maxAge ?? 7 * 24 * 60 * 60 * 1e3;
|
|
397
|
+
return ageMs > maxAge ? 0 : entry.confidence;
|
|
398
|
+
}
|
|
399
|
+
default:
|
|
400
|
+
return entry.confidence;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Filter out memories that have decayed below a threshold.
|
|
405
|
+
* Useful for periodic cleanup.
|
|
406
|
+
*/
|
|
407
|
+
filterDecayed(entries, threshold = 0.05) {
|
|
408
|
+
return entries.filter((e) => this.computeDecayedConfidence(e) >= threshold);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Get entries that should be cleaned up (decayed to near-zero).
|
|
412
|
+
*/
|
|
413
|
+
getExpired(entries, threshold = 0.01) {
|
|
414
|
+
return entries.filter((e) => this.computeDecayedConfidence(e) < threshold);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// src/core/memory.ts
|
|
419
|
+
var AgentMemory = class {
|
|
420
|
+
backend;
|
|
421
|
+
embedder;
|
|
422
|
+
retriever;
|
|
423
|
+
conflictDetector;
|
|
424
|
+
decayEngine;
|
|
425
|
+
checkpoints = /* @__PURE__ */ new Map();
|
|
426
|
+
defaultScope;
|
|
427
|
+
maxCheckpoints;
|
|
428
|
+
constructor(config = {}) {
|
|
429
|
+
this.backend = config.store === "local" || !config.store ? new LocalStore() : config.store;
|
|
430
|
+
this.embedder = config.embedder === "builtin" || !config.embedder ? new BuiltinEmbedder() : config.embedder;
|
|
431
|
+
this.retriever = new MultiSignalRetriever(this.backend, this.embedder);
|
|
432
|
+
this.conflictDetector = new ConflictDetector(this.backend, this.embedder);
|
|
433
|
+
this.decayEngine = new DecayEngine(config.decay);
|
|
434
|
+
this.defaultScope = config.defaultScope ?? "default";
|
|
435
|
+
this.maxCheckpoints = config.checkpoint?.maxCheckpoints ?? 10;
|
|
436
|
+
}
|
|
437
|
+
// ─── Store ───
|
|
438
|
+
/**
|
|
439
|
+
* Store a new memory entry.
|
|
440
|
+
* Automatically generates ID, timestamps, and embedding.
|
|
441
|
+
*/
|
|
442
|
+
async store(params) {
|
|
443
|
+
if (!params.content || params.content.trim().length === 0) {
|
|
444
|
+
throw new Error("Memory content cannot be empty");
|
|
445
|
+
}
|
|
446
|
+
if (this.embedder instanceof BuiltinEmbedder) {
|
|
447
|
+
this.embedder.train([params.content]);
|
|
448
|
+
}
|
|
449
|
+
const embedding = await this.embedder.embed(params.content);
|
|
450
|
+
const entry = {
|
|
451
|
+
id: generateId(),
|
|
452
|
+
content: params.content,
|
|
453
|
+
type: params.type ?? "fact",
|
|
454
|
+
scope: params.scope ?? this.defaultScope,
|
|
455
|
+
importance: params.importance ?? "soft",
|
|
456
|
+
confidence: params.confidence ?? 1,
|
|
457
|
+
embedding,
|
|
458
|
+
metadata: params.metadata ?? {},
|
|
459
|
+
createdAt: now(),
|
|
460
|
+
updatedAt: now(),
|
|
461
|
+
version: 1
|
|
462
|
+
};
|
|
463
|
+
await this.backend.set(entry);
|
|
464
|
+
return entry;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Update an existing memory entry.
|
|
468
|
+
* Increments version and updates timestamp.
|
|
469
|
+
*/
|
|
470
|
+
async update(id, updates) {
|
|
471
|
+
const entry = await this.backend.get(id);
|
|
472
|
+
if (!entry) return null;
|
|
473
|
+
const updated = {
|
|
474
|
+
...entry,
|
|
475
|
+
...updates,
|
|
476
|
+
updatedAt: now(),
|
|
477
|
+
version: entry.version + 1
|
|
478
|
+
};
|
|
479
|
+
if (updates.content && updates.content !== entry.content) {
|
|
480
|
+
if (updates.content.trim().length === 0) {
|
|
481
|
+
throw new Error("Memory content cannot be empty");
|
|
482
|
+
}
|
|
483
|
+
if (this.embedder instanceof BuiltinEmbedder) {
|
|
484
|
+
this.embedder.train([updates.content]);
|
|
485
|
+
}
|
|
486
|
+
updated.embedding = await this.embedder.embed(updates.content);
|
|
487
|
+
}
|
|
488
|
+
await this.backend.set(updated);
|
|
489
|
+
return updated;
|
|
490
|
+
}
|
|
491
|
+
/** Get a memory by ID */
|
|
492
|
+
async get(id) {
|
|
493
|
+
return this.backend.get(id);
|
|
494
|
+
}
|
|
495
|
+
/** Delete a memory by ID */
|
|
496
|
+
async delete(id) {
|
|
497
|
+
return this.backend.delete(id);
|
|
498
|
+
}
|
|
499
|
+
/** Get all memories, optionally filtered by scope */
|
|
500
|
+
async getAll(scope) {
|
|
501
|
+
return this.backend.getAll(scope);
|
|
502
|
+
}
|
|
503
|
+
/** Clear all memories, optionally for a specific scope only */
|
|
504
|
+
async clear(scope) {
|
|
505
|
+
return this.backend.clear(scope);
|
|
506
|
+
}
|
|
507
|
+
// ─── Retrieve ───
|
|
508
|
+
/**
|
|
509
|
+
* Multi-signal retrieval.
|
|
510
|
+
* Combines similarity, recency, importance, and task-relevance.
|
|
511
|
+
*/
|
|
512
|
+
async retrieve(query) {
|
|
513
|
+
return this.retriever.retrieve(query);
|
|
514
|
+
}
|
|
515
|
+
// ─── Conflict Detection ───
|
|
516
|
+
/**
|
|
517
|
+
* Check if content conflicts with stored memories.
|
|
518
|
+
* Returns conflicts sorted by confidence.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```typescript
|
|
522
|
+
* const conflicts = await memory.checkConflicts('User likes meat', 'user:123');
|
|
523
|
+
* if (conflicts.length > 0 && conflicts[0].action === 'clarify') {
|
|
524
|
+
* // Ask user to confirm preference change
|
|
525
|
+
* }
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
528
|
+
async checkConflicts(content, scope) {
|
|
529
|
+
return this.conflictDetector.check(content, scope ?? this.defaultScope);
|
|
530
|
+
}
|
|
531
|
+
// ─── Decay ───
|
|
532
|
+
/**
|
|
533
|
+
* Get the current effective confidence of a memory after decay.
|
|
534
|
+
*/
|
|
535
|
+
getDecayedConfidence(entry) {
|
|
536
|
+
return this.decayEngine.computeDecayedConfidence(entry);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Clean up expired memories (decayed below threshold).
|
|
540
|
+
* Returns the number of deleted entries.
|
|
541
|
+
*/
|
|
542
|
+
async cleanup(scope, threshold = 0.01) {
|
|
543
|
+
const entries = await this.backend.getAll(scope);
|
|
544
|
+
const expired = this.decayEngine.getExpired(entries, threshold);
|
|
545
|
+
for (const entry of expired) {
|
|
546
|
+
await this.backend.delete(entry.id);
|
|
547
|
+
}
|
|
548
|
+
return expired.length;
|
|
549
|
+
}
|
|
550
|
+
// ─── Checkpointing ───
|
|
551
|
+
/**
|
|
552
|
+
* Create a checkpoint of current task state.
|
|
553
|
+
* Use this before context overflow to preserve state.
|
|
554
|
+
*/
|
|
555
|
+
async checkpoint(params) {
|
|
556
|
+
const cp = {
|
|
557
|
+
id: generateId(),
|
|
558
|
+
taskGraph: params.taskGraph,
|
|
559
|
+
summary: params.summary,
|
|
560
|
+
toolOutputs: params.toolOutputs ?? {},
|
|
561
|
+
activeMemoryIds: params.activeMemoryIds ?? [],
|
|
562
|
+
createdAt: now()
|
|
563
|
+
};
|
|
564
|
+
this.checkpoints.set(cp.id, cp);
|
|
565
|
+
if (this.checkpoints.size > this.maxCheckpoints) {
|
|
566
|
+
const oldest = this.checkpoints.keys().next().value;
|
|
567
|
+
if (oldest) this.checkpoints.delete(oldest);
|
|
568
|
+
}
|
|
569
|
+
return cp;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Rehydrate from a checkpoint.
|
|
573
|
+
* Returns the checkpoint data + relevant memories.
|
|
574
|
+
*/
|
|
575
|
+
async rehydrate(checkpointId) {
|
|
576
|
+
const cp = this.checkpoints.get(checkpointId);
|
|
577
|
+
if (!cp) return null;
|
|
578
|
+
const memories = [];
|
|
579
|
+
for (const id of cp.activeMemoryIds) {
|
|
580
|
+
const entry = await this.backend.get(id);
|
|
581
|
+
if (entry) memories.push(entry);
|
|
582
|
+
}
|
|
583
|
+
return { checkpoint: cp, memories };
|
|
584
|
+
}
|
|
585
|
+
/** Get the latest checkpoint */
|
|
586
|
+
getLatestCheckpoint() {
|
|
587
|
+
const all = Array.from(this.checkpoints.values());
|
|
588
|
+
if (all.length === 0) return null;
|
|
589
|
+
return all[all.length - 1];
|
|
590
|
+
}
|
|
591
|
+
/** List all checkpoints */
|
|
592
|
+
listCheckpoints() {
|
|
593
|
+
return Array.from(this.checkpoints.values());
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/store/file.ts
|
|
598
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
599
|
+
import { dirname } from "path";
|
|
600
|
+
var FileStore = class {
|
|
601
|
+
entries = /* @__PURE__ */ new Map();
|
|
602
|
+
filePath;
|
|
603
|
+
dirty = false;
|
|
604
|
+
constructor(filePath) {
|
|
605
|
+
this.filePath = filePath;
|
|
606
|
+
this.load();
|
|
607
|
+
}
|
|
608
|
+
load() {
|
|
609
|
+
if (existsSync(this.filePath)) {
|
|
610
|
+
try {
|
|
611
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
612
|
+
const data = JSON.parse(raw);
|
|
613
|
+
for (const entry of data) {
|
|
614
|
+
this.entries.set(entry.id, entry);
|
|
615
|
+
}
|
|
616
|
+
} catch {
|
|
617
|
+
this.entries.clear();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
persist() {
|
|
622
|
+
if (!this.dirty) return;
|
|
623
|
+
const dir = dirname(this.filePath);
|
|
624
|
+
if (!existsSync(dir)) {
|
|
625
|
+
mkdirSync(dir, { recursive: true });
|
|
626
|
+
}
|
|
627
|
+
const data = Array.from(this.entries.values());
|
|
628
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
629
|
+
this.dirty = false;
|
|
630
|
+
}
|
|
631
|
+
async get(id) {
|
|
632
|
+
return this.entries.get(id) ?? null;
|
|
633
|
+
}
|
|
634
|
+
async getAll(scope) {
|
|
635
|
+
const all = Array.from(this.entries.values());
|
|
636
|
+
if (!scope) return all;
|
|
637
|
+
return all.filter((e) => e.scope === scope);
|
|
638
|
+
}
|
|
639
|
+
async set(entry) {
|
|
640
|
+
this.entries.set(entry.id, { ...entry });
|
|
641
|
+
this.dirty = true;
|
|
642
|
+
this.persist();
|
|
643
|
+
}
|
|
644
|
+
async delete(id) {
|
|
645
|
+
const deleted = this.entries.delete(id);
|
|
646
|
+
if (deleted) {
|
|
647
|
+
this.dirty = true;
|
|
648
|
+
this.persist();
|
|
649
|
+
}
|
|
650
|
+
return deleted;
|
|
651
|
+
}
|
|
652
|
+
async clear(scope) {
|
|
653
|
+
if (!scope) {
|
|
654
|
+
this.entries.clear();
|
|
655
|
+
} else {
|
|
656
|
+
for (const [id, entry] of this.entries) {
|
|
657
|
+
if (entry.scope === scope) {
|
|
658
|
+
this.entries.delete(id);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
this.dirty = true;
|
|
663
|
+
this.persist();
|
|
664
|
+
}
|
|
665
|
+
async search(query) {
|
|
666
|
+
let results = Array.from(this.entries.values());
|
|
667
|
+
if (query.scope) {
|
|
668
|
+
results = results.filter((e) => e.scope === query.scope);
|
|
669
|
+
}
|
|
670
|
+
if (query.types && query.types.length > 0) {
|
|
671
|
+
results = results.filter((e) => query.types.includes(e.type));
|
|
672
|
+
}
|
|
673
|
+
return results;
|
|
674
|
+
}
|
|
675
|
+
get size() {
|
|
676
|
+
return this.entries.size;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
export {
|
|
680
|
+
AgentMemory,
|
|
681
|
+
BuiltinEmbedder,
|
|
682
|
+
ConflictDetector,
|
|
683
|
+
DecayEngine,
|
|
684
|
+
FileStore,
|
|
685
|
+
LocalStore,
|
|
686
|
+
MultiSignalRetriever,
|
|
687
|
+
cosineSimilarity,
|
|
688
|
+
generateId
|
|
689
|
+
};
|