@mhingston5/lasso 0.1.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/README.md +707 -0
- package/docs/agent-wrangling.png +0 -0
- package/package.json +26 -0
- package/src/capabilities/matcher.ts +25 -0
- package/src/capabilities/registry.ts +103 -0
- package/src/capabilities/types.ts +15 -0
- package/src/cir/lower.ts +253 -0
- package/src/cir/optimize.ts +251 -0
- package/src/cir/types.ts +131 -0
- package/src/cir/validate.ts +265 -0
- package/src/compiler/compile.ts +601 -0
- package/src/compiler/feedback.ts +471 -0
- package/src/compiler/runtime-helpers.ts +455 -0
- package/src/composition/chain.ts +58 -0
- package/src/composition/conditional.ts +76 -0
- package/src/composition/parallel.ts +75 -0
- package/src/composition/types.ts +105 -0
- package/src/environment/analyzer.ts +56 -0
- package/src/environment/discovery.ts +179 -0
- package/src/environment/types.ts +68 -0
- package/src/failures/classifiers.ts +134 -0
- package/src/failures/generator.ts +421 -0
- package/src/failures/map-reference-failures.ts +23 -0
- package/src/failures/ontology.ts +210 -0
- package/src/failures/recovery.ts +214 -0
- package/src/failures/types.ts +14 -0
- package/src/index.ts +67 -0
- package/src/memory/advisor.ts +132 -0
- package/src/memory/extractor.ts +166 -0
- package/src/memory/store.ts +107 -0
- package/src/memory/types.ts +53 -0
- package/src/metaharness/engine.ts +256 -0
- package/src/metaharness/predictor.ts +168 -0
- package/src/metaharness/types.ts +40 -0
- package/src/mutation/derive.ts +308 -0
- package/src/mutation/diff.ts +52 -0
- package/src/mutation/engine.ts +256 -0
- package/src/mutation/types.ts +84 -0
- package/src/pi/command-input.ts +209 -0
- package/src/pi/commands.ts +351 -0
- package/src/pi/extension.ts +16 -0
- package/src/planner/synthesize.ts +83 -0
- package/src/planner/template-rules.ts +183 -0
- package/src/planner/types.ts +42 -0
- package/src/reference/catalog.ts +128 -0
- package/src/reference/patch-validation-strategies.ts +170 -0
- package/src/reference/patch-validation.ts +174 -0
- package/src/reference/pr-review-merge.ts +155 -0
- package/src/reference/strategies.ts +126 -0
- package/src/reference/types.ts +33 -0
- package/src/replanner/risk-rules.ts +161 -0
- package/src/replanner/runtime.ts +308 -0
- package/src/replanner/synthesize.ts +619 -0
- package/src/replanner/types.ts +73 -0
- package/src/spec/schema.ts +254 -0
- package/src/spec/types.ts +319 -0
- package/src/spec/validate.ts +296 -0
- package/src/state/snapshots.ts +43 -0
- package/src/state/types.ts +12 -0
- package/src/synthesis/graph-builder.ts +267 -0
- package/src/synthesis/harness-builder.ts +113 -0
- package/src/synthesis/intent-ir.ts +63 -0
- package/src/synthesis/policy-builder.ts +320 -0
- package/src/synthesis/risk-analyzer.ts +182 -0
- package/src/synthesis/skill-parser.ts +441 -0
- package/src/verification/engine.ts +230 -0
- package/src/versioning/file-store.ts +103 -0
- package/src/versioning/history.ts +43 -0
- package/src/versioning/store.ts +16 -0
- package/src/versioning/types.ts +31 -0
- package/test/capabilities/matcher.test.ts +67 -0
- package/test/capabilities/registry.test.ts +136 -0
- package/test/capabilities/synthesis.test.ts +264 -0
- package/test/cir/lower.test.ts +417 -0
- package/test/cir/optimize.test.ts +266 -0
- package/test/cir/validate.test.ts +368 -0
- package/test/compiler/adaptive-runtime.test.ts +157 -0
- package/test/compiler/compile.test.ts +1198 -0
- package/test/compiler/feedback.test.ts +784 -0
- package/test/compiler/guardrails.test.ts +191 -0
- package/test/compiler/trace.test.ts +404 -0
- package/test/composition/chain.test.ts +328 -0
- package/test/composition/conditional.test.ts +241 -0
- package/test/composition/parallel.test.ts +215 -0
- package/test/environment/analyzer.test.ts +204 -0
- package/test/environment/discovery.test.ts +149 -0
- package/test/failures/classifiers.test.ts +287 -0
- package/test/failures/generator.test.ts +203 -0
- package/test/failures/ontology.test.ts +439 -0
- package/test/failures/recovery.test.ts +300 -0
- package/test/helpers/createFixtureRepo.ts +84 -0
- package/test/helpers/createPatchValidationFixture.ts +144 -0
- package/test/helpers/runCompiledWorkflow.ts +208 -0
- package/test/memory/advisor.test.ts +332 -0
- package/test/memory/extractor.test.ts +295 -0
- package/test/memory/store.test.ts +244 -0
- package/test/metaharness/engine.test.ts +575 -0
- package/test/metaharness/predictor.test.ts +436 -0
- package/test/mutation/derive-failure.test.ts +209 -0
- package/test/mutation/engine.test.ts +622 -0
- package/test/package-smoke.test.ts +29 -0
- package/test/pi/command-input.test.ts +153 -0
- package/test/pi/commands.test.ts +623 -0
- package/test/planner/classify-template.test.ts +32 -0
- package/test/planner/synthesize.test.ts +901 -0
- package/test/reference/PatchValidation.failures.test.ts +137 -0
- package/test/reference/PatchValidation.test.ts +326 -0
- package/test/reference/PrReviewMerge.failures.test.ts +121 -0
- package/test/reference/PrReviewMerge.test.ts +55 -0
- package/test/reference/catalog-open.test.ts +70 -0
- package/test/replanner/runtime.test.ts +207 -0
- package/test/replanner/synthesize.test.ts +303 -0
- package/test/spec/validate.test.ts +1056 -0
- package/test/state/snapshots.test.ts +264 -0
- package/test/synthesis/custom-workflow.test.ts +264 -0
- package/test/synthesis/graph-builder.test.ts +370 -0
- package/test/synthesis/harness-builder.test.ts +128 -0
- package/test/synthesis/policy-builder.test.ts +149 -0
- package/test/synthesis/risk-analyzer.test.ts +230 -0
- package/test/synthesis/skill-parser.test.ts +796 -0
- package/test/verification/engine.test.ts +509 -0
- package/test/versioning/history.test.ts +144 -0
- package/test/versioning/store.test.ts +254 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { harnessSpecSchema } from "./schema.js";
|
|
3
|
+
import type { HarnessSpec, TaskNode } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ allErrors: true });
|
|
6
|
+
const validateSchema = ajv.compile(harnessSpecSchema);
|
|
7
|
+
|
|
8
|
+
export type ValidationResult =
|
|
9
|
+
| { valid: true }
|
|
10
|
+
| { valid: false; errors: string[] };
|
|
11
|
+
|
|
12
|
+
export function validateHarnessSpec(spec: HarnessSpec): ValidationResult {
|
|
13
|
+
const errors: string[] = [];
|
|
14
|
+
|
|
15
|
+
// Step 1: Validate against JSON schema
|
|
16
|
+
const schemaValid = validateSchema(spec);
|
|
17
|
+
if (!schemaValid && validateSchema.errors) {
|
|
18
|
+
for (const err of validateSchema.errors) {
|
|
19
|
+
errors.push(`Schema error at ${err.instancePath || "root"}: ${err.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Step 2: Structural validation
|
|
24
|
+
if (spec.graph) {
|
|
25
|
+
const nodeIds = new Set<string>();
|
|
26
|
+
const nodeKinds = new Map<string, string>();
|
|
27
|
+
|
|
28
|
+
// Check for duplicate node IDs and collect node metadata
|
|
29
|
+
for (const node of spec.graph.nodes) {
|
|
30
|
+
if (node.id) {
|
|
31
|
+
if (nodeIds.has(node.id)) {
|
|
32
|
+
errors.push(`Duplicate node ID: ${node.id}`);
|
|
33
|
+
}
|
|
34
|
+
nodeIds.add(node.id);
|
|
35
|
+
nodeKinds.set(node.id, node.kind);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate entry node exists
|
|
40
|
+
if (spec.graph.entryNodeId && !nodeIds.has(spec.graph.entryNodeId)) {
|
|
41
|
+
errors.push(`Entry node not found: ${spec.graph.entryNodeId}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate edge targets exist
|
|
45
|
+
for (const edge of spec.graph.edges) {
|
|
46
|
+
if (edge.from && !nodeIds.has(edge.from)) {
|
|
47
|
+
errors.push(`Edge references nonexistent source node: ${edge.from}`);
|
|
48
|
+
}
|
|
49
|
+
if (edge.to && !nodeIds.has(edge.to)) {
|
|
50
|
+
errors.push(`Edge references nonexistent target node: ${edge.to}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for condition node references
|
|
55
|
+
for (const node of spec.graph.nodes) {
|
|
56
|
+
if (node.kind === "condition") {
|
|
57
|
+
const condNode = node as any;
|
|
58
|
+
if (condNode.thenNodeId) {
|
|
59
|
+
if (!nodeIds.has(condNode.thenNodeId)) {
|
|
60
|
+
errors.push(`Condition node ${node.id} references nonexistent thenNodeId: ${condNode.thenNodeId}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (condNode.elseNodeId) {
|
|
64
|
+
if (!nodeIds.has(condNode.elseNodeId)) {
|
|
65
|
+
errors.push(`Condition node ${node.id} references nonexistent elseNodeId: ${condNode.elseNodeId}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for merge node references
|
|
72
|
+
for (const node of spec.graph.nodes) {
|
|
73
|
+
if (node.kind === "merge") {
|
|
74
|
+
const mergeNode = node as any;
|
|
75
|
+
if (mergeNode.waitFor) {
|
|
76
|
+
// Issue 3: Validate waitFor is not empty
|
|
77
|
+
if (mergeNode.waitFor.length === 0) {
|
|
78
|
+
errors.push(`Merge node ${node.id} has empty waitFor array`);
|
|
79
|
+
}
|
|
80
|
+
for (const waitNodeId of mergeNode.waitFor) {
|
|
81
|
+
if (!nodeIds.has(waitNodeId)) {
|
|
82
|
+
errors.push(`Merge node ${node.id} references nonexistent waitFor node: ${waitNodeId}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Issue 2: Validate choice interactions have options
|
|
90
|
+
for (const node of spec.graph.nodes) {
|
|
91
|
+
if (node.kind === "human") {
|
|
92
|
+
const humanNode = node as any;
|
|
93
|
+
if (humanNode.interactionType === "choice" && (!humanNode.options || humanNode.options.length === 0)) {
|
|
94
|
+
errors.push(`Human node ${node.id} has interactionType "choice" but missing or empty options`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Issue 1: Proper reachability validation using BFS from entryNodeId
|
|
100
|
+
const reachableNodes = new Set<string>();
|
|
101
|
+
const verificationNodes = new Set<string>();
|
|
102
|
+
|
|
103
|
+
// Collect all nodes referenced by verification rules
|
|
104
|
+
for (const node of spec.graph.nodes) {
|
|
105
|
+
const nodeAny = node as any;
|
|
106
|
+
if (nodeAny.verificationPolicy?.rules) {
|
|
107
|
+
for (const rule of nodeAny.verificationPolicy.rules) {
|
|
108
|
+
if (rule.checkNodeId) {
|
|
109
|
+
verificationNodes.add(rule.checkNodeId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// For condition nodes used as verifiers, also mark their branches as verification nodes
|
|
116
|
+
// (they're not executed in the normal flow)
|
|
117
|
+
for (const node of spec.graph.nodes) {
|
|
118
|
+
if (node.kind === "condition" && verificationNodes.has(node.id)) {
|
|
119
|
+
const condNode = node as any;
|
|
120
|
+
if (condNode.thenNodeId) {
|
|
121
|
+
verificationNodes.add(condNode.thenNodeId);
|
|
122
|
+
}
|
|
123
|
+
if (condNode.elseNodeId) {
|
|
124
|
+
verificationNodes.add(condNode.elseNodeId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (spec.graph.entryNodeId) {
|
|
130
|
+
const queue: string[] = [spec.graph.entryNodeId];
|
|
131
|
+
reachableNodes.add(spec.graph.entryNodeId);
|
|
132
|
+
|
|
133
|
+
// Build adjacency map for edges
|
|
134
|
+
const edgeMap = new Map<string, string[]>();
|
|
135
|
+
for (const edge of spec.graph.edges) {
|
|
136
|
+
if (edge.from && edge.to) {
|
|
137
|
+
if (!edgeMap.has(edge.from)) {
|
|
138
|
+
edgeMap.set(edge.from, []);
|
|
139
|
+
}
|
|
140
|
+
edgeMap.get(edge.from)!.push(edge.to);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build condition node map
|
|
145
|
+
const conditionMap = new Map<string, { thenNodeId: string; elseNodeId: string }>();
|
|
146
|
+
for (const node of spec.graph.nodes) {
|
|
147
|
+
if (node.kind === "condition") {
|
|
148
|
+
const condNode = node as any;
|
|
149
|
+
conditionMap.set(node.id, {
|
|
150
|
+
thenNodeId: condNode.thenNodeId,
|
|
151
|
+
elseNodeId: condNode.elseNodeId
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// BFS traversal
|
|
157
|
+
while (queue.length > 0) {
|
|
158
|
+
const current = queue.shift()!;
|
|
159
|
+
|
|
160
|
+
// Follow regular edges
|
|
161
|
+
const targets = edgeMap.get(current) || [];
|
|
162
|
+
for (const target of targets) {
|
|
163
|
+
if (!reachableNodes.has(target)) {
|
|
164
|
+
reachableNodes.add(target);
|
|
165
|
+
queue.push(target);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Follow condition branches (only if not used as verification node)
|
|
170
|
+
if (!verificationNodes.has(current)) {
|
|
171
|
+
const condBranches = conditionMap.get(current);
|
|
172
|
+
if (condBranches) {
|
|
173
|
+
if (condBranches.thenNodeId && !reachableNodes.has(condBranches.thenNodeId)) {
|
|
174
|
+
reachableNodes.add(condBranches.thenNodeId);
|
|
175
|
+
queue.push(condBranches.thenNodeId);
|
|
176
|
+
}
|
|
177
|
+
if (condBranches.elseNodeId && !reachableNodes.has(condBranches.elseNodeId)) {
|
|
178
|
+
reachableNodes.add(condBranches.elseNodeId);
|
|
179
|
+
queue.push(condBranches.elseNodeId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check for unreachable nodes (excluding verification nodes)
|
|
186
|
+
for (const nodeId of nodeIds) {
|
|
187
|
+
if (!reachableNodes.has(nodeId) && !verificationNodes.has(nodeId)) {
|
|
188
|
+
errors.push(`Unreachable node: ${nodeId}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Validate maxSteps is a positive integer
|
|
194
|
+
if (spec.executionPolicy?.maxSteps !== undefined) {
|
|
195
|
+
if (!Number.isInteger(spec.executionPolicy.maxSteps) || spec.executionPolicy.maxSteps < 1) {
|
|
196
|
+
errors.push("executionPolicy.maxSteps must be a positive integer");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Validate costLimitUsd is a positive number
|
|
201
|
+
if (spec.executionPolicy?.costLimitUsd !== undefined) {
|
|
202
|
+
if (typeof spec.executionPolicy.costLimitUsd !== "number" || spec.executionPolicy.costLimitUsd <= 0) {
|
|
203
|
+
errors.push("executionPolicy.costLimitUsd must be a positive number");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate retry policy is only on supported node kinds
|
|
208
|
+
const retryableKinds = new Set(["tool", "llm", "subworkflow"]);
|
|
209
|
+
for (const node of spec.graph.nodes) {
|
|
210
|
+
const nodeAny = node as any;
|
|
211
|
+
if (nodeAny.retryPolicy && !retryableKinds.has(node.kind)) {
|
|
212
|
+
errors.push(`retry policy not supported on node kind "${node.kind}" (node: ${node.id})`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validate verification policy checkNodeId references and kind matching
|
|
217
|
+
for (const node of spec.graph.nodes) {
|
|
218
|
+
const nodeAny = node as any;
|
|
219
|
+
if (nodeAny.verificationPolicy?.rules) {
|
|
220
|
+
for (const rule of nodeAny.verificationPolicy.rules) {
|
|
221
|
+
if (rule.checkNodeId && !nodeIds.has(rule.checkNodeId)) {
|
|
222
|
+
errors.push(`Verification rule in node ${node.id} references nonexistent checkNodeId: ${rule.checkNodeId}`);
|
|
223
|
+
}
|
|
224
|
+
// Check for self-reference
|
|
225
|
+
if (rule.checkNodeId === node.id) {
|
|
226
|
+
errors.push(`Verification rule in node ${node.id} cannot reference itself`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Validate verification rule kind matches verifier node kind
|
|
230
|
+
if (rule.kind && rule.checkNodeId && nodeIds.has(rule.checkNodeId)) {
|
|
231
|
+
const verifierKind = nodeKinds.get(rule.checkNodeId);
|
|
232
|
+
if (rule.kind === "tool" && verifierKind !== "tool") {
|
|
233
|
+
errors.push(`Verification rule in node ${node.id} has kind "tool" but references verifier ${rule.checkNodeId} of kind "${verifierKind}"`);
|
|
234
|
+
} else if (rule.kind === "llm" && verifierKind !== "llm") {
|
|
235
|
+
errors.push(`Verification rule in node ${node.id} has kind "llm" but references verifier ${rule.checkNodeId} of kind "${verifierKind}"`);
|
|
236
|
+
} else if (rule.kind === "expression" && verifierKind !== "condition") {
|
|
237
|
+
errors.push(`Verification rule in node ${node.id} has kind "expression" but references verifier ${rule.checkNodeId} of kind "${verifierKind}" (expected "condition")`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check for circular verification dependencies using DFS cycle detection
|
|
245
|
+
const verificationGraph = new Map<string, Set<string>>();
|
|
246
|
+
for (const node of spec.graph.nodes) {
|
|
247
|
+
const nodeAny = node as any;
|
|
248
|
+
if (nodeAny.verificationPolicy?.rules) {
|
|
249
|
+
const deps = new Set<string>();
|
|
250
|
+
for (const rule of nodeAny.verificationPolicy.rules) {
|
|
251
|
+
if (rule.checkNodeId) {
|
|
252
|
+
deps.add(rule.checkNodeId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
verificationGraph.set(node.id, deps);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Detect cycles of any length using DFS
|
|
260
|
+
const detectCycle = (start: string, visited: Set<string>, recStack: Set<string>, path: string[]): string[] | null => {
|
|
261
|
+
visited.add(start);
|
|
262
|
+
recStack.add(start);
|
|
263
|
+
path.push(start);
|
|
264
|
+
|
|
265
|
+
const deps = verificationGraph.get(start);
|
|
266
|
+
if (deps) {
|
|
267
|
+
for (const dep of deps) {
|
|
268
|
+
if (!visited.has(dep)) {
|
|
269
|
+
const cycle = detectCycle(dep, visited, recStack, [...path]);
|
|
270
|
+
if (cycle) return cycle;
|
|
271
|
+
} else if (recStack.has(dep)) {
|
|
272
|
+
// Found a cycle
|
|
273
|
+
const cycleStart = path.indexOf(dep);
|
|
274
|
+
return [...path.slice(cycleStart), dep];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
recStack.delete(start);
|
|
280
|
+
return null;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const visited = new Set<string>();
|
|
284
|
+
for (const nodeId of verificationGraph.keys()) {
|
|
285
|
+
if (!visited.has(nodeId)) {
|
|
286
|
+
const cycle = detectCycle(nodeId, visited, new Set(), []);
|
|
287
|
+
if (cycle) {
|
|
288
|
+
errors.push(`Circular verification dependency detected: ${cycle.join(" -> ")}`);
|
|
289
|
+
break; // Report first cycle found
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return errors.length === 0 ? { valid: true } : { valid: false, errors };
|
|
296
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FailureRecord } from "../failures/types.js";
|
|
2
|
+
import type { HarnessState } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function createHarnessState(input: unknown): HarnessState {
|
|
5
|
+
const inputs = input && typeof input === "object" && !Array.isArray(input)
|
|
6
|
+
? structuredClone(input as Record<string, unknown>)
|
|
7
|
+
: {};
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
inputs,
|
|
11
|
+
outputs: {},
|
|
12
|
+
nodeResults: {},
|
|
13
|
+
failures: [],
|
|
14
|
+
metrics: {
|
|
15
|
+
retries: 0,
|
|
16
|
+
durationMs: 0,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function addFailure(state: HarnessState, failure: FailureRecord): void {
|
|
22
|
+
state.failures.push(failure);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function recordNodeResult(state: HarnessState, nodeId: string, result: unknown): void {
|
|
26
|
+
state.nodeResults[nodeId] = result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function updateMetrics(
|
|
30
|
+
state: HarnessState,
|
|
31
|
+
metrics: { retries?: number; durationMs?: number },
|
|
32
|
+
): void {
|
|
33
|
+
if (metrics.retries !== undefined) {
|
|
34
|
+
state.metrics.retries = metrics.retries;
|
|
35
|
+
}
|
|
36
|
+
if (metrics.durationMs !== undefined) {
|
|
37
|
+
state.metrics.durationMs = metrics.durationMs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function captureSnapshot(state: HarnessState): HarnessState {
|
|
42
|
+
return structuredClone(state);
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FailureRecord } from "../failures/types.js";
|
|
2
|
+
|
|
3
|
+
export interface HarnessState {
|
|
4
|
+
inputs: Record<string, unknown>;
|
|
5
|
+
outputs: Record<string, unknown>;
|
|
6
|
+
nodeResults: Record<string, unknown>;
|
|
7
|
+
failures: FailureRecord[];
|
|
8
|
+
metrics: {
|
|
9
|
+
retries: number;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { IntentIR, IntentStep, SupportedWorkflowFamily } from "./intent-ir.js";
|
|
2
|
+
import type { CapabilityRegistry } from "../capabilities/types.js";
|
|
3
|
+
import type { EnvironmentModel } from "../environment/types.js";
|
|
4
|
+
import { matchCapabilities } from "../capabilities/matcher.js";
|
|
5
|
+
import { analyzeEnvironment } from "../environment/analyzer.js";
|
|
6
|
+
|
|
7
|
+
export interface TaskGraph {
|
|
8
|
+
family: SupportedWorkflowFamily;
|
|
9
|
+
stages: WorkflowStage[];
|
|
10
|
+
inputs: Record<string, unknown>;
|
|
11
|
+
goal: string;
|
|
12
|
+
capabilityMatch?: {
|
|
13
|
+
matched: string[];
|
|
14
|
+
missing: string[];
|
|
15
|
+
};
|
|
16
|
+
environmentAnalysis?: {
|
|
17
|
+
missingTools: string[];
|
|
18
|
+
highRiskConstraints: string[];
|
|
19
|
+
readinessScore: number;
|
|
20
|
+
preparatorySteps: string[];
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkflowStage {
|
|
25
|
+
id: string;
|
|
26
|
+
type: "setup" | "reproduce" | "apply" | "verify" | "review" | "merge" | "approval";
|
|
27
|
+
dependencies: string[];
|
|
28
|
+
description: string;
|
|
29
|
+
requiredInputs: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stepKindToStageType(kind: IntentStep["kind"]): WorkflowStage["type"] {
|
|
33
|
+
switch (kind) {
|
|
34
|
+
case "tool":
|
|
35
|
+
return "setup";
|
|
36
|
+
case "llm":
|
|
37
|
+
return "review";
|
|
38
|
+
case "human":
|
|
39
|
+
return "approval";
|
|
40
|
+
case "condition":
|
|
41
|
+
return "verify";
|
|
42
|
+
default:
|
|
43
|
+
return "setup";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildCapabilityStages(intent: IntentIR, registry: CapabilityRegistry): { stages: WorkflowStage[]; matched: string[]; missing: string[] } {
|
|
48
|
+
const stages: WorkflowStage[] = [];
|
|
49
|
+
const requiredTools = intent.capabilities || intent.requiredTools;
|
|
50
|
+
const { matched, missing } = matchCapabilities(requiredTools, registry);
|
|
51
|
+
|
|
52
|
+
if (matched.length === 0) {
|
|
53
|
+
return { stages: [], matched: [], missing };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const setupStageId = "capability-setup";
|
|
57
|
+
stages.push({
|
|
58
|
+
id: setupStageId,
|
|
59
|
+
type: "setup",
|
|
60
|
+
dependencies: [],
|
|
61
|
+
description: `Verify required capabilities: ${matched.map(c => c.name).join(", ")}`,
|
|
62
|
+
requiredInputs: []
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let prevStageId = setupStageId;
|
|
66
|
+
|
|
67
|
+
for (const cap of matched) {
|
|
68
|
+
if (cap.verification.length > 0) {
|
|
69
|
+
const verifyStageId = `capability-verify-${cap.id}`;
|
|
70
|
+
stages.push({
|
|
71
|
+
id: verifyStageId,
|
|
72
|
+
type: "verify",
|
|
73
|
+
dependencies: [prevStageId],
|
|
74
|
+
description: `Verify ${cap.name}: ${cap.verification.join("; ")}`,
|
|
75
|
+
requiredInputs: []
|
|
76
|
+
});
|
|
77
|
+
prevStageId = verifyStageId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (cap.risks.length > 0) {
|
|
81
|
+
const riskStageId = `capability-risk-${cap.id}`;
|
|
82
|
+
stages.push({
|
|
83
|
+
id: riskStageId,
|
|
84
|
+
type: "review",
|
|
85
|
+
dependencies: [prevStageId],
|
|
86
|
+
description: `Assess risks for ${cap.name}: ${cap.risks.join("; ")}`,
|
|
87
|
+
requiredInputs: []
|
|
88
|
+
});
|
|
89
|
+
prevStageId = riskStageId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (cap.kind === "human") {
|
|
93
|
+
stages.push({
|
|
94
|
+
id: `capability-approval-${cap.id}`,
|
|
95
|
+
type: "approval",
|
|
96
|
+
dependencies: [prevStageId],
|
|
97
|
+
description: `Human approval required: ${cap.name}`,
|
|
98
|
+
requiredInputs: []
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { stages, matched: matched.map(c => c.id), missing };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildTaskGraph(
|
|
107
|
+
intent: IntentIR,
|
|
108
|
+
registry?: CapabilityRegistry,
|
|
109
|
+
environment?: EnvironmentModel
|
|
110
|
+
): TaskGraph {
|
|
111
|
+
const stages: WorkflowStage[] = [];
|
|
112
|
+
let capabilityMatch: { matched: string[]; missing: string[] } | undefined;
|
|
113
|
+
let environmentAnalysis: TaskGraph["environmentAnalysis"];
|
|
114
|
+
|
|
115
|
+
if (environment) {
|
|
116
|
+
const analysis = analyzeEnvironment(environment);
|
|
117
|
+
const { matchedTools, missingTools, highRiskConstraints, readinessScore, preparatorySteps } = analysis;
|
|
118
|
+
|
|
119
|
+
environmentAnalysis = {
|
|
120
|
+
missingTools,
|
|
121
|
+
highRiskConstraints,
|
|
122
|
+
readinessScore,
|
|
123
|
+
preparatorySteps,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (preparatorySteps.length > 0) {
|
|
127
|
+
stages.push({
|
|
128
|
+
id: "environment-prep",
|
|
129
|
+
type: "setup",
|
|
130
|
+
dependencies: [],
|
|
131
|
+
description: `Prepare environment: ${preparatorySteps.join("; ")}`,
|
|
132
|
+
requiredInputs: [],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hasCapabilities = intent.capabilities && intent.capabilities.length > 0;
|
|
138
|
+
const useCapabilityPath = hasCapabilities && registry;
|
|
139
|
+
|
|
140
|
+
if (useCapabilityPath) {
|
|
141
|
+
const result = buildCapabilityStages(intent, registry);
|
|
142
|
+
stages.push(...result.stages);
|
|
143
|
+
capabilityMatch = { matched: result.matched, missing: result.missing };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (intent.family === "patch-validation" && !useCapabilityPath) {
|
|
147
|
+
stages.push({
|
|
148
|
+
id: "setup-baseline",
|
|
149
|
+
type: "setup",
|
|
150
|
+
dependencies: [],
|
|
151
|
+
description: "Check out baseline ref",
|
|
152
|
+
requiredInputs: ["repoPath", "baselineRef"]
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
stages.push({
|
|
156
|
+
id: "reproduce-bug",
|
|
157
|
+
type: "reproduce",
|
|
158
|
+
dependencies: ["setup-baseline"],
|
|
159
|
+
description: "Reproduce the bug on baseline",
|
|
160
|
+
requiredInputs: ["reproduceCommands"]
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
stages.push({
|
|
164
|
+
id: "apply-candidate",
|
|
165
|
+
type: "apply",
|
|
166
|
+
dependencies: ["reproduce-bug"],
|
|
167
|
+
description: "Apply candidate fix",
|
|
168
|
+
requiredInputs: ["candidateBranch", "patchFilePath"] // One of these must be present
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
stages.push({
|
|
172
|
+
id: "verify-fix",
|
|
173
|
+
type: "verify",
|
|
174
|
+
dependencies: ["apply-candidate"],
|
|
175
|
+
description: "Verify fix resolves issue and passes regression tests",
|
|
176
|
+
requiredInputs: ["reproduceCommands", "verificationCommands"]
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
stages.push({
|
|
180
|
+
id: "review-results",
|
|
181
|
+
type: "review",
|
|
182
|
+
dependencies: ["verify-fix"],
|
|
183
|
+
description: "Review validation results",
|
|
184
|
+
requiredInputs: ["reviewInstructions"]
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (intent.humanCheckpoints.includes("approval-gate")) {
|
|
188
|
+
stages.push({
|
|
189
|
+
id: "approval-gate",
|
|
190
|
+
type: "approval",
|
|
191
|
+
dependencies: ["review-results"],
|
|
192
|
+
description: "Human approval required",
|
|
193
|
+
requiredInputs: []
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} else if (intent.family === "pr-review-merge" && !useCapabilityPath) {
|
|
197
|
+
stages.push({
|
|
198
|
+
id: "setup-pr",
|
|
199
|
+
type: "setup",
|
|
200
|
+
dependencies: [],
|
|
201
|
+
description: "Fetch and checkout PR branches",
|
|
202
|
+
requiredInputs: ["repoPath", "sourceBranch", "targetBranch"]
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
stages.push({
|
|
206
|
+
id: "review-changes",
|
|
207
|
+
type: "review",
|
|
208
|
+
dependencies: ["setup-pr"],
|
|
209
|
+
description: "Review PR changes",
|
|
210
|
+
requiredInputs: ["reviewInstructions"]
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
stages.push({
|
|
214
|
+
id: "verify-tests",
|
|
215
|
+
type: "verify",
|
|
216
|
+
dependencies: ["review-changes"],
|
|
217
|
+
description: "Run verification tests",
|
|
218
|
+
requiredInputs: ["verificationCommands"]
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
stages.push({
|
|
222
|
+
id: "merge-pr",
|
|
223
|
+
type: "merge",
|
|
224
|
+
dependencies: ["verify-tests"],
|
|
225
|
+
description: "Merge PR",
|
|
226
|
+
requiredInputs: ["sourceBranch", "targetBranch"]
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (intent.steps && intent.steps.length > 0) {
|
|
231
|
+
for (let i = 0; i < intent.steps.length; i++) {
|
|
232
|
+
const step = intent.steps[i];
|
|
233
|
+
const prevStepId = i > 0 ? intent.steps[i - 1].id : undefined;
|
|
234
|
+
|
|
235
|
+
stages.push({
|
|
236
|
+
id: step.id,
|
|
237
|
+
type: stepKindToStageType(step.kind),
|
|
238
|
+
dependencies: prevStepId ? [prevStepId] : [],
|
|
239
|
+
description: step.label,
|
|
240
|
+
requiredInputs: []
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (intent.verificationTargets.length > 0) {
|
|
245
|
+
const lastStepId = intent.steps[intent.steps.length - 1].id;
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < intent.verificationTargets.length; i++) {
|
|
248
|
+
stages.push({
|
|
249
|
+
id: `verify-target-${i}`,
|
|
250
|
+
type: "verify",
|
|
251
|
+
dependencies: [lastStepId],
|
|
252
|
+
description: `Verify: ${intent.verificationTargets[i]}`,
|
|
253
|
+
requiredInputs: []
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
family: intent.family,
|
|
261
|
+
stages,
|
|
262
|
+
inputs: intent.inputs,
|
|
263
|
+
goal: intent.goal,
|
|
264
|
+
capabilityMatch,
|
|
265
|
+
environmentAnalysis,
|
|
266
|
+
};
|
|
267
|
+
}
|