@desplega.ai/agent-swarm 1.167.0 → 1.169.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/dist/{acp-adapter-zpvy6321.js → acp-adapter-m47zfx4a.js} +2 -2
- package/dist/{actions-fq2zrwtf.js → actions-ws7k3tvq.js} +25 -19
- package/dist/{amp-adapter-5gbk7501.js → amp-adapter-nvv8k8j5.js} +1 -1
- package/dist/{app-nctetn1e.js → app-80w6qspx.js} +5 -5
- package/dist/{assistant-0aq8gjvj.js → assistant-jtq439kw.js} +29 -20
- package/dist/{boot-reembed-1ze5853n.js → boot-reembed-3nc0aaed.js} +4 -4
- package/dist/{boot-reembed-fmp5ww69.js → boot-reembed-feda8wn8.js} +4 -4
- package/dist/{boot-scrub-sweep-cbp8xd4e.js → boot-scrub-sweep-grsa2er0.js} +5 -5
- package/dist/{boot-seeds-22tx8a53.js → boot-seeds-gcfgq44f.js} +7 -7
- package/dist/{claude-adapter-0jhgeh1d.js → claude-adapter-pjv2q7t8.js} +7 -7
- package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-0wa8se82.js} +2 -2
- package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-zxa9p9tx.js} +1 -1
- package/dist/{claude-sdk-session-wh77rh83.js → claude-sdk-session-c9j6k9kx.js} +8772 -8663
- package/dist/{cli-c2tpw9ck.js → cli-3ry5gx4f.js} +1 -1
- package/dist/{cli-wydz3feg.js → cli-61tbnjpf.js} +214 -2472
- package/dist/{cli-cexctgn5.js → cli-7vz49g7h.js} +101 -34
- package/dist/{cli-6qxmjpem.js → cli-8xjst8q2.js} +34 -22
- package/dist/{cli-tx2zvn23.js → cli-91r17hb1.js} +3 -3
- package/dist/{cli-577bxkf6.js → cli-a8vps458.js} +182 -2208
- package/dist/{cli-tc5zec7v.js → cli-b7y8113e.js} +1 -1
- package/dist/{cli-69egj654.js → cli-bqrs5ycj.js} +1 -1
- package/dist/{cli-cf2vgn9d.js → cli-dv76vq5d.js} +30 -28
- package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
- package/dist/{cli-c8r99akr.js → cli-e5qk3m2a.js} +209 -31
- package/dist/{cli-z1dqhc9y.js → cli-eqfbsxsn.js} +3 -3
- package/dist/{cli-kw0jcfje.js → cli-fyeft04q.js} +1 -1
- package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
- package/dist/{cli-ypgba1a8.js → cli-jkfxpg1b.js} +1 -1
- package/dist/{cli-mxvnnv5q.js → cli-jv36y8wz.js} +1 -1
- package/dist/{cli-m7vmeepg.js → cli-km8559dm.js} +6 -6
- package/dist/{cli-ykt8f7va.js → cli-kp3vfknv.js} +1 -1
- package/dist/{cli-ymmc1zxg.js → cli-m23r88re.js} +8 -5
- package/dist/{cli-v1pwxreq.js → cli-n4z21pt8.js} +1 -1
- package/dist/{cli-mxry91sa.js → cli-qg2zhjfc.js} +73 -4
- package/dist/{cli-1yrcfa5s.js → cli-rbr91whd.js} +4 -4
- package/dist/{cli-mkdsy1nq.js → cli-rg1c8ccw.js} +93 -7
- package/dist/cli-rh44xx46.js +2659 -0
- package/dist/cli-s0k2j7q9.js +2084 -0
- package/dist/{cli-ty4bzkqr.js → cli-sajr098b.js} +1 -1
- package/dist/{cli-6vfz44sr.js → cli-swrz3mj8.js} +1 -1
- package/dist/{cli-p8rt8spp.js → cli-vejds37t.js} +2 -2
- package/dist/{cli-jbzbr36n.js → cli-w6xa5nf5.js} +1 -1
- package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
- package/dist/{cli-qzmzwegq.js → cli-ysnkvdx6.js} +102 -9
- package/dist/{cli-y0kjpfm9.js → cli-yvxfaem2.js} +2 -2
- package/dist/{cli-pkj1w46r.js → cli-z48ggfyq.js} +1 -1
- package/dist/{cli-6bpvv8h4.js → cli-zzev32jw.js} +1 -1
- package/dist/cli.js +20 -17
- package/dist/{codex-adapter-cgesnjkr.js → codex-adapter-0kpg09ca.js} +3 -3
- package/dist/{codex-hook-15dgc4cb.js → codex-hook-295jpbfa.js} +2 -2
- package/dist/{codex-session-runner-8kd7ddhr.js → codex-session-runner-8n672hxh.js} +3 -3
- package/dist/{commands-h6zaaxym.js → commands-mhmg5n0x.js} +4 -4
- package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-wn8n5zzk.js} +2 -2
- package/dist/{db-qg3myn53.js → db-wa6s1mwv.js} +4 -4
- package/dist/{dispatcher-7vnwj9qw.js → dispatcher-nhs2pkr8.js} +16 -14
- package/dist/{dsh-adapter-tz6hfp5r.js → dsh-adapter-5423p70m.js} +1 -1
- package/dist/{grok-adapter-jdt2dz2s.js → grok-adapter-28pkdt80.js} +3 -3
- package/dist/{handlers-2qtkpncr.js → handlers-5rtf9nng.js} +19 -17
- package/dist/{hook-51yfcgsv.js → hook-krjfkpzy.js} +4 -4
- package/dist/{hook-qnbdgwvd.js → hook-qkm73zk6.js} +5 -5
- package/dist/{http-jsmtag08.js → http-a397xcn4.js} +4845 -3484
- package/dist/{index-2t9kesn1.js → index-ac0v8v9d.js} +4 -4
- package/dist/{index-ekywyv2v.js → index-kqre45fr.js} +16 -14
- package/dist/{index-bt8wkv1w.js → index-rtxg15ah.js} +17 -15
- package/dist/{index-t4g06w93.js → index-vvsq7v45.js} +26 -23
- package/dist/{index-cx1sb9bq.js → index-ytdhtdry.js} +50 -11
- package/dist/{keepalive-4aezx4bj.js → keepalive-psfh8ysx.js} +6 -6
- package/dist/{lead-r44bkpm5.js → lead-m6rppjn7.js} +29 -27
- package/dist/{maintenance-grz5jrzh.js → maintenance-41f837ke.js} +4 -4
- package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
- package/dist/{oauth-refresh-sweep-wm4ctjfj.js → oauth-refresh-sweep-6hr55hm8.js} +5 -5
- package/dist/{onboard-v60zgqf0.js → onboard-ktrtd7cz.js} +2 -2
- package/dist/{otel-impl-ghh4dyf8.js → otel-impl-dq4f2k3m.js} +1 -1
- package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-e5v9w0hv.js} +3 -2
- package/dist/{pricing-refresh-mjrqvnvw.js → pricing-refresh-kmk7j1ra.js} +6 -6
- package/dist/{rbac-roles-fd5tskj9.js → rbac-roles-9w1dkkxd.js} +4 -4
- package/dist/{rbac-roles-axv4d74x.js → rbac-roles-9ykx9tc5.js} +5 -5
- package/dist/{render-v2-k50j33qd.js → render-v2-ycqm53np.js} +5 -5
- package/dist/resume-cq2pkp8q.js +70 -0
- package/dist/{setup-4syh0pd2.js → setup-0dcmdvbn.js} +2 -2
- package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-1x9k4jcs.js} +1 -1
- package/dist/{work-objects-jcdbg8rr.js → work-objects-x01x2fh8.js} +19 -17
- package/dist/{worker-x8j8c9sx.js → worker-py1ssb1h.js} +29 -27
- package/openapi.json +15622 -15042
- package/package.json +8 -5
- package/src/apps/store.ts +1 -0
- package/src/artifact-sdk/feedback-overlay.ts +528 -0
- package/src/be/db/workflow-runs.ts +95 -0
- package/src/be/db-queries/slack-inbound.ts +489 -0
- package/src/be/db.ts +9 -2
- package/src/be/extensions/assets.ts +1 -1
- package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
- package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
- package/src/be/script-connections.ts +41 -7
- package/src/be/scripts/run-saved.ts +40 -20
- package/src/be/swarm-config-guard.ts +4 -0
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/catalog.ts +1 -1
- package/src/extensions/dispatcher.ts +3 -3
- package/src/heartbeat/heartbeat.ts +152 -2
- package/src/heartbeat/templates.ts +16 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/index.ts +2 -0
- package/src/http/integrations.ts +3 -1
- package/src/http/mcp-bridge.ts +1 -0
- package/src/http/pages-public.ts +7 -2
- package/src/http/pages.ts +178 -0
- package/src/http/script-connections.ts +35 -0
- package/src/http/slack-inbound.ts +107 -0
- package/src/http/tasks.ts +45 -27
- package/src/http/users.ts +1 -1
- package/src/http/webhooks.ts +20 -20
- package/src/prompts/session-templates.ts +32 -0
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +2 -2
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-managed-adapter.ts +2 -11
- package/src/providers/claude-sdk-session.ts +7 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +1 -2
- package/src/providers/pi-mono-adapter.ts +3 -3
- package/src/realtime/browser.generated.txt +8 -8
- package/src/realtime/browser.ts +3 -3
- package/src/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
- package/src/scripts-runtime/executors/quickjs.ts +9 -2
- package/src/scripts-runtime/executors/registry.ts +1 -1
- package/src/server.ts +1 -0
- package/src/slack/actions.ts +6 -2
- package/src/slack/app.ts +5 -0
- package/src/slack/assistant.ts +15 -5
- package/src/slack/egress-scrub.ts +3 -2
- package/src/slack/handlers.ts +36 -18
- package/src/slack/inbound-dispatch.ts +549 -0
- package/src/slack/inbound-files.ts +2 -0
- package/src/telemetry-context.ts +1 -0
- package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -0
- package/src/tests/codex-oauth-device.test.ts +1 -1
- package/src/tests/connection-embedded-auth.test.ts +51 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
- package/src/tests/heartbeat-checklist.test.ts +243 -0
- package/src/tests/pages-feedback.test.ts +218 -0
- package/src/tests/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
- package/src/tests/script-executor-registry.test.ts +56 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
- package/src/tests/slack-inbound-receipts.test.ts +684 -0
- package/src/tests/slack-session-status.test.ts +4 -2
- package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
- package/src/tests/user-token-routes.test.ts +1 -1
- package/src/tests/workflow-executors.test.ts +3 -2
- package/src/tests/workflow-sub-workflow.test.ts +363 -0
- package/src/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- package/src/utils/internal-ai/complete-structured.ts +6 -3
- package/src/utils/runtime-model-catalog.ts +1 -1
- package/src/workflows/engine.ts +17 -1
- package/src/workflows/executors/agent-task.ts +2 -2
- package/src/workflows/executors/human-in-the-loop.ts +3 -3
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +1 -1
- package/src/workflows/executors/sub-workflow.ts +135 -0
- package/src/workflows/executors/wait.ts +3 -3
- package/src/workflows/recovery.ts +11 -1
- package/src/workflows/resume.ts +77 -0
- package/src/workflows/wait-filter.ts +5 -7
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- package/templates/skills/heartbeat-runbook/content.md +3 -1
- package/templates/skills/pages/content.md +25 -0
- package/templates/skills/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
|
@@ -101,7 +101,11 @@ function propertyKeys(path: string[]): string[] {
|
|
|
101
101
|
return Object.keys((current as { properties: Record<string, unknown> }).properties).sort();
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
function expectSchemaKeys
|
|
104
|
+
function expectSchemaKeys<Actual extends object>(
|
|
105
|
+
actual: Actual,
|
|
106
|
+
path: string[],
|
|
107
|
+
required: string[],
|
|
108
|
+
): void {
|
|
105
109
|
const actualKeys = Object.keys(actual).sort();
|
|
106
110
|
expect(actualKeys).toEqual(required.slice().sort());
|
|
107
111
|
expect(actualKeys.every((key) => propertyKeys(path).includes(key))).toBe(true);
|
|
@@ -228,7 +228,7 @@ describe("ChatGPT connector codes", () => {
|
|
|
228
228
|
|
|
229
229
|
type CodeBody = { code: string; expiresAt: string; connectUrl: string };
|
|
230
230
|
|
|
231
|
-
async function createCode(userId: string, body:
|
|
231
|
+
async function createCode(userId: string, body: Record<string, unknown> = {}): Promise<CodeBody> {
|
|
232
232
|
const response = await authedFetch(`/api/users/${userId}/connector-codes`, {
|
|
233
233
|
method: "POST",
|
|
234
234
|
body: JSON.stringify(body),
|
|
@@ -984,7 +984,7 @@ describe("ValidateExecutor", () => {
|
|
|
984
984
|
// ─── Registry Wiring ─────────────────────────────────────────
|
|
985
985
|
|
|
986
986
|
describe("createExecutorRegistry", () => {
|
|
987
|
-
test("registers all
|
|
987
|
+
test("registers all 14 executors (9 instant + 5 async)", () => {
|
|
988
988
|
const registry = createExecutorRegistry(mockDeps);
|
|
989
989
|
const types = registry.types();
|
|
990
990
|
|
|
@@ -1001,7 +1001,8 @@ describe("createExecutorRegistry", () => {
|
|
|
1001
1001
|
expect(types).toContain("foreach");
|
|
1002
1002
|
expect(types).toContain("human-in-the-loop");
|
|
1003
1003
|
expect(types).toContain("wait");
|
|
1004
|
-
expect(types).
|
|
1004
|
+
expect(types).toContain("sub-workflow");
|
|
1005
|
+
expect(types).toHaveLength(14);
|
|
1005
1006
|
});
|
|
1006
1007
|
|
|
1007
1008
|
test("instant executors have mode instant, async executors have mode async", () => {
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as db from "../be/db";
|
|
5
|
+
import {
|
|
6
|
+
closeDb,
|
|
7
|
+
createWorkflow,
|
|
8
|
+
getDbClient,
|
|
9
|
+
getWaitStateByStepId,
|
|
10
|
+
getWorkflowRun,
|
|
11
|
+
getWorkflowRunStepsByRunId,
|
|
12
|
+
initDb,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import type { Workflow, WorkflowDefinition, WorkflowRun } from "../types";
|
|
15
|
+
import { startWorkflowExecution } from "../workflows/engine";
|
|
16
|
+
import { workflowEventBus } from "../workflows/event-bus";
|
|
17
|
+
import {
|
|
18
|
+
BaseExecutor,
|
|
19
|
+
type ExecutorDependencies,
|
|
20
|
+
type ExecutorInput,
|
|
21
|
+
type ExecutorResult,
|
|
22
|
+
} from "../workflows/executors/base";
|
|
23
|
+
import { createExecutorRegistry } from "../workflows/executors/registry";
|
|
24
|
+
import { MAX_SUB_WORKFLOW_DEPTH, SubWorkflowExecutor } from "../workflows/executors/sub-workflow";
|
|
25
|
+
import { recoverIncompleteRuns } from "../workflows/recovery";
|
|
26
|
+
import {
|
|
27
|
+
cancelWorkflowRun,
|
|
28
|
+
resumeWaitState,
|
|
29
|
+
setupWorkflowResumeListener,
|
|
30
|
+
} from "../workflows/resume";
|
|
31
|
+
|
|
32
|
+
const TEST_DB_PATH = "./test-workflow-sub-workflow.sqlite";
|
|
33
|
+
|
|
34
|
+
const deps: ExecutorDependencies = {
|
|
35
|
+
db,
|
|
36
|
+
eventBus: workflowEventBus,
|
|
37
|
+
interpolate: (t: string) => t,
|
|
38
|
+
};
|
|
39
|
+
const registry = createExecutorRegistry(deps);
|
|
40
|
+
let teardown: (() => void) | undefined;
|
|
41
|
+
|
|
42
|
+
/** Holds every `test-gate` node open until the test resolves it. */
|
|
43
|
+
let gate = Promise.withResolvers<void>();
|
|
44
|
+
|
|
45
|
+
/** An instant node that runs until the test opens the gate. */
|
|
46
|
+
class GateExecutor extends BaseExecutor<z.ZodObject, z.ZodObject> {
|
|
47
|
+
readonly type = "test-gate";
|
|
48
|
+
readonly mode = "instant" as const;
|
|
49
|
+
readonly configSchema = z.object({});
|
|
50
|
+
readonly outputSchema = z.object({});
|
|
51
|
+
|
|
52
|
+
protected async execute(): Promise<ExecutorResult<Record<string, never>>> {
|
|
53
|
+
await gate.promise;
|
|
54
|
+
return { status: "success", output: {} };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
registry.register(new GateExecutor(deps));
|
|
58
|
+
|
|
59
|
+
/** Runs `fn` with the live resume listener off, then turns it back on. */
|
|
60
|
+
async function withoutListener(fn: () => Promise<void>): Promise<void> {
|
|
61
|
+
teardown?.();
|
|
62
|
+
try {
|
|
63
|
+
await fn();
|
|
64
|
+
} finally {
|
|
65
|
+
teardown = setupWorkflowResumeListener(workflowEventBus, registry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function makeWorkflow(def: WorkflowDefinition): Promise<Workflow> {
|
|
70
|
+
return createWorkflow({ name: `wf-${crypto.randomUUID()}`, definition: def });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A one-node child that checks `trigger.n === 1`. */
|
|
74
|
+
const instantChild: WorkflowDefinition = {
|
|
75
|
+
nodes: [
|
|
76
|
+
{
|
|
77
|
+
id: "check",
|
|
78
|
+
type: "property-match",
|
|
79
|
+
config: { conditions: [{ field: "trigger.n", op: "eq", value: 1 }] },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** A child whose only instant node runs until the gate opens. */
|
|
85
|
+
const gatedChild: WorkflowDefinition = {
|
|
86
|
+
nodes: [{ id: "gate", type: "test-gate", config: {} }],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A child that parks on a long time wait, so it stays `waiting`. */
|
|
90
|
+
const waitingChild: WorkflowDefinition = {
|
|
91
|
+
nodes: [{ id: "pause", type: "wait", config: { mode: "time", durationMs: 3_600_000 } }],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function parentOf(childId: string): WorkflowDefinition {
|
|
95
|
+
return {
|
|
96
|
+
nodes: [
|
|
97
|
+
{
|
|
98
|
+
id: "child",
|
|
99
|
+
type: "sub-workflow",
|
|
100
|
+
config: { workflowId: childId, inputs: { n: 1 } },
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function childRunsOf(workflowId: string): Promise<{ id: string; parentStepId: string }[]> {
|
|
107
|
+
return getDbClient().query<{ id: string; parentStepId: string }>(
|
|
108
|
+
"SELECT id, parentStepId FROM workflow_runs WHERE workflowId = ?",
|
|
109
|
+
[workflowId],
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function waitForRun(
|
|
114
|
+
runId: string,
|
|
115
|
+
done: (run: WorkflowRun) => boolean,
|
|
116
|
+
): Promise<WorkflowRun> {
|
|
117
|
+
for (let i = 0; i < 250; i++) {
|
|
118
|
+
const run = await getWorkflowRun(runId);
|
|
119
|
+
if (run && done(run)) return run;
|
|
120
|
+
await Bun.sleep(20);
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`run ${runId} did not settle`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const isDone = (run: WorkflowRun) => run.status !== "running" && run.status !== "waiting";
|
|
126
|
+
|
|
127
|
+
async function childRunOf(workflowId: string, status: string): Promise<WorkflowRun> {
|
|
128
|
+
for (let i = 0; i < 250; i++) {
|
|
129
|
+
const [row] = await childRunsOf(workflowId);
|
|
130
|
+
if (row) return waitForRun(row.id, (r) => r.status === status);
|
|
131
|
+
await Bun.sleep(20);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`no child run of ${workflowId}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
beforeAll(() => {
|
|
137
|
+
initDb(TEST_DB_PATH);
|
|
138
|
+
teardown = setupWorkflowResumeListener(workflowEventBus, registry);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterAll(async () => {
|
|
142
|
+
teardown?.();
|
|
143
|
+
closeDb();
|
|
144
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
145
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => {});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("sub-workflow node", () => {
|
|
150
|
+
test("runs an instant child and exposes its node outputs", async () => {
|
|
151
|
+
const child = await makeWorkflow(instantChild);
|
|
152
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
153
|
+
|
|
154
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
155
|
+
|
|
156
|
+
const run = await waitForRun(runId, isDone);
|
|
157
|
+
expect(run?.status).toBe("completed");
|
|
158
|
+
const [childRun] = await childRunsOf(child.id);
|
|
159
|
+
const output = run?.context?.child as { runId: string; outputs: Record<string, unknown> };
|
|
160
|
+
expect(output.runId).toBe(childRun!.id);
|
|
161
|
+
expect(output.outputs.check).toMatchObject({ passed: true });
|
|
162
|
+
expect(output.outputs.trigger).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a failed child fails the node and the parent run", async () => {
|
|
166
|
+
const child = await makeWorkflow({
|
|
167
|
+
nodes: [{ id: "bad", type: "property-match", config: { conditions: [] } }],
|
|
168
|
+
});
|
|
169
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
170
|
+
|
|
171
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
172
|
+
|
|
173
|
+
const run = await waitForRun(runId, isDone);
|
|
174
|
+
expect(run?.status).toBe("failed");
|
|
175
|
+
const [step] = await getWorkflowRunStepsByRunId(runId);
|
|
176
|
+
expect(step?.status).toBe("failed");
|
|
177
|
+
expect(step?.error).toContain("failed");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("parent waits for an async child and resumes when it completes", async () => {
|
|
181
|
+
const child = await makeWorkflow(waitingChild);
|
|
182
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
183
|
+
|
|
184
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
185
|
+
expect((await getWorkflowRun(runId))?.status).toBe("waiting");
|
|
186
|
+
const childRun = await childRunOf(child.id, "waiting");
|
|
187
|
+
|
|
188
|
+
const [childStep] = await getWorkflowRunStepsByRunId(childRun.id);
|
|
189
|
+
const wait = await getWaitStateByStepId(childStep!.id);
|
|
190
|
+
await resumeWaitState(wait!.id, "fired", undefined, registry);
|
|
191
|
+
|
|
192
|
+
const run = await waitForRun(runId, (r) => r.status !== "waiting");
|
|
193
|
+
expect(run.status).toBe("completed");
|
|
194
|
+
expect((run.context?.child as { runId: string }).runId).toBe(childRun.id);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a cancelled child fails the waiting parent step", async () => {
|
|
198
|
+
const child = await makeWorkflow(waitingChild);
|
|
199
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
200
|
+
|
|
201
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
202
|
+
const childRun = await childRunOf(child.id, "waiting");
|
|
203
|
+
await cancelWorkflowRun(childRun.id, "stop");
|
|
204
|
+
|
|
205
|
+
const run = await waitForRun(runId, (r) => r.status !== "waiting");
|
|
206
|
+
expect(run.status).toBe("failed");
|
|
207
|
+
expect(run.error).toContain(`Child workflow run ${childRun.id} cancelled`);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("re-executing the step reconnects to the existing child; recovery resumes it", () =>
|
|
211
|
+
// No live listener for this test: the child finishing is only seen by recovery.
|
|
212
|
+
withoutListener(async () => {
|
|
213
|
+
const child = await makeWorkflow(waitingChild);
|
|
214
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
215
|
+
|
|
216
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
217
|
+
const [parentStep] = await getWorkflowRunStepsByRunId(runId);
|
|
218
|
+
const childRun = await childRunOf(child.id, "waiting");
|
|
219
|
+
|
|
220
|
+
// A restart re-runs the executor for the same step.
|
|
221
|
+
const again = await registry.get("sub-workflow").run({
|
|
222
|
+
config: { workflowId: child.id, inputs: { n: 1 } },
|
|
223
|
+
context: {},
|
|
224
|
+
meta: {
|
|
225
|
+
runId,
|
|
226
|
+
stepId: parentStep!.id,
|
|
227
|
+
nodeId: "child",
|
|
228
|
+
workflowId: parent.id,
|
|
229
|
+
dryRun: false,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
expect(again).toMatchObject({ async: true, correlationId: childRun.id });
|
|
233
|
+
expect(await childRunsOf(child.id)).toHaveLength(1);
|
|
234
|
+
|
|
235
|
+
// The child finishes while nothing listens.
|
|
236
|
+
const [childStep] = await getWorkflowRunStepsByRunId(childRun.id);
|
|
237
|
+
const wait = await getWaitStateByStepId(childStep!.id);
|
|
238
|
+
await resumeWaitState(wait!.id, "fired", undefined, registry);
|
|
239
|
+
expect((await getWorkflowRun(childRun.id))?.status).toBe("completed");
|
|
240
|
+
expect((await getWorkflowRun(runId))?.status).toBe("waiting");
|
|
241
|
+
|
|
242
|
+
await recoverIncompleteRuns(registry);
|
|
243
|
+
expect((await getWorkflowRun(runId))?.status).toBe("completed");
|
|
244
|
+
expect(await childRunsOf(child.id)).toHaveLength(1);
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
test("a slow instant child runs outside the parent executor's timeout", async () => {
|
|
248
|
+
// The gate holds the child's instant node open for as long as the test
|
|
249
|
+
// wants, past any watchdog. The parent's executor call returns regardless.
|
|
250
|
+
gate = Promise.withResolvers<void>();
|
|
251
|
+
const child = await makeWorkflow(gatedChild);
|
|
252
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
253
|
+
|
|
254
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
255
|
+
expect((await getWorkflowRun(runId))?.status).toBe("waiting");
|
|
256
|
+
const [childRun] = await childRunsOf(child.id);
|
|
257
|
+
expect((await getWorkflowRun(childRun!.id))?.status).toBe("running");
|
|
258
|
+
|
|
259
|
+
gate.resolve();
|
|
260
|
+
const run = await waitForRun(runId, isDone);
|
|
261
|
+
expect(run.status).toBe("completed");
|
|
262
|
+
expect((run.context?.child as { runId: string }).runId).toBe(childRun!.id);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a child that finishes before the parent step parks still resumes it", () =>
|
|
266
|
+
// No listener: the child's terminal event is lost, as in the race where it
|
|
267
|
+
// fires before the step is waiting. Only the post-park recheck can resume.
|
|
268
|
+
withoutListener(async () => {
|
|
269
|
+
gate = Promise.withResolvers<void>();
|
|
270
|
+
// Delays the executor's result, so parking happens after the child ends.
|
|
271
|
+
class LateParkExecutor extends SubWorkflowExecutor {
|
|
272
|
+
override async run(input: ExecutorInput) {
|
|
273
|
+
const result = await super.run(input);
|
|
274
|
+
const { correlationId } = result as { correlationId?: string };
|
|
275
|
+
gate.resolve();
|
|
276
|
+
if (correlationId) await waitForRun(correlationId, isDone);
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const lateRegistry = createExecutorRegistry(deps);
|
|
281
|
+
lateRegistry.register(new GateExecutor(deps));
|
|
282
|
+
lateRegistry.register(new LateParkExecutor(deps, lateRegistry));
|
|
283
|
+
const child = await makeWorkflow(gatedChild);
|
|
284
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
285
|
+
|
|
286
|
+
const runId = await startWorkflowExecution(parent, {}, lateRegistry);
|
|
287
|
+
|
|
288
|
+
expect((await getWorkflowRun(runId))?.status).toBe("completed");
|
|
289
|
+
}));
|
|
290
|
+
|
|
291
|
+
test("deleting an active child's workflow fails the waiting parent step", async () => {
|
|
292
|
+
const child = await makeWorkflow(waitingChild);
|
|
293
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
294
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
295
|
+
await childRunOf(child.id, "waiting");
|
|
296
|
+
|
|
297
|
+
await db.deleteWorkflow(child.id);
|
|
298
|
+
|
|
299
|
+
const run = await waitForRun(runId, isDone);
|
|
300
|
+
expect(run.status).toBe("failed");
|
|
301
|
+
expect(run.error).toContain("was deleted");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("recovery fails a waiting parent step whose child row is gone", () =>
|
|
305
|
+
withoutListener(async () => {
|
|
306
|
+
const child = await makeWorkflow(waitingChild);
|
|
307
|
+
const parent = await makeWorkflow(parentOf(child.id));
|
|
308
|
+
const runId = await startWorkflowExecution(parent, {}, registry);
|
|
309
|
+
await childRunOf(child.id, "waiting");
|
|
310
|
+
|
|
311
|
+
await db.deleteWorkflow(child.id);
|
|
312
|
+
expect((await getWorkflowRun(runId))?.status).toBe("waiting");
|
|
313
|
+
|
|
314
|
+
await recoverIncompleteRuns(registry);
|
|
315
|
+
const run = await getWorkflowRun(runId);
|
|
316
|
+
expect(run?.status).toBe("failed");
|
|
317
|
+
expect(run?.error).toContain("was deleted");
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
test("rejects a workflow that invokes itself", async () => {
|
|
321
|
+
const self = await makeWorkflow({ nodes: [] });
|
|
322
|
+
await db.updateWorkflow(self.id, { definition: parentOf(self.id) });
|
|
323
|
+
const wf = await db.getWorkflow(self.id);
|
|
324
|
+
|
|
325
|
+
const runId = await startWorkflowExecution(wf!, {}, registry);
|
|
326
|
+
|
|
327
|
+
const run = await getWorkflowRun(runId);
|
|
328
|
+
expect(run?.status).toBe("failed");
|
|
329
|
+
const [step] = await getWorkflowRunStepsByRunId(runId);
|
|
330
|
+
expect(step?.error).toContain("Sub-workflow recursion");
|
|
331
|
+
expect(await childRunsOf(self.id)).toHaveLength(1); // only the parent run itself
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("rejects an indirect cycle through an ancestor run", async () => {
|
|
335
|
+
const a = await makeWorkflow({ nodes: [] });
|
|
336
|
+
const b = await makeWorkflow(parentOf(a.id));
|
|
337
|
+
await db.updateWorkflow(a.id, { definition: parentOf(b.id) });
|
|
338
|
+
const wfA = await db.getWorkflow(a.id);
|
|
339
|
+
|
|
340
|
+
const runId = await startWorkflowExecution(wfA!, {}, registry);
|
|
341
|
+
|
|
342
|
+
const run = await waitForRun(runId, isDone);
|
|
343
|
+
expect(run.status).toBe("failed");
|
|
344
|
+
expect(run.error).toContain("Sub-workflow recursion");
|
|
345
|
+
expect(await childRunsOf(b.id)).toHaveLength(1);
|
|
346
|
+
expect(await childRunsOf(a.id)).toHaveLength(1);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("fails closed past the max nesting depth", async () => {
|
|
350
|
+
// A chain of distinct workflows, one level deeper than allowed.
|
|
351
|
+
let next = await makeWorkflow(instantChild);
|
|
352
|
+
for (let i = 0; i <= MAX_SUB_WORKFLOW_DEPTH; i++) next = await makeWorkflow(parentOf(next.id));
|
|
353
|
+
const root = next;
|
|
354
|
+
|
|
355
|
+
const runId = await startWorkflowExecution(root, {}, registry);
|
|
356
|
+
|
|
357
|
+
expect((await waitForRun(runId, isDone)).status).toBe("failed");
|
|
358
|
+
const rows = await getDbClient().query<{ n: number }>(
|
|
359
|
+
"SELECT COUNT(*) AS n FROM workflow_run_steps WHERE error LIKE 'Sub-workflow nesting exceeds%'",
|
|
360
|
+
);
|
|
361
|
+
expect(rows[0]?.n).toBe(1);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
@@ -4,6 +4,7 @@ import { getAgentById } from "@/be/db";
|
|
|
4
4
|
import {
|
|
5
5
|
ConnectionAuthInputSchema,
|
|
6
6
|
connectionAuthInputFromFlat,
|
|
7
|
+
deleteScriptConnection,
|
|
7
8
|
getScriptConnectionById,
|
|
8
9
|
listScriptConnections,
|
|
9
10
|
refreshScriptConnection,
|
|
@@ -16,13 +17,23 @@ import { resolveScopedResourceId, scopedResourceScopeIdSchema } from "@/utils/sc
|
|
|
16
17
|
|
|
17
18
|
const scriptConnectionsInputSchema = z.object({
|
|
18
19
|
action: z
|
|
19
|
-
.enum([
|
|
20
|
-
|
|
20
|
+
.enum([
|
|
21
|
+
"list",
|
|
22
|
+
"upsert-openapi",
|
|
23
|
+
"upsert-mcp",
|
|
24
|
+
"upsert-graphql",
|
|
25
|
+
"refresh",
|
|
26
|
+
"disable",
|
|
27
|
+
"delete",
|
|
28
|
+
])
|
|
29
|
+
.describe(
|
|
30
|
+
"List, create/update, refresh, disable, or delete a script connection. delete is permanent: it removes the connection, its managed credential binding, and any inline secret it owns.",
|
|
31
|
+
),
|
|
21
32
|
id: z
|
|
22
33
|
.string()
|
|
23
34
|
.uuid()
|
|
24
35
|
.optional()
|
|
25
|
-
.describe("Existing connection ID for update, refresh, or
|
|
36
|
+
.describe("Existing connection ID for update, refresh, disable, or delete."),
|
|
26
37
|
slug: z
|
|
27
38
|
.string()
|
|
28
39
|
.min(1)
|
|
@@ -193,6 +204,27 @@ export const registerScriptConnectionsTool = (server: McpServer) => {
|
|
|
193
204
|
});
|
|
194
205
|
}
|
|
195
206
|
|
|
207
|
+
if (args.action === "delete") {
|
|
208
|
+
if (!args.id) {
|
|
209
|
+
return toolErr("id is required for delete.", {
|
|
210
|
+
data: {
|
|
211
|
+
yourAgentId: requestInfo.agentId,
|
|
212
|
+
connections: listScriptConnections({ includeDisabled: true, allScopes: true }),
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const deleted = await deleteScriptConnection(args.id);
|
|
217
|
+
const connections = listScriptConnections({ includeDisabled: true, allScopes: true });
|
|
218
|
+
if (!deleted) {
|
|
219
|
+
return toolErr(`Script connection ${args.id} not found.`, {
|
|
220
|
+
data: { yourAgentId: requestInfo.agentId, connections },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return toolOk(`Script connection ${deleted.slug} deleted.`, {
|
|
224
|
+
data: { yourAgentId: requestInfo.agentId, connections },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
196
228
|
if (args.action === "refresh") {
|
|
197
229
|
if (!args.id) {
|
|
198
230
|
return toolErr("id is required for refresh.", {
|
package/src/tools/send-task.ts
CHANGED
|
@@ -355,7 +355,15 @@ export async function sendTaskHandler(
|
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
|
|
358
|
+
// The guard stops an agent session from handing work back to itself. A script runs as its
|
|
359
|
+
// owner (a scheduled script as the schedule's creator), so a Lead-owned poller that files
|
|
360
|
+
// work for the Lead is legitimate. The bridge marks script calls `script-sdk`; a caller that
|
|
361
|
+
// spoofs it can only send itself a task, which grants nothing it could not already do.
|
|
362
|
+
if (
|
|
363
|
+
ctx.kind === "owner" &&
|
|
364
|
+
requestedAgentId === ctx.agentId &&
|
|
365
|
+
ctx.requestInfo?.callOrigin !== "script-sdk"
|
|
366
|
+
) {
|
|
359
367
|
return toolErr("Cannot send a task to yourself, are you drunk?", {
|
|
360
368
|
data: { yourAgentId: ctx.agentId },
|
|
361
369
|
});
|
|
@@ -66,7 +66,7 @@ export interface CompleteStructuredOptions<TZod extends z.ZodTypeAny> {
|
|
|
66
66
|
prompt: string,
|
|
67
67
|
model: string,
|
|
68
68
|
signal?: AbortSignal,
|
|
69
|
-
jsonSchema?:
|
|
69
|
+
jsonSchema?: Record<string, unknown>,
|
|
70
70
|
) => Promise<string>;
|
|
71
71
|
/**
|
|
72
72
|
* Bypass `resolveCredential` entirely — opencode auth path (and tests)
|
|
@@ -111,7 +111,7 @@ export async function defaultSpawnClaudeCli(
|
|
|
111
111
|
prompt: string,
|
|
112
112
|
model: string,
|
|
113
113
|
signal?: AbortSignal,
|
|
114
|
-
jsonSchema?:
|
|
114
|
+
jsonSchema?: Record<string, unknown>,
|
|
115
115
|
): Promise<string> {
|
|
116
116
|
// SWARM_USE_CLAUDE_BRIDGE mirrors the main claude adapter's subscription-pool
|
|
117
117
|
// routing. Otherwise CLAUDE_BINARY may be a single binary, an absolute path,
|
|
@@ -260,7 +260,10 @@ export async function completeStructuredWithModel<TZod extends z.ZodTypeAny>(
|
|
|
260
260
|
// The CLI flag alone is unreliable — claude sometimes asks "where's
|
|
261
261
|
// the schema?" if the prompt doesn't reference one.
|
|
262
262
|
// The pinned Claude executable validates schemas with Draft 7.
|
|
263
|
-
const jsonSchema = z.toJSONSchema(opts.zodSchema, { target: "draft-7" }) as
|
|
263
|
+
const jsonSchema = z.toJSONSchema(opts.zodSchema, { target: "draft-7" }) as Record<
|
|
264
|
+
string,
|
|
265
|
+
unknown
|
|
266
|
+
>;
|
|
264
267
|
const schemaStr = JSON.stringify(jsonSchema);
|
|
265
268
|
const claudeUserPrompt = `${opts.userPrompt}\n\nRespond with ONLY a JSON object (no prose, no code fences) matching this schema:\n${schemaStr}`;
|
|
266
269
|
let lastErr: unknown = null;
|
|
@@ -38,7 +38,7 @@ export interface RuntimeCatalogProvider {
|
|
|
38
38
|
|
|
39
39
|
export type RuntimeCatalog = Record<string, RuntimeCatalogProvider | undefined>;
|
|
40
40
|
|
|
41
|
-
const SNAPSHOT = modelsDevSnapshot as
|
|
41
|
+
const SNAPSHOT = modelsDevSnapshot as RuntimeCatalog;
|
|
42
42
|
|
|
43
43
|
/** Default freshness window for the worker-side HTTP pull. */
|
|
44
44
|
export const RUNTIME_CATALOG_TTL_MS = 10 * 60_000;
|
package/src/workflows/engine.ts
CHANGED
|
@@ -54,6 +54,8 @@ function maxIterations(): number {
|
|
|
54
54
|
export interface WorkflowExecutionOptions {
|
|
55
55
|
requestedByUserId?: string;
|
|
56
56
|
triggerType?: "schedule" | "manual" | "event" | "api";
|
|
57
|
+
/** Set when a `sub-workflow` step starts this run as its child. */
|
|
58
|
+
parentStepId?: string;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -133,6 +135,7 @@ export async function startWorkflowExecution(
|
|
|
133
135
|
triggerType: options.triggerType ?? "manual",
|
|
134
136
|
triggerData,
|
|
135
137
|
createdBy: options.requestedByUserId,
|
|
138
|
+
parentStepId: options.parentStepId,
|
|
136
139
|
});
|
|
137
140
|
await updateWorkflowRun(runId, {
|
|
138
141
|
status: "skipped",
|
|
@@ -149,6 +152,7 @@ export async function startWorkflowExecution(
|
|
|
149
152
|
triggerType: options.triggerType ?? "manual",
|
|
150
153
|
triggerData,
|
|
151
154
|
createdBy: options.requestedByUserId,
|
|
155
|
+
parentStepId: options.parentStepId,
|
|
152
156
|
});
|
|
153
157
|
telemetry.workflow("started", {
|
|
154
158
|
workflowId: workflow.id,
|
|
@@ -195,7 +199,7 @@ export async function startWorkflowExecution(
|
|
|
195
199
|
|
|
196
200
|
const entryNodes = findEntryNodes(workflow.definition);
|
|
197
201
|
const secretKeys = getSecretInputKeys(workflow.input);
|
|
198
|
-
|
|
202
|
+
const walk = walkGraph(
|
|
199
203
|
workflow.definition,
|
|
200
204
|
runId,
|
|
201
205
|
ctx,
|
|
@@ -205,6 +209,13 @@ export async function startWorkflowExecution(
|
|
|
205
209
|
secretKeys,
|
|
206
210
|
options,
|
|
207
211
|
);
|
|
212
|
+
// A child run walks on its own, outside the parent executor's timeout. Its
|
|
213
|
+
// terminal status wakes the parent step (`resumeFromChildRun`).
|
|
214
|
+
if (options.parentStepId) {
|
|
215
|
+
walk.catch((err) => console.error(`[workflows] Child workflow run ${runId} failed:`, err));
|
|
216
|
+
return runId;
|
|
217
|
+
}
|
|
218
|
+
await walk;
|
|
208
219
|
return runId;
|
|
209
220
|
}
|
|
210
221
|
|
|
@@ -939,6 +950,11 @@ async function runClaimedStep(
|
|
|
939
950
|
// Check for async result
|
|
940
951
|
if ("async" in result && (result as AsyncExecutorResult).async) {
|
|
941
952
|
const waiting = await checkpointStepWaiting(runId, stepId, ctx);
|
|
953
|
+
// A child run that finished before the step parked had its event dropped.
|
|
954
|
+
if (waiting && (result as AsyncExecutorResult).waitFor === "workflow.child.finished") {
|
|
955
|
+
const { resumeFromChildRun } = await import("./resume");
|
|
956
|
+
await resumeFromChildRun(stepId, registry);
|
|
957
|
+
}
|
|
942
958
|
return { outcome: waiting ? "waiting" : "completed", successors: [] };
|
|
943
959
|
}
|
|
944
960
|
|
|
@@ -83,7 +83,7 @@ export class AgentTaskExecutor extends BaseExecutor<
|
|
|
83
83
|
async: true,
|
|
84
84
|
waitFor: "task.completed",
|
|
85
85
|
correlationId: existingTask.id,
|
|
86
|
-
} as
|
|
86
|
+
} as ExecutorResult<AgentTaskOutput>;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// 2. Inherit workflow-level dir/vcsRepo when node config doesn't specify them
|
|
@@ -154,6 +154,6 @@ export class AgentTaskExecutor extends BaseExecutor<
|
|
|
154
154
|
async: true,
|
|
155
155
|
waitFor: "task.completed",
|
|
156
156
|
correlationId: task.id,
|
|
157
|
-
} as
|
|
157
|
+
} as ExecutorResult<AgentTaskOutput>;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
@@ -286,7 +286,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
286
286
|
async: true,
|
|
287
287
|
waitFor: "approval.resolved",
|
|
288
288
|
correlationId: existing.id,
|
|
289
|
-
} as
|
|
289
|
+
} as ExecutorResult<HITLOutput>;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// 2. Create the approval request. `run` already replaced a dynamic token
|
|
@@ -314,7 +314,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
314
314
|
async: true,
|
|
315
315
|
waitFor: "approval.resolved",
|
|
316
316
|
correlationId: requestId,
|
|
317
|
-
} as
|
|
317
|
+
} as ExecutorResult<HITLOutput>;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
// 3. Dispatch notifications (fire-and-forget — failures must not block the workflow)
|
|
@@ -333,7 +333,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
|
|
|
333
333
|
async: true,
|
|
334
334
|
waitFor: "approval.resolved",
|
|
335
335
|
correlationId: requestId,
|
|
336
|
-
} as
|
|
336
|
+
} as ExecutorResult<HITLOutput>;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
/** Dispatch notifications for each configured channel. Updates DB with messageTs on success. */
|
|
@@ -8,6 +8,7 @@ import { NotifyExecutor } from "./notify";
|
|
|
8
8
|
import { PropertyMatchExecutor } from "./property-match";
|
|
9
9
|
import { RawLlmExecutor } from "./raw-llm";
|
|
10
10
|
import { ScriptExecutor } from "./script";
|
|
11
|
+
import { SubWorkflowExecutor } from "./sub-workflow";
|
|
11
12
|
import { SwarmScriptExecutor } from "./swarm-script";
|
|
12
13
|
import { SystemOneDecisionExecutor } from "./system-one-decision";
|
|
13
14
|
import { ValidateExecutor } from "./validate";
|
|
@@ -81,6 +82,7 @@ export function createExecutorRegistry(deps: ExecutorDependencies): ExecutorRegi
|
|
|
81
82
|
registry.register(new ForeachExecutor(deps));
|
|
82
83
|
registry.register(new HumanInTheLoopExecutor(deps));
|
|
83
84
|
registry.register(new WaitExecutor(deps));
|
|
85
|
+
registry.register(new SubWorkflowExecutor(deps, registry));
|
|
84
86
|
|
|
85
87
|
return registry;
|
|
86
88
|
}
|
|
@@ -98,7 +98,7 @@ export class ScriptExecutor extends BaseExecutor<
|
|
|
98
98
|
return {
|
|
99
99
|
status: "failed",
|
|
100
100
|
error: result.stderr || `Script exited with code ${result.exitCode}`,
|
|
101
|
-
output: result as
|
|
101
|
+
output: result as z.infer<typeof ScriptOutputSchema>,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|