@desplega.ai/agent-swarm 1.136.0 → 1.138.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 +12 -3
- package/dist/acp-adapter-6nttxe2q.js +4787 -0
- package/dist/{actions-agcbc8nd.js → actions-z9dzgh3f.js} +7 -8
- package/dist/{app-g2s78xya.js → app-wb6bc7tt.js} +4 -5
- package/dist/{assistant-yd3jkb9d.js → assistant-08pd6pgy.js} +9 -10
- package/dist/{boot-reembed-6spqw4q0.js → boot-reembed-hstr4e2c.js} +4 -4
- package/dist/{boot-reembed-b22s9w5x.js → boot-reembed-y7p7qjbf.js} +5 -5
- package/dist/{boot-scrub-logs-6h3cqkc0.js → boot-scrub-logs-azbnxwhn.js} +3 -3
- package/dist/{claude-adapter-ng5y8zd7.js → claude-adapter-rwaq6eda.js} +10 -9
- package/dist/{claude-managed-adapter-5zr3n86v.js → claude-managed-adapter-vtseey1x.js} +3 -3
- package/dist/{cli-qckgdqmr.js → cli-005yr57a.js} +2 -2
- package/dist/{cli-54ht104h.js → cli-0cka8dnm.js} +3 -3
- package/dist/{cli-gpdtxzar.js → cli-23cpnqk9.js} +1 -1
- package/dist/{cli-qhfkxrpg.js → cli-2ypjdmqg.js} +223 -474
- package/dist/{cli-bnq8h7ea.js → cli-3719ggyf.js} +17 -10
- package/dist/cli-390sgxn5.js +257 -0
- package/dist/{cli-z6e8eqrm.js → cli-47esr8ht.js} +2 -2
- package/dist/{cli-jjhnma34.js → cli-4jaj24py.js} +248 -101
- package/dist/{cli-6gwgzdgw.js → cli-69377c2a.js} +5 -5
- package/dist/{cli-ads0hfw2.js → cli-6v4d8r8c.js} +205 -75
- package/dist/{cli-mgef08pc.js → cli-8rtyss6g.js} +1 -1
- package/dist/{cli-35jknh9e.js → cli-9w7vsmbm.js} +5 -5
- package/dist/{cli-m3z53bed.js → cli-b4zdc3x6.js} +1 -1
- package/dist/{cli-fpjmcbhz.js → cli-ba87cw29.js} +4 -4
- package/dist/{cli-4gzv54ss.js → cli-bzk75kdc.js} +43 -11
- package/dist/{cli-d5ygfv18.js → cli-cywkhzqs.js} +35 -11
- package/dist/{cli-0crn5kaz.js → cli-d62cd9qs.js} +16 -4
- package/dist/{cli-5bnz1t86.js → cli-f9skf1v7.js} +51 -7
- package/dist/{cli-06ghj6pe.js → cli-fytpjraz.js} +4 -4
- package/dist/{cli-j7qp8gtf.js → cli-h7c5dam1.js} +16 -11
- package/dist/{cli-4r4jkqep.js → cli-kkn46gfr.js} +9 -3
- package/dist/{cli-tmn19cdz.js → cli-kq9htgbr.js} +415 -62
- package/dist/{cli-8v57p983.js → cli-pkmqqxp8.js} +45 -69
- package/dist/{cli-84vrhesc.js → cli-qf34ej1d.js} +1 -1
- package/dist/{cli-v3qda694.js → cli-rnyttbh0.js} +0 -5
- package/dist/cli-rx5cxzn2.js +7 -0
- package/dist/{cli-cw70hymf.js → cli-s25avqg9.js} +2 -0
- package/dist/{cli-tee8w7tq.js → cli-s6t1qv67.js} +15 -10
- package/dist/{cli-68vayryq.js → cli-tb4csb08.js} +2 -2
- package/dist/{cli-y16cesaq.js → cli-vy48y9bx.js} +2 -2
- package/dist/{cli-3y553gws.js → cli-y294regh.js} +1 -1
- package/dist/{cli-spf767vc.js → cli-y43043ws.js} +1 -1
- package/dist/{cli-hz5hp6wn.js → cli-yh2eybyt.js} +1 -1
- package/dist/{cli-edwrx42s.js → cli-zmyxt25p.js} +5 -5
- package/dist/cli.js +40 -18
- package/dist/{codex-adapter-kw11ps3j.js → codex-adapter-yzep6mvb.js} +7 -6
- package/dist/{codex-session-runner-sffb69v7.js → codex-session-runner-25e8tn72.js} +7 -6
- package/dist/{commands-0xxp4tz8.js → commands-0vv72d6a.js} +3 -3
- package/dist/{db-0npp39zd.js → db-x61ncfa1.js} +19 -3
- package/dist/{google-generative-ai-6xxdj4t3.js → google-generative-ai-zyhy3dab.js} +1 -1
- package/dist/{google-vertex-zma18wb0.js → google-vertex-t25h65yd.js} +1 -1
- package/dist/{handlers-5p7a1jx6.js → handlers-fwdcaahf.js} +9 -10
- package/dist/{hook-jjpxn4se.js → hook-mpwfgynm.js} +6 -5
- package/dist/{hook-hpefr0a8.js → hook-x18504ja.js} +8 -7
- package/dist/{http-wfp33653.js → http-83m6qdds.js} +590 -192
- package/dist/{index-pe58mf1r.js → index-1nsdwvn5.js} +2 -1
- package/dist/{index-17h657rg.js → index-38f98ck3.js} +9 -9
- package/dist/{index-m2p7ctyh.js → index-7kxvtd38.js} +24 -12
- package/dist/{index-racb4wfq.js → index-a4naswjn.js} +23 -10
- package/dist/{index-p13cfsc2.js → index-agykc6k2.js} +2 -1
- package/dist/{index-zh66wm65.js → index-dawtzfhw.js} +22 -9
- package/dist/{keepalive-wxjd633a.js → keepalive-y0m8g5wn.js} +5 -5
- package/dist/{lead-359ec2z0.js → lead-ka18ef6a.js} +36 -36
- package/dist/{maintenance-rr03qjtb.js → maintenance-x585p56c.js} +5 -5
- package/dist/{oauth-refresh-sweep-jwj22yb8.js → oauth-refresh-sweep-kdm9vj7a.js} +4 -4
- package/dist/{onboard-asnhchcv.js → onboard-5v229nvt.js} +664 -248
- package/dist/{opencode-adapter-sx38y7f7.js → opencode-adapter-46nnmnvv.js} +3 -3
- package/dist/{otel-impl-54cra6mw.js → otel-impl-k47aj4sd.js} +73 -4
- package/dist/{pi-mono-adapter-29g7axgj.js → pi-mono-adapter-wawfj09v.js} +42 -40
- package/dist/{pricing-refresh-mkyy7c2y.js → pricing-refresh-ky2gjp05.js} +5 -5
- package/dist/{rbac-roles-kb3afm95.js → rbac-roles-0j0f7s8r.js} +4 -4
- package/dist/{rbac-roles-ak56tr75.js → rbac-roles-az4wc15e.js} +3 -3
- package/dist/{render-v2-434asg49.js → render-v2-seyxq61z.js} +4 -5
- package/dist/{seed-pricing-nbmd7ah8.js → seed-pricing-fev611hx.js} +4 -4
- package/dist/{setup-wh4qmjs4.js → setup-hy404zwh.js} +2 -2
- package/dist/{worker-j1d27tjv.js → worker-g1j5hn16.js} +36 -36
- package/openapi.json +197 -27
- package/package.json +9 -3
- package/src/be/db-client.ts +58 -5
- package/src/be/db-retention.ts +550 -121
- package/src/be/db.ts +367 -55
- package/src/be/memory/raters/retrieval.ts +40 -0
- package/src/be/migrations/140_approval_request_cancelled_status.sql +64 -0
- package/src/be/pricing-normalize.ts +3 -0
- package/src/be/script-connections.ts +26 -6
- package/src/be/seed-pricing.ts +18 -8
- package/src/be/swarm-config-guard.ts +33 -0
- package/src/cli.tsx +20 -1
- package/src/commands/onboard/compose-generator.ts +63 -7
- package/src/commands/onboard/env-generator.ts +32 -4
- package/src/commands/onboard/manifest.ts +4 -1
- package/src/commands/onboard/non-interactive.ts +159 -0
- package/src/commands/onboard/steps/generate.tsx +1 -34
- package/src/commands/onboard/steps/harness-credentials.tsx +181 -1
- package/src/commands/onboard/steps/harness.tsx +20 -6
- package/src/commands/onboard/steps/review.tsx +3 -2
- package/src/commands/onboard/types.ts +48 -1
- package/src/commands/onboard.tsx +18 -13
- package/src/commands/provider-credentials.ts +17 -3
- package/src/commands/runner.ts +1 -0
- package/src/heartbeat/heartbeat.ts +47 -7
- package/src/http/approval-requests.ts +53 -8
- package/src/http/index.ts +9 -1
- package/src/http/mcp-servers.ts +2 -0
- package/src/http/memory.ts +27 -1
- package/src/http/oauth-callback.ts +33 -0
- package/src/http/repos.ts +6 -0
- package/src/http/script-connections.ts +77 -0
- package/src/http/session-data.ts +5 -1
- package/src/http/stats.ts +21 -30
- package/src/http/tasks.ts +10 -0
- package/src/oauth/presets.ts +29 -0
- package/src/otel-impl.ts +96 -1
- package/src/otel.ts +34 -0
- package/src/prompts/memories.ts +2 -2
- package/src/providers/acp-adapter.ts +325 -0
- package/src/providers/acp-swarm-events.ts +113 -0
- package/src/providers/acp-targets.ts +103 -0
- package/src/providers/claude-managed-models.ts +17 -1
- package/src/providers/codex-adapter.ts +3 -3
- package/src/providers/codex-oauth/auth-json-fs.ts +2 -2
- package/src/providers/codex-oauth/home.ts +13 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/types.ts +1 -1
- package/src/server-user.ts +1 -0
- package/src/slack/app.ts +5 -1
- package/src/tasks/worker-follow-up.ts +78 -7
- package/src/telemetry.ts +151 -0
- package/src/tests/acp-adapter.test.ts +192 -0
- package/src/tests/acp-swarm-events.test.ts +132 -0
- package/src/tests/agents-list-model-display.test.ts +20 -0
- package/src/tests/approval-cancellation-migration.test.ts +134 -0
- package/src/tests/approval-requests.test.ts +456 -2
- package/src/tests/apps-spike2.test.ts +4 -1
- package/src/tests/apps-sync-engine.test.ts +22 -18
- package/src/tests/claude-managed-adapter.test.ts +12 -0
- package/src/tests/codex-oauth-auth-json-fs.test.ts +19 -0
- package/src/tests/connection-embedded-auth.test.ts +4 -1
- package/src/tests/context-window.test.ts +2 -0
- package/src/tests/credential-check.test.ts +6 -0
- package/src/tests/db-client-busy-retry.test.ts +18 -0
- package/src/tests/db-client.test.ts +23 -0
- package/src/tests/db-retention-telemetry.test.ts +569 -0
- package/src/tests/db-retention.test.ts +456 -117
- package/src/tests/heartbeat.test.ts +140 -0
- package/src/tests/memory-http-recall-gating.test.ts +120 -13
- package/src/tests/memory-store.test.ts +41 -0
- package/src/tests/migration-046-budgets.test.ts +27 -5
- package/src/tests/oauth-presets.test.ts +17 -5
- package/src/tests/onboard-compose.test.ts +99 -0
- package/src/tests/onboard-env.test.ts +43 -0
- package/src/tests/onboard-manifest.test.ts +17 -1
- package/src/tests/onboard-non-interactive.test.ts +89 -0
- package/src/tests/pool-affinity.test.ts +100 -0
- package/src/tests/promote-draft-task-route.test.ts +32 -1
- package/src/tests/sandbox-spawn-test-helpers.ts +3 -0
- package/src/tests/sandboxed-process.test.ts +2 -2
- package/src/tests/schedule-target-type.test.ts +6 -3
- package/src/tests/script-connections-http.test.ts +4 -3
- package/src/tests/script-connections.test.ts +6 -1
- package/src/tests/script-workflows-runtime-e2e.test.ts +36 -28
- package/src/tests/scripts-external-api.test.ts +8 -5
- package/src/tests/scripts-http.test.ts +6 -3
- package/src/tests/scripts-mcp-e2e.test.ts +65 -60
- package/src/tests/scripts-retention.test.ts +6 -3
- package/src/tests/scripts-runtime-identity.test.ts +4 -2
- package/src/tests/scripts-runtime.test.ts +18 -16
- package/src/tests/session-costs-model-key-normalize.test.ts +2 -0
- package/src/tests/slack-read-boundaries.test.ts +3 -1
- package/src/tests/task-lifecycle-telemetry.test.ts +1 -0
- package/src/tests/task-tools-ctx.test.ts +88 -1
- package/src/tests/telemetry-init.test.ts +61 -0
- package/src/tests/tool-annotations.test.ts +4 -1
- package/src/tests/vendored-openapi.test.ts +15 -2
- package/src/tests/workflow-e2e.test.ts +4 -1
- package/src/tests/workflow-engine-v2.test.ts +5 -2
- package/src/tests/workflow-executors.test.ts +24 -19
- package/src/tests/workflow-http-v2.test.ts +17 -2
- package/src/tests/workflow-llm.test.ts +57 -0
- package/src/tests/workflow-retry-v2.test.ts +142 -1
- package/src/tests/workflow-swarm-script.test.ts +20 -17
- package/src/tools/get-task-details.ts +1 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +7 -1
- package/src/tools/memory-search.ts +27 -2
- package/src/tools/send-task.ts +28 -10
- package/src/tools/task-action.ts +13 -5
- package/src/types.ts +31 -2
- package/src/utils/context-window.ts +2 -0
- package/src/workflows/approval-notifications.ts +72 -0
- package/src/workflows/checkpoint.ts +16 -9
- package/src/workflows/completed-step-routing.ts +31 -0
- package/src/workflows/engine.ts +59 -43
- package/src/workflows/executors/human-in-the-loop.ts +38 -4
- package/src/workflows/executors/raw-llm.ts +8 -6
- package/src/workflows/executors/validate.ts +6 -5
- package/src/workflows/executors/workflow-llm.ts +35 -0
- package/src/workflows/resume.ts +90 -13
- package/src/workflows/retry-poller.ts +8 -1
- package/vendored-openapi/README.md +4 -2
- package/vendored-openapi/manifest.json +33 -0
- package/vendored-openapi/microsoft-graph.json +781 -0
- package/dist/cli-5tnr64pm.js +0 -29
- package/dist/{anthropic-messages-pm7pp23c.js → anthropic-messages-tvgm75yc.js} +3 -3
- package/dist/{azure-openai-responses-cwesc845.js → azure-openai-responses-hed7waek.js} +4 -4
- package/dist/{cli-s9gpxh9q.js → cli-5kmzp1wx.js} +0 -0
- package/dist/{cli-s73zwwqn.js → cli-xjx3fbtn.js} +4 -4
- package/dist/{openai-codex-responses-xxca1qr1.js → openai-codex-responses-gv3n8rg0.js} +3 -3
- package/dist/{openai-completions-yzftqrft.js → openai-completions-dkrj9433.js} +8 -8
- package/dist/{openai-responses-e4dcwm9g.js → openai-responses-spk6t7kh.js} +11 -11
- package/dist/{openrouter-images-asqqnbvh.js → openrouter-images-71hthwh9.js} +3 -3
|
@@ -111,6 +111,7 @@ describe("vendored OpenAPI", () => {
|
|
|
111
111
|
"linear",
|
|
112
112
|
"jira",
|
|
113
113
|
"gmail",
|
|
114
|
+
"microsoft-graph",
|
|
114
115
|
]);
|
|
115
116
|
for (const entry of manifest.integrations) {
|
|
116
117
|
const specText = readFileSync(path.join(directory, entry.specFile), "utf-8");
|
|
@@ -139,6 +140,17 @@ describe("vendored OpenAPI", () => {
|
|
|
139
140
|
const refreshed = await refreshScriptConnection(connection.id);
|
|
140
141
|
expect(refreshed?.generationError).toBeNull();
|
|
141
142
|
expect(refreshed?.version).toBe(connection.version + 1);
|
|
143
|
+
|
|
144
|
+
const microsoftGraph = await upsertScriptConnection({
|
|
145
|
+
slug: "microsoftGraph",
|
|
146
|
+
kind: "openapi",
|
|
147
|
+
openapiSpecSourceKind: "vendored",
|
|
148
|
+
openapiSpecSource: "microsoft-graph",
|
|
149
|
+
});
|
|
150
|
+
expect(microsoftGraph.baseUrl).toBe("https://graph.microsoft.com/v1.0");
|
|
151
|
+
expect(microsoftGraph.generationError).toBeNull();
|
|
152
|
+
expect(microsoftGraph.generatedTypes).toContain("sendChannelMessage");
|
|
153
|
+
expect(microsoftGraph.generatedTypes).toContain('"team-id"');
|
|
142
154
|
});
|
|
143
155
|
|
|
144
156
|
test("resolves vendored specs from the module location when cwd is unrelated (npm-package install)", () => {
|
|
@@ -187,7 +199,7 @@ describe("vendored OpenAPI", () => {
|
|
|
187
199
|
partial: boolean;
|
|
188
200
|
};
|
|
189
201
|
expect(down.partial).toBe(true);
|
|
190
|
-
expect(down.entries).toHaveLength(
|
|
202
|
+
expect(down.entries).toHaveLength(6);
|
|
191
203
|
expect(down.entries.every((entry) => entry.feeds.includes("blessed"))).toBe(true);
|
|
192
204
|
|
|
193
205
|
globalThis.fetch = (async () =>
|
|
@@ -209,13 +221,14 @@ describe("vendored OpenAPI", () => {
|
|
|
209
221
|
partial: boolean;
|
|
210
222
|
};
|
|
211
223
|
expect(merged.partial).toBe(false);
|
|
212
|
-
expect(merged.entries.slice(0,
|
|
224
|
+
expect(merged.entries.slice(0, 6).every((entry) => entry.feeds.includes("blessed"))).toBe(true);
|
|
213
225
|
expect(merged.entries.map((entry) => entry.slug)).toEqual([
|
|
214
226
|
"github",
|
|
215
227
|
"slack",
|
|
216
228
|
"linear",
|
|
217
229
|
"jira",
|
|
218
230
|
"gmail",
|
|
231
|
+
"microsoft-graph",
|
|
219
232
|
"stripe",
|
|
220
233
|
]);
|
|
221
234
|
});
|
|
@@ -21,6 +21,9 @@ import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
|
21
21
|
import { SwarmScriptExecutor } from "../workflows/executors/swarm-script";
|
|
22
22
|
import { setupWorkflowResumeListener } from "../workflows/resume";
|
|
23
23
|
import { interpolate } from "../workflows/template";
|
|
24
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
25
|
+
|
|
26
|
+
const spawnDescribe = describe.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
24
27
|
|
|
25
28
|
const TEST_DB_PATH = "./test-workflow-e2e.sqlite";
|
|
26
29
|
const API_KEY = "test-workflow-e2e-key-1234567890";
|
|
@@ -133,7 +136,7 @@ beforeEach(async () => {
|
|
|
133
136
|
await client.run("DELETE FROM workflows");
|
|
134
137
|
});
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
spawnDescribe("workflow e2e swarm-script", () => {
|
|
137
140
|
test("swarm-script full workflow run executes through the engine", async () => {
|
|
138
141
|
await saveScript(
|
|
139
142
|
"square",
|
|
@@ -25,6 +25,9 @@ import {
|
|
|
25
25
|
import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
26
26
|
import { ScriptExecutor } from "../workflows/executors/script";
|
|
27
27
|
import { resolveInputs } from "../workflows/input";
|
|
28
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
29
|
+
|
|
30
|
+
const skip = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
28
31
|
|
|
29
32
|
const TEST_DB_PATH = "./test-workflow-engine-v2.sqlite";
|
|
30
33
|
|
|
@@ -412,7 +415,7 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
412
415
|
// RLIMIT_AS), so the script dies before the watchdog behavior under test.
|
|
413
416
|
// Linux CI is the enforcing environment; the skip only unblocks local
|
|
414
417
|
// macOS pushes now that pre-push tests are blocking (#1216).
|
|
415
|
-
test.skipIf(process.platform === "darwin")(
|
|
418
|
+
test.skipIf(process.platform === "darwin" || SKIP_SANDBOX_SPAWN_TESTS)(
|
|
416
419
|
"inline script timeout also extends the workflow step watchdog",
|
|
417
420
|
async () => {
|
|
418
421
|
const registry = createTestRegistry();
|
|
@@ -828,7 +831,7 @@ describe("Workflow Engine v2 (Phase 3)", () => {
|
|
|
828
831
|
expect(step?.output).toBeUndefined();
|
|
829
832
|
});
|
|
830
833
|
|
|
831
|
-
|
|
834
|
+
skip("bundled ralph-loop scripts execute with trigger and upstream values passed as args", async () => {
|
|
832
835
|
const receipt = (await Bun.file(
|
|
833
836
|
new URL("../../docs-site/public/receipts/workflows/ralph-loop.json", import.meta.url),
|
|
834
837
|
).json()) as { definition: WorkflowDefinition };
|
|
@@ -20,6 +20,9 @@ import { createExecutorRegistry } from "../workflows/executors/registry";
|
|
|
20
20
|
import { ScriptExecutor, ScriptOutputSchema } from "../workflows/executors/script";
|
|
21
21
|
import { ValidateExecutor, ValidateOutputSchema } from "../workflows/executors/validate";
|
|
22
22
|
import { VcsExecutor, VcsOutputSchema } from "../workflows/executors/vcs";
|
|
23
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
24
|
+
|
|
25
|
+
const skip = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
23
26
|
|
|
24
27
|
const TEST_DB_PATH = "./test-workflow-executors.sqlite";
|
|
25
28
|
|
|
@@ -482,7 +485,7 @@ describe("ScriptExecutor", () => {
|
|
|
482
485
|
).toBe(false);
|
|
483
486
|
});
|
|
484
487
|
|
|
485
|
-
|
|
488
|
+
skip("runs bash script and captures stdout", async () => {
|
|
486
489
|
const result = await executor.run(input({ runtime: "bash", script: "echo 'hello world'" }, {}));
|
|
487
490
|
expect(result.status).toBe("success");
|
|
488
491
|
const out = result.output as { exitCode: number; stdout: string; stderr: string };
|
|
@@ -492,7 +495,7 @@ describe("ScriptExecutor", () => {
|
|
|
492
495
|
expect(result.nextPort).toBe("success");
|
|
493
496
|
});
|
|
494
497
|
|
|
495
|
-
|
|
498
|
+
skip("marks step failed and captures stderr on non-zero exit", async () => {
|
|
496
499
|
const result = await executor.run(
|
|
497
500
|
input({ runtime: "bash", script: "echo err >&2; exit 1" }, {}),
|
|
498
501
|
);
|
|
@@ -503,7 +506,7 @@ describe("ScriptExecutor", () => {
|
|
|
503
506
|
expect(out.stderr).toBe("err");
|
|
504
507
|
});
|
|
505
508
|
|
|
506
|
-
|
|
509
|
+
skip("marks step failed on non-zero exit code (exit 1)", async () => {
|
|
507
510
|
const result = await executor.run(input({ runtime: "bash", script: "exit 1" }, {}));
|
|
508
511
|
expect(result.status).toBe("failed");
|
|
509
512
|
expect(result.error).toBe("Script exited with code 1");
|
|
@@ -511,7 +514,7 @@ describe("ScriptExecutor", () => {
|
|
|
511
514
|
expect(out?.exitCode).toBe(1);
|
|
512
515
|
});
|
|
513
516
|
|
|
514
|
-
|
|
517
|
+
skip("marks step failed with exit code in error when no stderr (exit 42)", async () => {
|
|
515
518
|
const result = await executor.run(input({ runtime: "bash", script: "exit 42" }, {}));
|
|
516
519
|
expect(result.status).toBe("failed");
|
|
517
520
|
expect(result.error).toBe("Script exited with code 42");
|
|
@@ -519,7 +522,7 @@ describe("ScriptExecutor", () => {
|
|
|
519
522
|
expect(out?.exitCode).toBe(42);
|
|
520
523
|
});
|
|
521
524
|
|
|
522
|
-
|
|
525
|
+
skip("runs TypeScript script via bun", async () => {
|
|
523
526
|
const result = await executor.run(
|
|
524
527
|
input({ runtime: "ts", script: "console.log('ts works')" }, {}),
|
|
525
528
|
);
|
|
@@ -534,7 +537,7 @@ describe("ScriptExecutor", () => {
|
|
|
534
537
|
expect(valid.success).toBe(true);
|
|
535
538
|
});
|
|
536
539
|
|
|
537
|
-
|
|
540
|
+
skip("keeps raw {exitCode, stdout, stderr} when stdout is not valid JSON", async () => {
|
|
538
541
|
const result = await executor.run(
|
|
539
542
|
input({ runtime: "bash", script: "echo 'not-json {at all'" }, {}),
|
|
540
543
|
);
|
|
@@ -549,7 +552,7 @@ describe("ScriptExecutor", () => {
|
|
|
549
552
|
expect(Object.keys(out).sort()).toEqual(["exitCode", "stderr", "stdout"]);
|
|
550
553
|
});
|
|
551
554
|
|
|
552
|
-
|
|
555
|
+
skip("populates structured output on timeout instead of leaving it null", async () => {
|
|
553
556
|
const result = await executor.run(
|
|
554
557
|
input({ runtime: "bash", script: "sleep 5", timeout: 1000 }, {}),
|
|
555
558
|
);
|
|
@@ -562,7 +565,7 @@ describe("ScriptExecutor", () => {
|
|
|
562
565
|
expect(out.stderr).toContain("Script timed out after 1000ms");
|
|
563
566
|
});
|
|
564
567
|
|
|
565
|
-
|
|
568
|
+
skip("a timed-out script is TERMINATED, not just abandoned (Promise.race regression)", async () => {
|
|
566
569
|
// A `sleep` is not bounded by the CPU ulimit, so before this fix the
|
|
567
570
|
// wall-clock timeout only abandoned the promise and the child kept
|
|
568
571
|
// running — long enough to finish its side effect after the step failed.
|
|
@@ -589,7 +592,7 @@ describe("ScriptExecutor", () => {
|
|
|
589
592
|
|
|
590
593
|
// ─── Sandbox regression tests (superagent.sh c27edfd7, finding b132d7c5) ──
|
|
591
594
|
|
|
592
|
-
|
|
595
|
+
skip("child process never inherits the server's secrets — env is scrubbed, not passed through", async () => {
|
|
593
596
|
const savedKey = process.env.AGENT_SWARM_API_KEY;
|
|
594
597
|
process.env.AGENT_SWARM_API_KEY = "super-secret-operator-bearer";
|
|
595
598
|
process.env.SOME_OTHER_SERVER_SECRET = "also-should-not-leak";
|
|
@@ -618,9 +621,11 @@ describe("ScriptExecutor", () => {
|
|
|
618
621
|
// RLIMIT_AS); the spawned script fails before the behavior under test runs.
|
|
619
622
|
// Linux CI is the enforcing environment for these paths; the skips only
|
|
620
623
|
// unblock local macOS pushes now that pre-push tests are blocking (#1216).
|
|
621
|
-
const
|
|
624
|
+
const spawnTestUnlessMacOS = test.skipIf(
|
|
625
|
+
process.platform === "darwin" || SKIP_SANDBOX_SPAWN_TESTS,
|
|
626
|
+
);
|
|
622
627
|
|
|
623
|
-
|
|
628
|
+
spawnTestUnlessMacOS(
|
|
624
629
|
"resource ulimits actually apply to the spawned process (not just documented)",
|
|
625
630
|
async () => {
|
|
626
631
|
const result = await executor.run(input({ runtime: "bash", script: "ulimit -v" }, {}));
|
|
@@ -632,7 +637,7 @@ describe("ScriptExecutor", () => {
|
|
|
632
637
|
},
|
|
633
638
|
);
|
|
634
639
|
|
|
635
|
-
|
|
640
|
+
skip("no explicit cwd: runs in a scoped tmpdir, not the server's working directory", async () => {
|
|
636
641
|
const result = await executor.run(input({ runtime: "bash", script: "pwd" }, {}));
|
|
637
642
|
expect(result.status).toBe("success");
|
|
638
643
|
const out = result.output as { stdout: string };
|
|
@@ -642,7 +647,7 @@ describe("ScriptExecutor", () => {
|
|
|
642
647
|
|
|
643
648
|
// ─── Codex review follow-ups (PR #1112, review 4876200033) ──────────────
|
|
644
649
|
|
|
645
|
-
|
|
650
|
+
spawnTestUnlessMacOS(
|
|
646
651
|
"truncated stdout carries an explicit marker instead of silently presenting a partial result as complete (PRRT_kwDOQr3Tmc6XCRu1)",
|
|
647
652
|
async () => {
|
|
648
653
|
const result = await executor.run(
|
|
@@ -658,7 +663,7 @@ describe("ScriptExecutor", () => {
|
|
|
658
663
|
},
|
|
659
664
|
);
|
|
660
665
|
|
|
661
|
-
|
|
666
|
+
spawnTestUnlessMacOS(
|
|
662
667
|
"drain-deadline snapshot keeps the partial output already read instead of discarding it as empty (PRRT_kwDOQr3Tmc6XCRuy)",
|
|
663
668
|
async () => {
|
|
664
669
|
// The direct child prints known output, backgrounds a descendant that
|
|
@@ -692,7 +697,7 @@ describe("ScriptExecutor", () => {
|
|
|
692
697
|
// running them: an interpolated arg literally named `--eval=<code>` was a
|
|
693
698
|
// second, attacker-controlled script.
|
|
694
699
|
|
|
695
|
-
|
|
700
|
+
skip("bun runtime: an arg shaped like --eval=<code> is inert data, not executed", async () => {
|
|
696
701
|
const result = await executor.run(
|
|
697
702
|
input(
|
|
698
703
|
{
|
|
@@ -713,7 +718,7 @@ describe("ScriptExecutor", () => {
|
|
|
713
718
|
expect(JSON.parse(out.stdout)).toEqual(["--eval=console.log('INJECTED')"]);
|
|
714
719
|
});
|
|
715
720
|
|
|
716
|
-
|
|
721
|
+
skip("bun runtime: an arg shaped like --preload=<path> is inert data, not loaded", async () => {
|
|
717
722
|
const result = await executor.run(
|
|
718
723
|
input(
|
|
719
724
|
{
|
|
@@ -736,7 +741,7 @@ describe("ScriptExecutor", () => {
|
|
|
736
741
|
]);
|
|
737
742
|
});
|
|
738
743
|
|
|
739
|
-
|
|
744
|
+
skip("bun runtime: normal args keep their previous argv indexing after the -- fix", async () => {
|
|
740
745
|
const result = await executor.run(
|
|
741
746
|
input(
|
|
742
747
|
{
|
|
@@ -753,7 +758,7 @@ describe("ScriptExecutor", () => {
|
|
|
753
758
|
expect(JSON.parse(out.stdout)).toEqual(["hello", "world"]);
|
|
754
759
|
});
|
|
755
760
|
|
|
756
|
-
|
|
761
|
+
skip("bash runtime: an arg shaped like --eval=<code> was already inert (documents why -- is not added)", async () => {
|
|
757
762
|
// bash -c script [$0 [$1 ...]] binds the first trailing arg to $0, not to
|
|
758
763
|
// an option — there is no flag-reparsing surface to close here, and
|
|
759
764
|
// adding `--` would shift $0 into args[0], breaking existing workflows.
|
|
@@ -772,7 +777,7 @@ describe("ScriptExecutor", () => {
|
|
|
772
777
|
expect(out.stdout).toBe("[--eval=INJECTED][second]");
|
|
773
778
|
});
|
|
774
779
|
|
|
775
|
-
|
|
780
|
+
skip("python runtime: an arg shaped like -c <code> was already inert (documents why -- is not added)", async () => {
|
|
776
781
|
// python3 -c code also stops option parsing after the -c operand —
|
|
777
782
|
// trailing args (even another literal -c) land verbatim in sys.argv.
|
|
778
783
|
const result = await executor.run(
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
} from "node:http";
|
|
10
10
|
import {
|
|
11
11
|
closeDb,
|
|
12
|
+
createApprovalRequest,
|
|
12
13
|
createUser,
|
|
13
14
|
createWorkflowRun,
|
|
14
15
|
createWorkflowRunStep,
|
|
16
|
+
getApprovalRequestById,
|
|
15
17
|
getDbClient,
|
|
16
18
|
getWorkflowRun,
|
|
17
19
|
getWorkflowVersions,
|
|
@@ -964,11 +966,20 @@ describe("Workflow HTTP API v2", () => {
|
|
|
964
966
|
await createWorkflowRun({ id: runId, workflowId: workflow.id });
|
|
965
967
|
|
|
966
968
|
// Create a step in 'running' state
|
|
969
|
+
const stepId = crypto.randomUUID();
|
|
967
970
|
await createWorkflowRunStep({
|
|
968
|
-
id:
|
|
971
|
+
id: stepId,
|
|
969
972
|
runId,
|
|
970
973
|
nodeId: "n1",
|
|
971
|
-
nodeType: "
|
|
974
|
+
nodeType: "human-in-the-loop",
|
|
975
|
+
});
|
|
976
|
+
const approval = await createApprovalRequest({
|
|
977
|
+
id: crypto.randomUUID(),
|
|
978
|
+
title: "Approve release",
|
|
979
|
+
questions: [{ id: "approve", type: "approval", label: "Approve?" }],
|
|
980
|
+
approvers: { policy: "any" },
|
|
981
|
+
workflowRunId: runId,
|
|
982
|
+
workflowRunStepId: stepId,
|
|
972
983
|
});
|
|
973
984
|
|
|
974
985
|
// Cancel the run
|
|
@@ -988,6 +999,10 @@ describe("Workflow HTTP API v2", () => {
|
|
|
988
999
|
expect(run.status).toBe("cancelled");
|
|
989
1000
|
expect(run.error).toBe("Test cancellation");
|
|
990
1001
|
expect(run.finishedAt).toBeDefined();
|
|
1002
|
+
expect(await getApprovalRequestById(approval.id)).toMatchObject({
|
|
1003
|
+
status: "cancelled",
|
|
1004
|
+
resolutionReason: "Test cancellation",
|
|
1005
|
+
});
|
|
991
1006
|
});
|
|
992
1007
|
|
|
993
1008
|
test("returns 400 for already completed run", async () => {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveWorkflowLlmConfig } from "../workflows/executors/workflow-llm";
|
|
3
|
+
|
|
4
|
+
describe("resolveWorkflowLlmConfig", () => {
|
|
5
|
+
test("routes OpenRouter credentials through the configured gateway", async () => {
|
|
6
|
+
const config = await resolveWorkflowLlmConfig(undefined, {
|
|
7
|
+
OPENROUTER_API_KEY: "sk-or-test",
|
|
8
|
+
OPENROUTER_BASE_URL: "https://gateway.example.test/v1",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(config).toEqual({
|
|
12
|
+
apiKey: "sk-or-test",
|
|
13
|
+
baseURL: "https://gateway.example.test/v1",
|
|
14
|
+
model: "google/gemini-3-flash-preview",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("routes OpenAI credentials to the SDK default endpoint", async () => {
|
|
19
|
+
const config = await resolveWorkflowLlmConfig(undefined, {
|
|
20
|
+
OPENAI_API_KEY: "sk-openai-test",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(config).toEqual({
|
|
24
|
+
apiKey: "sk-openai-test",
|
|
25
|
+
baseURL: undefined,
|
|
26
|
+
model: "gpt-5.4-mini",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("does not reuse the memory-rater model as a workflow default", async () => {
|
|
31
|
+
const previous = process.env.MEMORY_RATER_MODEL;
|
|
32
|
+
process.env.MEMORY_RATER_MODEL = "openrouter/anthropic/claude-sonnet-4-5";
|
|
33
|
+
try {
|
|
34
|
+
const config = await resolveWorkflowLlmConfig(undefined, {
|
|
35
|
+
OPENAI_API_KEY: "sk-openai-test",
|
|
36
|
+
});
|
|
37
|
+
expect(config.model).toBe("gpt-5.4-mini");
|
|
38
|
+
} finally {
|
|
39
|
+
if (previous === undefined) delete process.env.MEMORY_RATER_MODEL;
|
|
40
|
+
else process.env.MEMORY_RATER_MODEL = previous;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("preserves an explicit provider-compatible model", async () => {
|
|
45
|
+
const config = await resolveWorkflowLlmConfig("openai/gpt-5.4", {
|
|
46
|
+
OPENAI_API_KEY: "sk-openai-test",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(config.model).toBe("gpt-5.4");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("rejects credential kinds without an OpenAI-compatible endpoint", async () => {
|
|
53
|
+
await expect(
|
|
54
|
+
resolveWorkflowLlmConfig(undefined, { ANTHROPIC_API_KEY: "sk-ant-test" }),
|
|
55
|
+
).rejects.toThrow("do not support the resolved anthropic credential yet");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "../workflows/executors/base";
|
|
27
27
|
import { ExecutorRegistry } from "../workflows/executors/registry";
|
|
28
28
|
import { recoverIncompleteRuns } from "../workflows/recovery";
|
|
29
|
+
import { retryFailedRun } from "../workflows/resume";
|
|
29
30
|
import { calculateDelay, startRetryPoller, stopRetryPoller } from "../workflows/retry-poller";
|
|
30
31
|
import { interpolate } from "../workflows/template";
|
|
31
32
|
|
|
@@ -40,7 +41,10 @@ class FailOnceExecutor extends BaseExecutor<
|
|
|
40
41
|
typeof FailOnceExecutor.schema,
|
|
41
42
|
typeof FailOnceExecutor.outSchema
|
|
42
43
|
> {
|
|
43
|
-
static readonly schema = z.object({
|
|
44
|
+
static readonly schema = z.object({
|
|
45
|
+
failUntilAttempt: z.number().default(1),
|
|
46
|
+
message: z.string().min(1).optional(),
|
|
47
|
+
});
|
|
44
48
|
static readonly outSchema = z.object({ attempt: z.number() });
|
|
45
49
|
|
|
46
50
|
readonly type = "fail-once";
|
|
@@ -91,10 +95,13 @@ class NotifyStubExecutor extends BaseExecutor<
|
|
|
91
95
|
readonly outputSchema = NotifyStubExecutor.outSchema;
|
|
92
96
|
|
|
93
97
|
protected async execute(): Promise<ExecutorResult<z.infer<typeof NotifyStubExecutor.outSchema>>> {
|
|
98
|
+
notifyCounter++;
|
|
94
99
|
return { status: "success", output: { sent: true } };
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
let notifyCounter = 0;
|
|
104
|
+
|
|
98
105
|
// ─── Mock Dependencies ───────────────────────────────────────
|
|
99
106
|
|
|
100
107
|
import * as db from "../be/db";
|
|
@@ -227,6 +234,59 @@ describe("Workflow Retry v2 (Phase 4)", () => {
|
|
|
227
234
|
});
|
|
228
235
|
|
|
229
236
|
describe("Retry Poller", () => {
|
|
237
|
+
test("rehydrates completed step outputs before interpolating retry inputs", async () => {
|
|
238
|
+
const workflow = await makeWorkflow({
|
|
239
|
+
nodes: [
|
|
240
|
+
{
|
|
241
|
+
id: "producer",
|
|
242
|
+
type: "echo",
|
|
243
|
+
config: { message: "release payload" },
|
|
244
|
+
next: "consumer",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "consumer",
|
|
248
|
+
type: "fail-once",
|
|
249
|
+
inputs: { payload: "producer.echo" },
|
|
250
|
+
config: { failUntilAttempt: 1, message: "{{payload}}" },
|
|
251
|
+
retry: {
|
|
252
|
+
strategy: "static",
|
|
253
|
+
maxRetries: 2,
|
|
254
|
+
baseDelayMs: 1,
|
|
255
|
+
maxDelayMs: 1,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
failCounter = 0;
|
|
262
|
+
const runId = await startWorkflowExecution(workflow, {}, registry);
|
|
263
|
+
const initialRun = (await getWorkflowRun(runId))!;
|
|
264
|
+
const consumerStep = (await getWorkflowRunStepsByRunId(runId)).find(
|
|
265
|
+
(step) => step.nodeId === "consumer",
|
|
266
|
+
)!;
|
|
267
|
+
expect(consumerStep.status).toBe("failed");
|
|
268
|
+
expect(consumerStep.input).toMatchObject({ producer: { echo: "release payload" } });
|
|
269
|
+
|
|
270
|
+
const { producer: _lostOutput, ...contextWithoutProducer } = initialRun.context as Record<
|
|
271
|
+
string,
|
|
272
|
+
unknown
|
|
273
|
+
>;
|
|
274
|
+
await updateWorkflowRun(runId, { context: contextWithoutProducer });
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
startRetryPoller(registry, 5);
|
|
278
|
+
for (let i = 0; i < 100 && (await getWorkflowRun(runId))?.status !== "completed"; i++) {
|
|
279
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
stopRetryPoller();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const completedRun = await getWorkflowRun(runId);
|
|
286
|
+
expect(completedRun?.status).toBe("completed");
|
|
287
|
+
expect(completedRun?.context).toMatchObject({ producer: { echo: "release payload" } });
|
|
288
|
+
});
|
|
289
|
+
|
|
230
290
|
test("rehydrates requester attribution before retrying an executor", async () => {
|
|
231
291
|
const user = await createUser({ name: "Retry Requester" });
|
|
232
292
|
const workflow = await makeWorkflow({
|
|
@@ -445,4 +505,85 @@ describe("Workflow Retry v2 (Phase 4)", () => {
|
|
|
445
505
|
expect(run!.status).toBe("completed");
|
|
446
506
|
});
|
|
447
507
|
});
|
|
508
|
+
|
|
509
|
+
describe("External retry", () => {
|
|
510
|
+
test("does not execute a node behind a gate port that was not taken", async () => {
|
|
511
|
+
const workflow = await makeWorkflow({
|
|
512
|
+
nodes: [
|
|
513
|
+
{
|
|
514
|
+
id: "gate",
|
|
515
|
+
type: "echo",
|
|
516
|
+
config: { message: "gate" },
|
|
517
|
+
next: { true: "inactive", false: "taken" },
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
id: "inactive",
|
|
521
|
+
type: "notify",
|
|
522
|
+
config: { channel: "test", template: "must not run" },
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: "taken",
|
|
526
|
+
type: "echo",
|
|
527
|
+
config: { message: "taken" },
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
const runId = crypto.randomUUID();
|
|
532
|
+
await createWorkflowRun({ id: runId, workflowId: workflow.id, triggerData: {} });
|
|
533
|
+
|
|
534
|
+
const gateStepId = crypto.randomUUID();
|
|
535
|
+
await createWorkflowRunStep({
|
|
536
|
+
id: gateStepId,
|
|
537
|
+
runId,
|
|
538
|
+
nodeId: "gate",
|
|
539
|
+
nodeType: "echo",
|
|
540
|
+
input: {},
|
|
541
|
+
});
|
|
542
|
+
await updateWorkflowRunStep(gateStepId, {
|
|
543
|
+
status: "completed",
|
|
544
|
+
output: { echo: "gate" },
|
|
545
|
+
nextPort: "false",
|
|
546
|
+
finishedAt: new Date().toISOString(),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const takenStepId = crypto.randomUUID();
|
|
550
|
+
await createWorkflowRunStep({
|
|
551
|
+
id: takenStepId,
|
|
552
|
+
runId,
|
|
553
|
+
nodeId: "taken",
|
|
554
|
+
nodeType: "echo",
|
|
555
|
+
input: {},
|
|
556
|
+
});
|
|
557
|
+
await updateWorkflowRunStep(takenStepId, {
|
|
558
|
+
status: "completed",
|
|
559
|
+
output: { echo: "taken" },
|
|
560
|
+
finishedAt: new Date().toISOString(),
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const failedStepId = crypto.randomUUID();
|
|
564
|
+
await createWorkflowRunStep({
|
|
565
|
+
id: failedStepId,
|
|
566
|
+
runId,
|
|
567
|
+
nodeId: "inactive",
|
|
568
|
+
nodeType: "notify",
|
|
569
|
+
input: {},
|
|
570
|
+
});
|
|
571
|
+
await updateWorkflowRunStep(failedStepId, {
|
|
572
|
+
status: "failed",
|
|
573
|
+
error: "retry target failed",
|
|
574
|
+
finishedAt: new Date().toISOString(),
|
|
575
|
+
});
|
|
576
|
+
await updateWorkflowRun(runId, { status: "failed", error: "retry target failed" });
|
|
577
|
+
|
|
578
|
+
notifyCounter = 0;
|
|
579
|
+
await retryFailedRun(runId, registry);
|
|
580
|
+
|
|
581
|
+
expect(notifyCounter).toBe(0);
|
|
582
|
+
expect((await getWorkflowRun(runId))?.status).toBe("completed");
|
|
583
|
+
expect(
|
|
584
|
+
(await getWorkflowRunStepsByRunId(runId)).find((step) => step.nodeId === "inactive")
|
|
585
|
+
?.status,
|
|
586
|
+
).toBe("cancelled");
|
|
587
|
+
});
|
|
588
|
+
});
|
|
448
589
|
});
|