@desplega.ai/agent-swarm 1.92.0 → 1.92.2

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.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +276 -3
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +416 -20
  6. package/src/be/memory/boot-reembed.ts +85 -0
  7. package/src/be/memory/constants.ts +44 -2
  8. package/src/be/memory/providers/openai-embedding.ts +15 -5
  9. package/src/be/memory/providers/sqlite-store.ts +325 -76
  10. package/src/be/memory/reranker.ts +35 -17
  11. package/src/be/memory/types.ts +43 -0
  12. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  13. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  14. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  15. package/src/be/migrations/087_skill_files.sql +19 -0
  16. package/src/be/modelsdev-cache.json +5622 -2543
  17. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  18. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  19. package/src/be/seed-scripts/catalog/compound-insights.ts +465 -0
  20. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  21. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  22. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  25. package/src/be/seed-scripts/index.ts +32 -4
  26. package/src/be/seed-skills/index.ts +0 -7
  27. package/src/be/skill-sync.ts +91 -7
  28. package/src/commands/runner.ts +6 -2
  29. package/src/heartbeat/templates.ts +20 -16
  30. package/src/http/index.ts +50 -7
  31. package/src/http/mcp-user.ts +23 -0
  32. package/src/http/mcp.ts +58 -0
  33. package/src/http/memory.ts +62 -0
  34. package/src/http/pages.ts +1 -1
  35. package/src/http/script-runs.ts +2 -0
  36. package/src/http/scripts.ts +39 -2
  37. package/src/http/skills.ts +225 -0
  38. package/src/providers/claude-adapter.ts +56 -24
  39. package/src/script-workflows/workflow-ctx.ts +7 -3
  40. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  41. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  42. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  43. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  44. package/src/server.ts +2 -0
  45. package/src/tasks/worker-follow-up.ts +12 -0
  46. package/src/tests/claude-adapter-binary.test.ts +135 -81
  47. package/src/tests/create-page-tool.test.ts +19 -2
  48. package/src/tests/heartbeat-checklist.test.ts +36 -0
  49. package/src/tests/mcp-transport-gc.test.ts +58 -0
  50. package/src/tests/memory-e2e.test.ts +6 -6
  51. package/src/tests/memory-health-endpoint.test.ts +78 -0
  52. package/src/tests/memory-rater-e2e.test.ts +4 -5
  53. package/src/tests/memory-reranker.test.ts +135 -124
  54. package/src/tests/memory-store.test.ts +221 -1
  55. package/src/tests/memory.test.ts +13 -12
  56. package/src/tests/pages-http.test.ts +20 -2
  57. package/src/tests/pages-storage.test.ts +26 -0
  58. package/src/tests/scripts-mcp-e2e.test.ts +53 -0
  59. package/src/tests/seed-scripts.test.ts +328 -3
  60. package/src/tests/skill-files-http.test.ts +171 -0
  61. package/src/tests/skill-files.test.ts +162 -0
  62. package/src/tests/skill-get-file-tool.test.ts +110 -0
  63. package/src/tests/skill-sync.test.ts +125 -6
  64. package/src/tests/task-cascade-fail.test.ts +304 -0
  65. package/src/tools/create-page.ts +2 -2
  66. package/src/tools/skills/index.ts +1 -0
  67. package/src/tools/skills/skill-get-file.ts +80 -0
  68. package/src/tools/tool-config.ts +2 -1
  69. package/src/types.ts +20 -0
  70. package/src/utils/internal-ai/complete-structured.ts +2 -2
  71. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  72. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  73. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  74. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  75. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  76. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  77. package/templates/skills/agentmail-sending/content.md +6 -7
  78. package/templates/skills/desloppify/content.md +8 -9
  79. package/templates/skills/jira-interaction/content.md +25 -33
  80. package/templates/skills/kapso-whatsapp/content.md +29 -30
  81. package/templates/skills/linear-interaction/content.md +8 -9
  82. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  83. package/templates/skills/sprite-cli/content.md +4 -5
  84. package/templates/skills/turso-interaction/content.md +14 -17
  85. package/templates/skills/workflow-iterate/content.md +38 -391
  86. package/templates/skills/x-api-interactions/content.md +4 -6
  87. package/templates/workflows/llm-safe-release-context/config.json +13 -0
  88. package/templates/workflows/llm-safe-release-context/content.md +69 -0
  89. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  90. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
