@davidorex/pi-workflows 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +178 -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 +724 -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 +171 -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 +63 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +136 -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 +165 -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 +738 -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 +457 -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 +476 -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 +107 -0
- package/skills/pi-workflows/SKILL.md +185 -0
- package/skills/pi-workflows/references/bundled-resources.md +87 -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 +94 -0
- package/workflows/create-handoff.workflow.yaml +66 -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 +81 -0
- package/workflows/plan-from-requirements.workflow.yaml +59 -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/checkpoint.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint and resume logic for workflow runs.
|
|
3
|
-
*
|
|
4
|
-
* Finds incomplete runs, validates they match the current spec,
|
|
5
|
-
* and provides state needed to resume execution.
|
|
6
|
-
*/
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import type { ExecutionState, WorkflowSpec } from "./types.ts";
|
|
10
|
-
import { readState } from "./state.ts";
|
|
11
|
-
import { WORKFLOWS_DIR } from "./workflows-dir.ts";
|
|
12
|
-
|
|
13
|
-
export interface IncompleteRun {
|
|
14
|
-
runId: string;
|
|
15
|
-
runDir: string;
|
|
16
|
-
state: ExecutionState;
|
|
17
|
-
completedSteps: string[]; // step names with status completed or skipped
|
|
18
|
-
failedStep?: string; // name of the step that failed (if any)
|
|
19
|
-
updatedAt?: string; // last update timestamp
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Find the most recent incomplete run for a workflow.
|
|
24
|
-
*
|
|
25
|
-
* Scans .workflows/runs/<name>/runs/ for state.json files
|
|
26
|
-
* with status "running" or "failed". Returns the most recent one
|
|
27
|
-
* (by directory name, which encodes timestamp).
|
|
28
|
-
*
|
|
29
|
-
* Returns null if no incomplete runs exist.
|
|
30
|
-
*/
|
|
31
|
-
export function findIncompleteRun(cwd: string, workflowName: string): IncompleteRun | null {
|
|
32
|
-
const runsDir = path.join(cwd, WORKFLOWS_DIR, "runs", workflowName, "runs");
|
|
33
|
-
if (!fs.existsSync(runsDir)) return null;
|
|
34
|
-
|
|
35
|
-
const entries = fs.readdirSync(runsDir)
|
|
36
|
-
.filter(e => {
|
|
37
|
-
const stat = fs.statSync(path.join(runsDir, e));
|
|
38
|
-
return stat.isDirectory();
|
|
39
|
-
})
|
|
40
|
-
.sort() // lexicographic — timestamp-based IDs sort chronologically
|
|
41
|
-
.reverse(); // most recent first
|
|
42
|
-
|
|
43
|
-
for (const runId of entries) {
|
|
44
|
-
const runDir = path.join(runsDir, runId);
|
|
45
|
-
const state = readState(runDir);
|
|
46
|
-
if (!state) continue;
|
|
47
|
-
|
|
48
|
-
if (state.status === "running" || state.status === "failed" || state.status === "paused") {
|
|
49
|
-
const completedSteps = Object.entries(state.steps)
|
|
50
|
-
.filter(([, r]) => r.status === "completed" || r.status === "skipped")
|
|
51
|
-
.map(([name]) => name);
|
|
52
|
-
|
|
53
|
-
const failedStep = Object.entries(state.steps)
|
|
54
|
-
.find(([, r]) => r.status === "failed")
|
|
55
|
-
?.[0];
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
runId,
|
|
59
|
-
runDir,
|
|
60
|
-
state,
|
|
61
|
-
completedSteps,
|
|
62
|
-
failedStep,
|
|
63
|
-
updatedAt: state.updatedAt,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Validate that a saved state is compatible with the current workflow spec.
|
|
73
|
-
*
|
|
74
|
-
* Checks:
|
|
75
|
-
* 1. Spec version matches (if both specify one)
|
|
76
|
-
* 2. All completed steps still exist in the spec
|
|
77
|
-
*
|
|
78
|
-
* Returns null if compatible, or a string describing the incompatibility.
|
|
79
|
-
*/
|
|
80
|
-
export function validateResumeCompatibility(
|
|
81
|
-
state: ExecutionState,
|
|
82
|
-
spec: WorkflowSpec,
|
|
83
|
-
): string | null {
|
|
84
|
-
// Check version match
|
|
85
|
-
if (state.specVersion && spec.version && state.specVersion !== spec.version) {
|
|
86
|
-
return `Spec version changed: run has '${state.specVersion}', current is '${spec.version}'`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Check all completed steps still exist in spec
|
|
90
|
-
const specStepNames = new Set(Object.keys(spec.steps));
|
|
91
|
-
for (const stepName of Object.keys(state.steps)) {
|
|
92
|
-
if (!specStepNames.has(stepName)) {
|
|
93
|
-
return `Step '${stepName}' was completed in the saved run but no longer exists in the workflow spec`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return null; // compatible
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Determine which steps need to run when resuming.
|
|
102
|
-
*
|
|
103
|
-
* Given the execution plan (layers of step names) and the set of
|
|
104
|
-
* already-completed steps, returns the layer index to resume from
|
|
105
|
-
* and which steps within that layer still need to run.
|
|
106
|
-
*
|
|
107
|
-
* For a partially-complete parallel layer (some steps done, some not),
|
|
108
|
-
* returns only the incomplete steps from that layer.
|
|
109
|
-
*/
|
|
110
|
-
export function computeResumePoint(
|
|
111
|
-
plan: Array<{ steps: string[] }>,
|
|
112
|
-
completedSteps: Set<string>,
|
|
113
|
-
): { resumeLayerIndex: number; pendingStepsInLayer: string[] } | null {
|
|
114
|
-
for (let i = 0; i < plan.length; i++) {
|
|
115
|
-
const layer = plan[i];
|
|
116
|
-
const pending = layer.steps.filter(s => !completedSteps.has(s));
|
|
117
|
-
|
|
118
|
-
if (pending.length > 0) {
|
|
119
|
-
return { resumeLayerIndex: i, pendingStepsInLayer: pending };
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// All steps completed — nothing to resume
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Format a human-readable summary of an incomplete run for display.
|
|
129
|
-
*/
|
|
130
|
-
export function formatIncompleteRun(run: IncompleteRun, spec: WorkflowSpec): string {
|
|
131
|
-
const totalSteps = Object.keys(spec.steps).length;
|
|
132
|
-
const completed = run.completedSteps.length;
|
|
133
|
-
const status = run.state.status === "paused" ? "paused"
|
|
134
|
-
: run.state.status === "running" ? "interrupted"
|
|
135
|
-
: "failed";
|
|
136
|
-
const failedInfo = run.failedStep ? ` at step '${run.failedStep}'` : "";
|
|
137
|
-
const timeInfo = run.updatedAt ? ` (last updated: ${run.updatedAt})` : "";
|
|
138
|
-
|
|
139
|
-
return `Found ${status} run${failedInfo}: ${completed}/${totalSteps} steps completed${timeInfo}. Run ID: ${run.runId}`;
|
|
140
|
-
}
|
package/src/completion.test.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { resolveCompletion } from "./completion.ts";
|
|
4
|
-
import type { CompletionSpec, WorkflowResult, StepResult, StepUsage } from "./types.ts";
|
|
5
|
-
|
|
6
|
-
const usage: StepUsage = { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0, cost: 0.03, turns: 2 };
|
|
7
|
-
|
|
8
|
-
const stepResult: StepResult = {
|
|
9
|
-
step: "summarize",
|
|
10
|
-
agent: "summarizer",
|
|
11
|
-
status: "completed",
|
|
12
|
-
output: { headline: "Found 3 issues" },
|
|
13
|
-
textOutput: "The codebase has 3 critical issues.",
|
|
14
|
-
usage,
|
|
15
|
-
durationMs: 45000,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const result: WorkflowResult = {
|
|
19
|
-
workflow: "explore-summarize",
|
|
20
|
-
runId: "explore-summarize-20260313-120000-abcd",
|
|
21
|
-
status: "completed",
|
|
22
|
-
steps: { summarize: stepResult },
|
|
23
|
-
output: { headline: "Found 3 issues" },
|
|
24
|
-
totalUsage: { input: 2000, output: 1000, cacheRead: 0, cacheWrite: 0, cost: 0.05, turns: 4 },
|
|
25
|
-
totalDurationMs: 92000,
|
|
26
|
-
runDir: "/tmp/runs/explore-summarize-20260313-120000-abcd",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const input = { path: "/src", question: "What are the main components?" };
|
|
30
|
-
|
|
31
|
-
describe("resolveCompletion", () => {
|
|
32
|
-
describe("template form", () => {
|
|
33
|
-
it("resolves expressions in template", () => {
|
|
34
|
-
const spec: CompletionSpec = {
|
|
35
|
-
template: "## Results\n\n${{ steps.summarize.textOutput }}\n\nDone in ${{ totalDurationMs | duration }}.",
|
|
36
|
-
};
|
|
37
|
-
const content = resolveCompletion(spec, result, input);
|
|
38
|
-
assert.ok(content.includes("## Results"));
|
|
39
|
-
assert.ok(content.includes("The codebase has 3 critical issues."));
|
|
40
|
-
assert.ok(content.includes("Done in 1m32s."));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("resolves input and workflow metadata in template", () => {
|
|
44
|
-
const spec: CompletionSpec = {
|
|
45
|
-
template: "Explored ${{ input.path }} for workflow '${{ workflow }}'. Status: ${{ status }}.",
|
|
46
|
-
};
|
|
47
|
-
const content = resolveCompletion(spec, result, input);
|
|
48
|
-
assert.strictEqual(content, "Explored /src for workflow 'explore-summarize'. Status: completed.");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("applies currency filter in template", () => {
|
|
52
|
-
const spec: CompletionSpec = {
|
|
53
|
-
template: "Cost: ${{ totalUsage.cost | currency }}",
|
|
54
|
-
};
|
|
55
|
-
const content = resolveCompletion(spec, result, input);
|
|
56
|
-
assert.strictEqual(content, "Cost: $0.05");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("includes runDir in template", () => {
|
|
60
|
-
const spec: CompletionSpec = {
|
|
61
|
-
template: "Logs at: ${{ runDir }}/sessions/",
|
|
62
|
-
};
|
|
63
|
-
const content = resolveCompletion(spec, result, input);
|
|
64
|
-
assert.strictEqual(content, "Logs at: /tmp/runs/explore-summarize-20260313-120000-abcd/sessions/");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("renders undefined as empty in template", () => {
|
|
68
|
-
const spec: CompletionSpec = {
|
|
69
|
-
template: "Answer: ${{ input.nonexistent }}",
|
|
70
|
-
};
|
|
71
|
-
const content = resolveCompletion(spec, result, input);
|
|
72
|
-
assert.strictEqual(content, "Answer: ");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("message form", () => {
|
|
77
|
-
it("resolves message with no include", () => {
|
|
78
|
-
const spec: CompletionSpec = {
|
|
79
|
-
message: "Present the findings to the user. The workflow '${{ workflow }}' is done.",
|
|
80
|
-
};
|
|
81
|
-
const content = resolveCompletion(spec, result, input);
|
|
82
|
-
assert.ok(content.includes("Present the findings to the user."));
|
|
83
|
-
assert.ok(content.includes("'explore-summarize' is done."));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("resolves message with include paths", () => {
|
|
87
|
-
const spec: CompletionSpec = {
|
|
88
|
-
message: "Show the user these results.",
|
|
89
|
-
include: ["steps.summarize.textOutput", "totalUsage"],
|
|
90
|
-
};
|
|
91
|
-
const content = resolveCompletion(spec, result, input);
|
|
92
|
-
assert.ok(content.includes("Show the user these results."));
|
|
93
|
-
assert.ok(content.includes("steps.summarize.textOutput"));
|
|
94
|
-
assert.ok(content.includes("The codebase has 3 critical issues."));
|
|
95
|
-
assert.ok(content.includes("totalUsage"));
|
|
96
|
-
assert.ok(content.includes('"cost"'));
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("formats included objects as JSON blocks", () => {
|
|
100
|
-
const spec: CompletionSpec = {
|
|
101
|
-
message: "Review:",
|
|
102
|
-
include: ["steps.summarize.output"],
|
|
103
|
-
};
|
|
104
|
-
const content = resolveCompletion(spec, result, input);
|
|
105
|
-
assert.ok(content.includes("```json"));
|
|
106
|
-
assert.ok(content.includes('"headline"'));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("formats included primitives as plain text", () => {
|
|
110
|
-
const spec: CompletionSpec = {
|
|
111
|
-
message: "Data:",
|
|
112
|
-
include: ["steps.summarize.textOutput"],
|
|
113
|
-
};
|
|
114
|
-
const content = resolveCompletion(spec, result, input);
|
|
115
|
-
assert.ok(content.includes("The codebase has 3 critical issues."));
|
|
116
|
-
// Primitive values should NOT be in JSON blocks
|
|
117
|
-
assert.ok(!content.includes("```json\nThe codebase"));
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("error handling", () => {
|
|
122
|
-
it("throws on invalid expression in template", () => {
|
|
123
|
-
const spec: CompletionSpec = {
|
|
124
|
-
template: "Bad: ${{ steps.nonexistent.output }}",
|
|
125
|
-
};
|
|
126
|
-
assert.throws(
|
|
127
|
-
() => resolveCompletion(spec, result, input),
|
|
128
|
-
(err: unknown) => err instanceof Error && err.message.includes("nonexistent"),
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("throws on invalid include path", () => {
|
|
133
|
-
const spec: CompletionSpec = {
|
|
134
|
-
message: "ok",
|
|
135
|
-
include: ["steps.nonexistent.output"],
|
|
136
|
-
};
|
|
137
|
-
assert.throws(
|
|
138
|
-
() => resolveCompletion(spec, result, input),
|
|
139
|
-
(err: unknown) => err instanceof Error && err.message.includes("nonexistent"),
|
|
140
|
-
);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
});
|
package/src/completion.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { CompletionSpec, CompletionScope, WorkflowResult } from "./types.ts";
|
|
2
|
-
import { resolveExpression, resolveExpressions } from "./expression.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolve a completion spec into the final message string injected into
|
|
6
|
-
* the main LLM conversation after workflow execution.
|
|
7
|
-
*
|
|
8
|
-
* Two forms:
|
|
9
|
-
* - Template: the template string is resolved with ${{ }} expressions
|
|
10
|
-
* and returned as the full content.
|
|
11
|
-
* - Message + include: the message is resolved, then each include path
|
|
12
|
-
* is resolved and appended as structured data.
|
|
13
|
-
*
|
|
14
|
-
* @param spec - the CompletionSpec from the workflow YAML
|
|
15
|
-
* @param result - the completed WorkflowResult
|
|
16
|
-
* @param executionInput - the original workflow input
|
|
17
|
-
* @returns the resolved message string
|
|
18
|
-
*/
|
|
19
|
-
export function resolveCompletion(
|
|
20
|
-
spec: CompletionSpec,
|
|
21
|
-
result: WorkflowResult,
|
|
22
|
-
executionInput: unknown,
|
|
23
|
-
): string {
|
|
24
|
-
const scope: CompletionScope = {
|
|
25
|
-
input: executionInput,
|
|
26
|
-
steps: result.steps,
|
|
27
|
-
totalUsage: result.totalUsage,
|
|
28
|
-
totalDurationMs: result.totalDurationMs,
|
|
29
|
-
runDir: result.runDir,
|
|
30
|
-
runId: result.runId,
|
|
31
|
-
workflow: result.workflow,
|
|
32
|
-
status: result.status,
|
|
33
|
-
output: result.output,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (spec.template) {
|
|
37
|
-
const resolved = resolveExpressions(spec.template, scope);
|
|
38
|
-
return String(resolved ?? "");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Message form
|
|
42
|
-
const parts: string[] = [];
|
|
43
|
-
|
|
44
|
-
if (spec.message) {
|
|
45
|
-
const resolvedMessage = resolveExpressions(spec.message, scope);
|
|
46
|
-
parts.push(String(resolvedMessage ?? ""));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (spec.include && spec.include.length > 0) {
|
|
50
|
-
const included: Record<string, unknown> = {};
|
|
51
|
-
for (const path of spec.include) {
|
|
52
|
-
const value = resolveExpression(path, scope);
|
|
53
|
-
included[path] = value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
parts.push("");
|
|
57
|
-
parts.push("---");
|
|
58
|
-
for (const [path, value] of Object.entries(included)) {
|
|
59
|
-
if (value !== null && typeof value === "object") {
|
|
60
|
-
parts.push(`\n### ${path}\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``);
|
|
61
|
-
} else {
|
|
62
|
-
parts.push(`\n### ${path}\n${String(value ?? "")}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return parts.join("\n");
|
|
68
|
-
}
|
package/src/dag.test.ts
DELETED
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert";
|
|
3
|
-
import { extractDependencies, buildExecutionPlan, isSequential } from "./dag.ts";
|
|
4
|
-
import type { StepSpec } from "./types.ts";
|
|
5
|
-
import { makeSpec as makeSpecFull } from "./test-helpers.ts";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Helper: build a minimal WorkflowSpec from a steps object.
|
|
9
|
-
*/
|
|
10
|
-
function makeSpec(steps: Record<string, StepSpec>) {
|
|
11
|
-
return makeSpecFull({ steps });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("extractDependencies", () => {
|
|
15
|
-
it("returns empty deps for steps with no expressions", () => {
|
|
16
|
-
const spec = makeSpec({
|
|
17
|
-
a: { agent: "foo" },
|
|
18
|
-
b: { agent: "bar" },
|
|
19
|
-
});
|
|
20
|
-
const deps = extractDependencies(spec);
|
|
21
|
-
assert.strictEqual(deps.get("a")!.size, 0);
|
|
22
|
-
assert.strictEqual(deps.get("b")!.size, 0);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("extracts deps from input expressions", () => {
|
|
26
|
-
const spec = makeSpec({
|
|
27
|
-
a: { agent: "foo" },
|
|
28
|
-
b: {
|
|
29
|
-
agent: "bar",
|
|
30
|
-
input: {
|
|
31
|
-
context: "${{ steps.a.textOutput }}",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
const deps = extractDependencies(spec);
|
|
36
|
-
assert.strictEqual(deps.get("a")!.size, 0);
|
|
37
|
-
assert.ok(deps.get("b")!.has("a"));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("extracts deps from when condition", () => {
|
|
41
|
-
const spec = makeSpec({
|
|
42
|
-
a: { agent: "foo" },
|
|
43
|
-
b: {
|
|
44
|
-
agent: "bar",
|
|
45
|
-
when: "${{ steps.a.output.shouldRun }}",
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
const deps = extractDependencies(spec);
|
|
49
|
-
assert.ok(deps.get("b")!.has("a"));
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("extracts deps from bare when condition (no ${{ }})", () => {
|
|
53
|
-
const spec = makeSpec({
|
|
54
|
-
a: { agent: "foo" },
|
|
55
|
-
b: {
|
|
56
|
-
agent: "bar",
|
|
57
|
-
when: "steps.a.output.shouldRun",
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
const deps = extractDependencies(spec);
|
|
61
|
-
assert.ok(deps.get("b")!.has("a"));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("extracts deps from gate check", () => {
|
|
65
|
-
const spec = makeSpec({
|
|
66
|
-
a: { agent: "foo" },
|
|
67
|
-
b: {
|
|
68
|
-
gate: { check: "${{ steps.a.output.reproCommand }}" },
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
const deps = extractDependencies(spec);
|
|
72
|
-
assert.ok(deps.get("b")!.has("a"));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("extracts deps from transform mapping", () => {
|
|
76
|
-
const spec = makeSpec({
|
|
77
|
-
a: { agent: "foo" },
|
|
78
|
-
b: { agent: "bar" },
|
|
79
|
-
c: {
|
|
80
|
-
transform: {
|
|
81
|
-
mapping: {
|
|
82
|
-
fromA: "${{ steps.a.textOutput }}",
|
|
83
|
-
fromB: "${{ steps.b.output.data }}",
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
const deps = extractDependencies(spec);
|
|
89
|
-
assert.ok(deps.get("c")!.has("a"));
|
|
90
|
-
assert.ok(deps.get("c")!.has("b"));
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("extracts multiple deps from a single expression string", () => {
|
|
94
|
-
const spec = makeSpec({
|
|
95
|
-
a: { agent: "foo" },
|
|
96
|
-
b: { agent: "bar" },
|
|
97
|
-
c: {
|
|
98
|
-
agent: "baz",
|
|
99
|
-
input: {
|
|
100
|
-
combined: "${{ steps.a.textOutput }}\n\n${{ steps.b.textOutput }}",
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
const deps = extractDependencies(spec);
|
|
105
|
-
assert.ok(deps.get("c")!.has("a"));
|
|
106
|
-
assert.ok(deps.get("c")!.has("b"));
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("extracts deps from nested input objects", () => {
|
|
110
|
-
const spec = makeSpec({
|
|
111
|
-
a: { agent: "foo" },
|
|
112
|
-
b: {
|
|
113
|
-
agent: "bar",
|
|
114
|
-
input: {
|
|
115
|
-
nested: {
|
|
116
|
-
deep: {
|
|
117
|
-
value: "${{ steps.a.output.x }}",
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
const deps = extractDependencies(spec);
|
|
124
|
-
assert.ok(deps.get("b")!.has("a"));
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("extracts deps from input arrays", () => {
|
|
128
|
-
const spec = makeSpec({
|
|
129
|
-
a: { agent: "foo" },
|
|
130
|
-
b: {
|
|
131
|
-
agent: "bar",
|
|
132
|
-
input: {
|
|
133
|
-
items: ["${{ steps.a.output.x }}", "static"],
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
const deps = extractDependencies(spec);
|
|
138
|
-
assert.ok(deps.get("b")!.has("a"));
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("ignores input-only references (no step deps)", () => {
|
|
142
|
-
const spec = makeSpec({
|
|
143
|
-
a: {
|
|
144
|
-
agent: "foo",
|
|
145
|
-
input: {
|
|
146
|
-
path: "${{ input.path }}",
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
const deps = extractDependencies(spec);
|
|
151
|
-
assert.strictEqual(deps.get("a")!.size, 0);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("ignores self-references", () => {
|
|
155
|
-
const spec = makeSpec({
|
|
156
|
-
a: {
|
|
157
|
-
agent: "foo",
|
|
158
|
-
input: {
|
|
159
|
-
self: "${{ steps.a.output }}",
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
const deps = extractDependencies(spec);
|
|
164
|
-
assert.strictEqual(deps.get("a")!.size, 0);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("ignores references to non-existent steps", () => {
|
|
168
|
-
const spec = makeSpec({
|
|
169
|
-
a: {
|
|
170
|
-
agent: "foo",
|
|
171
|
-
input: {
|
|
172
|
-
bad: "${{ steps.nonexistent.output }}",
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
const deps = extractDependencies(spec);
|
|
177
|
-
assert.strictEqual(deps.get("a")!.size, 0);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("does not descend into loop sub-steps", () => {
|
|
181
|
-
const spec = makeSpec({
|
|
182
|
-
a: { agent: "foo" },
|
|
183
|
-
b: {
|
|
184
|
-
loop: {
|
|
185
|
-
maxAttempts: 3,
|
|
186
|
-
steps: {
|
|
187
|
-
inner: {
|
|
188
|
-
agent: "bar",
|
|
189
|
-
input: { ctx: "${{ steps.a.textOutput }}" },
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
const deps = extractDependencies(spec);
|
|
196
|
-
// The loop step itself should depend on 'a' via its sub-step's expression,
|
|
197
|
-
// BUT per the design: we do NOT descend into loop sub-steps for top-level DAG.
|
|
198
|
-
// The loop step's own `input` or `when` would declare its real deps.
|
|
199
|
-
assert.strictEqual(deps.get("b")!.size, 0);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe("buildExecutionPlan", () => {
|
|
204
|
-
it("fully sequential: linear chain", () => {
|
|
205
|
-
const spec = makeSpec({
|
|
206
|
-
a: { agent: "foo" },
|
|
207
|
-
b: {
|
|
208
|
-
agent: "bar",
|
|
209
|
-
input: { ctx: "${{ steps.a.textOutput }}" },
|
|
210
|
-
},
|
|
211
|
-
c: {
|
|
212
|
-
agent: "baz",
|
|
213
|
-
input: { ctx: "${{ steps.b.output }}" },
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
const plan = buildExecutionPlan(spec);
|
|
217
|
-
assert.strictEqual(plan.length, 3);
|
|
218
|
-
assert.deepStrictEqual(plan[0].steps, ["a"]);
|
|
219
|
-
assert.deepStrictEqual(plan[1].steps, ["b"]);
|
|
220
|
-
assert.deepStrictEqual(plan[2].steps, ["c"]);
|
|
221
|
-
assert.ok(isSequential(plan));
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("fully parallel: no dependencies", () => {
|
|
225
|
-
const spec = makeSpec({
|
|
226
|
-
a: { agent: "foo", input: { x: "${{ input.path }}" } },
|
|
227
|
-
b: { agent: "bar", input: { x: "${{ input.path }}" } },
|
|
228
|
-
c: { agent: "baz", input: { x: "${{ input.path }}" } },
|
|
229
|
-
});
|
|
230
|
-
const plan = buildExecutionPlan(spec);
|
|
231
|
-
assert.strictEqual(plan.length, 1);
|
|
232
|
-
assert.strictEqual(plan[0].steps.length, 3);
|
|
233
|
-
assert.ok(plan[0].steps.includes("a"));
|
|
234
|
-
assert.ok(plan[0].steps.includes("b"));
|
|
235
|
-
assert.ok(plan[0].steps.includes("c"));
|
|
236
|
-
assert.ok(!isSequential(plan));
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("fan-out then merge: diamond pattern", () => {
|
|
240
|
-
const spec = makeSpec({
|
|
241
|
-
source: { agent: "foo" },
|
|
242
|
-
analyzerA: {
|
|
243
|
-
agent: "bar",
|
|
244
|
-
input: { ctx: "${{ steps.source.textOutput }}" },
|
|
245
|
-
},
|
|
246
|
-
analyzerB: {
|
|
247
|
-
agent: "baz",
|
|
248
|
-
input: { ctx: "${{ steps.source.textOutput }}" },
|
|
249
|
-
},
|
|
250
|
-
merge: {
|
|
251
|
-
transform: {
|
|
252
|
-
mapping: {
|
|
253
|
-
a: "${{ steps.analyzerA.output }}",
|
|
254
|
-
b: "${{ steps.analyzerB.output }}",
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
});
|
|
259
|
-
const plan = buildExecutionPlan(spec);
|
|
260
|
-
assert.strictEqual(plan.length, 3);
|
|
261
|
-
assert.deepStrictEqual(plan[0].steps, ["source"]);
|
|
262
|
-
assert.strictEqual(plan[1].steps.length, 2);
|
|
263
|
-
assert.ok(plan[1].steps.includes("analyzerA"));
|
|
264
|
-
assert.ok(plan[1].steps.includes("analyzerB"));
|
|
265
|
-
assert.deepStrictEqual(plan[2].steps, ["merge"]);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("mixed: some parallel, some sequential", () => {
|
|
269
|
-
const spec = makeSpec({
|
|
270
|
-
a: { agent: "foo" },
|
|
271
|
-
b: { agent: "bar" },
|
|
272
|
-
c: {
|
|
273
|
-
agent: "baz",
|
|
274
|
-
input: { ctx: "${{ steps.a.textOutput }}" },
|
|
275
|
-
},
|
|
276
|
-
d: {
|
|
277
|
-
transform: {
|
|
278
|
-
mapping: {
|
|
279
|
-
b: "${{ steps.b.output }}",
|
|
280
|
-
c: "${{ steps.c.output }}",
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
const plan = buildExecutionPlan(spec);
|
|
286
|
-
assert.strictEqual(plan.length, 3);
|
|
287
|
-
// Layer 0: a, b (both independent)
|
|
288
|
-
assert.strictEqual(plan[0].steps.length, 2);
|
|
289
|
-
assert.ok(plan[0].steps.includes("a"));
|
|
290
|
-
assert.ok(plan[0].steps.includes("b"));
|
|
291
|
-
// Layer 1: c (depends on a)
|
|
292
|
-
assert.deepStrictEqual(plan[1].steps, ["c"]);
|
|
293
|
-
// Layer 2: d (depends on b and c)
|
|
294
|
-
assert.deepStrictEqual(plan[2].steps, ["d"]);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("detects cycle", () => {
|
|
298
|
-
const spec = makeSpec({
|
|
299
|
-
a: {
|
|
300
|
-
agent: "foo",
|
|
301
|
-
input: { ctx: "${{ steps.b.output }}" },
|
|
302
|
-
},
|
|
303
|
-
b: {
|
|
304
|
-
agent: "bar",
|
|
305
|
-
input: { ctx: "${{ steps.a.output }}" },
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
assert.throws(
|
|
309
|
-
() => buildExecutionPlan(spec),
|
|
310
|
-
(err: Error) => err.message.includes("cycle"),
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("single step", () => {
|
|
315
|
-
const spec = makeSpec({
|
|
316
|
-
only: { agent: "foo" },
|
|
317
|
-
});
|
|
318
|
-
const plan = buildExecutionPlan(spec);
|
|
319
|
-
assert.strictEqual(plan.length, 1);
|
|
320
|
-
assert.deepStrictEqual(plan[0].steps, ["only"]);
|
|
321
|
-
assert.ok(isSequential(plan));
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("preserves YAML order within layers", () => {
|
|
325
|
-
// Steps with no deps should appear in their YAML declaration order
|
|
326
|
-
const spec = makeSpec({
|
|
327
|
-
z: { agent: "foo" },
|
|
328
|
-
a: { agent: "bar" },
|
|
329
|
-
m: { agent: "baz" },
|
|
330
|
-
});
|
|
331
|
-
const plan = buildExecutionPlan(spec);
|
|
332
|
-
assert.strictEqual(plan.length, 1);
|
|
333
|
-
// Should preserve insertion order: z, a, m
|
|
334
|
-
assert.deepStrictEqual(plan[0].steps, ["z", "a", "m"]);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("step with when referencing another step", () => {
|
|
338
|
-
const spec = makeSpec({
|
|
339
|
-
check: { gate: { check: "echo ok" } },
|
|
340
|
-
conditional: {
|
|
341
|
-
agent: "foo",
|
|
342
|
-
when: "${{ steps.check.output.passed }}",
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
const plan = buildExecutionPlan(spec);
|
|
346
|
-
assert.strictEqual(plan.length, 2);
|
|
347
|
-
assert.deepStrictEqual(plan[0].steps, ["check"]);
|
|
348
|
-
assert.deepStrictEqual(plan[1].steps, ["conditional"]);
|
|
349
|
-
});
|
|
350
|
-
});
|