@desplega.ai/agent-swarm 1.90.0 → 1.92.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.
Files changed (96) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +803 -150
  3. package/package.json +5 -5
  4. package/src/artifact-sdk/server.ts +2 -1
  5. package/src/be/db.ts +337 -1
  6. package/src/be/memory/providers/sqlite-store.ts +6 -1
  7. package/src/be/memory/types.ts +1 -0
  8. package/src/be/migrations/083_script_workflows.sql +51 -0
  9. package/src/be/modelsdev-cache.json +42352 -38595
  10. package/src/be/scripts/typecheck.ts +181 -1
  11. package/src/be/seed-scripts/catalog/compound-insights.ts +398 -0
  12. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
  13. package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
  14. package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
  15. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  16. package/src/be/seed-scripts/catalog/tool-usage.ts +59 -0
  17. package/src/be/seed-scripts/index.ts +54 -0
  18. package/src/be/seed-skills/index.ts +7 -0
  19. package/src/be/swarm-config-guard.ts +17 -0
  20. package/src/commands/artifact.ts +3 -2
  21. package/src/commands/profile-sync.ts +310 -0
  22. package/src/commands/runner.ts +134 -3
  23. package/src/hooks/hook.ts +32 -9
  24. package/src/http/db-query.ts +20 -5
  25. package/src/http/index.ts +57 -0
  26. package/src/http/integrations.ts +6 -1
  27. package/src/http/mcp-bridge.ts +117 -0
  28. package/src/http/mcp-oauth.ts +97 -39
  29. package/src/http/memory.ts +5 -2
  30. package/src/http/openapi.ts +2 -2
  31. package/src/http/pages-public.ts +10 -11
  32. package/src/http/pages.ts +7 -11
  33. package/src/http/script-runs.ts +555 -0
  34. package/src/http/scripts.ts +24 -1
  35. package/src/http/utils.ts +11 -4
  36. package/src/jira/app.ts +2 -3
  37. package/src/jira/webhook-lifecycle.ts +2 -1
  38. package/src/linear/app.ts +2 -3
  39. package/src/prompts/session-templates.ts +24 -4
  40. package/src/providers/claude-adapter.ts +86 -13
  41. package/src/script-workflows/executor.ts +110 -0
  42. package/src/script-workflows/harness.ts +73 -0
  43. package/src/script-workflows/label-lint.ts +51 -0
  44. package/src/script-workflows/limits.ts +22 -0
  45. package/src/script-workflows/supervisor.ts +139 -0
  46. package/src/script-workflows/workflow-ctx.ts +205 -0
  47. package/src/scripts-runtime/executors/native.ts +1 -0
  48. package/src/scripts-runtime/sdk-allowlist.ts +124 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +198 -3
  50. package/src/scripts-runtime/types/stdlib.d.ts +287 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +287 -0
  52. package/src/server.ts +2 -0
  53. package/src/slack/handlers.ts +11 -4
  54. package/src/slack/message-text.ts +98 -0
  55. package/src/slack/thread-buffer.ts +5 -3
  56. package/src/tests/claude-adapter-binary.test.ts +147 -4
  57. package/src/tests/claude-adapter-otel.test.ts +85 -1
  58. package/src/tests/db-query.test.ts +28 -0
  59. package/src/tests/error-tracker.test.ts +121 -0
  60. package/src/tests/harness-provider-resolution.test.ts +33 -0
  61. package/src/tests/hook-registration-nudge.test.ts +69 -0
  62. package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
  63. package/src/tests/mcp-tools.test.ts +6 -0
  64. package/src/tests/pages-public-html.test.ts +41 -0
  65. package/src/tests/pages-public-json-redirect.test.ts +37 -2
  66. package/src/tests/profile-sync.test.ts +282 -0
  67. package/src/tests/prompt-template-session.test.ts +34 -5
  68. package/src/tests/script-runs-http.test.ts +278 -0
  69. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  70. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  71. package/src/tests/scripts-mcp-e2e.test.ts +49 -2
  72. package/src/tests/scripts-runtime.test.ts +33 -0
  73. package/src/tests/seed-scripts.test.ts +347 -2
  74. package/src/tests/slack-message-text.test.ts +250 -0
  75. package/src/tests/system-default-skills.test.ts +40 -0
  76. package/src/tools/create-metric.ts +2 -3
  77. package/src/tools/create-page.ts +3 -6
  78. package/src/tools/db-query.ts +16 -6
  79. package/src/tools/memory-rate.ts +2 -1
  80. package/src/tools/memory-search.ts +1 -0
  81. package/src/tools/register-kapso-number.ts +2 -4
  82. package/src/tools/request-human-input.ts +2 -1
  83. package/src/tools/script-common.ts +2 -4
  84. package/src/tools/script-run.ts +7 -0
  85. package/src/tools/script-runs.ts +123 -0
  86. package/src/tools/slack-read.ts +12 -3
  87. package/src/tools/tool-config.ts +4 -1
  88. package/src/types.ts +52 -0
  89. package/src/utils/constants.ts +58 -8
  90. package/src/utils/error-tracker.ts +40 -1
  91. package/src/utils/internal-ai/complete-structured.ts +10 -4
  92. package/src/workflows/executors/raw-llm.ts +76 -59
  93. package/templates/skills/pages/content.md +205 -55
  94. package/templates/skills/script-workflows/config.json +14 -0
  95. package/templates/skills/script-workflows/content.md +68 -0
  96. package/templates/skills/swarm-scripts/content.md +45 -7
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { lintWorkflowLabels } from "../script-workflows/label-lint";
3
+
4
+ describe("lintWorkflowLabels", () => {
5
+ test("rejects a literal step label inside a loop", () => {
6
+ const result = lintWorkflowLabels(`
7
+ export default async function main(args, ctx) {
8
+ for (const item of args.items) {
9
+ await ctx.step.agentTask("process", { task: item.task });
10
+ }
11
+ }
12
+ `);
13
+
14
+ expect(result.ok).toBe(false);
15
+ if (!result.ok) {
16
+ expect(result.errors).toHaveLength(1);
17
+ expect(result.errors[0]?.label).toBe("process");
18
+ expect(result.errors[0]?.detail).toContain("inside a loop");
19
+ }
20
+ });
21
+
22
+ test("allows template literal step labels inside a loop", () => {
23
+ const result = lintWorkflowLabels(`
24
+ export default async function main(args, ctx) {
25
+ for (const item of args.items) {
26
+ await ctx.step.agentTask(\`process:\${item.id}\`, { task: item.task });
27
+ }
28
+ }
29
+ `);
30
+
31
+ expect(result).toEqual({ ok: true });
32
+ });
33
+
34
+ test("allows literal step labels outside loops", () => {
35
+ const result = lintWorkflowLabels(`
36
+ export default async function main(_args, ctx) {
37
+ await ctx.step.rawLlm("summarize", { prompt: "hello" });
38
+ }
39
+ `);
40
+
41
+ expect(result).toEqual({ ok: true });
42
+ });
43
+ });
@@ -0,0 +1,170 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { rm, unlink } from "node:fs/promises";
3
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
4
+ import { closeDb, createAgent, getDb, initDb, listScriptRunJournalSteps } from "../be/db";
5
+ import { handleCore } from "../http/core";
6
+ import { handleScriptRuns } from "../http/script-runs";
7
+ import { handleScripts } from "../http/scripts";
8
+ import { getPathSegments, parseQueryParams } from "../http/utils";
9
+ import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
10
+
11
+ const TEST_DB_PATH = "./test-script-workflows-runtime-e2e.sqlite";
12
+ const WORKFLOW_RUNTIME_DIR = "./test-script-workflows-runtime";
13
+ const API_KEY = "test-script-workflows-runtime-key-1234567890";
14
+
15
+ let agentId: string;
16
+ let server: Server;
17
+ let baseUrl: string;
18
+ let savedEnv: NodeJS.ProcessEnv;
19
+
20
+ async function removeDbFiles(path: string): Promise<void> {
21
+ for (const suffix of ["", "-wal", "-shm"]) {
22
+ try {
23
+ await unlink(path + suffix);
24
+ } catch (error) {
25
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
26
+ }
27
+ }
28
+ }
29
+
30
+ function listen(server: Server): Promise<number> {
31
+ return new Promise((resolve, reject) => {
32
+ server.once("error", reject);
33
+ server.listen(0, "127.0.0.1", () => {
34
+ const address = server.address();
35
+ if (!address || typeof address === "string") reject(new Error("No server address"));
36
+ else resolve(address.port);
37
+ });
38
+ });
39
+ }
40
+
41
+ function closeServer(server: Server): Promise<void> {
42
+ return new Promise((resolve, reject) => {
43
+ server.close((err) => (err ? reject(err) : resolve()));
44
+ });
45
+ }
46
+
47
+ async function route(req: IncomingMessage, res: ServerResponse): Promise<void> {
48
+ const agentId = req.headers["x-agent-id"] as string | undefined;
49
+ if (await handleCore(req, res, agentId, API_KEY)) return;
50
+ const pathSegments = getPathSegments(req.url || "");
51
+ const queryParams = parseQueryParams(req.url || "");
52
+ if (await handleScriptRuns(req, res, pathSegments, queryParams, agentId)) return;
53
+ if (await handleScripts(req, res, pathSegments, queryParams, agentId)) return;
54
+ res.writeHead(404);
55
+ res.end("Not Found");
56
+ }
57
+
58
+ async function api(path: string, init: RequestInit = {}): Promise<Response> {
59
+ return fetch(`${baseUrl}${path}`, {
60
+ ...init,
61
+ headers: {
62
+ Authorization: `Bearer ${API_KEY}`,
63
+ "X-Agent-ID": agentId,
64
+ "Content-Type": "application/json",
65
+ ...((init.headers as Record<string, string>) ?? {}),
66
+ },
67
+ });
68
+ }
69
+
70
+ async function waitForRun(
71
+ id: string,
72
+ ): Promise<{ status: string; output?: unknown; error?: string }> {
73
+ const deadline = Date.now() + 10_000;
74
+ while (Date.now() < deadline) {
75
+ const res = await api(`/api/script-runs/${id}`);
76
+ const body = (await res.json()) as {
77
+ run: { status: string; output?: unknown; error?: string };
78
+ };
79
+ if (["completed", "failed", "cancelled", "aborted_limit"].includes(body.run.status)) {
80
+ return body.run;
81
+ }
82
+ await Bun.sleep(250);
83
+ }
84
+ throw new Error("Timed out waiting for script run");
85
+ }
86
+
87
+ beforeAll(async () => {
88
+ savedEnv = { ...process.env };
89
+ await removeDbFiles(TEST_DB_PATH);
90
+ initDb(TEST_DB_PATH);
91
+ process.env.AGENT_SWARM_API_KEY = API_KEY;
92
+ process.env.API_KEY = API_KEY;
93
+ process.env.APP_URL = "https://app.example.test";
94
+ delete process.env.SCRIPT_RUN_SUPERVISOR_DISABLE;
95
+ await rm(WORKFLOW_RUNTIME_DIR, { recursive: true, force: true });
96
+ await Bun.$`bun build ./src/script-workflows/harness.ts --target bun --no-splitting --outfile ${WORKFLOW_RUNTIME_DIR}/harness.bundle.js`.quiet();
97
+ process.env.SCRIPT_WORKFLOW_RUNTIME_DIR = WORKFLOW_RUNTIME_DIR;
98
+ refreshSecretScrubberCache();
99
+
100
+ const agent = createAgent({ name: "script-workflow-e2e-worker", isLead: false, status: "idle" });
101
+ agentId = agent.id;
102
+ server = createServer((req, res) => {
103
+ route(req, res).catch((err) => {
104
+ res.writeHead(500, { "Content-Type": "application/json" });
105
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
106
+ });
107
+ });
108
+ const port = await listen(server);
109
+ baseUrl = `http://127.0.0.1:${port}`;
110
+ process.env.MCP_BASE_URL = baseUrl;
111
+ });
112
+
113
+ afterAll(async () => {
114
+ await closeServer(server);
115
+ closeDb();
116
+ await removeDbFiles(TEST_DB_PATH);
117
+ await rm(WORKFLOW_RUNTIME_DIR, { recursive: true, force: true });
118
+ for (const key of Object.keys(process.env)) {
119
+ if (!(key in savedEnv)) delete process.env[key];
120
+ }
121
+ for (const [key, value] of Object.entries(savedEnv)) {
122
+ if (value === undefined) delete process.env[key];
123
+ else process.env[key] = value;
124
+ }
125
+ refreshSecretScrubberCache();
126
+ });
127
+
128
+ beforeEach(() => {
129
+ getDb().run("DELETE FROM script_run_journal");
130
+ getDb().run("DELETE FROM script_runs");
131
+ });
132
+
133
+ describe("script workflow runtime", () => {
134
+ test("runs a durable one-off script and replays a completed step", async () => {
135
+ const source = `
136
+ export default async function main(args, ctx) {
137
+ const first = await ctx.step.swarmScript("double", {
138
+ source: "export default async (args) => args.value * 2;",
139
+ args: { value: args.value },
140
+ intent: "script-workflow-e2e"
141
+ });
142
+ const second = await ctx.step.swarmScript("double", {
143
+ source: "export default async () => 999;",
144
+ intent: "script-workflow-e2e-should-replay"
145
+ });
146
+ return { runId: ctx.run.id, first, second };
147
+ }
148
+ `;
149
+
150
+ const created = await api("/api/script-runs", {
151
+ method: "POST",
152
+ body: JSON.stringify({ source, args: { value: 7 }, background: true }),
153
+ });
154
+ expect(created.status).toBe(201);
155
+ const { id } = (await created.json()) as { id: string };
156
+
157
+ const run = await waitForRun(id);
158
+ expect(run.status).toBe("completed");
159
+ expect(run.output).toMatchObject({
160
+ runId: id,
161
+ first: { result: 14, exitCode: 0 },
162
+ second: { result: 14, exitCode: 0 },
163
+ });
164
+
165
+ const journal = listScriptRunJournalSteps(id);
166
+ expect(journal).toHaveLength(1);
167
+ expect(journal[0]?.stepKey).toBe("double");
168
+ expect(journal[0]?.stepType).toBe("swarm-script");
169
+ });
170
+ });
@@ -6,10 +6,12 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { closeDb, createAgent, getDb, initDb } from "../be/db";
7
7
  import { setScriptEmbeddingProviderForTests } from "../be/scripts/embeddings";
