@davidorex/pi-workflows 0.2.0 → 0.3.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/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +172 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +156 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +726 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +438 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +464 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +84 -0
- package/skills/pi-workflows/SKILL.md +247 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { executeParallelLayer, executeParallelStep } from "./step-parallel.ts";
|
|
7
|
-
import type { ParallelOptions, SingleStepExecutor } from "./step-parallel.ts";
|
|
8
|
-
import type { ExecutionLayer } from "./dag.ts";
|
|
9
|
-
import { zeroUsage, WIDGET_ID } from "./step-shared.ts";
|
|
10
|
-
import type { StepSpec, StepResult, ExecutionState, WorkflowSpec } from "./types.ts";
|
|
11
|
-
import { mockCtx, mockPi, makeSpec } from "./test-helpers.ts";
|
|
12
|
-
|
|
13
|
-
function makeTmpDir(t: any): string {
|
|
14
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-parallel-"));
|
|
15
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
16
|
-
fs.mkdirSync(path.join(tmpDir, "sessions"), { recursive: true });
|
|
17
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
18
|
-
return tmpDir;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function makeParallelOptions(tmpDir: string, overrides?: Partial<ParallelOptions>): ParallelOptions {
|
|
22
|
-
return {
|
|
23
|
-
ctx: mockCtx(tmpDir),
|
|
24
|
-
pi: mockPi(),
|
|
25
|
-
loadAgent: () => ({ name: "default" }),
|
|
26
|
-
runDir: tmpDir,
|
|
27
|
-
spec: makeSpec({
|
|
28
|
-
steps: {
|
|
29
|
-
a: { agent: "test" },
|
|
30
|
-
b: { agent: "test" },
|
|
31
|
-
c: { agent: "test" },
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
widgetState: {
|
|
35
|
-
spec: makeSpec({ steps: { a: { agent: "test" }, b: { agent: "test" }, c: { agent: "test" } } }),
|
|
36
|
-
state: { input: {}, steps: {}, status: "running" },
|
|
37
|
-
startTime: Date.now(),
|
|
38
|
-
},
|
|
39
|
-
...overrides,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Mock executor factory: controls behavior per step name.
|
|
45
|
-
*/
|
|
46
|
-
function mockExecutor(
|
|
47
|
-
behavior: Record<string, { success: boolean; output?: unknown; delayMs?: number; usage?: any }> = {},
|
|
48
|
-
): SingleStepExecutor {
|
|
49
|
-
return async (stepName, stepSpec, state, options) => {
|
|
50
|
-
const b = behavior[stepName] ?? { success: true };
|
|
51
|
-
if (b.delayMs) await new Promise((r) => setTimeout(r, b.delayMs));
|
|
52
|
-
// Check abort signal to simulate cancellation
|
|
53
|
-
if (options.signal?.aborted) {
|
|
54
|
-
state.steps[stepName] = {
|
|
55
|
-
step: stepName,
|
|
56
|
-
agent: stepSpec.agent ?? "mock",
|
|
57
|
-
status: "failed",
|
|
58
|
-
output: undefined,
|
|
59
|
-
usage: zeroUsage(),
|
|
60
|
-
durationMs: 0,
|
|
61
|
-
error: "aborted",
|
|
62
|
-
};
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
state.steps[stepName] = {
|
|
66
|
-
step: stepName,
|
|
67
|
-
agent: stepSpec.agent ?? "mock",
|
|
68
|
-
status: b.success ? "completed" : "failed",
|
|
69
|
-
output: b.output ?? { mock: true },
|
|
70
|
-
usage: b.usage ?? zeroUsage(),
|
|
71
|
-
durationMs: b.delayMs ?? 0,
|
|
72
|
-
error: b.success ? undefined : "mock failure",
|
|
73
|
-
};
|
|
74
|
-
return b.success;
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
describe("executeParallelLayer", () => {
|
|
79
|
-
it("executes all steps in the layer", async (t) => {
|
|
80
|
-
const tmpDir = makeTmpDir(t);
|
|
81
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
82
|
-
const spec = makeSpec({
|
|
83
|
-
steps: {
|
|
84
|
-
a: { agent: "test" },
|
|
85
|
-
b: { agent: "test" },
|
|
86
|
-
c: { agent: "test" },
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
const layer: ExecutionLayer = { steps: ["a", "b", "c"] };
|
|
90
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
91
|
-
|
|
92
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
93
|
-
a: { success: true },
|
|
94
|
-
b: { success: true },
|
|
95
|
-
c: { success: true },
|
|
96
|
-
}), options);
|
|
97
|
-
|
|
98
|
-
assert.ok(state.steps.a);
|
|
99
|
-
assert.ok(state.steps.b);
|
|
100
|
-
assert.ok(state.steps.c);
|
|
101
|
-
assert.strictEqual(state.steps.a.status, "completed");
|
|
102
|
-
assert.strictEqual(state.steps.b.status, "completed");
|
|
103
|
-
assert.strictEqual(state.steps.c.status, "completed");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("sets state.status to failed when any step fails", async (t) => {
|
|
107
|
-
const tmpDir = makeTmpDir(t);
|
|
108
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
109
|
-
const spec = makeSpec({
|
|
110
|
-
steps: {
|
|
111
|
-
a: { agent: "test" },
|
|
112
|
-
b: { agent: "test" },
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
const layer: ExecutionLayer = { steps: ["a", "b"] };
|
|
116
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
117
|
-
|
|
118
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
119
|
-
a: { success: true },
|
|
120
|
-
b: { success: false },
|
|
121
|
-
}), options);
|
|
122
|
-
|
|
123
|
-
assert.strictEqual(state.status, "failed");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("cancels remaining steps when one fails (abort signal)", async (t) => {
|
|
127
|
-
const tmpDir = makeTmpDir(t);
|
|
128
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
129
|
-
const spec = makeSpec({
|
|
130
|
-
steps: {
|
|
131
|
-
slow: { agent: "test" },
|
|
132
|
-
fast_fail: { agent: "test" },
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
const layer: ExecutionLayer = { steps: ["slow", "fast_fail"] };
|
|
136
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
137
|
-
|
|
138
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
139
|
-
slow: { success: true, delayMs: 200 },
|
|
140
|
-
fast_fail: { success: false, delayMs: 10 },
|
|
141
|
-
}), options);
|
|
142
|
-
|
|
143
|
-
assert.strictEqual(state.status, "failed");
|
|
144
|
-
// The slow step may have been aborted — it should still have a result
|
|
145
|
-
assert.ok(state.steps.fast_fail);
|
|
146
|
-
assert.strictEqual(state.steps.fast_fail.status, "failed");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("respects pre-aborted parent signal", async (t) => {
|
|
150
|
-
const tmpDir = makeTmpDir(t);
|
|
151
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
152
|
-
const spec = makeSpec({
|
|
153
|
-
steps: { a: { agent: "test" } },
|
|
154
|
-
});
|
|
155
|
-
const layer: ExecutionLayer = { steps: ["a"] };
|
|
156
|
-
const controller = new AbortController();
|
|
157
|
-
controller.abort();
|
|
158
|
-
const options = makeParallelOptions(tmpDir, { spec, signal: controller.signal });
|
|
159
|
-
|
|
160
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
161
|
-
a: { success: true },
|
|
162
|
-
}), options);
|
|
163
|
-
|
|
164
|
-
// Step should see aborted signal
|
|
165
|
-
if (state.steps.a) {
|
|
166
|
-
// If it ran at all, it should have seen the abort
|
|
167
|
-
assert.ok(true);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("updates widgetState.currentStep with all layer step names", async (t) => {
|
|
172
|
-
const tmpDir = makeTmpDir(t);
|
|
173
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
174
|
-
const spec = makeSpec({
|
|
175
|
-
steps: { x: { agent: "test" }, y: { agent: "test" } },
|
|
176
|
-
});
|
|
177
|
-
const layer: ExecutionLayer = { steps: ["x", "y"] };
|
|
178
|
-
const widgetState = {
|
|
179
|
-
spec,
|
|
180
|
-
state,
|
|
181
|
-
startTime: Date.now(),
|
|
182
|
-
currentStep: undefined as string | undefined,
|
|
183
|
-
};
|
|
184
|
-
const options = makeParallelOptions(tmpDir, { spec, widgetState });
|
|
185
|
-
|
|
186
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
187
|
-
x: { success: true },
|
|
188
|
-
y: { success: true },
|
|
189
|
-
}), options);
|
|
190
|
-
|
|
191
|
-
assert.strictEqual(widgetState.currentStep, "x, y");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("handles single-step layer (degenerate case)", async (t) => {
|
|
195
|
-
const tmpDir = makeTmpDir(t);
|
|
196
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
197
|
-
const spec = makeSpec({ steps: { only: { agent: "test" } } });
|
|
198
|
-
const layer: ExecutionLayer = { steps: ["only"] };
|
|
199
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
200
|
-
|
|
201
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
202
|
-
only: { success: true },
|
|
203
|
-
}), options);
|
|
204
|
-
|
|
205
|
-
assert.ok(state.steps.only);
|
|
206
|
-
assert.strictEqual(state.steps.only.status, "completed");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("handles empty layer (no steps)", async (t) => {
|
|
210
|
-
const tmpDir = makeTmpDir(t);
|
|
211
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
212
|
-
const spec = makeSpec({ steps: {} });
|
|
213
|
-
const layer: ExecutionLayer = { steps: [] };
|
|
214
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
215
|
-
|
|
216
|
-
await executeParallelLayer(layer, spec, state, mockExecutor(), options);
|
|
217
|
-
|
|
218
|
-
// No errors, no state changes
|
|
219
|
-
assert.strictEqual(Object.keys(state.steps).length, 0);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("does not set state.status on all-success", async (t) => {
|
|
223
|
-
const tmpDir = makeTmpDir(t);
|
|
224
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
225
|
-
const spec = makeSpec({
|
|
226
|
-
steps: { a: { agent: "test" }, b: { agent: "test" } },
|
|
227
|
-
});
|
|
228
|
-
const layer: ExecutionLayer = { steps: ["a", "b"] };
|
|
229
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
230
|
-
|
|
231
|
-
await executeParallelLayer(layer, spec, state, mockExecutor({
|
|
232
|
-
a: { success: true },
|
|
233
|
-
b: { success: true },
|
|
234
|
-
}), options);
|
|
235
|
-
|
|
236
|
-
// Status should remain "running" — setting to "completed" is the executor's job
|
|
237
|
-
assert.strictEqual(state.status, "running");
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("collects results via Promise.allSettled (no unhandled rejections)", async (t) => {
|
|
241
|
-
const tmpDir = makeTmpDir(t);
|
|
242
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
243
|
-
const spec = makeSpec({
|
|
244
|
-
steps: { a: { agent: "test" }, b: { agent: "test" } },
|
|
245
|
-
});
|
|
246
|
-
const layer: ExecutionLayer = { steps: ["a", "b"] };
|
|
247
|
-
const options = makeParallelOptions(tmpDir, { spec });
|
|
248
|
-
|
|
249
|
-
// Mock executor that throws an exception for step "b"
|
|
250
|
-
const executor: SingleStepExecutor = async (stepName, stepSpec, execState, opts) => {
|
|
251
|
-
if (stepName === "b") throw new Error("unexpected crash");
|
|
252
|
-
execState.steps[stepName] = {
|
|
253
|
-
step: stepName,
|
|
254
|
-
agent: "mock",
|
|
255
|
-
status: "completed",
|
|
256
|
-
output: {},
|
|
257
|
-
usage: zeroUsage(),
|
|
258
|
-
durationMs: 0,
|
|
259
|
-
};
|
|
260
|
-
return true;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
await executeParallelLayer(layer, spec, state, executor, options);
|
|
264
|
-
|
|
265
|
-
assert.strictEqual(state.status, "failed");
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe("executeParallelStep", () => {
|
|
270
|
-
it("returns completed result with aggregated sub-outputs", async (t) => {
|
|
271
|
-
const tmpDir = makeTmpDir(t);
|
|
272
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
273
|
-
const parallelSpec: Record<string, StepSpec> = {
|
|
274
|
-
sub1: { agent: "test" },
|
|
275
|
-
sub2: { agent: "test" },
|
|
276
|
-
};
|
|
277
|
-
const options = makeParallelOptions(tmpDir);
|
|
278
|
-
|
|
279
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
280
|
-
sub1: { success: true, output: { data: "from-sub1" } },
|
|
281
|
-
sub2: { success: true, output: { data: "from-sub2" } },
|
|
282
|
-
}), options);
|
|
283
|
-
|
|
284
|
-
assert.strictEqual(result.status, "completed");
|
|
285
|
-
const output = result.output as any;
|
|
286
|
-
assert.ok(output.sub1);
|
|
287
|
-
assert.ok(output.sub2);
|
|
288
|
-
assert.deepStrictEqual(output.sub1, { data: "from-sub1" });
|
|
289
|
-
assert.deepStrictEqual(output.sub2, { data: "from-sub2" });
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("returns failed result when any sub-step fails", async (t) => {
|
|
293
|
-
const tmpDir = makeTmpDir(t);
|
|
294
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
295
|
-
const parallelSpec: Record<string, StepSpec> = {
|
|
296
|
-
sub1: { agent: "test" },
|
|
297
|
-
sub2: { agent: "test" },
|
|
298
|
-
};
|
|
299
|
-
const options = makeParallelOptions(tmpDir);
|
|
300
|
-
|
|
301
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
302
|
-
sub1: { success: true },
|
|
303
|
-
sub2: { success: false },
|
|
304
|
-
}), options);
|
|
305
|
-
|
|
306
|
-
assert.strictEqual(result.status, "failed");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("aggregates usage across sub-steps", async (t) => {
|
|
310
|
-
const tmpDir = makeTmpDir(t);
|
|
311
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
312
|
-
const parallelSpec: Record<string, StepSpec> = {
|
|
313
|
-
sub1: { agent: "test" },
|
|
314
|
-
sub2: { agent: "test" },
|
|
315
|
-
};
|
|
316
|
-
const options = makeParallelOptions(tmpDir);
|
|
317
|
-
|
|
318
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
319
|
-
sub1: { success: true, usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 1 } },
|
|
320
|
-
sub2: { success: true, usage: { input: 200, output: 100, cacheRead: 10, cacheWrite: 5, cost: 0.02, turns: 2 } },
|
|
321
|
-
}), options);
|
|
322
|
-
|
|
323
|
-
assert.strictEqual(result.usage.input, 300);
|
|
324
|
-
assert.strictEqual(result.usage.output, 150);
|
|
325
|
-
assert.strictEqual(result.usage.cacheRead, 10);
|
|
326
|
-
assert.strictEqual(result.usage.cacheWrite, 5);
|
|
327
|
-
assert.strictEqual(result.usage.cost, 0.03);
|
|
328
|
-
assert.strictEqual(result.usage.turns, 3);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("cancels remaining sub-steps when one fails", async (t) => {
|
|
332
|
-
const tmpDir = makeTmpDir(t);
|
|
333
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
334
|
-
const parallelSpec: Record<string, StepSpec> = {
|
|
335
|
-
slow: { agent: "test" },
|
|
336
|
-
fast_fail: { agent: "test" },
|
|
337
|
-
};
|
|
338
|
-
const options = makeParallelOptions(tmpDir);
|
|
339
|
-
|
|
340
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
341
|
-
slow: { success: true, delayMs: 200 },
|
|
342
|
-
fast_fail: { success: false, delayMs: 10 },
|
|
343
|
-
}), options);
|
|
344
|
-
|
|
345
|
-
assert.strictEqual(result.status, "failed");
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("respects pre-aborted signal", async (t) => {
|
|
349
|
-
const tmpDir = makeTmpDir(t);
|
|
350
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
351
|
-
const parallelSpec: Record<string, StepSpec> = { sub1: { agent: "test" } };
|
|
352
|
-
const controller = new AbortController();
|
|
353
|
-
controller.abort();
|
|
354
|
-
const options = makeParallelOptions(tmpDir, { signal: controller.signal });
|
|
355
|
-
|
|
356
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
357
|
-
sub1: { success: true },
|
|
358
|
-
}), options);
|
|
359
|
-
|
|
360
|
-
// Should complete without hanging — the abort is propagated to sub-steps
|
|
361
|
-
assert.ok(result);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("sets agent to 'parallel'", async (t) => {
|
|
365
|
-
const tmpDir = makeTmpDir(t);
|
|
366
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
367
|
-
const parallelSpec: Record<string, StepSpec> = { sub1: { agent: "test" } };
|
|
368
|
-
const options = makeParallelOptions(tmpDir);
|
|
369
|
-
|
|
370
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
371
|
-
sub1: { success: true },
|
|
372
|
-
}), options);
|
|
373
|
-
|
|
374
|
-
assert.strictEqual(result.agent, "parallel");
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("sets step name correctly", async (t) => {
|
|
378
|
-
const tmpDir = makeTmpDir(t);
|
|
379
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
380
|
-
const parallelSpec: Record<string, StepSpec> = { sub1: { agent: "test" } };
|
|
381
|
-
const options = makeParallelOptions(tmpDir);
|
|
382
|
-
|
|
383
|
-
const result = await executeParallelStep(parallelSpec, "my-parallel", state, mockExecutor({
|
|
384
|
-
sub1: { success: true },
|
|
385
|
-
}), options);
|
|
386
|
-
|
|
387
|
-
assert.strictEqual(result.step, "my-parallel");
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("includes textOutput as JSON stringified sub-outputs", async (t) => {
|
|
391
|
-
const tmpDir = makeTmpDir(t);
|
|
392
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
393
|
-
const parallelSpec: Record<string, StepSpec> = { sub1: { agent: "test" } };
|
|
394
|
-
const options = makeParallelOptions(tmpDir);
|
|
395
|
-
|
|
396
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
397
|
-
sub1: { success: true, output: { val: 1 } },
|
|
398
|
-
}), options);
|
|
399
|
-
|
|
400
|
-
assert.ok(result.textOutput);
|
|
401
|
-
const parsed = JSON.parse(result.textOutput!);
|
|
402
|
-
assert.deepStrictEqual(parsed.sub1, { val: 1 });
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("records positive durationMs", async (t) => {
|
|
406
|
-
const tmpDir = makeTmpDir(t);
|
|
407
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
408
|
-
const parallelSpec: Record<string, StepSpec> = { sub1: { agent: "test" } };
|
|
409
|
-
const options = makeParallelOptions(tmpDir);
|
|
410
|
-
|
|
411
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
412
|
-
sub1: { success: true },
|
|
413
|
-
}), options);
|
|
414
|
-
|
|
415
|
-
assert.ok(result.durationMs >= 0);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it("handles single sub-step", async (t) => {
|
|
419
|
-
const tmpDir = makeTmpDir(t);
|
|
420
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
421
|
-
const parallelSpec: Record<string, StepSpec> = { only: { agent: "test" } };
|
|
422
|
-
const options = makeParallelOptions(tmpDir);
|
|
423
|
-
|
|
424
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
425
|
-
only: { success: true, output: { single: true } },
|
|
426
|
-
}), options);
|
|
427
|
-
|
|
428
|
-
assert.strictEqual(result.status, "completed");
|
|
429
|
-
assert.deepStrictEqual((result.output as any).only, { single: true });
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it("handles all sub-steps failing", async (t) => {
|
|
433
|
-
const tmpDir = makeTmpDir(t);
|
|
434
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
435
|
-
const parallelSpec: Record<string, StepSpec> = {
|
|
436
|
-
sub1: { agent: "test" },
|
|
437
|
-
sub2: { agent: "test" },
|
|
438
|
-
};
|
|
439
|
-
const options = makeParallelOptions(tmpDir);
|
|
440
|
-
|
|
441
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, mockExecutor({
|
|
442
|
-
sub1: { success: false },
|
|
443
|
-
sub2: { success: false },
|
|
444
|
-
}), options);
|
|
445
|
-
|
|
446
|
-
assert.strictEqual(result.status, "failed");
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("handles rejected promise from executor (exception, not false return)", async (t) => {
|
|
450
|
-
const tmpDir = makeTmpDir(t);
|
|
451
|
-
const state: ExecutionState = { input: {}, steps: {}, status: "running" };
|
|
452
|
-
const parallelSpec: Record<string, StepSpec> = {
|
|
453
|
-
sub1: { agent: "test" },
|
|
454
|
-
sub2: { agent: "test" },
|
|
455
|
-
};
|
|
456
|
-
const options = makeParallelOptions(tmpDir);
|
|
457
|
-
|
|
458
|
-
const executor: SingleStepExecutor = async (stepName, stepSpec, execState, opts) => {
|
|
459
|
-
if (stepName === "sub2") throw new Error("boom");
|
|
460
|
-
execState.steps[stepName] = {
|
|
461
|
-
step: stepName,
|
|
462
|
-
agent: "mock",
|
|
463
|
-
status: "completed",
|
|
464
|
-
output: {},
|
|
465
|
-
usage: zeroUsage(),
|
|
466
|
-
durationMs: 0,
|
|
467
|
-
};
|
|
468
|
-
return true;
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const result = await executeParallelStep(parallelSpec, "parallel1", state, executor, options);
|
|
472
|
-
|
|
473
|
-
assert.strictEqual(result.status, "failed");
|
|
474
|
-
});
|
|
475
|
-
});
|
package/src/step-parallel.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parallel step executors — concurrent step execution within a layer
|
|
3
|
-
* or within a single parallel step declaration.
|
|
4
|
-
*/
|
|
5
|
-
import type { StepResult, StepSpec, ExecutionState, WorkflowSpec } from "./types.ts";
|
|
6
|
-
import type { ProgressWidgetState } from "./tui.ts";
|
|
7
|
-
import type { ExecutionLayer } from "./dag.ts";
|
|
8
|
-
import { createProgressWidget } from "./tui.ts";
|
|
9
|
-
import type nunjucks from "nunjucks";
|
|
10
|
-
import { zeroUsage, addUsage, WIDGET_ID } from "./step-shared.ts";
|
|
11
|
-
|
|
12
|
-
/** Options shared by parallel execution helpers. */
|
|
13
|
-
export interface ParallelOptions {
|
|
14
|
-
ctx: any;
|
|
15
|
-
pi: any;
|
|
16
|
-
signal?: AbortSignal;
|
|
17
|
-
loadAgent: (name: string) => any;
|
|
18
|
-
runDir: string;
|
|
19
|
-
spec: WorkflowSpec;
|
|
20
|
-
widgetState: ProgressWidgetState;
|
|
21
|
-
templateEnv?: nunjucks.Environment;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Callback type for executing a single step — injected to avoid circular imports. */
|
|
25
|
-
export type SingleStepExecutor = (
|
|
26
|
-
stepName: string,
|
|
27
|
-
stepSpec: StepSpec,
|
|
28
|
-
state: ExecutionState,
|
|
29
|
-
options: ParallelOptions,
|
|
30
|
-
) => Promise<boolean>;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Execute all steps in a layer concurrently.
|
|
34
|
-
*
|
|
35
|
-
* All steps start at the same time. If any step fails, remaining steps
|
|
36
|
-
* are cancelled via a shared AbortController. All results are collected
|
|
37
|
-
* before proceeding to the next layer.
|
|
38
|
-
*
|
|
39
|
-
* Parallel steps write to distinct keys in `state.steps`, which is safe
|
|
40
|
-
* in single-threaded Node.js. `writeState` uses atomic write (tmp + rename),
|
|
41
|
-
* so concurrent calls are safe — last one wins.
|
|
42
|
-
*/
|
|
43
|
-
export async function executeParallelLayer(
|
|
44
|
-
layer: ExecutionLayer,
|
|
45
|
-
spec: WorkflowSpec,
|
|
46
|
-
state: ExecutionState,
|
|
47
|
-
executeSingleStep: SingleStepExecutor,
|
|
48
|
-
options: ParallelOptions,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
const { ctx, signal, widgetState } = options;
|
|
51
|
-
|
|
52
|
-
// Create a child AbortController to cancel siblings on failure
|
|
53
|
-
const layerController = new AbortController();
|
|
54
|
-
if (signal) {
|
|
55
|
-
if (signal.aborted) {
|
|
56
|
-
layerController.abort(signal.reason);
|
|
57
|
-
} else {
|
|
58
|
-
signal.addEventListener("abort", () => layerController.abort(signal.reason), { once: true });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Update widget to show all parallel steps as running
|
|
63
|
-
widgetState.currentStep = layer.steps.join(", ");
|
|
64
|
-
if (ctx.hasUI) {
|
|
65
|
-
ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Launch all steps concurrently
|
|
69
|
-
const promises = layer.steps.map(async (stepName) => {
|
|
70
|
-
const stepSpec = spec.steps[stepName];
|
|
71
|
-
const success = await executeSingleStep(stepName, stepSpec, state, {
|
|
72
|
-
...options,
|
|
73
|
-
signal: layerController.signal,
|
|
74
|
-
});
|
|
75
|
-
if (!success && !layerController.signal.aborted) {
|
|
76
|
-
layerController.abort(new Error(`Step '${stepName}' failed`));
|
|
77
|
-
}
|
|
78
|
-
return { stepName, success };
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const results = await Promise.allSettled(promises);
|
|
82
|
-
|
|
83
|
-
// Check for failures
|
|
84
|
-
for (const result of results) {
|
|
85
|
-
if (result.status === "rejected") {
|
|
86
|
-
state.status = "failed";
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
if (result.status === "fulfilled" && !result.value.success) {
|
|
90
|
-
state.status = "failed";
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Execute a parallel step — runs all named sub-steps concurrently.
|
|
98
|
-
*
|
|
99
|
-
* Similar to executeParallelLayer but operates on sub-steps within
|
|
100
|
-
* a single declared step. The parallel step's result aggregates
|
|
101
|
-
* all sub-step results. Sub-step outputs are accessible via
|
|
102
|
-
* `${{ steps.<parallelStepName>.output.<subStepName> }}`.
|
|
103
|
-
*/
|
|
104
|
-
export async function executeParallelStep(
|
|
105
|
-
parallelSpec: Record<string, StepSpec>,
|
|
106
|
-
stepName: string,
|
|
107
|
-
state: ExecutionState,
|
|
108
|
-
executeSingleStep: SingleStepExecutor,
|
|
109
|
-
options: ParallelOptions,
|
|
110
|
-
): Promise<StepResult> {
|
|
111
|
-
const startTime = Date.now();
|
|
112
|
-
const { signal } = options;
|
|
113
|
-
|
|
114
|
-
const parallelController = new AbortController();
|
|
115
|
-
if (signal) {
|
|
116
|
-
if (signal.aborted) {
|
|
117
|
-
parallelController.abort(signal.reason);
|
|
118
|
-
} else {
|
|
119
|
-
signal.addEventListener("abort", () => parallelController.abort(signal.reason), { once: true });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Sub-steps share the outer state for reading but write to their own keys
|
|
124
|
-
const subResults: Record<string, StepResult> = {};
|
|
125
|
-
|
|
126
|
-
const subPromises = Object.entries(parallelSpec).map(async ([subName, subSpec]) => {
|
|
127
|
-
const success = await executeSingleStep(subName, subSpec, state, {
|
|
128
|
-
...options,
|
|
129
|
-
signal: parallelController.signal,
|
|
130
|
-
});
|
|
131
|
-
subResults[subName] = state.steps[subName];
|
|
132
|
-
if (!success && !parallelController.signal.aborted) {
|
|
133
|
-
parallelController.abort(new Error(`Sub-step '${subName}' failed`));
|
|
134
|
-
}
|
|
135
|
-
return success;
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const settled = await Promise.allSettled(subPromises);
|
|
139
|
-
|
|
140
|
-
// Aggregate usage and outputs
|
|
141
|
-
const totalUsage = zeroUsage();
|
|
142
|
-
const subOutputs: Record<string, unknown> = {};
|
|
143
|
-
let anyFailed = false;
|
|
144
|
-
|
|
145
|
-
for (const [subName] of Object.entries(parallelSpec)) {
|
|
146
|
-
const sub = subResults[subName];
|
|
147
|
-
if (sub) {
|
|
148
|
-
addUsage(totalUsage, sub.usage);
|
|
149
|
-
subOutputs[subName] = sub.output ?? sub.textOutput;
|
|
150
|
-
if (sub.status === "failed") anyFailed = true;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check for rejected promises too
|
|
155
|
-
for (const s of settled) {
|
|
156
|
-
if (s.status === "rejected") anyFailed = true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
step: stepName,
|
|
161
|
-
agent: "parallel",
|
|
162
|
-
status: anyFailed ? "failed" : "completed",
|
|
163
|
-
output: subOutputs,
|
|
164
|
-
textOutput: JSON.stringify(subOutputs, null, 2),
|
|
165
|
-
usage: totalUsage,
|
|
166
|
-
durationMs: Date.now() - startTime,
|
|
167
|
-
};
|
|
168
|
-
}
|
package/src/step-pause.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { executePause } from "./step-pause.ts";
|
|
4
|
-
|
|
5
|
-
describe("executePause", () => {
|
|
6
|
-
it("returns completed result with message", () => {
|
|
7
|
-
const result = executePause("review-point", "Review exploration results");
|
|
8
|
-
assert.strictEqual(result.status, "completed");
|
|
9
|
-
assert.strictEqual(result.step, "review-point");
|
|
10
|
-
assert.strictEqual(result.agent, "pause");
|
|
11
|
-
assert.strictEqual(result.textOutput, "Review exploration results");
|
|
12
|
-
assert.deepStrictEqual(result.output, { message: "Review exploration results" });
|
|
13
|
-
assert.strictEqual(result.durationMs, 0);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("returns completed result without message (boolean true)", () => {
|
|
17
|
-
const result = executePause("checkpoint");
|
|
18
|
-
assert.strictEqual(result.status, "completed");
|
|
19
|
-
assert.strictEqual(result.textOutput, "Workflow paused");
|
|
20
|
-
assert.strictEqual(result.output, undefined);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("has zero usage", () => {
|
|
24
|
-
const result = executePause("p");
|
|
25
|
-
assert.strictEqual(result.usage.cost, 0);
|
|
26
|
-
assert.strictEqual(result.usage.turns, 0);
|
|
27
|
-
assert.strictEqual(result.usage.input, 0);
|
|
28
|
-
assert.strictEqual(result.usage.output, 0);
|
|
29
|
-
});
|
|
30
|
-
});
|