@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,192 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { accessBoost, computeScore, recencyDecay, rerank } from "../be/memory/reranker";
|
|
3
|
+
import type { MemoryCandidate } from "../be/memory/types";
|
|
4
|
+
|
|
5
|
+
function makeCandidate(
|
|
6
|
+
overrides: Partial<MemoryCandidate> & { similarity: number },
|
|
7
|
+
): MemoryCandidate {
|
|
8
|
+
return {
|
|
9
|
+
id: crypto.randomUUID(),
|
|
10
|
+
agentId: "00000000-0000-0000-0000-000000000001",
|
|
11
|
+
scope: "agent",
|
|
12
|
+
name: "test",
|
|
13
|
+
content: "test content",
|
|
14
|
+
summary: null,
|
|
15
|
+
source: "manual",
|
|
16
|
+
sourceTaskId: null,
|
|
17
|
+
sourcePath: null,
|
|
18
|
+
chunkIndex: 0,
|
|
19
|
+
totalChunks: 1,
|
|
20
|
+
tags: [],
|
|
21
|
+
createdAt: new Date().toISOString(),
|
|
22
|
+
accessedAt: new Date().toISOString(),
|
|
23
|
+
accessCount: 0,
|
|
24
|
+
expiresAt: null,
|
|
25
|
+
embeddingModel: null,
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("recencyDecay", () => {
|
|
31
|
+
const now = new Date("2026-04-12T12:00:00Z");
|
|
32
|
+
|
|
33
|
+
test("fresh memory → ~1.0", () => {
|
|
34
|
+
const decay = recencyDecay(now.toISOString(), now);
|
|
35
|
+
expect(decay).toBeCloseTo(1.0, 5);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("memory at half-life (14d) → ~0.5", () => {
|
|
39
|
+
const created = new Date(now.getTime() - 14 * 86400000).toISOString();
|
|
40
|
+
const decay = recencyDecay(created, now);
|
|
41
|
+
expect(decay).toBeCloseTo(0.5, 2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("memory at 2× half-life (28d) → ~0.25", () => {
|
|
45
|
+
const created = new Date(now.getTime() - 28 * 86400000).toISOString();
|
|
46
|
+
const decay = recencyDecay(created, now);
|
|
47
|
+
expect(decay).toBeCloseTo(0.25, 2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("very old memory (365d) → near 0", () => {
|
|
51
|
+
const created = new Date(now.getTime() - 365 * 86400000).toISOString();
|
|
52
|
+
const decay = recencyDecay(created, now);
|
|
53
|
+
expect(decay).toBeLessThan(0.001);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("future memory → 1.0", () => {
|
|
57
|
+
const created = new Date(now.getTime() + 86400000).toISOString();
|
|
58
|
+
const decay = recencyDecay(created, now);
|
|
59
|
+
expect(decay).toBe(1.0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("accessBoost", () => {
|
|
64
|
+
const now = new Date("2026-04-12T12:00:00Z");
|
|
65
|
+
|
|
66
|
+
test("accessCount=0 → exactly 1.0", () => {
|
|
67
|
+
expect(accessBoost(now.toISOString(), 0, now)).toBe(1.0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("accessCount=10, accessed within window → max boost", () => {
|
|
71
|
+
const boost = accessBoost(now.toISOString(), 10, now);
|
|
72
|
+
expect(boost).toBeCloseTo(1.5, 2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("accessCount=10, accessed outside window → partial boost", () => {
|
|
76
|
+
const accessed = new Date(now.getTime() - 72 * 3600000).toISOString(); // 72h ago
|
|
77
|
+
const boost = accessBoost(accessed, 10, now);
|
|
78
|
+
// recencyFactor = 0.5, boost = 1 + min(10/10, 0.5) * 0.5 = 1.25
|
|
79
|
+
expect(boost).toBeCloseTo(1.25, 2);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("accessCount=100 (capped) → same as 10+", () => {
|
|
83
|
+
const boost = accessBoost(now.toISOString(), 100, now);
|
|
84
|
+
expect(boost).toBeCloseTo(1.5, 2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("accessCount=3 → partial boost", () => {
|
|
88
|
+
const boost = accessBoost(now.toISOString(), 3, now);
|
|
89
|
+
// boost = 1 + min(3/10, 0.5) * 1.0 = 1 + 0.3 = 1.3
|
|
90
|
+
expect(boost).toBeCloseTo(1.3, 2);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("computeScore", () => {
|
|
95
|
+
const now = new Date("2026-04-12T12:00:00Z");
|
|
96
|
+
|
|
97
|
+
test("multiplies similarity × decay × boost", () => {
|
|
98
|
+
const candidate = makeCandidate({
|
|
99
|
+
similarity: 0.8,
|
|
100
|
+
createdAt: now.toISOString(),
|
|
101
|
+
accessedAt: now.toISOString(),
|
|
102
|
+
accessCount: 0,
|
|
103
|
+
});
|
|
104
|
+
const score = computeScore(candidate, now);
|
|
105
|
+
// 0.8 * 1.0 * 1.0 = 0.8
|
|
106
|
+
expect(score).toBeCloseTo(0.8, 5);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("old memory with no access gets penalized", () => {
|
|
110
|
+
const candidate = makeCandidate({
|
|
111
|
+
similarity: 0.8,
|
|
112
|
+
createdAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
113
|
+
accessedAt: new Date(now.getTime() - 14 * 86400000).toISOString(),
|
|
114
|
+
accessCount: 0,
|
|
115
|
+
});
|
|
116
|
+
const score = computeScore(candidate, now);
|
|
117
|
+
// 0.8 * 0.5 * 1.0 = 0.4
|
|
118
|
+
expect(score).toBeCloseTo(0.4, 2);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("rerank", () => {
|
|
123
|
+
const now = new Date("2026-04-12T12:00:00Z");
|
|
124
|
+
|
|
125
|
+
test("sorts by final score descending", () => {
|
|
126
|
+
const candidates = [
|
|
127
|
+
makeCandidate({
|
|
128
|
+
similarity: 0.6,
|
|
129
|
+
createdAt: now.toISOString(),
|
|
130
|
+
}),
|
|
131
|
+
makeCandidate({
|
|
132
|
+
similarity: 0.9,
|
|
133
|
+
createdAt: now.toISOString(),
|
|
134
|
+
}),
|
|
135
|
+
makeCandidate({
|
|
136
|
+
similarity: 0.3,
|
|
137
|
+
createdAt: now.toISOString(),
|
|
138
|
+
}),
|
|
139
|
+
];
|
|
140
|
+
const result = rerank(candidates, { limit: 10, now });
|
|
141
|
+
expect(result[0]!.similarity).toBeGreaterThan(result[1]!.similarity);
|
|
142
|
+
expect(result[1]!.similarity).toBeGreaterThan(result[2]!.similarity);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("respects limit parameter", () => {
|
|
146
|
+
const candidates = Array.from({ length: 10 }, (_, i) =>
|
|
147
|
+
makeCandidate({ similarity: i / 10, createdAt: now.toISOString() }),
|
|
148
|
+
);
|
|
149
|
+
const result = rerank(candidates, { limit: 3, now });
|
|
150
|
+
expect(result).toHaveLength(3);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("handles empty candidate array", () => {
|
|
154
|
+
const result = rerank([], { limit: 5, now });
|
|
155
|
+
expect(result).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("handles candidates with zero accessCount", () => {
|
|
159
|
+
const candidates = [
|
|
160
|
+
makeCandidate({ similarity: 0.8, accessCount: 0, createdAt: now.toISOString() }),
|
|
161
|
+
makeCandidate({ similarity: 0.7, accessCount: 0, createdAt: now.toISOString() }),
|
|
162
|
+
];
|
|
163
|
+
const result = rerank(candidates, { limit: 2, now });
|
|
164
|
+
expect(result[0]!.similarity).toBeGreaterThan(result[1]!.similarity);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("recency boosts newer memory over older with same raw similarity", () => {
|
|
168
|
+
const candidates = [
|
|
169
|
+
makeCandidate({
|
|
170
|
+
similarity: 0.8,
|
|
171
|
+
createdAt: new Date(now.getTime() - 14 * 86400000).toISOString(), // 14d old
|
|
172
|
+
}),
|
|
173
|
+
makeCandidate({
|
|
174
|
+
similarity: 0.8,
|
|
175
|
+
createdAt: now.toISOString(), // fresh
|
|
176
|
+
}),
|
|
177
|
+
];
|
|
178
|
+
const result = rerank(candidates, { limit: 2, now });
|
|
179
|
+
// Fresh memory should rank higher due to recency decay
|
|
180
|
+
expect(result[0]!.createdAt).toBe(now.toISOString());
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("now parameter enables deterministic testing", () => {
|
|
184
|
+
const candidate = makeCandidate({
|
|
185
|
+
similarity: 0.8,
|
|
186
|
+
createdAt: new Date(now.getTime() - 7 * 86400000).toISOString(),
|
|
187
|
+
});
|
|
188
|
+
const result1 = rerank([candidate], { limit: 1, now });
|
|
189
|
+
const result2 = rerank([candidate], { limit: 1, now });
|
|
190
|
+
expect(result1[0]!.similarity).toBe(result2[0]!.similarity);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, createAgent, initDb } from "../be/db";
|
|
4
|
+
import { serializeEmbedding } from "../be/embedding";
|
|
5
|
+
import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
6
|
+
|
|
7
|
+
const TEST_DB_PATH = "./test-memory-store.sqlite";
|
|
8
|
+
|
|
9
|
+
describe("SqliteMemoryStore", () => {
|
|
10
|
+
const agentA = "aaaa0000-0000-4000-8000-000000000001";
|
|
11
|
+
const agentB = "bbbb0000-0000-4000-8000-000000000002";
|
|
12
|
+
let store: SqliteMemoryStore;
|
|
13
|
+
|
|
14
|
+
beforeAll(async () => {
|
|
15
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
16
|
+
try {
|
|
17
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
initDb(TEST_DB_PATH);
|
|
21
|
+
createAgent({ id: agentA, name: "Test Agent A", isLead: false, status: "idle" });
|
|
22
|
+
createAgent({ id: agentB, name: "Test Agent B", isLead: false, status: "idle" });
|
|
23
|
+
store = new SqliteMemoryStore();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
closeDb();
|
|
28
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
29
|
+
try {
|
|
30
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("store()", () => {
|
|
36
|
+
test("creates memory with correct fields", () => {
|
|
37
|
+
const memory = store.store({
|
|
38
|
+
agentId: agentA,
|
|
39
|
+
scope: "agent",
|
|
40
|
+
name: "test memory",
|
|
41
|
+
content: "test content",
|
|
42
|
+
source: "manual",
|
|
43
|
+
});
|
|
44
|
+
expect(memory.id).toBeDefined();
|
|
45
|
+
expect(memory.agentId).toBe(agentA);
|
|
46
|
+
expect(memory.scope).toBe("agent");
|
|
47
|
+
expect(memory.name).toBe("test memory");
|
|
48
|
+
expect(memory.content).toBe("test content");
|
|
49
|
+
expect(memory.source).toBe("manual");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("task_completion → expiresAt ≈ now + 7d", () => {
|
|
53
|
+
const before = Date.now();
|
|
54
|
+
const memory = store.store({
|
|
55
|
+
agentId: agentA,
|
|
56
|
+
scope: "agent",
|
|
57
|
+
name: "task mem",
|
|
58
|
+
content: "task content",
|
|
59
|
+
source: "task_completion",
|
|
60
|
+
});
|
|
61
|
+
expect(memory.expiresAt).toBeDefined();
|
|
62
|
+
const expires = new Date(memory.expiresAt!).getTime();
|
|
63
|
+
const expectedMin = before + 7 * 86400000 - 5000;
|
|
64
|
+
const expectedMax = Date.now() + 7 * 86400000 + 5000;
|
|
65
|
+
expect(expires).toBeGreaterThan(expectedMin);
|
|
66
|
+
expect(expires).toBeLessThan(expectedMax);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("session_summary → expiresAt ≈ now + 3d", () => {
|
|
70
|
+
const memory = store.store({
|
|
71
|
+
agentId: agentA,
|
|
72
|
+
scope: "agent",
|
|
73
|
+
name: "session mem",
|
|
74
|
+
content: "session content",
|
|
75
|
+
source: "session_summary",
|
|
76
|
+
});
|
|
77
|
+
expect(memory.expiresAt).toBeDefined();
|
|
78
|
+
const expires = new Date(memory.expiresAt!).getTime();
|
|
79
|
+
const expected = Date.now() + 3 * 86400000;
|
|
80
|
+
expect(Math.abs(expires - expected)).toBeLessThan(5000);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("manual → expiresAt is null", () => {
|
|
84
|
+
const memory = store.store({
|
|
85
|
+
agentId: agentA,
|
|
86
|
+
scope: "agent",
|
|
87
|
+
name: "manual mem",
|
|
88
|
+
content: "manual content",
|
|
89
|
+
source: "manual",
|
|
90
|
+
});
|
|
91
|
+
expect(memory.expiresAt).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("storeBatch()", () => {
|
|
96
|
+
test("atomically stores multiple memories", () => {
|
|
97
|
+
const memories = store.storeBatch([
|
|
98
|
+
{ agentId: agentA, scope: "agent", name: "batch1", content: "c1", source: "manual" },
|
|
99
|
+
{ agentId: agentA, scope: "agent", name: "batch2", content: "c2", source: "manual" },
|
|
100
|
+
]);
|
|
101
|
+
expect(memories).toHaveLength(2);
|
|
102
|
+
expect(memories[0]!.name).toBe("batch1");
|
|
103
|
+
expect(memories[1]!.name).toBe("batch2");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("get() and peek()", () => {
|
|
108
|
+
test("get returns memory and increments accessCount", () => {
|
|
109
|
+
const created = store.store({
|
|
110
|
+
agentId: agentA,
|
|
111
|
+
scope: "agent",
|
|
112
|
+
name: "get test",
|
|
113
|
+
content: "content",
|
|
114
|
+
source: "manual",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const first = store.get(created.id);
|
|
118
|
+
expect(first).toBeDefined();
|
|
119
|
+
expect(first!.name).toBe("get test");
|
|
120
|
+
|
|
121
|
+
const second = store.get(created.id);
|
|
122
|
+
expect(second).toBeDefined();
|
|
123
|
+
|
|
124
|
+
// Verify accessCount incremented by peeking (no side effects)
|
|
125
|
+
const peeked = store.peek(created.id);
|
|
126
|
+
expect(peeked!.accessCount).toBe(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("peek does NOT increment accessCount", () => {
|
|
130
|
+
const created = store.store({
|
|
131
|
+
agentId: agentA,
|
|
132
|
+
scope: "agent",
|
|
133
|
+
name: "peek test",
|
|
134
|
+
content: "content",
|
|
135
|
+
source: "manual",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
store.peek(created.id);
|
|
139
|
+
store.peek(created.id);
|
|
140
|
+
store.peek(created.id);
|
|
141
|
+
|
|
142
|
+
const peeked = store.peek(created.id);
|
|
143
|
+
expect(peeked!.accessCount).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("get returns null for non-existent ID", () => {
|
|
147
|
+
expect(store.get("00000000-0000-0000-0000-000000000000")).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("search()", () => {
|
|
152
|
+
test("returns candidates sorted by similarity", () => {
|
|
153
|
+
// Create memories with known embeddings
|
|
154
|
+
const m1 = store.store({
|
|
155
|
+
agentId: agentA,
|
|
156
|
+
scope: "agent",
|
|
157
|
+
name: "search1",
|
|
158
|
+
content: "first",
|
|
159
|
+
source: "manual",
|
|
160
|
+
});
|
|
161
|
+
store.updateEmbedding(m1.id, new Float32Array([1, 0, 0]), "test-model");
|
|
162
|
+
|
|
163
|
+
const m2 = store.store({
|
|
164
|
+
agentId: agentA,
|
|
165
|
+
scope: "agent",
|
|
166
|
+
name: "search2",
|
|
167
|
+
content: "second",
|
|
168
|
+
source: "manual",
|
|
169
|
+
});
|
|
170
|
+
store.updateEmbedding(m2.id, new Float32Array([0.9, 0.1, 0]), "test-model");
|
|
171
|
+
|
|
172
|
+
const query = new Float32Array([1, 0, 0]);
|
|
173
|
+
const results = store.search(query, agentA, { limit: 10 });
|
|
174
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
175
|
+
// First result should be most similar (exact match)
|
|
176
|
+
expect(results[0]!.similarity).toBeGreaterThan(results[1]!.similarity);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("respects scope filtering", () => {
|
|
180
|
+
const m1 = store.store({
|
|
181
|
+
agentId: agentB,
|
|
182
|
+
scope: "agent",
|
|
183
|
+
name: "agent-only",
|
|
184
|
+
content: "agent scoped",
|
|
185
|
+
source: "manual",
|
|
186
|
+
});
|
|
187
|
+
store.updateEmbedding(m1.id, new Float32Array([0, 0.5, 0.5]), "test-model");
|
|
188
|
+
|
|
189
|
+
const m2 = store.store({
|
|
190
|
+
agentId: agentB,
|
|
191
|
+
scope: "swarm",
|
|
192
|
+
name: "swarm-shared",
|
|
193
|
+
content: "swarm scoped",
|
|
194
|
+
source: "manual",
|
|
195
|
+
});
|
|
196
|
+
store.updateEmbedding(m2.id, new Float32Array([0, 0.5, 0.5]), "test-model");
|
|
197
|
+
|
|
198
|
+
const query = new Float32Array([0, 0.5, 0.5]);
|
|
199
|
+
|
|
200
|
+
const agentOnly = store.search(query, agentB, { scope: "agent", limit: 50 });
|
|
201
|
+
expect(agentOnly.every((r) => r.scope === "agent")).toBe(true);
|
|
202
|
+
|
|
203
|
+
const swarmOnly = store.search(query, agentB, { scope: "swarm", limit: 50 });
|
|
204
|
+
expect(swarmOnly.every((r) => r.scope === "swarm")).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("isLead=true sees all memories", () => {
|
|
208
|
+
const query = new Float32Array([1, 0, 0]);
|
|
209
|
+
const results = store.search(query, agentA, { isLead: true, limit: 100 });
|
|
210
|
+
// Lead should see both agentA and agentB memories
|
|
211
|
+
const agents = new Set(results.map((r) => r.agentId));
|
|
212
|
+
expect(agents.size).toBeGreaterThanOrEqual(1);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("delete()", () => {
|
|
217
|
+
test("removes memory", () => {
|
|
218
|
+
const memory = store.store({
|
|
219
|
+
agentId: agentA,
|
|
220
|
+
scope: "agent",
|
|
221
|
+
name: "to delete",
|
|
222
|
+
content: "deleteme",
|
|
223
|
+
source: "manual",
|
|
224
|
+
});
|
|
225
|
+
const deleted = store.delete(memory.id);
|
|
226
|
+
expect(deleted).toBe(true);
|
|
227
|
+
expect(store.peek(memory.id)).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("returns false for non-existent", () => {
|
|
231
|
+
expect(store.delete("00000000-0000-0000-0000-000000000000")).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("deleteBySourcePath()", () => {
|
|
236
|
+
test("removes all matching memories", () => {
|
|
237
|
+
const path = "/test/delete-path.ts";
|
|
238
|
+
store.store({
|
|
239
|
+
agentId: agentA,
|
|
240
|
+
scope: "agent",
|
|
241
|
+
name: "chunk1",
|
|
242
|
+
content: "c1",
|
|
243
|
+
source: "file_index",
|
|
244
|
+
sourcePath: path,
|
|
245
|
+
});
|
|
246
|
+
store.store({
|
|
247
|
+
agentId: agentA,
|
|
248
|
+
scope: "agent",
|
|
249
|
+
name: "chunk2",
|
|
250
|
+
content: "c2",
|
|
251
|
+
source: "file_index",
|
|
252
|
+
sourcePath: path,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const deleted = store.deleteBySourcePath(path, agentA);
|
|
256
|
+
expect(deleted).toBe(2);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("updateEmbedding()", () => {
|
|
261
|
+
test("sets embedding and model", () => {
|
|
262
|
+
const memory = store.store({
|
|
263
|
+
agentId: agentA,
|
|
264
|
+
scope: "agent",
|
|
265
|
+
name: "embed test",
|
|
266
|
+
content: "embeddable",
|
|
267
|
+
source: "manual",
|
|
268
|
+
});
|
|
269
|
+
store.updateEmbedding(
|
|
270
|
+
memory.id,
|
|
271
|
+
new Float32Array([1, 2, 3]),
|
|
272
|
+
"openai/text-embedding-3-small",
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const updated = store.peek(memory.id);
|
|
276
|
+
expect(updated!.embeddingModel).toBe("openai/text-embedding-3-small");
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("getStats()", () => {
|
|
281
|
+
test("returns correct counts", () => {
|
|
282
|
+
const statsAgent = "cccc0000-0000-4000-8000-000000000003";
|
|
283
|
+
createAgent({ id: statsAgent, name: "Stats Agent", isLead: false, status: "idle" });
|
|
284
|
+
|
|
285
|
+
store.store({
|
|
286
|
+
agentId: statsAgent,
|
|
287
|
+
scope: "agent",
|
|
288
|
+
name: "s1",
|
|
289
|
+
content: "c1",
|
|
290
|
+
source: "manual",
|
|
291
|
+
});
|
|
292
|
+
store.store({
|
|
293
|
+
agentId: statsAgent,
|
|
294
|
+
scope: "swarm",
|
|
295
|
+
name: "s2",
|
|
296
|
+
content: "c2",
|
|
297
|
+
source: "task_completion",
|
|
298
|
+
});
|
|
299
|
+
store.store({
|
|
300
|
+
agentId: statsAgent,
|
|
301
|
+
scope: "agent",
|
|
302
|
+
name: "s3",
|
|
303
|
+
content: "c3",
|
|
304
|
+
source: "manual",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const stats = store.getStats(statsAgent);
|
|
308
|
+
expect(stats.total).toBe(3);
|
|
309
|
+
expect(stats.bySource.manual).toBe(2);
|
|
310
|
+
expect(stats.bySource.task_completion).toBe(1);
|
|
311
|
+
expect(stats.byScope.agent).toBe(2);
|
|
312
|
+
expect(stats.byScope.swarm).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("listForReembedding()", () => {
|
|
317
|
+
test("returns id and content", () => {
|
|
318
|
+
const all = store.listForReembedding();
|
|
319
|
+
expect(all.length).toBeGreaterThan(0);
|
|
320
|
+
expect(all[0]).toHaveProperty("id");
|
|
321
|
+
expect(all[0]).toHaveProperty("content");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("filters by agentId", () => {
|
|
325
|
+
const filtered = store.listForReembedding({ agentId: agentA });
|
|
326
|
+
expect(filtered.every((m) => true)).toBe(true); // just verifying it doesn't throw
|
|
327
|
+
expect(filtered.length).toBeGreaterThan(0);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|