@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.
Files changed (215) 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 +172 -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 +675 -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 +156 -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 +57 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +125 -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 +164 -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 +726 -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 +438 -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 +464 -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 +84 -0
  140. package/skills/pi-workflows/SKILL.md +247 -0
  141. package/templates/architecture-designer/task.md +117 -0
  142. package/templates/architecture-inferrer/task.md +61 -0
  143. package/templates/gap-identifier/task.md +103 -0
  144. package/templates/handoff-writer/task.md +91 -0
  145. package/templates/plan-creator/task.md +143 -0
  146. package/templates/project-definer/task.md +67 -0
  147. package/templates/project-inferrer/task.md +56 -0
  148. package/templates/requirements-gatherer/task.md +90 -0
  149. package/workflows/analyze-existing-project.workflow.yaml +74 -0
  150. package/workflows/create-handoff.workflow.yaml +58 -0
  151. package/workflows/create-phase.workflow.yaml +1 -1
  152. package/workflows/do-gap.workflow.yaml +4 -4
  153. package/workflows/fix-audit.workflow.yaml +1 -1
  154. package/workflows/gap-to-phase.workflow.yaml +1 -1
  155. package/workflows/init-new-project.workflow.yaml +55 -0
  156. package/workflows/plan-from-requirements.workflow.yaml +51 -0
  157. package/workflows/self-implement.workflow.yaml +3 -3
  158. package/workflows/typed-analysis.workflow.yaml +4 -4
  159. package/src/agent-spec.test.ts +0 -289
  160. package/src/agent-spec.ts +0 -122
  161. package/src/block-validation.test.ts +0 -350
  162. package/src/checkpoint.test.ts +0 -237
  163. package/src/checkpoint.ts +0 -140
  164. package/src/completion.test.ts +0 -143
  165. package/src/completion.ts +0 -68
  166. package/src/dag.test.ts +0 -350
  167. package/src/dag.ts +0 -219
  168. package/src/dispatch.test.ts +0 -475
  169. package/src/dispatch.ts +0 -353
  170. package/src/expression.test.ts +0 -353
  171. package/src/expression.ts +0 -332
  172. package/src/format.test.ts +0 -80
  173. package/src/format.ts +0 -41
  174. package/src/graduated-failure.test.ts +0 -556
  175. package/src/index.test.ts +0 -114
  176. package/src/index.ts +0 -516
  177. package/src/loop.test.ts +0 -551
  178. package/src/output.test.ts +0 -213
  179. package/src/output.ts +0 -70
  180. package/src/parallel-integration.test.ts +0 -175
  181. package/src/resume.test.ts +0 -192
  182. package/src/state.test.ts +0 -192
  183. package/src/state.ts +0 -254
  184. package/src/step-agent.test.ts +0 -327
  185. package/src/step-agent.ts +0 -178
  186. package/src/step-command.test.ts +0 -330
  187. package/src/step-command.ts +0 -132
  188. package/src/step-foreach.test.ts +0 -647
  189. package/src/step-foreach.ts +0 -148
  190. package/src/step-gate.test.ts +0 -185
  191. package/src/step-gate.ts +0 -116
  192. package/src/step-loop.test.ts +0 -626
  193. package/src/step-loop.ts +0 -323
  194. package/src/step-parallel.test.ts +0 -475
  195. package/src/step-parallel.ts +0 -168
  196. package/src/step-pause.test.ts +0 -30
  197. package/src/step-shared.test.ts +0 -355
  198. package/src/step-shared.ts +0 -157
  199. package/src/step-transform.test.ts +0 -155
  200. package/src/step-transform.ts +0 -47
  201. package/src/template-integration.test.ts +0 -72
  202. package/src/template.test.ts +0 -162
  203. package/src/test-helpers.ts +0 -51
  204. package/src/tui.test.ts +0 -355
  205. package/src/tui.ts +0 -182
  206. package/src/types.ts +0 -195
  207. package/src/verifier-schema.test.ts +0 -226
  208. package/src/workflow-discovery.test.ts +0 -105
  209. package/src/workflow-discovery.ts +0 -114
  210. package/src/workflow-executor.test.ts +0 -1532
  211. package/src/workflow-executor.ts +0 -810
  212. package/src/workflow-sdk.test.ts +0 -238
  213. package/src/workflow-sdk.ts +0 -262
  214. package/src/workflow-spec.test.ts +0 -576
  215. 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
- });