@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,327 +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 nunjucks from "nunjucks";
7
- import { executeAgentStep } from "./step-agent.ts";
8
- import type { AgentStepOptions } from "./step-agent.ts";
9
- import { zeroUsage } from "./step-shared.ts";
10
- import { dispatch } from "./dispatch.ts";
11
- import type { StepSpec, StepResult, ExecutionState, AgentSpec } from "./types.ts";
12
- import { mockCtx } from "./test-helpers.ts";
13
-
14
- /**
15
- * Mock dispatch factory: returns a controlled StepResult.
16
- * Optionally captures call arguments for assertion.
17
- */
18
- function mockDispatch(
19
- result: Partial<StepResult> = {},
20
- capture?: { calls: any[] },
21
- ): typeof dispatch {
22
- return async (step, agent, prompt, opts) => {
23
- if (capture) capture.calls.push({ step, agent, prompt, opts });
24
- return {
25
- step: opts.stepName,
26
- agent: step.agent ?? "mock",
27
- status: "completed",
28
- usage: zeroUsage(),
29
- durationMs: 100,
30
- textOutput: "mock output",
31
- ...result,
32
- } as StepResult;
33
- };
34
- }
35
-
36
- function makeTmpRunDir(t: any): string {
37
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-agent-"));
38
- fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
39
- fs.mkdirSync(path.join(tmpDir, "sessions"), { recursive: true });
40
- t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
41
- return tmpDir;
42
- }
43
-
44
- function makeOptions(tmpDir: string, overrides?: Partial<AgentStepOptions>): AgentStepOptions {
45
- return {
46
- ctx: mockCtx(tmpDir),
47
- loadAgent: () => ({ name: "default" }),
48
- runDir: tmpDir,
49
- specFilePath: path.join(tmpDir, "spec.yaml"),
50
- widgetState: {
51
- spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
52
- state: { input: {}, steps: {}, status: "running" },
53
- startTime: Date.now(),
54
- },
55
- dispatchFn: mockDispatch(),
56
- ...overrides,
57
- };
58
- }
59
-
60
- describe("executeAgentStep", () => {
61
- // Happy path
62
- it("dispatches to dispatchFn and returns its result", async (t) => {
63
- const tmpDir = makeTmpRunDir(t);
64
- const stepSpec: StepSpec = { agent: "test-agent" };
65
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
66
-
67
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
68
- assert.strictEqual(result.status, "completed");
69
- assert.strictEqual(result.agent, "test-agent");
70
- });
71
-
72
- it("resolves input expressions before dispatch", async (t) => {
73
- const tmpDir = makeTmpRunDir(t);
74
- const stepSpec: StepSpec = { agent: "test-agent", input: { data: "${{ input.value }}" } };
75
- const state: ExecutionState = { input: { value: "resolved" }, steps: {}, status: "running" };
76
- const capture = { calls: [] as any[] };
77
-
78
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
79
- dispatchFn: mockDispatch({}, capture),
80
- }));
81
- assert.strictEqual(result.status, "completed");
82
- // The prompt should contain the resolved input
83
- assert.ok(capture.calls.length > 0);
84
- const prompt = capture.calls[0].prompt;
85
- assert.ok(prompt.includes("resolved"), `Expected prompt to contain "resolved", got: ${prompt}`);
86
- });
87
-
88
- it("loads agent via loadAgent callback", async (t) => {
89
- const tmpDir = makeTmpRunDir(t);
90
- const stepSpec: StepSpec = { agent: "my-special-agent" };
91
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
92
- let loadedAgentName = "";
93
-
94
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
95
- loadAgent: (name: string) => {
96
- loadedAgentName = name;
97
- return { name };
98
- },
99
- }));
100
- assert.strictEqual(loadedAgentName, "my-special-agent");
101
- assert.strictEqual(result.status, "completed");
102
- });
103
-
104
- it("compiles agent spec through templateEnv when provided", async (t) => {
105
- const tmpDir = makeTmpRunDir(t);
106
- // stepSpec.input maps role from state.input so resolvedInput carries it to compileAgentSpec
107
- const stepSpec: StepSpec = { agent: "test-agent", input: { role: "${{ input.role }}" } };
108
- const state: ExecutionState = { input: { role: "analyzer" }, steps: {}, status: "running" };
109
- const env = new nunjucks.Environment(undefined, { autoescape: false, throwOnUndefined: false });
110
- const capture = { calls: [] as any[] };
111
-
112
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
113
- loadAgent: () => ({ name: "test", systemPrompt: "You are a {{ role }}" }),
114
- templateEnv: env,
115
- dispatchFn: mockDispatch({}, capture),
116
- }));
117
- assert.strictEqual(result.status, "completed");
118
- // The compiled agent should have the rendered systemPrompt
119
- assert.ok(capture.calls.length > 0);
120
- const passedAgent = capture.calls[0].agent;
121
- assert.strictEqual(passedAgent.systemPrompt, "You are a analyzer");
122
- });
123
-
124
- it("passes signal to dispatchFn", async (t) => {
125
- const tmpDir = makeTmpRunDir(t);
126
- const stepSpec: StepSpec = { agent: "test-agent" };
127
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
128
- const controller = new AbortController();
129
- const capture = { calls: [] as any[] };
130
-
131
- await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
132
- signal: controller.signal,
133
- dispatchFn: mockDispatch({}, capture),
134
- }));
135
- assert.ok(capture.calls.length > 0);
136
- assert.strictEqual(capture.calls[0].opts.signal, controller.signal);
137
- });
138
-
139
- it("passes timeout to dispatchFn when stepSpec.timeout is set", async (t) => {
140
- const tmpDir = makeTmpRunDir(t);
141
- const stepSpec: StepSpec = { agent: "test-agent", timeout: { seconds: 30 } };
142
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
143
- const capture = { calls: [] as any[] };
144
-
145
- await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
146
- dispatchFn: mockDispatch({}, capture),
147
- }));
148
- assert.ok(capture.calls.length > 0);
149
- assert.strictEqual(capture.calls[0].opts.timeoutMs, 30000);
150
- });
151
-
152
- // Expression resolution failure
153
- it("returns failed result when input expression resolution throws", async (t) => {
154
- const tmpDir = makeTmpRunDir(t);
155
- const stepSpec: StepSpec = { agent: "test-agent", input: { bad: "${{ steps.missing.output }}" } };
156
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
157
-
158
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
159
- assert.strictEqual(result.status, "failed");
160
- assert.ok(result.error);
161
- assert.ok(result.error!.includes("missing"));
162
- assert.strictEqual(result.durationMs, 0);
163
- assert.deepStrictEqual(result.usage, zeroUsage());
164
- });
165
-
166
- // Output validation (schema-bound output)
167
- it("validates output file against schema when output.schema is set and file exists", async (t) => {
168
- const tmpDir = makeTmpRunDir(t);
169
-
170
- // Create schema file
171
- const schemaDir = path.join(tmpDir, "schemas");
172
- fs.mkdirSync(schemaDir, { recursive: true });
173
- fs.writeFileSync(path.join(schemaDir, "output.json"), JSON.stringify({
174
- type: "object",
175
- required: ["summary"],
176
- properties: { summary: { type: "string" } },
177
- }));
178
-
179
- // Create outputs file simulating what the subprocess would write
180
- fs.writeFileSync(
181
- path.join(tmpDir, "outputs", "step1.json"),
182
- JSON.stringify({ summary: "all good" }),
183
- );
184
-
185
- const stepSpec: StepSpec = {
186
- agent: "test-agent",
187
- output: { schema: path.join(schemaDir, "output.json") },
188
- };
189
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
190
-
191
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
192
- assert.strictEqual(result.status, "completed");
193
- assert.deepStrictEqual(result.output, { summary: "all good" });
194
- });
195
-
196
- it("falls back to textOutput parsing when output file does not exist", async (t) => {
197
- const tmpDir = makeTmpRunDir(t);
198
-
199
- const schemaDir = path.join(tmpDir, "schemas");
200
- fs.mkdirSync(schemaDir, { recursive: true });
201
- fs.writeFileSync(path.join(schemaDir, "output.json"), JSON.stringify({
202
- type: "object",
203
- required: ["summary"],
204
- properties: { summary: { type: "string" } },
205
- }));
206
-
207
- const stepSpec: StepSpec = {
208
- agent: "test-agent",
209
- output: { schema: path.join(schemaDir, "output.json") },
210
- };
211
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
212
-
213
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
214
- dispatchFn: mockDispatch({ textOutput: JSON.stringify({ summary: "from text" }) }),
215
- }));
216
- assert.strictEqual(result.status, "completed");
217
- assert.deepStrictEqual(result.output, { summary: "from text" });
218
- });
219
-
220
- it("fails when schema validation rejects output", async (t) => {
221
- const tmpDir = makeTmpRunDir(t);
222
-
223
- const schemaDir = path.join(tmpDir, "schemas");
224
- fs.mkdirSync(schemaDir, { recursive: true });
225
- fs.writeFileSync(path.join(schemaDir, "output.json"), JSON.stringify({
226
- type: "object",
227
- required: ["summary"],
228
- properties: { summary: { type: "string" } },
229
- }));
230
-
231
- // Write invalid output (missing required field "summary")
232
- fs.writeFileSync(
233
- path.join(tmpDir, "outputs", "step1.json"),
234
- JSON.stringify({ other: "nope" }),
235
- );
236
-
237
- const stepSpec: StepSpec = {
238
- agent: "test-agent",
239
- output: { schema: path.join(schemaDir, "output.json") },
240
- };
241
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
242
-
243
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
244
- assert.strictEqual(result.status, "failed");
245
- assert.ok(result.error);
246
- assert.ok(result.error!.toLowerCase().includes("validat") || result.error!.includes("required"));
247
- });
248
-
249
- it("fails when no valid JSON output is available for schema-bound step", async (t) => {
250
- const tmpDir = makeTmpRunDir(t);
251
-
252
- const schemaDir = path.join(tmpDir, "schemas");
253
- fs.mkdirSync(schemaDir, { recursive: true });
254
- fs.writeFileSync(path.join(schemaDir, "output.json"), JSON.stringify({
255
- type: "object",
256
- properties: { summary: { type: "string" } },
257
- }));
258
-
259
- const stepSpec: StepSpec = {
260
- agent: "test-agent",
261
- output: { schema: path.join(schemaDir, "output.json") },
262
- };
263
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
264
-
265
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir, {
266
- dispatchFn: mockDispatch({ textOutput: "not valid JSON at all" }),
267
- }));
268
- assert.strictEqual(result.status, "failed");
269
- assert.ok(result.error);
270
- assert.ok(result.error!.includes("no valid JSON output"));
271
- });
272
-
273
- // Output persistence (non-schema path)
274
- it("persists output via persistStepOutput when no schema", async (t) => {
275
- const tmpDir = makeTmpRunDir(t);
276
- const stepSpec: StepSpec = { agent: "test-agent" };
277
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
278
-
279
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
280
- // persistStepOutput should have been called — outputPath set
281
- assert.ok(result.outputPath);
282
- });
283
-
284
- it("resolves output.path expressions for persistence", async (t) => {
285
- const tmpDir = makeTmpRunDir(t);
286
- const customPath = path.join(tmpDir, "resolved-output.json");
287
- const stepSpec: StepSpec = {
288
- agent: "test-agent",
289
- output: { path: customPath },
290
- };
291
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
292
-
293
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
294
- assert.strictEqual(result.outputPath, customPath);
295
- });
296
-
297
- // Edge cases
298
- it("handles undefined stepSpec.input (defaults to {})", async (t) => {
299
- const tmpDir = makeTmpRunDir(t);
300
- const stepSpec: StepSpec = { agent: "test-agent" };
301
- const state: ExecutionState = { input: {}, steps: {}, status: "running" };
302
-
303
- const result = await executeAgentStep("step1", stepSpec, state, makeOptions(tmpDir));
304
- assert.strictEqual(result.status, "completed");
305
- });
306
-
307
- it("uses real dispatch when dispatchFn not provided (structural check)", async (t) => {
308
- // This test verifies the fallback code path exists structurally
309
- // without actually spawning a subprocess (which requires pi on PATH).
310
- // We verify by checking that the function signature accepts options without dispatchFn.
311
- const tmpDir = makeTmpRunDir(t);
312
- const options: AgentStepOptions = {
313
- ctx: mockCtx(tmpDir),
314
- loadAgent: () => ({ name: "default" }),
315
- runDir: tmpDir,
316
- specFilePath: path.join(tmpDir, "spec.yaml"),
317
- widgetState: {
318
- spec: { name: "test", description: "", steps: {}, source: "project" as const, filePath: "" },
319
- state: { input: {}, steps: {}, status: "running" },
320
- startTime: Date.now(),
321
- },
322
- // dispatchFn intentionally omitted — defaults to real dispatch
323
- };
324
- // Just verify the options type is valid without actually calling
325
- assert.ok(options.dispatchFn === undefined);
326
- });
327
- });
package/src/step-agent.ts DELETED
@@ -1,178 +0,0 @@
1
- /**
2
- * Agent step executor — dispatches an LLM subprocess and validates output.
3
- */
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import type { StepSpec, AgentSpec, StepResult, ExecutionState, ExpressionScope } from "./types.ts";
7
- import type { ProgressWidgetState } from "./tui.ts";
8
- import { resolveExpressions } from "./expression.ts";
9
- import { dispatch } from "./dispatch.ts";
10
- import { validateFromFile } from "@davidorex/pi-project/src/schema-validator.ts";
11
- import { persistStepOutput } from "./output.ts";
12
- import { zeroUsage, resolveSchemaPath, buildPrompt, compileAgentSpec } from "./step-shared.ts";
13
- import type nunjucks from "nunjucks";
14
-
15
- /** Retry context passed from the executor on retry attempts. */
16
- export interface RetryContext {
17
- attempt: number;
18
- priorErrors: string[];
19
- steeringMessage?: string;
20
- }
21
-
22
- export interface AgentStepOptions {
23
- ctx: any;
24
- signal?: AbortSignal;
25
- loadAgent: (name: string) => AgentSpec;
26
- runDir: string;
27
- specFilePath: string;
28
- widgetState: ProgressWidgetState;
29
- templateEnv?: nunjucks.Environment;
30
- dispatchFn?: typeof dispatch; // injectable for testing; defaults to real dispatch
31
- modelConfig?: import("./dispatch.ts").ModelConfig;
32
- retryContext?: RetryContext; // set on retry attempts (attempt > 1)
33
- onStepActivity?: (activity: { tool: string; preview: string; timestamp: number }) => void;
34
- }
35
-
36
- /**
37
- * Execute an agent step: resolve input, render templates, dispatch subprocess,
38
- * validate output, persist result.
39
- *
40
- * Returns the StepResult.
41
- */
42
- export async function executeAgentStep(
43
- stepName: string,
44
- stepSpec: StepSpec,
45
- state: ExecutionState,
46
- options: AgentStepOptions,
47
- ): Promise<StepResult> {
48
- const { ctx, signal, loadAgent, runDir, specFilePath, templateEnv } = options;
49
- const scope: ExpressionScope = { input: state.input, steps: state.steps };
50
-
51
- // Expose forEach bindings (as name + forEach metadata) if present on the state
52
- const stateAny = state as Record<string, unknown>;
53
- if (stateAny.forEach !== undefined) {
54
- scope.forEach = stateAny.forEach;
55
- }
56
- for (const key of Object.keys(stateAny)) {
57
- if (key !== "input" && key !== "steps" && key !== "status" && key !== "loop" &&
58
- key !== "workflowName" && key !== "specVersion" && key !== "startedAt" &&
59
- key !== "updatedAt" && key !== "forEach") {
60
- scope[key] = stateAny[key];
61
- }
62
- }
63
-
64
- // Resolve input expressions
65
- let resolvedInput: unknown;
66
- try {
67
- resolvedInput = resolveExpressions(stepSpec.input ?? {}, scope);
68
- } catch (err) {
69
- return {
70
- step: stepName,
71
- agent: stepSpec.agent,
72
- status: "failed",
73
- usage: zeroUsage(),
74
- durationMs: 0,
75
- error: err instanceof Error ? err.message : String(err),
76
- };
77
- }
78
-
79
- // Load and optionally render agent template
80
- let agentSpec: AgentSpec;
81
- try {
82
- agentSpec = loadAgent(stepSpec.agent);
83
- } catch (err) {
84
- return {
85
- step: stepName,
86
- agent: stepSpec.agent,
87
- status: "failed",
88
- usage: zeroUsage(),
89
- durationMs: 0,
90
- error: err instanceof Error ? err.message : String(err),
91
- };
92
- }
93
- // Inject output schema into template context if available
94
- if (stepSpec.output?.schema && typeof resolvedInput === "object" && resolvedInput !== null) {
95
- const schemaPath = resolveSchemaPath(stepSpec.output.schema, options.specFilePath);
96
- try {
97
- const schemaContent = fs.readFileSync(schemaPath, "utf8");
98
- (resolvedInput as Record<string, unknown>).output_schema = schemaContent;
99
- } catch { /* schema file not found — template can still render without it */ }
100
- }
101
- agentSpec = compileAgentSpec(agentSpec, resolvedInput, templateEnv);
102
-
103
- let prompt = buildPrompt(stepSpec, agentSpec, resolvedInput, runDir, stepName);
104
-
105
- // Inject retry context if this is a retry attempt
106
- if (options.retryContext) {
107
- const rc = options.retryContext;
108
- const retryParts: string[] = [];
109
- retryParts.push(`## Retry Context (attempt ${rc.attempt})\n`);
110
- retryParts.push("Your previous attempt failed. The filesystem has been rolled back to its pre-attempt state.\n");
111
- retryParts.push("### Prior Errors");
112
- for (let i = 0; i < rc.priorErrors.length; i++) {
113
- retryParts.push(`${i + 1}. ${rc.priorErrors[i]}`);
114
- }
115
- retryParts.push("");
116
- if (rc.steeringMessage) {
117
- retryParts.push("### Steering");
118
- retryParts.push(rc.steeringMessage);
119
- retryParts.push("");
120
- }
121
- retryParts.push("---\n");
122
- prompt = retryParts.join("\n") + prompt;
123
- }
124
-
125
- const dispatchFn = options.dispatchFn ?? dispatch;
126
- const result = await dispatchFn(stepSpec, agentSpec, prompt, {
127
- cwd: ctx.cwd,
128
- sessionLogDir: path.join(runDir, "sessions"),
129
- stepName,
130
- signal,
131
- timeoutMs: stepSpec.timeout ? stepSpec.timeout.seconds * 1000 : undefined,
132
- onEvent: (event) => {
133
- if (event.type === "tool_execution_start" && event.toolName && options.onStepActivity) {
134
- options.onStepActivity({
135
- tool: event.toolName,
136
- preview: event.toolArgs || "",
137
- timestamp: Date.now(),
138
- });
139
- }
140
- },
141
- modelConfig: options.modelConfig,
142
- });
143
-
144
- // Resolve output path from spec (may contain ${{ }} expressions)
145
- const resolvedOutputPath = stepSpec.output?.path
146
- ? String(resolveExpressions(stepSpec.output.path, scope))
147
- : undefined;
148
-
149
- // Validate output against schema (if defined)
150
- if (stepSpec.output?.schema && result.status === "completed") {
151
- const schemaPath = resolveSchemaPath(stepSpec.output.schema, specFilePath);
152
- try {
153
- const outputFilePath = path.join(runDir, "outputs", `${stepName}.json`);
154
- if (fs.existsSync(outputFilePath)) {
155
- const rawOutput = JSON.parse(fs.readFileSync(outputFilePath, "utf-8"));
156
- validateFromFile(schemaPath, rawOutput, `step output for '${stepName}'`);
157
- result.output = rawOutput;
158
- } else {
159
- try {
160
- const parsed = JSON.parse(result.textOutput || "");
161
- validateFromFile(schemaPath, parsed, `step output for '${stepName}'`);
162
- result.output = parsed;
163
- result.outputPath = persistStepOutput(runDir, stepName, parsed, undefined, resolvedOutputPath);
164
- } catch {
165
- result.status = "failed";
166
- result.error = `Step '${stepName}' has output schema but no valid JSON output was produced`;
167
- }
168
- }
169
- } catch (err) {
170
- result.status = "failed";
171
- result.error = err instanceof Error ? err.message : String(err);
172
- }
173
- } else {
174
- result.outputPath = persistStepOutput(runDir, stepName, result.output, result.textOutput, resolvedOutputPath);
175
- }
176
-
177
- return result;
178
- }