@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,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
- }