@desplega.ai/agent-swarm 1.103.0 → 1.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
- package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
- package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
- package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
- package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
- package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
- package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
- package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
- package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
- package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
- package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
- package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
- package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
- package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
- package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
- package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
- package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
- package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
- package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
- package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
- package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
- package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
- package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
- package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
- package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
- package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
- package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
- package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
- package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
- package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
- package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
- package/dist/cli.js +10 -10
- package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
- package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
- package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
- package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
- package/dist/http-59ygpn2h.js +17263 -0
- package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
- package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
- package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
- package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
- package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
- package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
- package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
- package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
- package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
- package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
- package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
- package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
- package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
- package/openapi.json +90 -4
- package/package.json +3 -3
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/providers/sqlite-store.ts +468 -71
- package/src/be/memory/raters/retrieval.ts +4 -2
- package/src/be/memory/reranker.ts +5 -2
- package/src/be/memory/retrieval-store.ts +2 -0
- package/src/be/memory/types.ts +46 -0
- package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
- package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
- package/src/be/migrations/101_script_connections.sql +67 -0
- package/src/be/script-connections.ts +744 -0
- package/src/be/script-credential-broker.ts +38 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
- package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
- package/src/commands/runner.ts +8 -6
- package/src/heartbeat/heartbeat.ts +42 -1
- package/src/heartbeat/index.ts +7 -1
- package/src/http/memory.ts +127 -14
- package/src/http/scripts.ts +15 -14
- package/src/scripts-runtime/api-client.ts +100 -0
- package/src/scripts-runtime/api-types.ts +45 -0
- package/src/scripts-runtime/credential-broker/README.md +40 -0
- package/src/scripts-runtime/credential-broker/broker.ts +51 -0
- package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
- package/src/scripts-runtime/credential-broker/index.ts +17 -0
- package/src/scripts-runtime/credential-broker/store.ts +72 -0
- package/src/scripts-runtime/credential-broker/types.ts +54 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/egress-secrets.ts +14 -76
- package/src/scripts-runtime/eval-harness.ts +1 -1
- package/src/scripts-runtime/executors/types.ts +5 -5
- package/src/scripts-runtime/loader.ts +6 -1
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
- package/src/server.ts +6 -0
- package/src/tests/credential-broker.test.ts +281 -0
- package/src/tests/heartbeat.test.ts +239 -0
- package/src/tests/memory-edit.test.ts +177 -0
- package/src/tests/memory-hybrid.test.ts +212 -0
- package/src/tests/runner-repo-autostash.test.ts +39 -8
- package/src/tests/script-connections.test.ts +360 -0
- package/src/tests/scripts-http.test.ts +16 -10
- package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
- package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
- package/src/tools/credential-bindings/index.ts +1 -0
- package/src/tools/credential-bindings/tool.ts +238 -0
- package/src/tools/memory-edit.ts +148 -0
- package/src/tools/memory-search.ts +17 -10
- package/src/tools/schedules/delete-schedule.ts +18 -8
- package/src/tools/schedules/update-schedule.ts +4 -9
- package/src/tools/script-connections/index.ts +1 -0
- package/src/tools/script-connections/tool.ts +167 -0
- package/src/tools/swarm-config/delete-config.ts +5 -0
- package/src/tools/swarm-config/set-config.ts +23 -1
- package/src/tools/tool-config.ts +6 -1
- package/src/types.ts +6 -0
- package/src/workflows/executors/swarm-script.ts +2 -0
- package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
- package/templates/community/dora-metrics/README.md +39 -0
- package/templates/community/dora-metrics/lead-prompt.md +56 -0
- package/templates/community/dora-metrics/report.mjs +393 -0
- package/templates/community/dora-metrics/run.sh +113 -0
- package/templates/schedules/weekly-dora-metrics/config.json +13 -0
- package/templates/schedules/weekly-dora-metrics/content.md +61 -0
- package/dist/cli-2307phk1.js +0 -24223
- package/dist/cli-2deg0cen.js +0 -120
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, createAgent, getDb, initDb } from "../be/db";
|
|
4
|
+
import { applyEditMode, SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
5
|
+
|
|
6
|
+
const TEST_DB_PATH = "./test-memory-edit.sqlite";
|
|
7
|
+
const agentId = "aaaa0000-0000-4000-8000-000000000201";
|
|
8
|
+
|
|
9
|
+
describe("applyEditMode", () => {
|
|
10
|
+
test("replace mode returns new content", () => {
|
|
11
|
+
const result = applyEditMode("replace", "old body", { content: "new body" });
|
|
12
|
+
expect(result).toBe("new body");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("replace mode throws without content", () => {
|
|
16
|
+
expect(() => applyEditMode("replace", "old body", {})).toThrow("replace mode requires content");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("exact mode performs surgical replacement", () => {
|
|
20
|
+
const result = applyEditMode("exact", "hello world foo", {
|
|
21
|
+
oldString: "world",
|
|
22
|
+
newString: "earth",
|
|
23
|
+
});
|
|
24
|
+
expect(result).toBe("hello earth foo");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("exact mode with empty newString deletes the substring", () => {
|
|
28
|
+
const result = applyEditMode("exact", "hello world", {
|
|
29
|
+
oldString: " world",
|
|
30
|
+
newString: "",
|
|
31
|
+
});
|
|
32
|
+
expect(result).toBe("hello");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("exact mode throws when oldString is not found", () => {
|
|
36
|
+
expect(() =>
|
|
37
|
+
applyEditMode("exact", "hello world", { oldString: "missing", newString: "x" }),
|
|
38
|
+
).toThrow("oldString not found");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("exact mode throws when oldString is ambiguous", () => {
|
|
42
|
+
expect(() =>
|
|
43
|
+
applyEditMode("exact", "alpha beta alpha", { oldString: "alpha", newString: "x" }),
|
|
44
|
+
).toThrow("oldString is ambiguous");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("exact mode throws without required fields", () => {
|
|
48
|
+
expect(() => applyEditMode("exact", "content", { newString: "x" })).toThrow(
|
|
49
|
+
"exact mode requires oldString and newString",
|
|
50
|
+
);
|
|
51
|
+
expect(() => applyEditMode("exact", "content", { oldString: "c" })).toThrow(
|
|
52
|
+
"exact mode requires oldString and newString",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("memory editing", () => {
|
|
58
|
+
let store: SqliteMemoryStore;
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
62
|
+
try {
|
|
63
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
initDb(TEST_DB_PATH);
|
|
67
|
+
createAgent({ id: agentId, name: "Edit Test Agent", isLead: false, status: "idle" });
|
|
68
|
+
store = new SqliteMemoryStore();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterAll(async () => {
|
|
72
|
+
closeDb();
|
|
73
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
74
|
+
try {
|
|
75
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("preserves id and posterior while writing a version row", () => {
|
|
81
|
+
const memory = store.store({
|
|
82
|
+
agentId,
|
|
83
|
+
scope: "agent",
|
|
84
|
+
name: "editable",
|
|
85
|
+
content: "old body",
|
|
86
|
+
source: "manual",
|
|
87
|
+
key: "notes/editable.md",
|
|
88
|
+
intent: "seed test memory",
|
|
89
|
+
});
|
|
90
|
+
getDb().prepare("UPDATE agent_memory SET alpha = 3, beta = 2 WHERE id = ?").run(memory.id);
|
|
91
|
+
|
|
92
|
+
const result = store.edit({
|
|
93
|
+
id: memory.id,
|
|
94
|
+
mode: "replace",
|
|
95
|
+
content: "new body",
|
|
96
|
+
intent: "correct stale body",
|
|
97
|
+
changedByAgentId: agentId,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.changed).toBe(true);
|
|
101
|
+
expect(result.memory.id).toBe(memory.id);
|
|
102
|
+
expect(result.version).toBe(2);
|
|
103
|
+
const row = getDb()
|
|
104
|
+
.prepare<{ alpha: number; beta: number; content: string; version: number }, [string]>(
|
|
105
|
+
"SELECT alpha, beta, content, version FROM agent_memory WHERE id = ?",
|
|
106
|
+
)
|
|
107
|
+
.get(memory.id);
|
|
108
|
+
expect(row).toEqual({ alpha: 3, beta: 2, content: "new body", version: 2 });
|
|
109
|
+
|
|
110
|
+
const versions = getDb()
|
|
111
|
+
.prepare<{ version: number; operation: string; intent: string }, [string]>(
|
|
112
|
+
"SELECT version, operation, intent FROM agent_memory_version WHERE memory_id = ? ORDER BY version",
|
|
113
|
+
)
|
|
114
|
+
.all(memory.id);
|
|
115
|
+
expect(versions.map((version) => [version.version, version.operation])).toEqual([
|
|
116
|
+
[1, "create"],
|
|
117
|
+
[2, "edit"],
|
|
118
|
+
]);
|
|
119
|
+
expect(versions[1]?.intent).toBe("correct stale body");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("short-circuits unchanged content", () => {
|
|
123
|
+
const memory = store.store({
|
|
124
|
+
agentId,
|
|
125
|
+
scope: "agent",
|
|
126
|
+
name: "same",
|
|
127
|
+
content: "same body",
|
|
128
|
+
source: "manual",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = store.edit({
|
|
132
|
+
id: memory.id,
|
|
133
|
+
mode: "replace",
|
|
134
|
+
content: "same body",
|
|
135
|
+
intent: "noop",
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(result.changed).toBe(false);
|
|
139
|
+
expect(result.version).toBe(1);
|
|
140
|
+
const versionCount = getDb()
|
|
141
|
+
.prepare<{ count: number }, [string]>(
|
|
142
|
+
"SELECT COUNT(*) AS count FROM agent_memory_version WHERE memory_id = ?",
|
|
143
|
+
)
|
|
144
|
+
.get(memory.id)?.count;
|
|
145
|
+
expect(versionCount).toBe(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("exact mode rejects missing and ambiguous old strings", () => {
|
|
149
|
+
const memory = store.store({
|
|
150
|
+
agentId,
|
|
151
|
+
scope: "agent",
|
|
152
|
+
name: "exact",
|
|
153
|
+
content: "alpha beta alpha",
|
|
154
|
+
source: "manual",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(() =>
|
|
158
|
+
store.edit({
|
|
159
|
+
id: memory.id,
|
|
160
|
+
mode: "exact",
|
|
161
|
+
oldString: "missing",
|
|
162
|
+
newString: "x",
|
|
163
|
+
intent: "test missing",
|
|
164
|
+
}),
|
|
165
|
+
).toThrow("oldString not found");
|
|
166
|
+
|
|
167
|
+
expect(() =>
|
|
168
|
+
store.edit({
|
|
169
|
+
id: memory.id,
|
|
170
|
+
mode: "exact",
|
|
171
|
+
oldString: "alpha",
|
|
172
|
+
newString: "x",
|
|
173
|
+
intent: "test ambiguous",
|
|
174
|
+
}),
|
|
175
|
+
).toThrow("oldString is ambiguous");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, createAgent, getDb, initDb } from "../be/db";
|
|
4
|
+
import { computeRrfScore, SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
5
|
+
|
|
6
|
+
const TEST_DB_PATH = "./test-memory-hybrid.sqlite";
|
|
7
|
+
const agentId = "aaaa0000-0000-4000-8000-000000000101";
|
|
8
|
+
|
|
9
|
+
function vector(value: number): Float32Array {
|
|
10
|
+
const embedding = new Float32Array(512);
|
|
11
|
+
embedding[0] = value;
|
|
12
|
+
return embedding;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("computeRrfScore", () => {
|
|
16
|
+
test("rank 0 with no decay produces max reciprocal score", () => {
|
|
17
|
+
const score = computeRrfScore(0, 1.0);
|
|
18
|
+
expect(score).toBeCloseTo(1 / 61, 6);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("higher rank produces lower score", () => {
|
|
22
|
+
expect(computeRrfScore(0, 1.0)).toBeGreaterThan(computeRrfScore(5, 1.0));
|
|
23
|
+
expect(computeRrfScore(5, 1.0)).toBeGreaterThan(computeRrfScore(50, 1.0));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("decay factor scales the score linearly", () => {
|
|
27
|
+
const full = computeRrfScore(3, 1.0);
|
|
28
|
+
const half = computeRrfScore(3, 0.5);
|
|
29
|
+
expect(half).toBeCloseTo(full * 0.5, 6);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("custom k changes the smoothing constant", () => {
|
|
33
|
+
const defaultK = computeRrfScore(0, 1.0, 60);
|
|
34
|
+
const smallK = computeRrfScore(0, 1.0, 10);
|
|
35
|
+
expect(smallK).toBeGreaterThan(defaultK);
|
|
36
|
+
expect(smallK).toBeCloseTo(1 / 11, 6);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("two arms sum to compound a memory that appears in both", () => {
|
|
40
|
+
const vecScore = computeRrfScore(0, 1.0);
|
|
41
|
+
const ftsScore = computeRrfScore(2, 1.0);
|
|
42
|
+
const compound = vecScore + ftsScore;
|
|
43
|
+
expect(compound).toBeGreaterThan(vecScore);
|
|
44
|
+
expect(compound).toBeGreaterThan(ftsScore);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("memory hybrid search", () => {
|
|
49
|
+
let store: SqliteMemoryStore;
|
|
50
|
+
let prevHybridFlag: string | undefined;
|
|
51
|
+
|
|
52
|
+
beforeAll(async () => {
|
|
53
|
+
prevHybridFlag = process.env.MEMORY_HYBRID_SEARCH;
|
|
54
|
+
process.env.MEMORY_HYBRID_SEARCH = "1";
|
|
55
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
56
|
+
try {
|
|
57
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
initDb(TEST_DB_PATH);
|
|
61
|
+
createAgent({ id: agentId, name: "Hybrid Test Agent", isLead: false, status: "idle" });
|
|
62
|
+
store = new SqliteMemoryStore();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
if (prevHybridFlag === undefined) delete process.env.MEMORY_HYBRID_SEARCH;
|
|
67
|
+
else process.env.MEMORY_HYBRID_SEARCH = prevHybridFlag;
|
|
68
|
+
closeDb();
|
|
69
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
70
|
+
try {
|
|
71
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("syncs FTS rows on store and delete", () => {
|
|
77
|
+
const memory = store.store({
|
|
78
|
+
agentId,
|
|
79
|
+
scope: "agent",
|
|
80
|
+
name: "lexical row",
|
|
81
|
+
content: "contains frobnicate-token",
|
|
82
|
+
source: "manual",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const inserted = getDb()
|
|
86
|
+
.prepare<{ count: number }, [string]>(
|
|
87
|
+
"SELECT COUNT(*) AS count FROM memory_fts WHERE memory_id = ?",
|
|
88
|
+
)
|
|
89
|
+
.get(memory.id)?.count;
|
|
90
|
+
expect(inserted).toBe(1);
|
|
91
|
+
|
|
92
|
+
store.delete(memory.id);
|
|
93
|
+
const deleted = getDb()
|
|
94
|
+
.prepare<{ count: number }, [string]>(
|
|
95
|
+
"SELECT COUNT(*) AS count FROM memory_fts WHERE memory_id = ?",
|
|
96
|
+
)
|
|
97
|
+
.get(memory.id)?.count;
|
|
98
|
+
expect(deleted).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("uses keyword arm for exact terms and dedupes fused results", () => {
|
|
102
|
+
const exact = store.store({
|
|
103
|
+
agentId,
|
|
104
|
+
scope: "agent",
|
|
105
|
+
name: "runbook",
|
|
106
|
+
content: "The incident codeword is quasarneedle.",
|
|
107
|
+
source: "manual",
|
|
108
|
+
});
|
|
109
|
+
const semantic = store.store({
|
|
110
|
+
agentId,
|
|
111
|
+
scope: "agent",
|
|
112
|
+
name: "general note",
|
|
113
|
+
content: "A generic operational note.",
|
|
114
|
+
source: "manual",
|
|
115
|
+
});
|
|
116
|
+
store.updateEmbedding(exact.id, vector(1), "test");
|
|
117
|
+
store.updateEmbedding(semantic.id, vector(1), "test");
|
|
118
|
+
|
|
119
|
+
const results = store.search(vector(1), agentId, {
|
|
120
|
+
scope: "agent",
|
|
121
|
+
limit: 10,
|
|
122
|
+
queryText: "quasarneedle",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(results.some((result) => result.id === exact.id)).toBe(true);
|
|
126
|
+
expect(new Set(results.map((result) => result.id)).size).toBe(results.length);
|
|
127
|
+
expect(results.find((result) => result.id === exact.id)?.retrievalSource).toBe("hybrid");
|
|
128
|
+
expect(results.find((result) => result.id === semantic.id)?.retrievalSource).toBe("vec");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("hybrid RRF compounds memories present in both vector and FTS arms", () => {
|
|
132
|
+
const both = store.store({
|
|
133
|
+
agentId,
|
|
134
|
+
scope: "agent",
|
|
135
|
+
name: "compound exact",
|
|
136
|
+
content: "The exact compound marker is rrfneedle.",
|
|
137
|
+
source: "manual",
|
|
138
|
+
});
|
|
139
|
+
const vectorOnly = store.store({
|
|
140
|
+
agentId,
|
|
141
|
+
scope: "agent",
|
|
142
|
+
name: "compound semantic",
|
|
143
|
+
content: "A semantic-only note.",
|
|
144
|
+
source: "manual",
|
|
145
|
+
});
|
|
146
|
+
store.updateEmbedding(both.id, vector(1), "test");
|
|
147
|
+
store.updateEmbedding(vectorOnly.id, vector(1), "test");
|
|
148
|
+
|
|
149
|
+
const results = store.search(vector(1), agentId, {
|
|
150
|
+
scope: "agent",
|
|
151
|
+
limit: 10,
|
|
152
|
+
queryText: "rrfneedle",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const bothResult = results.find((result) => result.id === both.id);
|
|
156
|
+
const vectorOnlyResult = results.find((result) => result.id === vectorOnly.id);
|
|
157
|
+
expect(bothResult?.retrievalSource).toBe("hybrid");
|
|
158
|
+
expect(vectorOnlyResult?.retrievalSource).toBe("vec");
|
|
159
|
+
expect(bothResult!.similarity).toBeGreaterThan(vectorOnlyResult!.similarity);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("falls back to keyword-only search when vector query is unavailable", () => {
|
|
163
|
+
const exact = store.store({
|
|
164
|
+
agentId,
|
|
165
|
+
scope: "agent",
|
|
166
|
+
name: "keyword fallback",
|
|
167
|
+
content: "The fallback marker is lexiconneedle.",
|
|
168
|
+
source: "manual",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const results = store.search(new Float32Array(0), agentId, {
|
|
172
|
+
scope: "agent",
|
|
173
|
+
limit: 5,
|
|
174
|
+
queryText: "lexiconneedle",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(results.map((result) => result.id)).toContain(exact.id);
|
|
178
|
+
expect(results.find((result) => result.id === exact.id)?.retrievalSource).toBe("fts");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("applies source-aware recency decay to FTS-only ranking", () => {
|
|
182
|
+
const stale = store.store({
|
|
183
|
+
agentId,
|
|
184
|
+
scope: "agent",
|
|
185
|
+
name: "stale keyword",
|
|
186
|
+
content: "The decay marker is decayneedle.",
|
|
187
|
+
source: "task_completion",
|
|
188
|
+
});
|
|
189
|
+
const fresh = store.store({
|
|
190
|
+
agentId,
|
|
191
|
+
scope: "agent",
|
|
192
|
+
name: "fresh keyword",
|
|
193
|
+
content: "The decay marker is decayneedle.",
|
|
194
|
+
source: "task_completion",
|
|
195
|
+
});
|
|
196
|
+
getDb()
|
|
197
|
+
.prepare("UPDATE agent_memory SET createdAt = ? WHERE id = ?")
|
|
198
|
+
.run(new Date(Date.now() - 60 * 86400000).toISOString(), stale.id);
|
|
199
|
+
|
|
200
|
+
const results = store.search(new Float32Array(0), agentId, {
|
|
201
|
+
scope: "agent",
|
|
202
|
+
limit: 10,
|
|
203
|
+
queryText: "decayneedle",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const staleIndex = results.findIndex((result) => result.id === stale.id);
|
|
207
|
+
const freshIndex = results.findIndex((result) => result.id === fresh.id);
|
|
208
|
+
expect(freshIndex).toBeGreaterThanOrEqual(0);
|
|
209
|
+
expect(staleIndex).toBeGreaterThanOrEqual(0);
|
|
210
|
+
expect(freshIndex).toBeLessThan(staleIndex);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -10,13 +10,40 @@ const execFileAsync = promisify(execFile);
|
|
|
10
10
|
|
|
11
11
|
let tempRoot = "";
|
|
12
12
|
|
|
13
|
+
function gitEnv(): NodeJS.ProcessEnv {
|
|
14
|
+
const env = { ...process.env };
|
|
15
|
+
for (const key of Object.keys(env)) {
|
|
16
|
+
if (key === "GIT_CONFIG_NOSYSTEM" || key.startsWith("GIT_TRACE")) continue;
|
|
17
|
+
if (key.startsWith("GIT_")) delete env[key];
|
|
18
|
+
}
|
|
19
|
+
return env;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
async function git(cwd: string, args: string[]): Promise<string> {
|
|
14
|
-
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args]);
|
|
23
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], { env: gitEnv() });
|
|
15
24
|
return stdout;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
async function gitRaw(args: string[]): Promise<void> {
|
|
19
|
-
await execFileAsync("git", args);
|
|
28
|
+
await execFileAsync("git", args, { env: gitEnv() });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function withCleanGitEnv<T>(fn: () => Promise<T>): Promise<T> {
|
|
32
|
+
const removed = new Map<string, string>();
|
|
33
|
+
for (const key of Object.keys(process.env)) {
|
|
34
|
+
if (key === "GIT_CONFIG_NOSYSTEM" || key.startsWith("GIT_TRACE")) continue;
|
|
35
|
+
if (!key.startsWith("GIT_")) continue;
|
|
36
|
+
const value = process.env[key];
|
|
37
|
+
if (value !== undefined) removed.set(key, value);
|
|
38
|
+
delete process.env[key];
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return await fn();
|
|
42
|
+
} finally {
|
|
43
|
+
for (const [key, value] of removed) {
|
|
44
|
+
process.env[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
20
47
|
}
|
|
21
48
|
|
|
22
49
|
async function commitAll(cwd: string, message: string): Promise<void> {
|
|
@@ -63,9 +90,11 @@ describe("ensureRepoForTask auto-stash refresh", () => {
|
|
|
63
90
|
await commitAll(upstreamPath, "remote update");
|
|
64
91
|
await git(upstreamPath, ["push", "origin", "main"]);
|
|
65
92
|
|
|
66
|
-
const result = await
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
const result = await withCleanGitEnv(() =>
|
|
94
|
+
ensureRepoForTask(
|
|
95
|
+
{ url: remotePath, name: "repo", clonePath, defaultBranch: "main" },
|
|
96
|
+
"test",
|
|
97
|
+
),
|
|
69
98
|
);
|
|
70
99
|
|
|
71
100
|
expect(result.warning).toBeNull();
|
|
@@ -103,9 +132,11 @@ describe("ensureRepoForTask auto-stash refresh", () => {
|
|
|
103
132
|
await commitAll(upstreamPath, "remote commit");
|
|
104
133
|
await git(upstreamPath, ["push", "origin", "main"]);
|
|
105
134
|
|
|
106
|
-
const result = await
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
const result = await withCleanGitEnv(() =>
|
|
136
|
+
ensureRepoForTask(
|
|
137
|
+
{ url: remotePath, name: "repo", clonePath, defaultBranch: "main" },
|
|
138
|
+
"test",
|
|
139
|
+
),
|
|
109
140
|
);
|
|
110
141
|
|
|
111
142
|
expect(result.warning).toBeNull();
|