8
8
  import { handleCore } from "../http/core";
9
+ import { handleScriptRuns } from "../http/script-runs";
9
10
  import { handleScripts } from "../http/scripts";
10
11
  import { getPathSegments, parseQueryParams } from "../http/utils";
11
12
  import { registerScriptDeleteTool } from "../tools/script-delete";
12
13
  import { registerScriptRunTool } from "../tools/script-run";
14
+ import { registerScriptRunsTools } from "../tools/script-runs";
13
15
  import { registerScriptSearchTool } from "../tools/script-search";
14
16
  import { registerScriptUpsertTool } from "../tools/script-upsert";
15
17
  import { refreshSecretScrubberCache } from "../utils/secret-scrubber";
@@ -65,6 +67,7 @@ function buildToolServer() {
65
67
  const server = new McpServer({ name: "scripts-mcp-e2e", version: "1.0.0" });
66
68
  registerScriptSearchTool(server);
67
69
  registerScriptRunTool(server);
70
+ registerScriptRunsTools(server);
68
71
  registerScriptUpsertTool(server);
69
72
  registerScriptDeleteTool(server);
70
73
  const registered = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
@@ -74,6 +77,9 @@ function buildToolServer() {
74
77
  run: registered["script-run"]!,
75
78
  upsert: registered["script-upsert"]!,
76
79
  del: registered["script-delete"]!,
80
+ launchScriptRun: registered["launch-script-run"]!,
81
+ getScriptRun: registered["get-script-run"]!,
82
+ listScriptRuns: registered["list-script-runs"]!,
77
83
  };
78
84
  }
