@desplega.ai/agent-swarm 1.80.0 → 1.80.1
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 +399 -14
- package/package.json +3 -1
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +1 -1
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +2 -1
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +72 -10
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/index.ts +40 -29
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/linear/outbound.ts +9 -2
- package/src/otel.ts +5 -0
- package/src/providers/claude-adapter.ts +22 -1
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +12 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +35 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/tool-config.ts +7 -0
- package/src/types.ts +60 -1
- package/src/utils/api-key.ts +28 -0
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +22 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- package/src/workflows/resume.ts +43 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { NativeScriptExecutor } from "../scripts-runtime/executors/native";
|
|
3
|
+
import { getScriptExecutor } from "../scripts-runtime/executors/registry";
|
|
4
|
+
|
|
5
|
+
describe("getScriptExecutor", () => {
|
|
6
|
+
test("defaults to native", () => {
|
|
7
|
+
expect(getScriptExecutor()).toBeInstanceOf(NativeScriptExecutor);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("returns native when requested", () => {
|
|
11
|
+
expect(getScriptExecutor("native")).toBeInstanceOf(NativeScriptExecutor);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("throws for unknown executors", () => {
|
|
15
|
+
expect(() => getScriptExecutor("e2b")).toThrow("Available: native");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getDb, initDb } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
deleteScript,
|
|
6
|
+
getScript,
|
|
7
|
+
getScriptVersion,
|
|
8
|
+
insertScript,
|
|
9
|
+
listScripts,
|
|
10
|
+
upsertScriptByName,
|
|
11
|
+
} from "../be/scripts/db";
|
|
12
|
+
import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-scripts-db.sqlite";
|
|
15
|
+
|
|
16
|
+
const noOpEmbeddingProvider = {
|
|
17
|
+
name: "test/noop-script-embedding",
|
|
18
|
+
dimensions: 1,
|
|
19
|
+
async embed() {
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
async embedBatch(texts: string[]) {
|
|
23
|
+
return texts.map(() => null);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const signatureJson = JSON.stringify({
|
|
28
|
+
args: { type: "object", properties: { value: { type: "number" } } },
|
|
29
|
+
result: { type: "object", properties: { doubled: { type: "number" } } },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
async function clearDb() {
|
|
33
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
34
|
+
try {
|
|
35
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function source(multiplier: number) {
|
|
41
|
+
return `export default function run(args: { value: number }) { return { doubled: args.value * ${multiplier} }; }`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("scripts DB helpers", () => {
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
await clearDb();
|
|
47
|
+
initDb(TEST_DB_PATH);
|
|
48
|
+
setScriptEmbeddingProviderForTests(noOpEmbeddingProvider);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
setScriptEmbeddingProviderForTests(null);
|
|
53
|
+
closeDb();
|
|
54
|
+
await clearDb();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
getDb().run("DELETE FROM scripts");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("insertScript stores a live row and initial version", () => {
|
|
62
|
+
const script = insertScript({
|
|
63
|
+
name: "double",
|
|
64
|
+
scope: "agent",
|
|
65
|
+
scopeId: "agent-1",
|
|
66
|
+
source: source(2),
|
|
67
|
+
description: "Double a value",
|
|
68
|
+
intent: "Reusable arithmetic transform",
|
|
69
|
+
signatureJson,
|
|
70
|
+
agentId: "agent-1",
|
|
71
|
+
typeChecked: true,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(script.name).toBe("double");
|
|
75
|
+
expect(script.scope).toBe("agent");
|
|
76
|
+
expect(script.scopeId).toBe("agent-1");
|
|
77
|
+
expect(script.version).toBe(1);
|
|
78
|
+
expect(script.isScratch).toBe(false);
|
|
79
|
+
expect(script.typeChecked).toBe(true);
|
|
80
|
+
expect(script.fsMode).toBe("none");
|
|
81
|
+
|
|
82
|
+
const version = getScriptVersion({ scriptId: script.id, version: 1 });
|
|
83
|
+
expect(version?.source).toBe(source(2));
|
|
84
|
+
expect(version?.changedByAgentId).toBe("agent-1");
|
|
85
|
+
expect(version?.changeReason).toBe("Initial creation");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("upsertScriptByName deduplicates matching content without bumping version", async () => {
|
|
89
|
+
const first = await upsertScriptByName({
|
|
90
|
+
name: "same",
|
|
91
|
+
scope: "global",
|
|
92
|
+
source: source(2),
|
|
93
|
+
description: "First description",
|
|
94
|
+
intent: "First intent",
|
|
95
|
+
signatureJson,
|
|
96
|
+
agentId: "lead-1",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const second = await upsertScriptByName({
|
|
100
|
+
name: "same",
|
|
101
|
+
scope: "global",
|
|
102
|
+
source: source(2),
|
|
103
|
+
description: "Changed metadata should update without version bump",
|
|
104
|
+
intent: "Changed intent",
|
|
105
|
+
signatureJson,
|
|
106
|
+
agentId: "lead-1",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(first.isNew).toBe(true);
|
|
110
|
+
expect(second.isNew).toBe(false);
|
|
111
|
+
expect(second.contentDeduped).toBe(true);
|
|
112
|
+
expect(second.script.id).toBe(first.script.id);
|
|
113
|
+
expect(second.script.version).toBe(1);
|
|
114
|
+
expect(second.script.description).toBe("Changed metadata should update without version bump");
|
|
115
|
+
expect(
|
|
116
|
+
getDb()
|
|
117
|
+
.prepare<{ count: number }, [string]>(
|
|
118
|
+
"SELECT COUNT(*) as count FROM script_versions WHERE scriptId = ?",
|
|
119
|
+
)
|
|
120
|
+
.get(first.script.id)?.count,
|
|
121
|
+
).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("upsertScriptByName bumps version and writes history on source change", async () => {
|
|
125
|
+
const first = await upsertScriptByName({
|
|
126
|
+
name: "mutating",
|
|
127
|
+
scope: "agent",
|
|
128
|
+
scopeId: "agent-1",
|
|
129
|
+
source: source(2),
|
|
130
|
+
description: "v1",
|
|
131
|
+
intent: "Initial version",
|
|
132
|
+
signatureJson,
|
|
133
|
+
agentId: "agent-1",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const second = await upsertScriptByName({
|
|
137
|
+
name: "mutating",
|
|
138
|
+
scope: "agent",
|
|
139
|
+
scopeId: "agent-1",
|
|
140
|
+
source: source(3),
|
|
141
|
+
description: "v2",
|
|
142
|
+
intent: "Updated multiplier",
|
|
143
|
+
signatureJson,
|
|
144
|
+
agentId: "agent-2",
|
|
145
|
+
changeReason: "Use triple",
|
|
146
|
+
typeChecked: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(second.isNew).toBe(false);
|
|
150
|
+
expect(second.contentDeduped).toBe(false);
|
|
151
|
+
expect(second.script.id).toBe(first.script.id);
|
|
152
|
+
expect(second.script.version).toBe(2);
|
|
153
|
+
expect(second.script.typeChecked).toBe(true);
|
|
154
|
+
|
|
155
|
+
const v1 = getScriptVersion({ scriptId: first.script.id, version: 1 });
|
|
156
|
+
const v2 = getScriptVersion({ scriptId: first.script.id, version: 2 });
|
|
157
|
+
expect(v1?.source).toBe(source(2));
|
|
158
|
+
expect(v2?.source).toBe(source(3));
|
|
159
|
+
expect(v2?.changeReason).toBe("Use triple");
|
|
160
|
+
expect(
|
|
161
|
+
getScriptVersion({ scriptId: first.script.id, contentHash: second.script.contentHash })
|
|
162
|
+
?.version,
|
|
163
|
+
).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("scope uniqueness treats global null scopeId as one scope and isolates agent scopes", () => {
|
|
167
|
+
insertScript({
|
|
168
|
+
name: "shared-name",
|
|
169
|
+
scope: "global",
|
|
170
|
+
source: source(2),
|
|
171
|
+
description: "Global",
|
|
172
|
+
intent: "Global script",
|
|
173
|
+
signatureJson,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(() =>
|
|
177
|
+
insertScript({
|
|
178
|
+
name: "shared-name",
|
|
179
|
+
scope: "global",
|
|
180
|
+
source: source(3),
|
|
181
|
+
description: "Duplicate global",
|
|
182
|
+
intent: "Should fail",
|
|
183
|
+
signatureJson,
|
|
184
|
+
}),
|
|
185
|
+
).toThrow();
|
|
186
|
+
|
|
187
|
+
const agentOne = insertScript({
|
|
188
|
+
name: "shared-name",
|
|
189
|
+
scope: "agent",
|
|
190
|
+
scopeId: "agent-1",
|
|
191
|
+
source: source(2),
|
|
192
|
+
description: "Agent one",
|
|
193
|
+
intent: "Agent script",
|
|
194
|
+
signatureJson,
|
|
195
|
+
});
|
|
196
|
+
const agentTwo = insertScript({
|
|
197
|
+
name: "shared-name",
|
|
198
|
+
scope: "agent",
|
|
199
|
+
scopeId: "agent-2",
|
|
200
|
+
source: source(2),
|
|
201
|
+
description: "Agent two",
|
|
202
|
+
intent: "Agent script",
|
|
203
|
+
signatureJson,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(agentOne.id).not.toBe(agentTwo.id);
|
|
207
|
+
expect(() =>
|
|
208
|
+
insertScript({
|
|
209
|
+
name: "missing-scope",
|
|
210
|
+
scope: "agent",
|
|
211
|
+
source: source(2),
|
|
212
|
+
description: "No scopeId",
|
|
213
|
+
intent: "Should fail",
|
|
214
|
+
signatureJson,
|
|
215
|
+
}),
|
|
216
|
+
).toThrow("scopeId is required");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("listScripts filters scratch scripts by default", () => {
|
|
220
|
+
insertScript({
|
|
221
|
+
name: "explicit",
|
|
222
|
+
scope: "agent",
|
|
223
|
+
scopeId: "agent-1",
|
|
224
|
+
source: source(2),
|
|
225
|
+
description: "Explicit",
|
|
226
|
+
intent: "Explicit script",
|
|
227
|
+
signatureJson,
|
|
228
|
+
});
|
|
229
|
+
insertScript({
|
|
230
|
+
name: "scratch",
|
|
231
|
+
scope: "agent",
|
|
232
|
+
scopeId: "agent-1",
|
|
233
|
+
source: source(3),
|
|
234
|
+
description: "Scratch",
|
|
235
|
+
intent: "Scratch script",
|
|
236
|
+
signatureJson,
|
|
237
|
+
isScratch: true,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(
|
|
241
|
+
listScripts({ scope: "agent", scopeId: "agent-1" }).map((script) => script.name),
|
|
242
|
+
).toEqual(["explicit"]);
|
|
243
|
+
expect(
|
|
244
|
+
listScripts({ scope: "agent", scopeId: "agent-1", includeScratch: true }).map(
|
|
245
|
+
(script) => script.name,
|
|
246
|
+
),
|
|
247
|
+
).toEqual(["explicit", "scratch"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("deleteScript cascades script_versions", async () => {
|
|
251
|
+
const result = await upsertScriptByName({
|
|
252
|
+
name: "delete-me",
|
|
253
|
+
scope: "global",
|
|
254
|
+
source: source(2),
|
|
255
|
+
description: "Delete me",
|
|
256
|
+
intent: "Cascade check",
|
|
257
|
+
signatureJson,
|
|
258
|
+
});
|
|
259
|
+
await upsertScriptByName({
|
|
260
|
+
name: "delete-me",
|
|
261
|
+
scope: "global",
|
|
262
|
+
source: source(4),
|
|
263
|
+
description: "Delete me v2",
|
|
264
|
+
intent: "Cascade check",
|
|
265
|
+
signatureJson,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(deleteScript({ name: "delete-me", scope: "global" })).toBe(true);
|
|
269
|
+
expect(deleteScript({ name: "delete-me", scope: "global" })).toBe(false);
|
|
270
|
+
expect(getScript({ name: "delete-me", scope: "global" })).toBeNull();
|
|
271
|
+
expect(
|
|
272
|
+
getDb()
|
|
273
|
+
.prepare<{ count: number }, [string]>(
|
|
274
|
+
"SELECT COUNT(*) as count FROM script_versions WHERE scriptId = ?",
|
|
275
|
+
)
|
|
276
|
+
.get(result.script.id)?.count,
|
|
277
|
+
).toBe(0);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("full lifecycle: upsert, dedup, version bump, history, delete", async () => {
|
|
281
|
+
const created = await upsertScriptByName({
|
|
282
|
+
name: "lifecycle",
|
|
283
|
+
scope: "agent",
|
|
284
|
+
scopeId: "agent-1",
|
|
285
|
+
source: source(2),
|
|
286
|
+
description: "Lifecycle",
|
|
287
|
+
intent: "Exercise full lifecycle",
|
|
288
|
+
signatureJson,
|
|
289
|
+
agentId: "agent-1",
|
|
290
|
+
});
|
|
291
|
+
const deduped = await upsertScriptByName({
|
|
292
|
+
name: "lifecycle",
|
|
293
|
+
scope: "agent",
|
|
294
|
+
scopeId: "agent-1",
|
|
295
|
+
source: source(2),
|
|
296
|
+
description: "Lifecycle changed",
|
|
297
|
+
intent: "No version bump",
|
|
298
|
+
signatureJson,
|
|
299
|
+
agentId: "agent-1",
|
|
300
|
+
});
|
|
301
|
+
const updated = await upsertScriptByName({
|
|
302
|
+
name: "lifecycle",
|
|
303
|
+
scope: "agent",
|
|
304
|
+
scopeId: "agent-1",
|
|
305
|
+
source: source(5),
|
|
306
|
+
description: "Lifecycle updated",
|
|
307
|
+
intent: "Version bump",
|
|
308
|
+
signatureJson,
|
|
309
|
+
agentId: "agent-1",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(created.isNew).toBe(true);
|
|
313
|
+
expect(deduped.contentDeduped).toBe(true);
|
|
314
|
+
expect(deduped.script.version).toBe(1);
|
|
315
|
+
expect(updated.script.version).toBe(2);
|
|
316
|
+
expect(getScriptVersion({ scriptId: created.script.id, version: 1 })?.source).toBe(source(2));
|
|
317
|
+
expect(getScriptVersion({ scriptId: created.script.id, version: 2 })?.source).toBe(source(5));
|
|
318
|
+
|
|
319
|
+
expect(deleteScript({ name: "lifecycle", scope: "agent", scopeId: "agent-1" })).toBe(true);
|
|
320
|
+
expect(getScript({ name: "lifecycle", scope: "agent", scopeId: "agent-1" })).toBeNull();
|
|
321
|
+
expect(
|
|
322
|
+
getDb()
|
|
323
|
+
.prepare<{ count: number }, [string]>(
|
|
324
|
+
"SELECT COUNT(*) as count FROM script_versions WHERE scriptId = ?",
|
|
325
|
+
)
|
|
326
|
+
.get(created.script.id)?.count,
|
|
327
|
+
).toBe(0);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getDb, initDb } from "../be/db";
|
|
4
|
+
import type { EmbeddingProvider } from "../be/memory/types";
|
|
5
|
+
import { getScript, upsertScriptByName } from "../be/scripts/db";
|
|
6
|
+
import {
|
|
7
|
+
reembedAllScripts,
|
|
8
|
+
searchScripts,
|
|
9
|
+
setScriptEmbeddingProviderForTests,
|
|
10
|
+
} from "../be/scripts/embeddings";
|
|
11
|
+
import { runScriptsMaintenanceCommand } from "../be/scripts/maintenance";
|
|
12
|
+
|
|
13
|
+
const TEST_DB_PATH = "./test-scripts-embeddings.sqlite";
|
|
14
|
+
|
|
15
|
+
const signatureJson = JSON.stringify({
|
|
16
|
+
argsType: "{ value: string }",
|
|
17
|
+
resultType: "Promise<{ ok: boolean }>",
|
|
18
|
+
description: "",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
async function clearDb() {
|
|
22
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
23
|
+
try {
|
|
24
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function source(label: string) {
|
|
30
|
+
return `export default async () => ({ label: ${JSON.stringify(label)} });`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function embeddingFor(text: string): Float32Array {
|
|
34
|
+
const lower = text.toLowerCase();
|
|
35
|
+
if (lower.includes("exact-name")) return new Float32Array([0.1, 0.995, 0, 0, 0]);
|
|
36
|
+
|
|
37
|
+
const vector = [0, 0, 0, 0, 0];
|
|
38
|
+
if (/(linear|issue|ticket|triage)/.test(lower)) vector[0] += 1;
|
|
39
|
+
if (/(github|pull request|\bpr\b|review|comments?)/.test(lower)) vector[1] += 1;
|
|
40
|
+
if (/(memory|recall|remember|search)/.test(lower)) vector[2] += 1;
|
|
41
|
+
if (/(slack|message|channel)/.test(lower)) vector[3] += 1;
|
|
42
|
+
if (/(csv|spreadsheet|table|rows?)/.test(lower)) vector[4] += 1;
|
|
43
|
+
return new Float32Array(vector);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class FakeEmbeddingProvider implements EmbeddingProvider {
|
|
47
|
+
readonly name = "test/fake-script-embedding";
|
|
48
|
+
readonly dimensions = 5;
|
|
49
|
+
readonly calls: string[] = [];
|
|
50
|
+
|
|
51
|
+
async embed(text: string): Promise<Float32Array | null> {
|
|
52
|
+
this.calls.push(text);
|
|
53
|
+
return embeddingFor(text);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {
|
|
57
|
+
return Promise.all(texts.map((text) => this.embed(text)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reset(): void {
|
|
61
|
+
this.calls.length = 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let provider: FakeEmbeddingProvider;
|
|
66
|
+
|
|
67
|
+
function embeddingCount(scriptId: string): number {
|
|
68
|
+
return (
|
|
69
|
+
getDb()
|
|
70
|
+
.prepare<{ count: number }, [string]>(
|
|
71
|
+
"SELECT COUNT(*) as count FROM script_embeddings WHERE scriptId = ?",
|
|
72
|
+
)
|
|
73
|
+
.get(scriptId)?.count ?? 0
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function embeddedText(scriptId: string): string | null {
|
|
78
|
+
return (
|
|
79
|
+
getDb()
|
|
80
|
+
.prepare<{ embeddedText: string }, [string]>(
|
|
81
|
+
"SELECT embeddedText FROM script_embeddings WHERE scriptId = ?",
|
|
82
|
+
)
|
|
83
|
+
.get(scriptId)?.embeddedText ?? null
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function upsertFixture(args: {
|
|
88
|
+
name: string;
|
|
89
|
+
sourceLabel?: string;
|
|
90
|
+
description: string;
|
|
91
|
+
intent?: string;
|
|
92
|
+
isScratch?: boolean;
|
|
93
|
+
}) {
|
|
94
|
+
return upsertScriptByName({
|
|
95
|
+
name: args.name,
|
|
96
|
+
scope: "agent",
|
|
97
|
+
scopeId: "agent-1",
|
|
98
|
+
source: source(args.sourceLabel ?? args.name),
|
|
99
|
+
description: args.description,
|
|
100
|
+
intent: args.intent ?? args.description,
|
|
101
|
+
signatureJson,
|
|
102
|
+
agentId: "agent-1",
|
|
103
|
+
isScratch: args.isScratch,
|
|
104
|
+
typeChecked: !args.isScratch,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
beforeAll(async () => {
|
|
109
|
+
await clearDb();
|
|
110
|
+
initDb(TEST_DB_PATH);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterAll(async () => {
|
|
114
|
+
setScriptEmbeddingProviderForTests(null);
|
|
115
|
+
closeDb();
|
|
116
|
+
await clearDb();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
getDb().run("DELETE FROM scripts");
|
|
121
|
+
provider = new FakeEmbeddingProvider();
|
|
122
|
+
setScriptEmbeddingProviderForTests(provider);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("script embeddings", () => {
|
|
126
|
+
test("migration applies and creates script_embeddings storage", () => {
|
|
127
|
+
const schema = getDb()
|
|
128
|
+
.prepare<{ sql: string }, []>(
|
|
129
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'script_embeddings'",
|
|
130
|
+
)
|
|
131
|
+
.get();
|
|
132
|
+
expect(schema?.sql).toContain("scriptId TEXT PRIMARY KEY");
|
|
133
|
+
expect(schema?.sql).toContain("embedding BLOB NOT NULL");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("embeds explicit upserts and skips scratch upserts", async () => {
|
|
137
|
+
const explicit = await upsertFixture({
|
|
138
|
+
name: "linear-parser",
|
|
139
|
+
description: "Parse Linear issue payloads",
|
|
140
|
+
});
|
|
141
|
+
expect(embeddingCount(explicit.script.id)).toBe(1);
|
|
142
|
+
|
|
143
|
+
provider.reset();
|
|
144
|
+
const scratch = await upsertFixture({
|
|
145
|
+
name: "scratch-linear-parser",
|
|
146
|
+
description: "Scratch Linear issue payloads",
|
|
147
|
+
isScratch: true,
|
|
148
|
+
});
|
|
149
|
+
expect(embeddingCount(scratch.script.id)).toBe(0);
|
|
150
|
+
expect(provider.calls).toHaveLength(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("re-embeds on body or searchable metadata changes, but not on no-op upsert", async () => {
|
|
154
|
+
const first = await upsertFixture({
|
|
155
|
+
name: "github-review",
|
|
156
|
+
sourceLabel: "v1",
|
|
157
|
+
description: "Group GitHub review comments",
|
|
158
|
+
});
|
|
159
|
+
expect(provider.calls).toHaveLength(1);
|
|
160
|
+
|
|
161
|
+
provider.reset();
|
|
162
|
+
await upsertFixture({
|
|
163
|
+
name: "github-review",
|
|
164
|
+
sourceLabel: "v2",
|
|
165
|
+
description: "Group GitHub review comments",
|
|
166
|
+
});
|
|
167
|
+
expect(provider.calls).toHaveLength(1);
|
|
168
|
+
|
|
169
|
+
provider.reset();
|
|
170
|
+
await upsertFixture({
|
|
171
|
+
name: "github-review",
|
|
172
|
+
sourceLabel: "v2",
|
|
173
|
+
description: "Summarize pull request review feedback",
|
|
174
|
+
});
|
|
175
|
+
expect(provider.calls).toHaveLength(1);
|
|
176
|
+
expect(embeddedText(first.script.id)).toContain("Summarize pull request review feedback");
|
|
177
|
+
|
|
178
|
+
provider.reset();
|
|
179
|
+
await upsertFixture({
|
|
180
|
+
name: "github-review",
|
|
181
|
+
sourceLabel: "v2",
|
|
182
|
+
description: "Summarize pull request review feedback",
|
|
183
|
+
});
|
|
184
|
+
expect(provider.calls).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("scripts reembed command backfills scripts promoted after scratch save", async () => {
|
|
188
|
+
const scratch = await upsertFixture({
|
|
189
|
+
name: "promoted-scratch",
|
|
190
|
+
description: "Parse Slack channel messages",
|
|
191
|
+
isScratch: true,
|
|
192
|
+
});
|
|
193
|
+
expect(embeddingCount(scratch.script.id)).toBe(0);
|
|
194
|
+
|
|
195
|
+
getDb().run("UPDATE scripts SET isScratch = 0 WHERE id = ?", [scratch.script.id]);
|
|
196
|
+
provider.reset();
|
|
197
|
+
await runScriptsMaintenanceCommand(["reembed"]);
|
|
198
|
+
|
|
199
|
+
expect(provider.calls).toHaveLength(1);
|
|
200
|
+
expect(embeddingCount(scratch.script.id)).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("semantic search outranks name-substring-only matches", async () => {
|
|
204
|
+
await upsertFixture({
|
|
205
|
+
name: "review-grouper",
|
|
206
|
+
description: "Group GitHub pull request feedback by reviewer",
|
|
207
|
+
});
|
|
208
|
+
await upsertFixture({
|
|
209
|
+
name: "comments-sorter",
|
|
210
|
+
description: "Sort CSV table rows alphabetically",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
provider.reset();
|
|
214
|
+
const results = await searchScripts({ query: "comments", scopeId: "agent-1", limit: 2 });
|
|
215
|
+
expect(results.map((result) => result.script.name)).toEqual([
|
|
216
|
+
"review-grouper",
|
|
217
|
+
"comments-sorter",
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("hybrid ranking lets an exact name match outrank a weaker semantic match", async () => {
|
|
222
|
+
await upsertFixture({
|
|
223
|
+
name: "exact-name",
|
|
224
|
+
description: "Unrelated helper",
|
|
225
|
+
});
|
|
226
|
+
await upsertFixture({
|
|
227
|
+
name: "semantic-weaker",
|
|
228
|
+
description: "Linear issue triage helper",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const results = await searchScripts({ query: "exact-name", scopeId: "agent-1", limit: 2 });
|
|
232
|
+
expect(results[0]?.script.name).toBe("exact-name");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("semantic recall returns expected top results for overlapping intents", async () => {
|
|
236
|
+
const fixtures = [
|
|
237
|
+
["linear-json-parser", "Parse Linear issue JSON into task fields"],
|
|
238
|
+
["linear-ticket-router", "Route Linear tickets by team and priority"],
|
|
239
|
+
["github-pr-comment-grouper", "Group GitHub pull request comments by file"],
|
|
240
|
+
["github-review-summary", "Summarize PR review feedback and blockers"],
|
|
241
|
+
["memory-fanout", "Fan out memory recall searches across related terms"],
|
|
242
|
+
["memory-ranking", "Rank remembered notes by usefulness"],
|
|
243
|
+
["slack-thread-digest", "Digest Slack channel messages into a summary"],
|
|
244
|
+
["slack-alert-router", "Route Slack alerts to the right channel"],
|
|
245
|
+
["csv-normalizer", "Normalize CSV spreadsheet rows for table output"],
|
|
246
|
+
["table-formatter", "Format table rows into aligned text"],
|
|
247
|
+
] as const;
|
|
248
|
+
|
|
249
|
+
for (const [name, description] of fixtures) {
|
|
250
|
+
await upsertFixture({ name, description });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const queries = [
|
|
254
|
+
["issue payload fields", "linear-json-parser"],
|
|
255
|
+
["pull request comments", "github-pr-comment-grouper"],
|
|
256
|
+
["remembered search fanout", "memory-fanout"],
|
|
257
|
+
["channel message digest", "slack-thread-digest"],
|
|
258
|
+
["spreadsheet rows", "csv-normalizer"],
|
|
259
|
+
] as const;
|
|
260
|
+
|
|
261
|
+
let topOneHits = 0;
|
|
262
|
+
for (const [query, expected] of queries) {
|
|
263
|
+
const results = await searchScripts({ query, scopeId: "agent-1", limit: 3 });
|
|
264
|
+
if (results[0]?.script.name === expected) topOneHits++;
|
|
265
|
+
expect(results.slice(0, 3).map((result) => result.script.name)).toContain(expected);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
expect(topOneHits).toBeGreaterThanOrEqual(4);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("reembedAllScripts updates every explicit script", async () => {
|
|
272
|
+
await upsertFixture({ name: "linear-one", description: "Linear issue parser" });
|
|
273
|
+
await upsertFixture({ name: "slack-one", description: "Slack message digest" });
|
|
274
|
+
|
|
275
|
+
provider.reset();
|
|
276
|
+
await reembedAllScripts();
|
|
277
|
+
expect(provider.calls).toHaveLength(2);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("delete cascades script_embeddings", async () => {
|
|
281
|
+
const created = await upsertFixture({
|
|
282
|
+
name: "delete-embedding",
|
|
283
|
+
description: "Memory search helper",
|
|
284
|
+
});
|
|
285
|
+
expect(embeddingCount(created.script.id)).toBe(1);
|
|
286
|
+
|
|
287
|
+
getDb().run("DELETE FROM scripts WHERE id = ?", [created.script.id]);
|
|
288
|
+
expect(getScript({ name: "delete-embedding", scope: "agent", scopeId: "agent-1" })).toBeNull();
|
|
289
|
+
expect(embeddingCount(created.script.id)).toBe(0);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { extractScriptSignature } from "../scripts-runtime/extract-signature";
|
|
3
|
+
|
|
4
|
+
describe("extractScriptSignature", () => {
|
|
5
|
+
test("extracts arrow function destructured args and return type", () => {
|
|
6
|
+
const sig = extractScriptSignature(`
|
|
7
|
+
/** Adds one */
|
|
8
|
+
export default async ({ x }: { x: number }): Promise<{ y: number }> => ({ y: x + 1 });
|
|
9
|
+
`);
|
|
10
|
+
expect(sig.argsType).toBe("{ x: number }");
|
|
11
|
+
expect(sig.resultType).toBe("{ y: number }");
|
|
12
|
+
expect(sig.description).toBe("Adds one");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("extracts generic async function declarations", () => {
|
|
16
|
+
const sig = extractScriptSignature(`
|
|
17
|
+
/** Generic mapper */
|
|
18
|
+
export default async function <T extends { id: string }>(
|
|
19
|
+
args: T
|
|
20
|
+
): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
}> {
|
|
24
|
+
return { id: args.id, ok: true };
|
|
25
|
+
}
|
|
26
|
+
`);
|
|
27
|
+
expect(sig.argsType).toBe("T");
|
|
28
|
+
expect(sig.resultType).toContain("ok: boolean");
|
|
29
|
+
expect(sig.description).toBe("Generic mapper");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("falls back when no default export exists", () => {
|
|
33
|
+
expect(extractScriptSignature("export const x = 1")).toEqual({
|
|
34
|
+
argsType: "unknown",
|
|
35
|
+
resultType: "unknown",
|
|
36
|
+
description: "",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("falls back on syntax error", () => {
|
|
41
|
+
expect(extractScriptSignature("export default async (")).toEqual({
|
|
42
|
+
argsType: "unknown",
|
|
43
|
+
resultType: "unknown",
|
|
44
|
+
description: "",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|