@desplega.ai/agent-swarm 1.64.0 → 1.65.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/openapi.json +42 -1
- package/package.json +3 -1
- package/src/be/db.ts +18 -290
- package/src/be/embedding.ts +0 -38
- package/src/be/memory/constants.ts +26 -0
- package/src/be/memory/index.ts +22 -0
- package/src/be/memory/providers/openai-embedding.ts +94 -0
- package/src/be/memory/providers/sqlite-store.ts +530 -0
- package/src/be/memory/reranker.ts +59 -0
- package/src/be/memory/types.ts +83 -0
- package/src/be/migrations/036_memory_ttl_staleness.sql +8 -0
- package/src/http/memory.ts +99 -46
- package/src/server.ts +2 -0
- package/src/tests/artifact-sdk.test.ts +2 -1
- package/src/tests/memory-e2e.test.ts +453 -0
- package/src/tests/memory-reranker.test.ts +192 -0
- package/src/tests/memory-store.test.ts +330 -0
- package/src/tests/memory.test.ts +105 -121
- package/src/tests/package-publish.test.ts +47 -0
- package/src/tests/self-improvement.test.ts +18 -19
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tools/inject-learning.ts +7 -5
- package/src/tools/memory-delete.ts +89 -0
- package/src/tools/memory-get.ts +2 -2
- package/src/tools/memory-search.ts +13 -7
- package/src/tools/store-progress.ts +12 -11
- package/src/tools/tool-config.ts +1 -0
- package/src/types.ts +3 -0
- package/tsconfig.json +49 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { getDb, isSqliteVecAvailable } from "@/be/db";
|
|
2
|
+
import { cosineSimilarity, deserializeEmbedding, serializeEmbedding } from "@/be/embedding";
|
|
3
|
+
import type { AgentMemory, AgentMemoryScope, AgentMemorySource } from "@/types";
|
|
4
|
+
import { TTL_DEFAULTS } from "../constants";
|
|
5
|
+
import type {
|
|
6
|
+
MemoryCandidate,
|
|
7
|
+
MemoryInput,
|
|
8
|
+
MemoryListOptions,
|
|
9
|
+
MemorySearchOptions,
|
|
10
|
+
MemoryStats,
|
|
11
|
+
MemoryStore,
|
|
12
|
+
} from "../types";
|
|
13
|
+
|
|
14
|
+
type AgentMemoryRow = {
|
|
15
|
+
id: string;
|
|
16
|
+
agentId: string | null;
|
|
17
|
+
scope: string;
|
|
18
|
+
name: string;
|
|
19
|
+
content: string;
|
|
20
|
+
summary: string | null;
|
|
21
|
+
embedding: Buffer | null;
|
|
22
|
+
source: string;
|
|
23
|
+
sourceTaskId: string | null;
|
|
24
|
+
sourcePath: string | null;
|
|
25
|
+
chunkIndex: number;
|
|
26
|
+
totalChunks: number;
|
|
27
|
+
tags: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
accessedAt: string;
|
|
30
|
+
expiresAt: string | null;
|
|
31
|
+
accessCount: number;
|
|
32
|
+
embeddingModel: string | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
|
|
36
|
+
return {
|
|
37
|
+
id: row.id,
|
|
38
|
+
agentId: row.agentId,
|
|
39
|
+
scope: row.scope as AgentMemoryScope,
|
|
40
|
+
name: row.name,
|
|
41
|
+
content: row.content,
|
|
42
|
+
summary: row.summary,
|
|
43
|
+
source: row.source as AgentMemorySource,
|
|
44
|
+
sourceTaskId: row.sourceTaskId,
|
|
45
|
+
sourcePath: row.sourcePath,
|
|
46
|
+
chunkIndex: row.chunkIndex,
|
|
47
|
+
totalChunks: row.totalChunks,
|
|
48
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
49
|
+
createdAt: row.createdAt,
|
|
50
|
+
accessedAt: row.accessedAt,
|
|
51
|
+
expiresAt: row.expiresAt ?? null,
|
|
52
|
+
accessCount: row.accessCount ?? 0,
|
|
53
|
+
embeddingModel: row.embeddingModel ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rowToCandidate(row: AgentMemoryRow, similarity: number): MemoryCandidate {
|
|
58
|
+
return {
|
|
59
|
+
...rowToAgentMemory(row),
|
|
60
|
+
similarity,
|
|
61
|
+
accessCount: row.accessCount ?? 0,
|
|
62
|
+
expiresAt: row.expiresAt ?? null,
|
|
63
|
+
embeddingModel: row.embeddingModel ?? null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function computeExpiresAt(source: AgentMemorySource): string | null {
|
|
68
|
+
const ttlDays = TTL_DEFAULTS[source];
|
|
69
|
+
if (ttlDays == null) return null;
|
|
70
|
+
return new Date(Date.now() + ttlDays * 86400000).toISOString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class SqliteMemoryStore implements MemoryStore {
|
|
74
|
+
private vecInitialized = false;
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
this.ensureVecTable();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private ensureVecTable(): void {
|
|
81
|
+
if (this.vecInitialized || !isSqliteVecAvailable()) return;
|
|
82
|
+
|
|
83
|
+
const db = getDb();
|
|
84
|
+
// Create the virtual table if it doesn't exist
|
|
85
|
+
try {
|
|
86
|
+
db.run(`
|
|
87
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
|
|
88
|
+
memory_id TEXT PRIMARY KEY,
|
|
89
|
+
embedding float[512]
|
|
90
|
+
)
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
// Populate from existing embeddings that aren't yet in the vec table
|
|
94
|
+
const existing = db
|
|
95
|
+
.prepare<{ id: string; embedding: Buffer }, []>(
|
|
96
|
+
"SELECT id, embedding FROM agent_memory WHERE embedding IS NOT NULL",
|
|
97
|
+
)
|
|
98
|
+
.all();
|
|
99
|
+
|
|
100
|
+
if (existing.length > 0) {
|
|
101
|
+
const vecCount = db
|
|
102
|
+
.prepare<{ count: number }, []>("SELECT COUNT(*) as count FROM memory_vec")
|
|
103
|
+
.get();
|
|
104
|
+
|
|
105
|
+
if ((vecCount?.count ?? 0) < existing.length) {
|
|
106
|
+
const insert = db.prepare(
|
|
107
|
+
"INSERT OR IGNORE INTO memory_vec(memory_id, embedding) VALUES (?, ?)",
|
|
108
|
+
);
|
|
109
|
+
const tx = db.transaction(() => {
|
|
110
|
+
for (const row of existing) {
|
|
111
|
+
insert.run(row.id, row.embedding);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
tx();
|
|
115
|
+
console.log(`[memory] Synced ${existing.length} embeddings to memory_vec`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.vecInitialized = true;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn("[memory] Failed to initialize memory_vec:", (err as Error).message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
store(input: MemoryInput): AgentMemory {
|
|
126
|
+
const id = crypto.randomUUID();
|
|
127
|
+
const now = new Date().toISOString();
|
|
128
|
+
const expiresAt = computeExpiresAt(input.source);
|
|
129
|
+
|
|
130
|
+
const row = getDb()
|
|
131
|
+
.prepare<
|
|
132
|
+
AgentMemoryRow,
|
|
133
|
+
[
|
|
134
|
+
string,
|
|
135
|
+
string | null,
|
|
136
|
+
string,
|
|
137
|
+
string,
|
|
138
|
+
string,
|
|
139
|
+
string | null,
|
|
140
|
+
string,
|
|
141
|
+
string | null,
|
|
142
|
+
string | null,
|
|
143
|
+
number,
|
|
144
|
+
number,
|
|
145
|
+
string,
|
|
146
|
+
string,
|
|
147
|
+
string,
|
|
148
|
+
string | null,
|
|
149
|
+
number,
|
|
150
|
+
string | null,
|
|
151
|
+
]
|
|
152
|
+
>(
|
|
153
|
+
`INSERT INTO agent_memory (id, agentId, scope, name, content, summary, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt, expiresAt, accessCount, embeddingModel)
|
|
154
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
155
|
+
)
|
|
156
|
+
.get(
|
|
157
|
+
id,
|
|
158
|
+
input.agentId ?? null,
|
|
159
|
+
input.scope,
|
|
160
|
+
input.name,
|
|
161
|
+
input.content,
|
|
162
|
+
input.summary ?? null,
|
|
163
|
+
input.source,
|
|
164
|
+
input.sourceTaskId ?? null,
|
|
165
|
+
input.sourcePath ?? null,
|
|
166
|
+
input.chunkIndex ?? 0,
|
|
167
|
+
input.totalChunks ?? 1,
|
|
168
|
+
JSON.stringify(input.tags ?? []),
|
|
169
|
+
now,
|
|
170
|
+
now,
|
|
171
|
+
expiresAt,
|
|
172
|
+
0,
|
|
173
|
+
null,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!row) throw new Error("Failed to create memory");
|
|
177
|
+
return rowToAgentMemory(row);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
storeBatch(inputs: MemoryInput[]): AgentMemory[] {
|
|
181
|
+
const db = getDb();
|
|
182
|
+
const results: AgentMemory[] = [];
|
|
183
|
+
const tx = db.transaction(() => {
|
|
184
|
+
for (const input of inputs) {
|
|
185
|
+
results.push(this.store(input));
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
tx();
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get(id: string): AgentMemory | null {
|
|
193
|
+
const db = getDb();
|
|
194
|
+
const row = db
|
|
195
|
+
.prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
|
|
196
|
+
.get(id);
|
|
197
|
+
if (!row) return null;
|
|
198
|
+
|
|
199
|
+
// Update accessedAt and increment accessCount
|
|
200
|
+
db.prepare(
|
|
201
|
+
"UPDATE agent_memory SET accessedAt = ?, accessCount = accessCount + 1 WHERE id = ?",
|
|
202
|
+
).run(new Date().toISOString(), id);
|
|
203
|
+
|
|
204
|
+
return rowToAgentMemory(row);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
peek(id: string): AgentMemory | null {
|
|
208
|
+
const row = getDb()
|
|
209
|
+
.prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
|
|
210
|
+
.get(id);
|
|
211
|
+
if (!row) return null;
|
|
212
|
+
return rowToAgentMemory(row);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
search(
|
|
216
|
+
embedding: Float32Array,
|
|
217
|
+
agentId: string,
|
|
218
|
+
options: MemorySearchOptions = {},
|
|
219
|
+
): MemoryCandidate[] {
|
|
220
|
+
const { scope = "all", limit = 10, source, isLead = false, includeExpired = false } = options;
|
|
221
|
+
|
|
222
|
+
if (isSqliteVecAvailable() && this.vecInitialized) {
|
|
223
|
+
return this.searchWithVec(embedding, agentId, {
|
|
224
|
+
scope,
|
|
225
|
+
limit,
|
|
226
|
+
source,
|
|
227
|
+
isLead,
|
|
228
|
+
includeExpired,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return this.searchBruteForce(embedding, agentId, {
|
|
232
|
+
scope,
|
|
233
|
+
limit,
|
|
234
|
+
source,
|
|
235
|
+
isLead,
|
|
236
|
+
includeExpired,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private searchWithVec(
|
|
241
|
+
queryEmbedding: Float32Array,
|
|
242
|
+
agentId: string,
|
|
243
|
+
options: {
|
|
244
|
+
scope: string;
|
|
245
|
+
limit: number;
|
|
246
|
+
source?: AgentMemorySource;
|
|
247
|
+
isLead: boolean;
|
|
248
|
+
includeExpired: boolean;
|
|
249
|
+
},
|
|
250
|
+
): MemoryCandidate[] {
|
|
251
|
+
const db = getDb();
|
|
252
|
+
const { scope, limit, source, isLead, includeExpired } = options;
|
|
253
|
+
|
|
254
|
+
// KNN query — fetch more candidates than needed for post-filtering
|
|
255
|
+
const knnLimit = limit * 5; // over-fetch to account for scope/expiry filters
|
|
256
|
+
const embeddingBuffer = serializeEmbedding(queryEmbedding);
|
|
257
|
+
|
|
258
|
+
const vecRows = db
|
|
259
|
+
.prepare<{ memory_id: string; distance: number }, [Buffer, number]>(
|
|
260
|
+
"SELECT memory_id, distance FROM memory_vec WHERE embedding MATCH ? AND k = ?",
|
|
261
|
+
)
|
|
262
|
+
.all(embeddingBuffer, knnLimit);
|
|
263
|
+
|
|
264
|
+
if (vecRows.length === 0) return [];
|
|
265
|
+
|
|
266
|
+
// Build ID list and distance map
|
|
267
|
+
const distanceMap = new Map<string, number>();
|
|
268
|
+
const ids: string[] = [];
|
|
269
|
+
for (const vr of vecRows) {
|
|
270
|
+
distanceMap.set(vr.memory_id, vr.distance);
|
|
271
|
+
ids.push(vr.memory_id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Hydrate from agent_memory with filters
|
|
275
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
276
|
+
const conditions: string[] = [`id IN (${placeholders})`];
|
|
277
|
+
const params: (string | null)[] = [...ids];
|
|
278
|
+
|
|
279
|
+
this.addScopeConditions(conditions, params, agentId, scope, isLead);
|
|
280
|
+
|
|
281
|
+
if (source) {
|
|
282
|
+
conditions.push("source = ?");
|
|
283
|
+
params.push(source);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!includeExpired) {
|
|
287
|
+
conditions.push("(expiresAt IS NULL OR expiresAt > datetime('now'))");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const rows = db
|
|
291
|
+
.prepare<AgentMemoryRow, (string | null)[]>(
|
|
292
|
+
`SELECT * FROM agent_memory WHERE ${conditions.join(" AND ")}`,
|
|
293
|
+
)
|
|
294
|
+
.all(...params);
|
|
295
|
+
|
|
296
|
+
// Map to candidates with similarity scores
|
|
297
|
+
const candidates: MemoryCandidate[] = [];
|
|
298
|
+
for (const row of rows) {
|
|
299
|
+
const distance = distanceMap.get(row.id) ?? 1;
|
|
300
|
+
const similarity = 1 - distance; // cosine distance to similarity
|
|
301
|
+
candidates.push(rowToCandidate(row, similarity));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
305
|
+
return candidates.slice(0, limit);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private searchBruteForce(
|
|
309
|
+
queryEmbedding: Float32Array,
|
|
310
|
+
agentId: string,
|
|
311
|
+
options: {
|
|
312
|
+
scope: string;
|
|
313
|
+
limit: number;
|
|
314
|
+
source?: AgentMemorySource;
|
|
315
|
+
isLead: boolean;
|
|
316
|
+
includeExpired: boolean;
|
|
317
|
+
},
|
|
318
|
+
): MemoryCandidate[] {
|
|
319
|
+
const { scope, limit, source, isLead, includeExpired } = options;
|
|
320
|
+
const db = getDb();
|
|
321
|
+
|
|
322
|
+
const conditions: string[] = ["embedding IS NOT NULL"];
|
|
323
|
+
const params: (string | null)[] = [];
|
|
324
|
+
|
|
325
|
+
this.addScopeConditions(conditions, params, agentId, scope, isLead);
|
|
326
|
+
|
|
327
|
+
if (source) {
|
|
328
|
+
conditions.push("source = ?");
|
|
329
|
+
params.push(source);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!includeExpired) {
|
|
333
|
+
conditions.push("(expiresAt IS NULL OR expiresAt > datetime('now'))");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
337
|
+
|
|
338
|
+
const rows = db
|
|
339
|
+
.prepare<AgentMemoryRow, (string | null)[]>(`SELECT * FROM agent_memory ${whereClause}`)
|
|
340
|
+
.all(...params);
|
|
341
|
+
|
|
342
|
+
const candidates: MemoryCandidate[] = [];
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
if (!row.embedding) continue;
|
|
345
|
+
const emb = deserializeEmbedding(row.embedding);
|
|
346
|
+
if (emb.length !== queryEmbedding.length) continue;
|
|
347
|
+
const similarity = cosineSimilarity(queryEmbedding, emb);
|
|
348
|
+
candidates.push(rowToCandidate(row, similarity));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
352
|
+
return candidates.slice(0, limit);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private addScopeConditions(
|
|
356
|
+
conditions: string[],
|
|
357
|
+
params: (string | null)[],
|
|
358
|
+
agentId: string,
|
|
359
|
+
scope: string,
|
|
360
|
+
isLead: boolean,
|
|
361
|
+
): void {
|
|
362
|
+
if (!isLead) {
|
|
363
|
+
if (scope === "agent") {
|
|
364
|
+
conditions.push("agentId = ? AND scope = 'agent'");
|
|
365
|
+
params.push(agentId);
|
|
366
|
+
} else if (scope === "swarm") {
|
|
367
|
+
conditions.push("scope = 'swarm'");
|
|
368
|
+
} else {
|
|
369
|
+
conditions.push("(agentId = ? OR scope = 'swarm')");
|
|
370
|
+
params.push(agentId);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
if (scope === "agent") {
|
|
374
|
+
conditions.push("scope = 'agent'");
|
|
375
|
+
} else if (scope === "swarm") {
|
|
376
|
+
conditions.push("scope = 'swarm'");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
list(agentId: string, options: MemoryListOptions = {}): AgentMemory[] {
|
|
382
|
+
const { scope = "all", limit = 20, offset = 0, isLead = false } = options;
|
|
383
|
+
const db = getDb();
|
|
384
|
+
|
|
385
|
+
const conditions: string[] = [];
|
|
386
|
+
const params: (string | number)[] = [];
|
|
387
|
+
|
|
388
|
+
if (!isLead) {
|
|
389
|
+
if (scope === "agent") {
|
|
390
|
+
conditions.push("agentId = ? AND scope = 'agent'");
|
|
391
|
+
params.push(agentId);
|
|
392
|
+
} else if (scope === "swarm") {
|
|
393
|
+
conditions.push("scope = 'swarm'");
|
|
394
|
+
} else {
|
|
395
|
+
conditions.push("(agentId = ? OR scope = 'swarm')");
|
|
396
|
+
params.push(agentId);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
if (scope === "agent") {
|
|
400
|
+
conditions.push("scope = 'agent'");
|
|
401
|
+
} else if (scope === "swarm") {
|
|
402
|
+
conditions.push("scope = 'swarm'");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
407
|
+
params.push(limit, offset);
|
|
408
|
+
|
|
409
|
+
const rows = db
|
|
410
|
+
.prepare<AgentMemoryRow, (string | number)[]>(
|
|
411
|
+
`SELECT * FROM agent_memory ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
|
|
412
|
+
)
|
|
413
|
+
.all(...params);
|
|
414
|
+
|
|
415
|
+
return rows.map(rowToAgentMemory);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
listForReembedding(options?: { agentId?: string }): { id: string; content: string }[] {
|
|
419
|
+
const db = getDb();
|
|
420
|
+
if (options?.agentId) {
|
|
421
|
+
return db
|
|
422
|
+
.prepare<{ id: string; content: string }, [string]>(
|
|
423
|
+
"SELECT id, content FROM agent_memory WHERE agentId = ?",
|
|
424
|
+
)
|
|
425
|
+
.all(options.agentId);
|
|
426
|
+
}
|
|
427
|
+
return db
|
|
428
|
+
.prepare<{ id: string; content: string }, []>("SELECT id, content FROM agent_memory")
|
|
429
|
+
.all();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
delete(id: string): boolean {
|
|
433
|
+
const db = getDb();
|
|
434
|
+
if (isSqliteVecAvailable() && this.vecInitialized) {
|
|
435
|
+
db.prepare("DELETE FROM memory_vec WHERE memory_id = ?").run(id);
|
|
436
|
+
}
|
|
437
|
+
const result = db.prepare("DELETE FROM agent_memory WHERE id = ?").run(id);
|
|
438
|
+
return result.changes > 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
deleteBySourcePath(sourcePath: string, agentId: string): number {
|
|
442
|
+
const db = getDb();
|
|
443
|
+
|
|
444
|
+
if (isSqliteVecAvailable() && this.vecInitialized) {
|
|
445
|
+
// Get IDs first for vec table cleanup
|
|
446
|
+
const ids = db
|
|
447
|
+
.prepare<{ id: string }, [string, string]>(
|
|
448
|
+
"SELECT id FROM agent_memory WHERE sourcePath = ? AND agentId = ?",
|
|
449
|
+
)
|
|
450
|
+
.all(sourcePath, agentId);
|
|
451
|
+
|
|
452
|
+
if (ids.length > 0) {
|
|
453
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
454
|
+
db.prepare(`DELETE FROM memory_vec WHERE memory_id IN (${placeholders})`).run(
|
|
455
|
+
...ids.map((r) => r.id),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const result = db
|
|
461
|
+
.prepare("DELETE FROM agent_memory WHERE sourcePath = ? AND agentId = ?")
|
|
462
|
+
.run(sourcePath, agentId);
|
|
463
|
+
return result.changes;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
updateEmbedding(id: string, embedding: Float32Array, model: string): void {
|
|
467
|
+
const db = getDb();
|
|
468
|
+
const buffer = serializeEmbedding(embedding);
|
|
469
|
+
db.prepare("UPDATE agent_memory SET embedding = ?, embeddingModel = ? WHERE id = ?").run(
|
|
470
|
+
buffer,
|
|
471
|
+
model,
|
|
472
|
+
id,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (isSqliteVecAvailable() && this.vecInitialized) {
|
|
476
|
+
db.prepare("INSERT OR REPLACE INTO memory_vec(memory_id, embedding) VALUES (?, ?)").run(
|
|
477
|
+
id,
|
|
478
|
+
buffer,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
getStats(agentId: string): MemoryStats {
|
|
484
|
+
const db = getDb();
|
|
485
|
+
|
|
486
|
+
const total = db
|
|
487
|
+
.prepare<{ count: number }, [string]>(
|
|
488
|
+
"SELECT COUNT(*) as count FROM agent_memory WHERE agentId = ?",
|
|
489
|
+
)
|
|
490
|
+
.get(agentId);
|
|
491
|
+
|
|
492
|
+
const bySourceRows = db
|
|
493
|
+
.prepare<{ source: string; count: number }, [string]>(
|
|
494
|
+
"SELECT source, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY source",
|
|
495
|
+
)
|
|
496
|
+
.all(agentId);
|
|
497
|
+
|
|
498
|
+
const byScopeRows = db
|
|
499
|
+
.prepare<{ scope: string; count: number }, [string]>(
|
|
500
|
+
"SELECT scope, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY scope",
|
|
501
|
+
)
|
|
502
|
+
.all(agentId);
|
|
503
|
+
|
|
504
|
+
const withEmbeddings = db
|
|
505
|
+
.prepare<{ count: number }, [string]>(
|
|
506
|
+
"SELECT COUNT(*) as count FROM agent_memory WHERE agentId = ? AND embedding IS NOT NULL",
|
|
507
|
+
)
|
|
508
|
+
.get(agentId);
|
|
509
|
+
|
|
510
|
+
const expired = db
|
|
511
|
+
.prepare<{ count: number }, [string]>(
|
|
512
|
+
"SELECT COUNT(*) as count FROM agent_memory WHERE agentId = ? AND expiresAt IS NOT NULL AND expiresAt <= datetime('now')",
|
|
513
|
+
)
|
|
514
|
+
.get(agentId);
|
|
515
|
+
|
|
516
|
+
const bySource: Record<string, number> = {};
|
|
517
|
+
for (const row of bySourceRows) bySource[row.source] = row.count;
|
|
518
|
+
|
|
519
|
+
const byScope: Record<string, number> = {};
|
|
520
|
+
for (const row of byScopeRows) byScope[row.scope] = row.count;
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
total: total?.count ?? 0,
|
|
524
|
+
bySource,
|
|
525
|
+
byScope,
|
|
526
|
+
withEmbeddings: withEmbeddings?.count ?? 0,
|
|
527
|
+
expired: expired?.count ?? 0,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESS_BOOST_MAX_MULTIPLIER,
|
|
3
|
+
ACCESS_BOOST_RECENCY_WINDOW_HOURS,
|
|
4
|
+
RECENCY_DECAY_HALF_LIFE_DAYS,
|
|
5
|
+
} from "./constants";
|
|
6
|
+
import type { MemoryCandidate, RerankOptions } from "./types";
|
|
7
|
+
|
|
8
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
9
|
+
const MS_PER_HOUR = 1000 * 60 * 60;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Exponential decay based on age. A memory at exactly HALF_LIFE_DAYS old
|
|
13
|
+
* gets multiplied by 0.5. Fresh memories get ~1.0.
|
|
14
|
+
*/
|
|
15
|
+
export function recencyDecay(createdAt: string, now: Date): number {
|
|
16
|
+
const ageDays = (now.getTime() - new Date(createdAt).getTime()) / MS_PER_DAY;
|
|
17
|
+
if (ageDays <= 0) return 1.0;
|
|
18
|
+
return 2 ** (-ageDays / RECENCY_DECAY_HALF_LIFE_DAYS);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Boost for frequently/recently accessed memories.
|
|
23
|
+
* Range: [1.0, ACCESS_BOOST_MAX_MULTIPLIER].
|
|
24
|
+
*/
|
|
25
|
+
export function accessBoost(accessedAt: string, accessCount: number, now: Date): number {
|
|
26
|
+
if (accessCount <= 0) return 1.0;
|
|
27
|
+
|
|
28
|
+
const hoursSinceAccess = (now.getTime() - new Date(accessedAt).getTime()) / MS_PER_HOUR;
|
|
29
|
+
const recencyFactor = hoursSinceAccess <= ACCESS_BOOST_RECENCY_WINDOW_HOURS ? 1.0 : 0.5;
|
|
30
|
+
const boost = 1 + Math.min(accessCount / 10, ACCESS_BOOST_MAX_MULTIPLIER - 1) * recencyFactor;
|
|
31
|
+
return boost;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Final score combining similarity, recency decay, and access boost.
|
|
36
|
+
*/
|
|
37
|
+
export function computeScore(candidate: MemoryCandidate, now: Date): number {
|
|
38
|
+
return (
|
|
39
|
+
candidate.similarity *
|
|
40
|
+
recencyDecay(candidate.createdAt, now) *
|
|
41
|
+
accessBoost(candidate.accessedAt, candidate.accessCount, now)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Rerank candidates by combining similarity with recency and access signals.
|
|
47
|
+
* Returns the top `limit` candidates sorted by final score.
|
|
48
|
+
*/
|
|
49
|
+
export function rerank(candidates: MemoryCandidate[], options: RerankOptions): MemoryCandidate[] {
|
|
50
|
+
const { limit, now = new Date() } = options;
|
|
51
|
+
|
|
52
|
+
const scored = candidates.map((candidate) => ({
|
|
53
|
+
...candidate,
|
|
54
|
+
similarity: computeScore(candidate, now),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
58
|
+
return scored.slice(0, limit);
|
|
59
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AgentMemory, AgentMemoryScope, AgentMemorySource } from "@/types";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// EmbeddingProvider — text to vector, swappable
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export interface EmbeddingProvider {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly dimensions: number;
|
|
10
|
+
embed(text: string): Promise<Float32Array | null>;
|
|
11
|
+
embedBatch(texts: string[]): Promise<(Float32Array | null)[]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// MemoryStore — persist and retrieve memories, swappable
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export interface MemoryStore {
|
|
19
|
+
store(input: MemoryInput): AgentMemory;
|
|
20
|
+
storeBatch(inputs: MemoryInput[]): AgentMemory[];
|
|
21
|
+
get(id: string): AgentMemory | null;
|
|
22
|
+
peek(id: string): AgentMemory | null;
|
|
23
|
+
search(embedding: Float32Array, agentId: string, options: MemorySearchOptions): MemoryCandidate[];
|
|
24
|
+
list(agentId: string, options: MemoryListOptions): AgentMemory[];
|
|
25
|
+
listForReembedding(options?: { agentId?: string }): { id: string; content: string }[];
|
|
26
|
+
delete(id: string): boolean;
|
|
27
|
+
deleteBySourcePath(sourcePath: string, agentId: string): number;
|
|
28
|
+
updateEmbedding(id: string, embedding: Float32Array, model: string): void;
|
|
29
|
+
getStats(agentId: string): MemoryStats;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Supporting types
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export interface MemoryInput {
|
|
37
|
+
agentId: string | null;
|
|
38
|
+
scope: AgentMemoryScope;
|
|
39
|
+
name: string;
|
|
40
|
+
content: string;
|
|
41
|
+
summary?: string | null;
|
|
42
|
+
source: AgentMemorySource;
|
|
43
|
+
sourceTaskId?: string | null;
|
|
44
|
+
sourcePath?: string | null;
|
|
45
|
+
chunkIndex?: number;
|
|
46
|
+
totalChunks?: number;
|
|
47
|
+
tags?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MemoryCandidate extends AgentMemory {
|
|
51
|
+
similarity: number;
|
|
52
|
+
accessCount: number;
|
|
53
|
+
expiresAt: string | null;
|
|
54
|
+
embeddingModel: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MemorySearchOptions {
|
|
58
|
+
scope?: "agent" | "swarm" | "all";
|
|
59
|
+
limit?: number;
|
|
60
|
+
source?: AgentMemorySource;
|
|
61
|
+
isLead?: boolean;
|
|
62
|
+
includeExpired?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface MemoryListOptions {
|
|
66
|
+
scope?: "agent" | "swarm" | "all";
|
|
67
|
+
limit?: number;
|
|
68
|
+
offset?: number;
|
|
69
|
+
isLead?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MemoryStats {
|
|
73
|
+
total: number;
|
|
74
|
+
bySource: Record<string, number>;
|
|
75
|
+
byScope: Record<string, number>;
|
|
76
|
+
withEmbeddings: number;
|
|
77
|
+
expired: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RerankOptions {
|
|
81
|
+
limit: number;
|
|
82
|
+
now?: Date;
|
|
83
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- New columns for TTL, access tracking, and embedding model
|
|
2
|
+
ALTER TABLE agent_memory ADD COLUMN expiresAt TEXT;
|
|
3
|
+
ALTER TABLE agent_memory ADD COLUMN accessCount INTEGER NOT NULL DEFAULT 0;
|
|
4
|
+
ALTER TABLE agent_memory ADD COLUMN embeddingModel TEXT;
|
|
5
|
+
|
|
6
|
+
-- Index for TTL queries (filtering expired memories)
|
|
7
|
+
CREATE INDEX IF NOT EXISTS idx_agent_memory_expires
|
|
8
|
+
ON agent_memory(expiresAt);
|