79
85
 
@@ -126,7 +132,10 @@ async function dispatchScriptsApi(url: string, init: RequestInit = {}): Promise<
126
132
  if (!(await handleCore(req, res, agentId, API_KEY))) {
127
133
  const pathSegments = getPathSegments(req.url || "");
128
134
  const queryParams = parseQueryParams(req.url || "");
129
- if (!(await handleScripts(req, res, pathSegments, queryParams, agentId))) {
135
+ if (
136
+ !(await handleScripts(req, res, pathSegments, queryParams, agentId)) &&
137
+ !(await handleScriptRuns(req, res, pathSegments, queryParams, agentId))
138
+ ) {
130
139
  res.writeHead(404);
131
140
  res.end("Not Found");
132
141
  }
@@ -148,6 +157,7 @@ beforeAll(async () => {
148
157
  await removeDbFiles(TEST_DB_PATH);
149
158
  initDb(TEST_DB_PATH);
150
159
  process.env.AGENT_SWARM_API_KEY = API_KEY;
160
+ process.env.SCRIPT_RUN_SUPERVISOR_DISABLE = "true";
151
161
  delete process.env.API_KEY;
152
162
  refreshSecretScrubberCache();
153
163
  setScriptEmbeddingProviderForTests(fakeEmbeddingProvider);
@@ -155,7 +165,10 @@ beforeAll(async () => {
155
165
  process.env.MCP_BASE_URL = "http://scripts-mcp-e2e.test";
156
166
  globalThis.fetch = (async (input, init) => {
157
167
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
158
- if (url.startsWith("http://scripts-mcp-e2e.test/api/scripts/")) {
168
+ if (
169
+ url.startsWith("http://scripts-mcp-e2e.test/api/scripts/") ||
170
+ url.startsWith("http://scripts-mcp-e2e.test/api/script-runs")
171
+ ) {
159
172
  return dispatchScriptsApi(url, init);
160
173
  }
161
174
  return savedFetch(input, init);
@@ -179,6 +192,8 @@ afterAll(async () => {
179
192
 
180
193
  beforeEach(() => {
181
194
  getDb().run("DELETE FROM scripts");
195
+ getDb().run("DELETE FROM script_run_journal");
196
+ getDb().run("DELETE FROM script_runs");
182
197
  });
183
198
 
184
199
  describe("script_ MCP HTTP proxy tools", () => {
@@ -226,6 +241,38 @@ describe("script_ MCP HTTP proxy tools", () => {
226
241
  expect(result.structuredContent.error).toContain("HTTP MCP transport");
227
242
  });
228
243
 
244
+ test("launches, lists, and inspects durable script workflow runs", async () => {
245
+ const tools = buildToolServer();
246
+ const source = `export default async function main() { return { ok: true }; }`;
247
+
248
+ const launched = (await tools.launchScriptRun.handler(
249
+ { source, args: { input: true }, scriptName: "mcp-script-workflow" },
250
+ meta(workerId),
251
+ )) as StructuredResult<{ id: string; status: string; url: string }>;
252
+ expect(launched.structuredContent.success).toBe(true);
253
+ expect(launched.structuredContent.status).toBe(201);
254
+ expect(launched.structuredContent.data?.status).toBe("running");
255
+ const runId = launched.structuredContent.data?.id;
256
+ expect(runId).toBeTruthy();
257
+
258
+ const listed = (await tools.listScriptRuns.handler(
259
+ { status: "running", limit: 10, offset: 0 },
260
+ meta(workerId),
261
+ )) as StructuredResult<{ runs: Array<{ id: string }>; total: number }>;
262
+ expect(listed.structuredContent.success).toBe(true);
263
+ expect(listed.structuredContent.data?.total).toBe(1);
264
+ expect(listed.structuredContent.data?.runs[0]?.id).toBe(runId);
265
+
266
+ const detail = (await tools.getScriptRun.handler(
267
+ { id: runId },
268
+ meta(workerId),
269
+ )) as StructuredResult<{ run: { id: string; status: string }; journal: unknown[] }>;
270
+ expect(detail.structuredContent.success).toBe(true);
271
+ expect(detail.structuredContent.data?.run.id).toBe(runId);
272
+ expect(detail.structuredContent.data?.run.status).toBe("running");
273
+ expect(detail.structuredContent.data?.journal).toEqual([]);
274
+ });
275
+
229
276
  test("typed SDK fixture passes upsert typecheck and wrong arg type fails", async () => {
230
277
  const tools = buildToolServer();
231
278
  const source = `
@@ -288,6 +288,39 @@ describe("runScript", () => {
288
288
  }
289
289
  });
290
290
 
291
+ test("zod import works in compiled binary mode (SCRIPT_RUNTIME_DIR)", async () => {
292
+ const tmpdir = `${process.env.TMPDIR ?? "/tmp"}/script-runtime-test-${crypto.randomUUID()}`;
293
+ await Bun.$`mkdir -p ${tmpdir}`;
294
+ try {
295
+ const runtimeSrc = new URL("../scripts-runtime", import.meta.url).pathname;
296
+ await Bun.$`bun build ${runtimeSrc}/eval-harness.ts --target bun --no-splitting --outfile ${tmpdir}/eval-harness.bundle.js`.quiet();
297
+ await Bun.$`bun build ${runtimeSrc}/stdlib/index.ts --target bun --no-splitting --outfile ${tmpdir}/stdlib.bundle.js`.quiet();
298
+ await Bun.$`bun build ${runtimeSrc}/swarm-sdk.ts --target bun --no-splitting --outfile ${tmpdir}/swarm-sdk.bundle.js`.quiet();
299
+ const zodEntry = Bun.resolveSync("zod", import.meta.dir);
300
+ await Bun.$`bun build ${zodEntry} --target bun --no-splitting --outfile ${tmpdir}/zod.bundle.js`.quiet();
301
+
302
+ process.env.SCRIPT_RUNTIME_DIR = tmpdir;
303
+
304
+ const output = await runScript({
305
+ agentId: "agent-1",
306
+ args: { name: "test" },
307
+ resources,
308
+ source: `
309
+ import { z } from "zod";
310
+ export const argsSchema = z.object({ name: z.string() });
311
+ export default async (args: z.infer<typeof argsSchema>) => ({ greeting: "hello " + args.name });
312
+ `,
313
+ });
314
+
315
+ expect(output.error).toBeUndefined();
316
+ expect(output.result).toEqual({ greeting: "hello test" });
317
+ expect(output.exitCode).toBe(0);
318
+ } finally {
319
+ delete process.env.SCRIPT_RUNTIME_DIR;
320
+ await Bun.$`rm -rf ${tmpdir}`;
321
+ }
322
+ });
323
+
291
324
  test("argsSchema rejects invalid args with a formatted Zod error", async () => {
292
325
  const output = await runScript({
293
326
  agentId: "agent-1",