- import { closeDb, createAgent, initDb } from "../be/db";
3
+ import { closeDb, createAgent, getDb, initDb } from "../be/db";
4
+ import { serializeEmbedding } from "../be/embedding";
4
5
  import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
5
6
 
6
7
  const TEST_DB_PATH = "./test-memory-store.sqlite";
@@ -10,6 +11,14 @@ describe("SqliteMemoryStore", () => {
10
11
  const agentB = "bbbb0000-0000-4000-8000-000000000002";
11
12
  let store: SqliteMemoryStore;
12
13
 
14
+ function vector(values: Record<number, number>): Float32Array {
15
+ const embedding = new Float32Array(512);
16
+ for (const [index, value] of Object.entries(values)) {
17
+ embedding[Number(index)] = value;
18
+ }
19
+ return embedding;
20
+ }
21
+
13
22
  beforeAll(async () => {
14
23
  for (const suffix of ["", "-wal", "-shm"]) {
15
24
  try {
@@ -210,6 +219,95 @@ describe("SqliteMemoryStore", () => {
210
219
  const agents = new Set(results.map((r) => r.agentId));
211
220
  expect(agents.size).toBeGreaterThanOrEqual(1);
212
221
  });
222
+
223
+ test("uses sqlite-vec for 512d embeddings with scope-filter parity", () => {
224
+ for (let i = 0; i < 6; i++) {
225
+ const otherAgent = store.store({
226
+ agentId: agentB,
227
+ scope: "agent",
228
+ name: `vec-other-agent-exact-${i}`,
229
+ content: "exact but invisible to agentA",
230
+ source: "manual",
231
+ });
232
+ store.updateEmbedding(otherAgent.id, vector({ 0: 1 }), "test-model");
233
+ }
234
+
235
+ const visible = store.store({
236
+ agentId: agentA,
237
+ scope: "agent",
238
+ name: "vec-agent-visible",
239
+ content: "visible to agentA",
240
+ source: "manual",
241
+ });
242
+ store.updateEmbedding(visible.id, vector({ 0: 0.8, 1: 0.2 }), "test-model");
243
+
244
+ const query = vector({ 0: 1 });
245
+ const results = store.search(query, agentA, { scope: "agent", limit: 5 });
246
+
247
+ expect(results[0]!.id).toBe(visible.id);
248
+ expect(results.every((r) => r.agentId === agentA && r.scope === "agent")).toBe(true);
249
+
250
+ const health = store.getHealth();
251
+ expect(health.sqliteVec.schema).toContain("distance_metric=cosine");
252
+ expect(health.counts.memoryVec).toBeGreaterThanOrEqual(2);
253
+ expect(health.retrievalMode).toBe("vec");
254
+ });
255
+ });
256
+
257
+ describe("memory_vec population", () => {
258
+ test("populates existing embeddings on startup and reports health counts", () => {
259
+ const raw = store.store({
260
+ agentId: agentA,
261
+ scope: "agent",
262
+ name: "raw-existing-embedding",
263
+ content: "raw existing embedding",
264
+ source: "manual",
265
+ });
266
+ getDb()
267
+ .prepare("UPDATE agent_memory SET embedding = ?, embeddingModel = ? WHERE id = ?")
268
+ .run(serializeEmbedding(vector({ 2: 1 })), "test-model", raw.id);
269
+ getDb().prepare("DELETE FROM memory_vec WHERE memory_id = ?").run(raw.id);
270
+
271
+ const freshStore = new SqliteMemoryStore();
272
+ const health = freshStore.getHealth();
273
+
274
+ expect(health.counts.missingFromVec).toBe(0);
275
+ expect(health.sqliteVec.lastPopulate?.attempted).toBeGreaterThanOrEqual(1);
276
+ expect(health.sqliteVec.lastPopulate?.failed).toBe(0);
277
+
278
+ const resultIds = freshStore
279
+ .search(vector({ 2: 1 }), agentA, { scope: "agent", limit: 20 })
280
+ .map((r) => r.id);
281
+ expect(resultIds).toContain(raw.id);
282
+ });
283
+
284
+ test("rebuilds an old non-cosine memory_vec table from agent_memory", () => {
285
+ const raw = store.store({
286
+ agentId: agentA,
287
+ scope: "agent",
288
+ name: "stale-schema-embedding",
289
+ content: "stale schema embedding",
290
+ source: "manual",
291
+ });
292
+ getDb()
293
+ .prepare("UPDATE agent_memory SET embedding = ?, embeddingModel = ? WHERE id = ?")
294
+ .run(serializeEmbedding(vector({ 3: 1 })), "test-model", raw.id);
295
+
296
+ getDb().run("DROP TABLE memory_vec");
297
+ getDb().run(`
298
+ CREATE VIRTUAL TABLE memory_vec USING vec0(
299
+ memory_id TEXT PRIMARY KEY,
300
+ embedding float[512]
301
+ )
302
+ `);
303
+
304
+ const freshStore = new SqliteMemoryStore();
305
+ const health = freshStore.getHealth();
306
+
307
+ expect(health.sqliteVec.schema).toContain("distance_metric=cosine");
308
+ expect(health.counts.missingFromVec).toBe(0);
309
+ expect(health.retrievalMode).toBe("vec");
310
+ });
213
311
  });
214
312
 
215
313
  describe("delete()", () => {
@@ -326,4 +424,126 @@ describe("SqliteMemoryStore", () => {
326
424
  expect(filtered.length).toBeGreaterThan(0);
327
425
  });
328
426
  });
427
+
428
+ describe("purgeExpired()", () => {
429
+ const purgeAgent = "cccc0000-0000-4000-8000-000000000003";
430
+
431
+ beforeAll(() => {
432
+ try {
433
+ createAgent({ id: purgeAgent, name: "Purge Agent", isLead: false, status: "idle" });
434
+ } catch {
435
+ // agent may already exist from a prior run
436
+ }
437
+ });
438
+
439
+ test("deletes rows past their expiresAt and returns count", () => {
440
+ const db = getDb();
441
+
442
+ // Store a session_summary (3-day TTL) then backdateits expiresAt to the past
443
+ const mem = store.store({
444
+ agentId: purgeAgent,
445
+ scope: "agent",
446
+ name: "expired session",
447
+ content: "old session data",
448
+ source: "session_summary",
449
+ });
450
+ db.prepare("UPDATE agent_memory SET expiresAt = datetime('now', '-1 day') WHERE id = ?").run(
451
+ mem.id,
452
+ );
453
+
454
+ // Store a manual memory (never expires) — should survive
455
+ const keeper = store.store({
456
+ agentId: purgeAgent,
457
+ scope: "agent",
458
+ name: "keeper",
459
+ content: "manual content",
460
+ source: "manual",
461
+ });
462
+
463
+ const purged = store.purgeExpired();
464
+ expect(purged).toBeGreaterThanOrEqual(1);
465
+
466
+ expect(store.get(mem.id)).toBeNull();
467
+ expect(store.get(keeper.id)).not.toBeNull();
468
+ });
469
+
470
+ test("returns 0 when nothing is expired", () => {
471
+ const purged = store.purgeExpired();
472
+ expect(purged).toBe(0);
473
+ });
474
+
475
+ test("also removes corresponding vec rows", () => {
476
+ const db = getDb();
477
+ const emb = vector({ 0: 0.9, 100: 0.1 });
478
+
479
+ const mem = store.store({
480
+ agentId: purgeAgent,
481
+ scope: "agent",
482
+ name: "vec-purge-test",
483
+ content: "will be purged",
484
+ source: "task_completion",
485
+ });
486
+ store.updateEmbedding(mem.id, emb, "test-model");
487
+
488
+ const vecBefore = db
489
+ .prepare<{ count: number }, [string]>(
490
+ "SELECT COUNT(*) as count FROM memory_vec WHERE memory_id = ?",
491
+ )
492
+ .get(mem.id);
493
+
494
+ // Only check vec cleanup if the row was actually inserted
495
+ if (vecBefore && vecBefore.count > 0) {
496
+ db.prepare(
497
+ "UPDATE agent_memory SET expiresAt = datetime('now', '-1 day') WHERE id = ?",
498
+ ).run(mem.id);
499
+
500
+ store.purgeExpired();
501
+
502
+ const vecAfter = db
503
+ .prepare<{ count: number }, [string]>(
504
+ "SELECT COUNT(*) as count FROM memory_vec WHERE memory_id = ?",
505
+ )
506
+ .get(mem.id);
507
+ expect(vecAfter?.count ?? 0).toBe(0);
508
+ }
509
+ });
510
+ });
511
+
512
+ describe("knn-k cap", () => {
513
+ test("search does not throw when vec table exceeds 4096 rows", () => {
514
+ const db = getDb();
515
+ const emb = vector({ 0: 1.0 });
516
+ const embBuffer = serializeEmbedding(emb);
517
+
518
+ // Check if vec is available — if not, skip (test is meaningful only with vec)
519
+ const health = store.getHealth();
520
+ if (health.retrievalMode !== "vec") return;
521
+
522
+ // Insert enough rows to exceed 4096 in the vec table
523
+ const currentCount =
524
+ db.prepare<{ c: number }, []>("SELECT COUNT(*) as c FROM memory_vec").get()?.c ?? 0;
525
+ const needed = Math.max(0, 4097 - currentCount);
526
+
527
+ for (let i = 0; i < needed; i++) {
528
+ const id = `knn-test-${i}-${Date.now()}`;
529
+ db.prepare(
530
+ "INSERT INTO agent_memory (id, agentId, scope, name, content, source, embedding, chunkIndex, totalChunks, tags, alpha, beta, createdAt, accessedAt) VALUES (?, ?, 'agent', ?, 'knn test', 'manual', ?, 0, 1, '[]', 1, 1, datetime('now'), datetime('now'))",
531
+ ).run(id, agentA, `knn-${i}`, embBuffer);
532
+ const vecBuf = new Float32Array(emb);
533
+ db.prepare("INSERT INTO memory_vec (memory_id, embedding) VALUES (?, ?)").run(
534
+ id,
535
+ Buffer.from(vecBuf.buffer),
536
+ );
537
+ }
538
+
539
+ const vecCount =
540
+ db.prepare<{ c: number }, []>("SELECT COUNT(*) as c FROM memory_vec").get()?.c ?? 0;
541
+ expect(vecCount).toBeGreaterThanOrEqual(4097);
542
+
543
+ // This should NOT throw — it should clamp k to 4096
544
+ const results = store.search(emb, agentA, { scope: "agent", limit: 10 });
545
+ expect(results).toBeDefined();
546
+ expect(Array.isArray(results)).toBe(true);
547
+ });
548
+ });
329
549
  });
@@ -342,8 +342,9 @@ describe("Memory System", () => {
342
342
  status: "idle",
343
343
  });
344
344
 
345
- // Create memories with known embeddings
346
- // Memory 1: agent scope for searchAgentId, embedding [1,0,0]
345
+ // Create memories with known embeddings (all share a baseline component
346
+ // so pairwise cosine similarity stays above the MIN_SIMILARITY floor).
347
+ // Memory 1: agent scope for searchAgentId
347
348
  const m1 = store.store({
348
349
  agentId: searchAgentId,
349
350
  scope: "agent",
@@ -351,9 +352,9 @@ describe("Memory System", () => {
351
352
  content: "Agent-scoped content",
352
353
  source: "manual",
353
354
  });
354
- store.updateEmbedding(m1.id, new Float32Array([1, 0, 0]), "test-model");
355
+ store.updateEmbedding(m1.id, new Float32Array([1, 0.3, 0.3]), "test-model");
355
356
 
356
- // Memory 2: swarm scope, embedding [0,1,0]
357
+ // Memory 2: swarm scope
357
358
  const m2 = store.store({
358
359
  agentId: searchAgentId,
359
360
  scope: "swarm",
@@ -361,9 +362,9 @@ describe("Memory System", () => {
361
362
  content: "Swarm-scoped content",
362
363
  source: "file_index",
363
364
  });
364
- store.updateEmbedding(m2.id, new Float32Array([0, 1, 0]), "test-model");
365
+ store.updateEmbedding(m2.id, new Float32Array([0.3, 1, 0.3]), "test-model");
365
366
 
366
- // Memory 3: agent scope for OTHER agent, embedding [0,0,1]
367
+ // Memory 3: agent scope for OTHER agent
367
368
  const m3 = store.store({
368
369
  agentId: searchAgentId2,
369
370
  scope: "agent",
@@ -371,11 +372,11 @@ describe("Memory System", () => {
371
372
  content: "Other agent's private memory",
372
373
  source: "manual",
373
374
  });
374
- store.updateEmbedding(m3.id, new Float32Array([0, 0, 1]), "test-model");
375
+ store.updateEmbedding(m3.id, new Float32Array([0.3, 0.3, 1]), "test-model");
375
376
  });
376
377
 
377
378
  test("worker sees own agent-scoped + swarm memories", () => {
378
- const query = new Float32Array([1, 0, 0]); // closest to Memory 1
379
+ const query = new Float32Array([1, 0.3, 0.3]); // closest to Memory 1
379
380
  const results = store.search(query, searchAgentId, { isLead: false });
380
381
  const names = results.map((r) => r.name);
381
382
 
@@ -385,7 +386,7 @@ describe("Memory System", () => {
385
386
  });
386
387
 
387
388
  test("worker does not see other agent's agent-scoped memories", () => {
388
- const query = new Float32Array([0, 0, 1]); // closest to Memory 3
389
+ const query = new Float32Array([0.3, 0.3, 1]); // closest to Memory 3
389
390
  const results = store.search(query, searchAgentId, { isLead: false });
390
391
  const names = results.map((r) => r.name);
391
392
 
@@ -393,7 +394,7 @@ describe("Memory System", () => {
393
394
  });
394
395
 
395
396
  test("lead sees ALL memories across agents", () => {
396
- const query = new Float32Array([0, 0, 1]); // closest to Memory 3
397
+ const query = new Float32Array([0.3, 0.3, 1]); // closest to Memory 3
397
398
  const results = store.search(query, searchAgentId, { isLead: true });
398
399
  const names = results.map((r) => r.name);
399
400
 
@@ -403,12 +404,12 @@ describe("Memory System", () => {
403
404
  });
404
405
 
405
406
  test("results sorted by similarity (highest first)", () => {
406
- const query = new Float32Array([1, 0, 0]); // identical to Memory 1's embedding
407
+ const query = new Float32Array([1, 0.3, 0.3]); // closest to Memory 1's embedding
407
408
  const results = store.search(query, searchAgentId, { isLead: true });
408
409
 
409
410
  expect(results.length).toBeGreaterThan(0);
410
411
  expect(results[0].name).toBe("Agent Memory 1");
411
- expect(results[0].similarity).toBeCloseTo(1.0, 3);
412
+ expect(results[0].similarity).toBeGreaterThan(0.9);
412
413
 
413
414
  // Each subsequent result should have lower or equal similarity
414
415
  for (let i = 1; i < results.length; i++) {
@@ -61,14 +61,13 @@ describe("Pages HTTP API", () => {
61
61
  }
62
62
  });
63
63
 
64
- test("POST /api/pages creates a page and returns {id, version}", async () => {
64
+ test("POST /api/pages creates an authed page by default and returns {id, version}", async () => {
65
65
  const res = await fetch(`${baseUrl}/api/pages`, {
66
66
  method: "POST",
67
67
  headers,
68
68
  body: JSON.stringify({
69
69
  title: "Hello",
70
70
  contentType: "text/html",
71
- authMode: "public",
72
71
  body: "<h1>hi</h1>",
73
72
  }),
74
73
  });
@@ -87,6 +86,25 @@ describe("Pages HTTP API", () => {
87
86
  expect(page.agentId).toBe(agentId);
88
87
  expect(page.slug).toBe("hello"); // auto-slug from title
89
88
  expect(page.contentType).toBe("text/html");
89
+ expect(page.authMode).toBe("authed");
90
+ });
91
+
92
+ test("POST /api/pages can explicitly opt in to public auth", async () => {
93
+ const res = await fetch(`${baseUrl}/api/pages`, {
94
+ method: "POST",
95
+ headers,
96
+ body: JSON.stringify({
97
+ title: "Public",
98
+ contentType: "text/html",
99
+ authMode: "public",
100
+ body: "<h1>public</h1>",
101
+ }),
102
+ });
103
+
104
+ expect(res.status).toBe(201);
105
+ const json = (await res.json()) as { id: string };
106
+ const got = await fetch(`${baseUrl}/api/pages/${json.id}`, { headers });
107
+ const page = (await got.json()) as Page;
90
108
  expect(page.authMode).toBe("public");
91
109
  });
92
110
 
@@ -5,6 +5,7 @@ import {
5
5
  closeDb,
6
6
  createPage,
7
7
  deletePage,
8
+ getDb,
8
9
  getPage,
9
10
  getPageBySlug,
10
11
  getPageVersion,
@@ -81,6 +82,31 @@ describe("pages storage CRUD", () => {
81
82
  expect(getPageVersions(created.id)).toHaveLength(0);
82
83
  });
83
84
 
85
+ test("createPage defaults authMode to authed when omitted", () => {
86
+ const agentId = makeAgentId();
87
+ const created = createPage({
88
+ agentId,
89
+ slug: "default-auth",
90
+ title: "Default Auth",
91
+ contentType: "text/html",
92
+ body: "<h1>default</h1>",
93
+ });
94
+
95
+ expect(created.authMode).toBe("authed");
96
+ });
97
+
98
+ test("pages table defaults authMode to authed when omitted", () => {
99
+ const agentId = makeAgentId();
100
+ const row = getDb()
101
+ .prepare<{ authMode: string }, [string, string, string, string, string]>(
102
+ `INSERT INTO pages (agentId, slug, title, contentType, body)
103
+ VALUES (?, ?, ?, ?, ?) RETURNING authMode`,
104
+ )
105
+ .get(agentId, "sql-default-auth", "SQL Default Auth", "text/html", "<h1>default</h1>");
106
+
107
+ expect(row?.authMode).toBe("authed");
108
+ });
109
+
84
110
  test("snapshotPage captures PRE-update content; post-update lives on parent", () => {
85
111
  const agentId = makeAgentId();
86
112
  const page = createPage({
@@ -232,6 +232,59 @@ describe("script_ MCP HTTP proxy tools", () => {
232
232
  expect(del.structuredContent.data?.deleted).toBe(true);
233
233
  });
234
234
 
235
+ test("persists a successful inline run with kind 'inline' and no journal", async () => {
236
+ const tools = buildToolServer();
237
+ const source = `export default async (args: { value: number }) => ({ doubled: args.value * 2 });`;
238
+
239
+ const run = (await tools.run.handler(
240
+ { source, args: { value: 21 }, intent: "inline persist e2e" },
241
+ meta(workerId),
242
+ )) as StructuredResult<{ result: { doubled: number } }>;
243
+ expect(run.structuredContent.success).toBe(true);
244
+ expect(run.structuredContent.data?.result).toEqual({ doubled: 42 });
245
+
246
+ const listed = (await tools.listScriptRuns.handler(
247
+ { limit: 10, offset: 0 },
248
+ meta(workerId),
249
+ )) as StructuredResult<{
250
+ runs: Array<{ id: string; kind: string; status: string }>;
251
+ total: number;
252
+ }>;
253
+ expect(listed.structuredContent.data?.total).toBe(1);
254
+ const inlineRun = listed.structuredContent.data?.runs[0];
255
+ expect(inlineRun?.kind).toBe("inline");
256
+ expect(inlineRun?.status).toBe("completed");
257
+
258
+ const detail = (await tools.getScriptRun.handler(
259
+ { id: inlineRun?.id },
260
+ meta(workerId),
261
+ )) as StructuredResult<{ run: { kind: string }; journal: unknown[] }>;
262
+ expect(detail.structuredContent.data?.run.kind).toBe("inline");
263
+ expect(detail.structuredContent.data?.journal).toEqual([]);
264
+ });
265
+
266
+ test("persists a failed inline run with kind 'inline' and an error", async () => {
267
+ const tools = buildToolServer();
268
+ const source = `export default async () => { throw new Error("boom"); };`;
269
+
270
+ const run = (await tools.run.handler(
271
+ { source, intent: "inline failure e2e" },
272
+ meta(workerId),
273
+ )) as StructuredResult<unknown>;
274
+ expect(run.structuredContent.success).toBe(true);
275
+
276
+ const listed = (await tools.listScriptRuns.handler(
277
+ { limit: 10, offset: 0 },
278
+ meta(workerId),
279
+ )) as StructuredResult<{
280
+ runs: Array<{ kind: string; status: string; error?: string }>;
281
+ }>;
282
+ const failed = listed.structuredContent.data?.runs[0];
283
+ expect(failed?.kind).toBe("inline");
284
+ expect(failed?.status).toBe("failed");
285
+ expect(failed?.error).toBeTruthy();
286
+ });
287
+
235
288
  test("stdio-style missing agent identity short-circuits clearly", async () => {
236
289
  const tools = buildToolServer();
237
290
  const result = (await tools.search.handler({ query: "anything" }, meta())) as StructuredResult<{