@davidorex/pi-workflows 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +12 -2
  3. package/agents/architecture-designer.agent.yaml +58 -0
  4. package/agents/architecture-inferrer.agent.yaml +60 -0
  5. package/agents/gap-identifier.agent.yaml +66 -0
  6. package/agents/handoff-writer.agent.yaml +64 -0
  7. package/agents/plan-creator.agent.yaml +63 -0
  8. package/agents/project-definer.agent.yaml +53 -0
  9. package/agents/project-inferrer.agent.yaml +63 -0
  10. package/agents/requirements-gatherer.agent.yaml +58 -0
  11. package/dist/agent-spec.d.ts +27 -0
  12. package/dist/agent-spec.d.ts.map +1 -0
  13. package/dist/agent-spec.js +112 -0
  14. package/dist/agent-spec.js.map +1 -0
  15. package/dist/checkpoint.d.ts +50 -0
  16. package/dist/checkpoint.d.ts.map +1 -0
  17. package/dist/checkpoint.js +109 -0
  18. package/dist/checkpoint.js.map +1 -0
  19. package/dist/completion.d.ts +18 -0
  20. package/dist/completion.d.ts.map +1 -0
  21. package/dist/completion.js +58 -0
  22. package/dist/completion.js.map +1 -0
  23. package/dist/dag.d.ts +56 -0
  24. package/dist/dag.d.ts.map +1 -0
  25. package/dist/dag.js +178 -0
  26. package/dist/dag.js.map +1 -0
  27. package/dist/dispatch.d.ts +35 -0
  28. package/dist/dispatch.d.ts.map +1 -0
  29. package/dist/dispatch.js +311 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/expression.d.ts +64 -0
  32. package/dist/expression.d.ts.map +1 -0
  33. package/dist/expression.js +294 -0
  34. package/dist/expression.js.map +1 -0
  35. package/dist/format.d.ts +19 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +42 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +724 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/output.d.ts +12 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +59 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/state.d.ts +66 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +227 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/step-agent.d.ts +35 -0
  52. package/dist/step-agent.d.ts.map +1 -0
  53. package/dist/step-agent.js +171 -0
  54. package/dist/step-agent.js.map +1 -0
  55. package/dist/step-command.d.ts +21 -0
  56. package/dist/step-command.d.ts.map +1 -0
  57. package/dist/step-command.js +150 -0
  58. package/dist/step-command.js.map +1 -0
  59. package/dist/step-foreach.d.ts +33 -0
  60. package/dist/step-foreach.d.ts.map +1 -0
  61. package/dist/step-foreach.js +112 -0
  62. package/dist/step-foreach.js.map +1 -0
  63. package/dist/step-gate.d.ts +18 -0
  64. package/dist/step-gate.d.ts.map +1 -0
  65. package/dist/step-gate.js +137 -0
  66. package/dist/step-gate.js.map +1 -0
  67. package/dist/step-loop.d.ts +32 -0
  68. package/dist/step-loop.d.ts.map +1 -0
  69. package/dist/step-loop.js +278 -0
  70. package/dist/step-loop.js.map +1 -0
  71. package/dist/step-monitor.d.ts +56 -0
  72. package/dist/step-monitor.d.ts.map +1 -0
  73. package/dist/step-monitor.js +317 -0
  74. package/dist/step-monitor.js.map +1 -0
  75. package/dist/step-parallel.d.ts +43 -0
  76. package/dist/step-parallel.d.ts.map +1 -0
  77. package/dist/step-parallel.js +122 -0
  78. package/dist/step-parallel.js.map +1 -0
  79. package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
  80. package/dist/step-pause.d.ts.map +1 -0
  81. package/dist/step-pause.js +22 -0
  82. package/dist/step-pause.js.map +1 -0
  83. package/dist/step-shared.d.ts +63 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +136 -0
  86. package/dist/step-shared.js.map +1 -0
  87. package/dist/step-transform.d.ts +11 -0
  88. package/dist/step-transform.d.ts.map +1 -0
  89. package/dist/step-transform.js +41 -0
  90. package/dist/step-transform.js.map +1 -0
  91. package/dist/template.d.ts +51 -0
  92. package/dist/template.d.ts.map +1 -0
  93. package/{src/template.ts → dist/template.js} +32 -38
  94. package/dist/template.js.map +1 -0
  95. package/dist/test-helpers.d.ts +17 -0
  96. package/dist/test-helpers.d.ts.map +1 -0
  97. package/dist/test-helpers.js +46 -0
  98. package/dist/test-helpers.js.map +1 -0
  99. package/dist/tui.d.ts +47 -0
  100. package/dist/tui.d.ts.map +1 -0
  101. package/dist/tui.js +143 -0
  102. package/dist/tui.js.map +1 -0
  103. package/dist/types.d.ts +165 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/workflow-discovery.d.ts +23 -0
  108. package/dist/workflow-discovery.d.ts.map +1 -0
  109. package/dist/workflow-discovery.js +103 -0
  110. package/dist/workflow-discovery.js.map +1 -0
  111. package/dist/workflow-executor.d.ts +50 -0
  112. package/dist/workflow-executor.d.ts.map +1 -0
  113. package/dist/workflow-executor.js +738 -0
  114. package/dist/workflow-executor.js.map +1 -0
  115. package/dist/workflow-sdk.d.ts +44 -0
  116. package/dist/workflow-sdk.d.ts.map +1 -0
  117. package/dist/workflow-sdk.js +457 -0
  118. package/dist/workflow-sdk.js.map +1 -0
  119. package/dist/workflow-spec.d.ts +33 -0
  120. package/dist/workflow-spec.d.ts.map +1 -0
  121. package/dist/workflow-spec.js +476 -0
  122. package/dist/workflow-spec.js.map +1 -0
  123. package/dist/workflows-dir.d.ts +3 -0
  124. package/dist/workflows-dir.d.ts.map +1 -0
  125. package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
  126. package/dist/workflows-dir.js.map +1 -0
  127. package/package.json +53 -38
  128. package/schemas/decomposition-specs.schema.json +48 -48
  129. package/schemas/execution-results.schema.json +48 -48
  130. package/schemas/investigation-findings.schema.json +43 -43
  131. package/schemas/pattern-analysis.schema.json +41 -41
  132. package/schemas/phase.schema.json +48 -0
  133. package/schemas/plan-breakdown.schema.json +20 -20
  134. package/schemas/quality-analysis.schema.json +33 -33
  135. package/schemas/research-findings.schema.json +42 -42
  136. package/schemas/structure-analysis.schema.json +40 -40
  137. package/schemas/synthesis.schema.json +31 -31
  138. package/schemas/verifier-output.schema.json +94 -94
  139. package/skill-narrative.md +107 -0
  140. package/skills/pi-workflows/SKILL.md +185 -0
  141. package/skills/pi-workflows/references/bundled-resources.md +87 -0
  142. package/templates/architecture-designer/task.md +117 -0
  143. package/templates/architecture-inferrer/task.md +61 -0
  144. package/templates/gap-identifier/task.md +103 -0
  145. package/templates/handoff-writer/task.md +91 -0
  146. package/templates/plan-creator/task.md +143 -0
  147. package/templates/project-definer/task.md +67 -0
  148. package/templates/project-inferrer/task.md +56 -0
  149. package/templates/requirements-gatherer/task.md +90 -0
  150. package/workflows/analyze-existing-project.workflow.yaml +94 -0
  151. package/workflows/create-handoff.workflow.yaml +66 -0
  152. package/workflows/create-phase.workflow.yaml +1 -1
  153. package/workflows/do-gap.workflow.yaml +4 -4
  154. package/workflows/fix-audit.workflow.yaml +1 -1
  155. package/workflows/gap-to-phase.workflow.yaml +1 -1
  156. package/workflows/init-new-project.workflow.yaml +81 -0
  157. package/workflows/plan-from-requirements.workflow.yaml +59 -0
  158. package/workflows/self-implement.workflow.yaml +3 -3
  159. package/workflows/typed-analysis.workflow.yaml +4 -4
  160. package/src/agent-spec.test.ts +0 -289
  161. package/src/agent-spec.ts +0 -122
  162. package/src/block-validation.test.ts +0 -350
  163. package/src/checkpoint.test.ts +0 -237
  164. package/src/checkpoint.ts +0 -140
  165. package/src/completion.test.ts +0 -143
  166. package/src/completion.ts +0 -68
  167. package/src/dag.test.ts +0 -350
  168. package/src/dag.ts +0 -219
  169. package/src/dispatch.test.ts +0 -475
  170. package/src/dispatch.ts +0 -353
  171. package/src/expression.test.ts +0 -353
  172. package/src/expression.ts +0 -332
  173. package/src/format.test.ts +0 -80
  174. package/src/format.ts +0 -41
  175. package/src/graduated-failure.test.ts +0 -556
  176. package/src/index.test.ts +0 -114
  177. package/src/index.ts +0 -516
  178. package/src/loop.test.ts +0 -551
  179. package/src/output.test.ts +0 -213
  180. package/src/output.ts +0 -70
  181. package/src/parallel-integration.test.ts +0 -175
  182. package/src/resume.test.ts +0 -192
  183. package/src/state.test.ts +0 -192
  184. package/src/state.ts +0 -254
  185. package/src/step-agent.test.ts +0 -327
  186. package/src/step-agent.ts +0 -178
  187. package/src/step-command.test.ts +0 -330
  188. package/src/step-command.ts +0 -132
  189. package/src/step-foreach.test.ts +0 -647
  190. package/src/step-foreach.ts +0 -148
  191. package/src/step-gate.test.ts +0 -185
  192. package/src/step-gate.ts +0 -116
  193. package/src/step-loop.test.ts +0 -626
  194. package/src/step-loop.ts +0 -323
  195. package/src/step-parallel.test.ts +0 -475
  196. package/src/step-parallel.ts +0 -168
  197. package/src/step-pause.test.ts +0 -30
  198. package/src/step-shared.test.ts +0 -355
  199. package/src/step-shared.ts +0 -157
  200. package/src/step-transform.test.ts +0 -155
  201. package/src/step-transform.ts +0 -47
  202. package/src/template-integration.test.ts +0 -72
  203. package/src/template.test.ts +0 -162
  204. package/src/test-helpers.ts +0 -51
  205. package/src/tui.test.ts +0 -355
  206. package/src/tui.ts +0 -182
  207. package/src/types.ts +0 -195
  208. package/src/verifier-schema.test.ts +0 -226
  209. package/src/workflow-discovery.test.ts +0 -105
  210. package/src/workflow-discovery.ts +0 -114
  211. package/src/workflow-executor.test.ts +0 -1532
  212. package/src/workflow-executor.ts +0 -810
  213. package/src/workflow-sdk.test.ts +0 -238
  214. package/src/workflow-sdk.ts +0 -262
  215. package/src/workflow-spec.test.ts +0 -576
  216. 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
- });
@@ -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
- }
@@ -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
- });