@davidorex/pi-workflows 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +172 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +156 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +726 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +438 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +464 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +84 -0
- package/skills/pi-workflows/SKILL.md +247 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- package/src/workflow-spec.ts +0 -471
package/src/step-loop.test.ts
DELETED
|
@@ -1,626 +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 { executeLoop } from "./step-loop.ts";
|
|
7
|
-
import type { LoopExecuteOptions } from "./step-loop.ts";
|
|
8
|
-
import { zeroUsage, DEFAULT_MAX_ATTEMPTS } from "./step-shared.ts";
|
|
9
|
-
import type { LoopSpec, StepResult, StepSpec, ExecutionState, AgentSpec } from "./types.ts";
|
|
10
|
-
import { mockCtx, mockPi, makeSpec } from "./test-helpers.ts";
|
|
11
|
-
|
|
12
|
-
function makeState(overrides?: Partial<ExecutionState>): ExecutionState {
|
|
13
|
-
return { input: {}, steps: {}, status: "running", ...overrides };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function makeLoopOptions(t: any, overrides?: Partial<LoopExecuteOptions>): LoopExecuteOptions {
|
|
17
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "wf-loop-"));
|
|
18
|
-
fs.mkdirSync(path.join(tmpDir, "outputs"), { recursive: true });
|
|
19
|
-
fs.mkdirSync(path.join(tmpDir, "sessions"), { recursive: true });
|
|
20
|
-
t.after(() => fs.rmSync(tmpDir, { recursive: true, force: true }));
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
ctx: mockCtx(tmpDir),
|
|
24
|
-
pi: mockPi(),
|
|
25
|
-
loadAgent: () => ({ name: "default" }),
|
|
26
|
-
dispatchAgent: async (step, agent, prompt, opts) => ({
|
|
27
|
-
step: opts.stepName,
|
|
28
|
-
agent: step.agent ?? "default",
|
|
29
|
-
status: "completed" as const,
|
|
30
|
-
usage: zeroUsage(),
|
|
31
|
-
durationMs: 50,
|
|
32
|
-
textOutput: "agent output",
|
|
33
|
-
}),
|
|
34
|
-
runDir: tmpDir,
|
|
35
|
-
spec: makeSpec({ steps: {} }),
|
|
36
|
-
...overrides,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("executeLoop — direct unit tests", () => {
|
|
41
|
-
// Gate-only loops (no agent dispatch needed)
|
|
42
|
-
it("breaks on first gate pass (onPass: break)", async (t) => {
|
|
43
|
-
const loopSpec: LoopSpec = {
|
|
44
|
-
maxAttempts: 3,
|
|
45
|
-
steps: {
|
|
46
|
-
check: {
|
|
47
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
const state = makeState();
|
|
52
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
53
|
-
|
|
54
|
-
assert.strictEqual(result.status, "completed");
|
|
55
|
-
assert.strictEqual((result.output as any).iterations, 1);
|
|
56
|
-
}, { timeout: 10000 });
|
|
57
|
-
|
|
58
|
-
it("retries and exhausts when gate always fails (onFail: continue)", async (t) => {
|
|
59
|
-
const loopSpec: LoopSpec = {
|
|
60
|
-
maxAttempts: 2,
|
|
61
|
-
steps: {
|
|
62
|
-
check: {
|
|
63
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
const state = makeState();
|
|
68
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
69
|
-
|
|
70
|
-
assert.strictEqual(result.status, "failed");
|
|
71
|
-
assert.strictEqual((result.output as any).iterations, 2);
|
|
72
|
-
}, { timeout: 10000 });
|
|
73
|
-
|
|
74
|
-
it("stops on gate onFail: fail", async (t) => {
|
|
75
|
-
const loopSpec: LoopSpec = {
|
|
76
|
-
maxAttempts: 3,
|
|
77
|
-
steps: {
|
|
78
|
-
check: {
|
|
79
|
-
gate: { check: "exit 1", onFail: "fail" },
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
const state = makeState();
|
|
84
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
85
|
-
|
|
86
|
-
assert.strictEqual(result.status, "failed");
|
|
87
|
-
assert.strictEqual((result.output as any).iterations, 1);
|
|
88
|
-
}, { timeout: 10000 });
|
|
89
|
-
|
|
90
|
-
it("stops on gate onFail: break without marking as completed", async (t) => {
|
|
91
|
-
const loopSpec: LoopSpec = {
|
|
92
|
-
maxAttempts: 3,
|
|
93
|
-
steps: {
|
|
94
|
-
check: {
|
|
95
|
-
gate: { check: "exit 1", onFail: "break" },
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
const state = makeState();
|
|
100
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
101
|
-
|
|
102
|
-
// "break" on fail stops the loop but no gate passed, so status remains "failed"
|
|
103
|
-
assert.strictEqual(result.status, "failed");
|
|
104
|
-
assert.strictEqual((result.output as any).iterations, 1);
|
|
105
|
-
}, { timeout: 10000 });
|
|
106
|
-
|
|
107
|
-
// Transform sub-steps
|
|
108
|
-
it("executes transform sub-steps within loop", async (t) => {
|
|
109
|
-
const loopSpec: LoopSpec = {
|
|
110
|
-
maxAttempts: 1,
|
|
111
|
-
steps: {
|
|
112
|
-
prep: {
|
|
113
|
-
transform: { mapping: { msg: "hello" } },
|
|
114
|
-
},
|
|
115
|
-
check: {
|
|
116
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
const state = makeState();
|
|
121
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
122
|
-
|
|
123
|
-
assert.strictEqual(result.status, "completed");
|
|
124
|
-
const attempts = (result.output as any).attempts;
|
|
125
|
-
assert.ok(attempts[0].steps.prep);
|
|
126
|
-
assert.strictEqual(attempts[0].steps.prep.status, "completed");
|
|
127
|
-
assert.deepStrictEqual(attempts[0].steps.prep.output, { msg: "hello" });
|
|
128
|
-
}, { timeout: 10000 });
|
|
129
|
-
|
|
130
|
-
it("provides loop.iteration in scope for transforms", async (t) => {
|
|
131
|
-
const loopSpec: LoopSpec = {
|
|
132
|
-
maxAttempts: 2,
|
|
133
|
-
steps: {
|
|
134
|
-
track: {
|
|
135
|
-
transform: { mapping: { iter: "${{ loop.iteration }}" } },
|
|
136
|
-
},
|
|
137
|
-
check: {
|
|
138
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
const state = makeState();
|
|
143
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
144
|
-
|
|
145
|
-
const attempts = (result.output as any).attempts;
|
|
146
|
-
assert.strictEqual(attempts[0].steps.track.output.iter, 0);
|
|
147
|
-
assert.strictEqual(attempts[1].steps.track.output.iter, 1);
|
|
148
|
-
}, { timeout: 10000 });
|
|
149
|
-
|
|
150
|
-
it("provides loop.maxAttempts in scope", async (t) => {
|
|
151
|
-
const loopSpec: LoopSpec = {
|
|
152
|
-
maxAttempts: 5,
|
|
153
|
-
steps: {
|
|
154
|
-
track: {
|
|
155
|
-
transform: { mapping: { max: "${{ loop.maxAttempts }}" } },
|
|
156
|
-
},
|
|
157
|
-
check: {
|
|
158
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
const state = makeState();
|
|
163
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
164
|
-
|
|
165
|
-
const attempts = (result.output as any).attempts;
|
|
166
|
-
assert.strictEqual(attempts[0].steps.track.output.max, 5);
|
|
167
|
-
}, { timeout: 10000 });
|
|
168
|
-
|
|
169
|
-
it("provides loop.priorAttempts in scope", async (t) => {
|
|
170
|
-
// Note: loop.priorAttempts is a live reference to the allAttempts array,
|
|
171
|
-
// so transforms that capture it get the same array instance. We verify
|
|
172
|
-
// the array is accessible and has the right type. After the loop completes,
|
|
173
|
-
// the array contains all iterations (shared reference).
|
|
174
|
-
const loopSpec: LoopSpec = {
|
|
175
|
-
maxAttempts: 2,
|
|
176
|
-
steps: {
|
|
177
|
-
// Use a transform that reads the length instead of capturing the array reference,
|
|
178
|
-
// to avoid circular structure issues
|
|
179
|
-
track: {
|
|
180
|
-
transform: { mapping: { hasPrior: "${{ loop.iteration }}" } },
|
|
181
|
-
},
|
|
182
|
-
check: {
|
|
183
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
};
|
|
187
|
-
const state = makeState();
|
|
188
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
189
|
-
|
|
190
|
-
const attempts = (result.output as any).attempts;
|
|
191
|
-
// Verify loop.iteration (which correlates with priorAttempts count) is accessible
|
|
192
|
-
assert.strictEqual(attempts[0].steps.track.output.hasPrior, 0);
|
|
193
|
-
assert.strictEqual(attempts[1].steps.track.output.hasPrior, 1);
|
|
194
|
-
// Verify the loop output has the attempts array
|
|
195
|
-
assert.strictEqual(attempts.length, 2);
|
|
196
|
-
}, { timeout: 10000 });
|
|
197
|
-
|
|
198
|
-
// Agent sub-steps (mocked dispatch)
|
|
199
|
-
it("dispatches agent sub-steps via dispatchAgent callback", async (t) => {
|
|
200
|
-
let dispatched = false;
|
|
201
|
-
const loopSpec: LoopSpec = {
|
|
202
|
-
maxAttempts: 1,
|
|
203
|
-
steps: {
|
|
204
|
-
work: { agent: "worker" },
|
|
205
|
-
check: {
|
|
206
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
const state = makeState();
|
|
211
|
-
const opts = makeLoopOptions(t, {
|
|
212
|
-
dispatchAgent: async (step, agent, prompt, dispatchOpts) => {
|
|
213
|
-
dispatched = true;
|
|
214
|
-
return {
|
|
215
|
-
step: dispatchOpts.stepName,
|
|
216
|
-
agent: step.agent ?? "worker",
|
|
217
|
-
status: "completed",
|
|
218
|
-
usage: zeroUsage(),
|
|
219
|
-
durationMs: 50,
|
|
220
|
-
textOutput: "done",
|
|
221
|
-
};
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
225
|
-
|
|
226
|
-
assert.ok(dispatched);
|
|
227
|
-
assert.strictEqual(result.status, "completed");
|
|
228
|
-
const attempts = (result.output as any).attempts;
|
|
229
|
-
assert.ok(attempts[0].steps.work);
|
|
230
|
-
assert.strictEqual(attempts[0].steps.work.status, "completed");
|
|
231
|
-
}, { timeout: 10000 });
|
|
232
|
-
|
|
233
|
-
it("fails loop when agent sub-step fails", async (t) => {
|
|
234
|
-
const loopSpec: LoopSpec = {
|
|
235
|
-
maxAttempts: 3,
|
|
236
|
-
steps: {
|
|
237
|
-
work: { agent: "worker" },
|
|
238
|
-
check: {
|
|
239
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
const state = makeState();
|
|
244
|
-
const opts = makeLoopOptions(t, {
|
|
245
|
-
dispatchAgent: async (step, agent, prompt, dispatchOpts) => ({
|
|
246
|
-
step: dispatchOpts.stepName,
|
|
247
|
-
agent: step.agent ?? "worker",
|
|
248
|
-
status: "failed",
|
|
249
|
-
usage: zeroUsage(),
|
|
250
|
-
durationMs: 10,
|
|
251
|
-
error: "agent crashed",
|
|
252
|
-
}),
|
|
253
|
-
});
|
|
254
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
255
|
-
|
|
256
|
-
assert.strictEqual(result.status, "failed");
|
|
257
|
-
// Loop stopped immediately on agent failure
|
|
258
|
-
assert.strictEqual((result.output as any).iterations, 1);
|
|
259
|
-
}, { timeout: 10000 });
|
|
260
|
-
|
|
261
|
-
it("includes agent sub-step usage in aggregated loop usage", async (t) => {
|
|
262
|
-
const loopSpec: LoopSpec = {
|
|
263
|
-
maxAttempts: 1,
|
|
264
|
-
steps: {
|
|
265
|
-
work: { agent: "worker" },
|
|
266
|
-
check: {
|
|
267
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
const state = makeState();
|
|
272
|
-
const opts = makeLoopOptions(t, {
|
|
273
|
-
dispatchAgent: async (step, agent, prompt, dispatchOpts) => ({
|
|
274
|
-
step: dispatchOpts.stepName,
|
|
275
|
-
agent: step.agent ?? "worker",
|
|
276
|
-
status: "completed",
|
|
277
|
-
usage: { input: 100, output: 50, cacheRead: 10, cacheWrite: 5, cost: 0.01, turns: 1 },
|
|
278
|
-
durationMs: 50,
|
|
279
|
-
textOutput: "done",
|
|
280
|
-
}),
|
|
281
|
-
});
|
|
282
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
283
|
-
|
|
284
|
-
assert.strictEqual(result.usage.input, 100);
|
|
285
|
-
assert.strictEqual(result.usage.output, 50);
|
|
286
|
-
assert.strictEqual(result.usage.cost, 0.01);
|
|
287
|
-
assert.strictEqual(result.usage.turns, 1);
|
|
288
|
-
}, { timeout: 10000 });
|
|
289
|
-
|
|
290
|
-
// Agent input expression failure
|
|
291
|
-
it("fails iteration when agent input expression resolution throws", async (t) => {
|
|
292
|
-
const loopSpec: LoopSpec = {
|
|
293
|
-
maxAttempts: 3,
|
|
294
|
-
steps: {
|
|
295
|
-
work: {
|
|
296
|
-
agent: "worker",
|
|
297
|
-
input: { bad: "${{ steps.undefined_step.output }}" },
|
|
298
|
-
},
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
const state = makeState();
|
|
302
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
303
|
-
|
|
304
|
-
assert.strictEqual(result.status, "failed");
|
|
305
|
-
const attempts = (result.output as any).attempts;
|
|
306
|
-
assert.ok(attempts[0].steps.work);
|
|
307
|
-
assert.strictEqual(attempts[0].steps.work.status, "failed");
|
|
308
|
-
}, { timeout: 10000 });
|
|
309
|
-
|
|
310
|
-
// maxAttempts from expression
|
|
311
|
-
it("resolves maxAttempts from ${{ }} expression", async (t) => {
|
|
312
|
-
const loopSpec: LoopSpec = {
|
|
313
|
-
maxAttempts: 10, // fallback
|
|
314
|
-
attempts: "${{ input.retries }}",
|
|
315
|
-
steps: {
|
|
316
|
-
check: {
|
|
317
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
318
|
-
},
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
const state = makeState({ input: { retries: 2 } });
|
|
322
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
323
|
-
|
|
324
|
-
assert.strictEqual((result.output as any).maxAttempts, 2);
|
|
325
|
-
assert.strictEqual((result.output as any).iterations, 2);
|
|
326
|
-
}, { timeout: 10000 });
|
|
327
|
-
|
|
328
|
-
it("falls back to loopSpec.maxAttempts when expression resolves to NaN", async (t) => {
|
|
329
|
-
const loopSpec: LoopSpec = {
|
|
330
|
-
maxAttempts: 2,
|
|
331
|
-
attempts: "${{ input.bogus }}",
|
|
332
|
-
steps: {
|
|
333
|
-
check: {
|
|
334
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
335
|
-
},
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
// input.bogus is undefined — resolves to NaN when Number() is applied
|
|
339
|
-
const state = makeState({ input: {} });
|
|
340
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
341
|
-
|
|
342
|
-
assert.strictEqual((result.output as any).maxAttempts, 2);
|
|
343
|
-
}, { timeout: 10000 });
|
|
344
|
-
|
|
345
|
-
it("falls back to DEFAULT_MAX_ATTEMPTS when both are absent", async (t) => {
|
|
346
|
-
const loopSpec: LoopSpec = {
|
|
347
|
-
maxAttempts: undefined as any, // simulate absent
|
|
348
|
-
steps: {
|
|
349
|
-
check: {
|
|
350
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
};
|
|
354
|
-
const state = makeState();
|
|
355
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
356
|
-
|
|
357
|
-
assert.strictEqual((result.output as any).maxAttempts, DEFAULT_MAX_ATTEMPTS);
|
|
358
|
-
assert.strictEqual((result.output as any).iterations, DEFAULT_MAX_ATTEMPTS);
|
|
359
|
-
}, { timeout: 15000 });
|
|
360
|
-
|
|
361
|
-
// when conditional on sub-steps
|
|
362
|
-
it("skips sub-step when its when condition is falsy", async (t) => {
|
|
363
|
-
const loopSpec: LoopSpec = {
|
|
364
|
-
maxAttempts: 1,
|
|
365
|
-
steps: {
|
|
366
|
-
skip_me: {
|
|
367
|
-
transform: { mapping: { val: "should not run" } },
|
|
368
|
-
when: "${{ loop.iteration == 99 }}",
|
|
369
|
-
},
|
|
370
|
-
check: {
|
|
371
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
const state = makeState();
|
|
376
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
377
|
-
|
|
378
|
-
assert.strictEqual(result.status, "completed");
|
|
379
|
-
const attempts = (result.output as any).attempts;
|
|
380
|
-
assert.strictEqual(attempts[0].steps.skip_me.status, "skipped");
|
|
381
|
-
}, { timeout: 10000 });
|
|
382
|
-
|
|
383
|
-
// onExhausted
|
|
384
|
-
it("runs onExhausted agent when all iterations fail without break", async (t) => {
|
|
385
|
-
let exhaustedDispatched = false;
|
|
386
|
-
const loopSpec: LoopSpec = {
|
|
387
|
-
maxAttempts: 2,
|
|
388
|
-
steps: {
|
|
389
|
-
check: {
|
|
390
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
onExhausted: { agent: "fallback" },
|
|
394
|
-
};
|
|
395
|
-
const state = makeState();
|
|
396
|
-
const opts = makeLoopOptions(t, {
|
|
397
|
-
dispatchAgent: async (step, agent, prompt, dispatchOpts) => {
|
|
398
|
-
exhaustedDispatched = true;
|
|
399
|
-
return {
|
|
400
|
-
step: dispatchOpts.stepName,
|
|
401
|
-
agent: step.agent ?? "fallback",
|
|
402
|
-
status: "completed",
|
|
403
|
-
usage: zeroUsage(),
|
|
404
|
-
durationMs: 10,
|
|
405
|
-
textOutput: "exhausted recovery",
|
|
406
|
-
};
|
|
407
|
-
},
|
|
408
|
-
});
|
|
409
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
410
|
-
|
|
411
|
-
assert.ok(exhaustedDispatched);
|
|
412
|
-
const lastIteration = (result.output as any).lastIteration;
|
|
413
|
-
assert.ok(lastIteration._exhausted);
|
|
414
|
-
}, { timeout: 10000 });
|
|
415
|
-
|
|
416
|
-
it("records expression error in onExhausted when input resolution fails", async (t) => {
|
|
417
|
-
const loopSpec: LoopSpec = {
|
|
418
|
-
maxAttempts: 1,
|
|
419
|
-
steps: {
|
|
420
|
-
check: {
|
|
421
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
onExhausted: {
|
|
425
|
-
agent: "fallback",
|
|
426
|
-
input: { bad: "${{ steps.nonexistent.output }}" },
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
const state = makeState();
|
|
430
|
-
const opts = makeLoopOptions(t, {
|
|
431
|
-
dispatchAgent: async (step, agent, prompt, dispatchOpts) => ({
|
|
432
|
-
step: dispatchOpts.stepName,
|
|
433
|
-
agent: step.agent ?? "fallback",
|
|
434
|
-
status: "completed",
|
|
435
|
-
usage: zeroUsage(),
|
|
436
|
-
durationMs: 10,
|
|
437
|
-
textOutput: "ran anyway",
|
|
438
|
-
}),
|
|
439
|
-
});
|
|
440
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
441
|
-
|
|
442
|
-
const lastIteration = (result.output as any).lastIteration;
|
|
443
|
-
assert.ok(lastIteration._exhausted);
|
|
444
|
-
assert.ok(lastIteration._exhausted.error);
|
|
445
|
-
assert.ok(lastIteration._exhausted.error.includes("Expression error"));
|
|
446
|
-
}, { timeout: 10000 });
|
|
447
|
-
|
|
448
|
-
it("does not run onExhausted when loop completed via break", async (t) => {
|
|
449
|
-
let exhaustedDispatched = false;
|
|
450
|
-
const loopSpec: LoopSpec = {
|
|
451
|
-
maxAttempts: 3,
|
|
452
|
-
steps: {
|
|
453
|
-
check: {
|
|
454
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
onExhausted: { agent: "fallback" },
|
|
458
|
-
};
|
|
459
|
-
const state = makeState();
|
|
460
|
-
const opts = makeLoopOptions(t, {
|
|
461
|
-
dispatchAgent: async (step, agent, prompt, dispatchOpts) => {
|
|
462
|
-
exhaustedDispatched = true;
|
|
463
|
-
return {
|
|
464
|
-
step: dispatchOpts.stepName,
|
|
465
|
-
agent: step.agent ?? "fallback",
|
|
466
|
-
status: "completed",
|
|
467
|
-
usage: zeroUsage(),
|
|
468
|
-
durationMs: 10,
|
|
469
|
-
textOutput: "should not run",
|
|
470
|
-
};
|
|
471
|
-
},
|
|
472
|
-
});
|
|
473
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
474
|
-
|
|
475
|
-
assert.strictEqual(result.status, "completed");
|
|
476
|
-
assert.ok(!exhaustedDispatched);
|
|
477
|
-
const lastIteration = (result.output as any).lastIteration;
|
|
478
|
-
assert.ok(!lastIteration._exhausted);
|
|
479
|
-
}, { timeout: 10000 });
|
|
480
|
-
|
|
481
|
-
// Scope visibility
|
|
482
|
-
it("current iteration sub-steps are visible to later sub-steps in same iteration", async (t) => {
|
|
483
|
-
const loopSpec: LoopSpec = {
|
|
484
|
-
maxAttempts: 1,
|
|
485
|
-
steps: {
|
|
486
|
-
first: {
|
|
487
|
-
transform: { mapping: { val: "from-first" } },
|
|
488
|
-
},
|
|
489
|
-
second: {
|
|
490
|
-
transform: { mapping: { ref: "${{ steps.first.output.val }}" } },
|
|
491
|
-
},
|
|
492
|
-
check: {
|
|
493
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
494
|
-
},
|
|
495
|
-
},
|
|
496
|
-
};
|
|
497
|
-
const state = makeState();
|
|
498
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
499
|
-
|
|
500
|
-
assert.strictEqual(result.status, "completed");
|
|
501
|
-
const attempts = (result.output as any).attempts;
|
|
502
|
-
assert.strictEqual(attempts[0].steps.second.output.ref, "from-first");
|
|
503
|
-
}, { timeout: 10000 });
|
|
504
|
-
|
|
505
|
-
it("outer state.steps accessible from inside loop", async (t) => {
|
|
506
|
-
const state = makeState({
|
|
507
|
-
steps: {
|
|
508
|
-
outer: {
|
|
509
|
-
step: "outer",
|
|
510
|
-
agent: "test",
|
|
511
|
-
status: "completed",
|
|
512
|
-
output: { value: "outer-data" },
|
|
513
|
-
usage: zeroUsage(),
|
|
514
|
-
durationMs: 0,
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
});
|
|
518
|
-
const loopSpec: LoopSpec = {
|
|
519
|
-
maxAttempts: 1,
|
|
520
|
-
steps: {
|
|
521
|
-
ref: {
|
|
522
|
-
transform: { mapping: { fromOuter: "${{ steps.outer.output.value }}" } },
|
|
523
|
-
},
|
|
524
|
-
check: {
|
|
525
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
526
|
-
},
|
|
527
|
-
},
|
|
528
|
-
};
|
|
529
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
530
|
-
|
|
531
|
-
assert.strictEqual(result.status, "completed");
|
|
532
|
-
const attempts = (result.output as any).attempts;
|
|
533
|
-
assert.strictEqual(attempts[0].steps.ref.output.fromOuter, "outer-data");
|
|
534
|
-
}, { timeout: 10000 });
|
|
535
|
-
|
|
536
|
-
// Cancellation
|
|
537
|
-
it("stops loop when signal is aborted", async (t) => {
|
|
538
|
-
const controller = new AbortController();
|
|
539
|
-
controller.abort();
|
|
540
|
-
|
|
541
|
-
const loopSpec: LoopSpec = {
|
|
542
|
-
maxAttempts: 10,
|
|
543
|
-
steps: {
|
|
544
|
-
check: {
|
|
545
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
};
|
|
549
|
-
const state = makeState();
|
|
550
|
-
const opts = makeLoopOptions(t, { signal: controller.signal });
|
|
551
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
552
|
-
|
|
553
|
-
assert.strictEqual((result.output as any).iterations, 0);
|
|
554
|
-
}, { timeout: 10000 });
|
|
555
|
-
|
|
556
|
-
// Output structure
|
|
557
|
-
it("result output has iterations, maxAttempts, attempts, lastIteration", async (t) => {
|
|
558
|
-
const loopSpec: LoopSpec = {
|
|
559
|
-
maxAttempts: 1,
|
|
560
|
-
steps: {
|
|
561
|
-
check: {
|
|
562
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
563
|
-
},
|
|
564
|
-
},
|
|
565
|
-
};
|
|
566
|
-
const state = makeState();
|
|
567
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
568
|
-
|
|
569
|
-
const output = result.output as any;
|
|
570
|
-
assert.ok("iterations" in output);
|
|
571
|
-
assert.ok("maxAttempts" in output);
|
|
572
|
-
assert.ok("attempts" in output);
|
|
573
|
-
assert.ok("lastIteration" in output);
|
|
574
|
-
}, { timeout: 10000 });
|
|
575
|
-
|
|
576
|
-
it("textOutput summarizes loop execution", async (t) => {
|
|
577
|
-
const loopSpec: LoopSpec = {
|
|
578
|
-
maxAttempts: 2,
|
|
579
|
-
steps: {
|
|
580
|
-
check: {
|
|
581
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
};
|
|
585
|
-
const state = makeState();
|
|
586
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
587
|
-
|
|
588
|
-
assert.ok(result.textOutput);
|
|
589
|
-
assert.ok(result.textOutput!.includes("myloop"));
|
|
590
|
-
assert.ok(result.textOutput!.includes("2/2"));
|
|
591
|
-
}, { timeout: 10000 });
|
|
592
|
-
|
|
593
|
-
it("output is persisted to runDir", async (t) => {
|
|
594
|
-
const loopSpec: LoopSpec = {
|
|
595
|
-
maxAttempts: 1,
|
|
596
|
-
steps: {
|
|
597
|
-
check: {
|
|
598
|
-
gate: { check: "echo pass", onPass: "break" },
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
};
|
|
602
|
-
const state = makeState();
|
|
603
|
-
const opts = makeLoopOptions(t);
|
|
604
|
-
const result = await executeLoop(loopSpec, "myloop", state, opts);
|
|
605
|
-
|
|
606
|
-
assert.ok(result.outputPath);
|
|
607
|
-
assert.ok(fs.existsSync(result.outputPath!));
|
|
608
|
-
}, { timeout: 10000 });
|
|
609
|
-
|
|
610
|
-
// Usage aggregation
|
|
611
|
-
it("aggregates usage across all iterations", async (t) => {
|
|
612
|
-
const loopSpec: LoopSpec = {
|
|
613
|
-
maxAttempts: 2,
|
|
614
|
-
steps: {
|
|
615
|
-
check: {
|
|
616
|
-
gate: { check: "exit 1", onFail: "continue" },
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
const state = makeState();
|
|
621
|
-
const result = await executeLoop(loopSpec, "myloop", state, makeLoopOptions(t));
|
|
622
|
-
|
|
623
|
-
// Gates have zero usage, so total should be zero
|
|
624
|
-
assert.deepStrictEqual(result.usage, zeroUsage());
|
|
625
|
-
}, { timeout: 10000 });
|
|
626
|
-
});
|