@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-agent.test.ts
DELETED
|
@@ -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
|
-
}
|