@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,601 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { registerWorkflow, type RegisteredWorkflow, type WorkflowContext, type WorkflowOptions, type YieldItem } from "pi-duroxide";
|
|
3
|
+
import type { CirMergeNode, CirNode, CirTransition, CirWorkflow } from "../cir/types.js";
|
|
4
|
+
import { lowerHarnessSpecToCir } from "../cir/lower.js";
|
|
5
|
+
import { optimizeCirWorkflow } from "../cir/optimize.js";
|
|
6
|
+
import { validateCirWorkflow } from "../cir/validate.js";
|
|
7
|
+
import type { HarnessSpec } from "../spec/types.js";
|
|
8
|
+
import { validateHarnessSpec } from "../spec/validate.js";
|
|
9
|
+
import { addFailure, createHarnessState, recordNodeResult, updateMetrics } from "../state/snapshots.js";
|
|
10
|
+
import type { HarnessState } from "../state/types.js";
|
|
11
|
+
import {
|
|
12
|
+
buildShellCommand,
|
|
13
|
+
checkGuardrails,
|
|
14
|
+
evaluateConditionExpression,
|
|
15
|
+
GuardrailExceededError,
|
|
16
|
+
recordTrace,
|
|
17
|
+
runWithRetry,
|
|
18
|
+
type ExecutionState,
|
|
19
|
+
} from "./runtime-helpers.js";
|
|
20
|
+
import { runVerification } from "../verification/engine.js";
|
|
21
|
+
import { unwrapAdaptiveInput, prepareRuntimeReplan, type AdaptiveRuntimeMetadata } from "../replanner/runtime.js";
|
|
22
|
+
import type { LineageEntry } from "../versioning/types.js";
|
|
23
|
+
import type { HarnessExecutionTrace } from "../versioning/types.js";
|
|
24
|
+
import { buildReferenceHarnessSpec } from "../reference/catalog.js";
|
|
25
|
+
|
|
26
|
+
export interface CompiledHarnessResult {
|
|
27
|
+
status: "completed";
|
|
28
|
+
terminalNodeId: string;
|
|
29
|
+
result: unknown;
|
|
30
|
+
outputs: Record<string, unknown>;
|
|
31
|
+
trace: HarnessExecutionTrace;
|
|
32
|
+
harnessState: HarnessState;
|
|
33
|
+
adaptiveMetadata?: AdaptiveRuntimeMetadata;
|
|
34
|
+
lineage?: LineageEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompiledHarnessWorkflow {
|
|
38
|
+
name: string;
|
|
39
|
+
spec: HarnessSpec;
|
|
40
|
+
cir: CirWorkflow;
|
|
41
|
+
workflows: RegisteredWorkflow[];
|
|
42
|
+
optimizations?: string[];
|
|
43
|
+
adaptive?: {
|
|
44
|
+
currentVersion: { version: number; parentVersion?: number; reason: string };
|
|
45
|
+
lineage: LineageEntry[];
|
|
46
|
+
};
|
|
47
|
+
register(pi?: ExtensionAPI): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ParallelMergePlan {
|
|
51
|
+
mergeNodeId: string;
|
|
52
|
+
branchNodeIds: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function compileHarnessSpec(spec: HarnessSpec): CompiledHarnessWorkflow {
|
|
56
|
+
const specValidation = validateHarnessSpec(spec);
|
|
57
|
+
if (!specValidation.valid) {
|
|
58
|
+
throw new Error(`HarnessSpec validation failed:\n- ${specValidation.errors.join("\n- ")}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cir = lowerHarnessSpecToCir(spec);
|
|
62
|
+
const { optimized: optimizedCir, passes: optimizationPasses } = optimizeCirWorkflow(cir);
|
|
63
|
+
const cirValidation = validateCirWorkflow(optimizedCir);
|
|
64
|
+
if (!cirValidation.valid) {
|
|
65
|
+
throw new Error(`CIR validation failed:\n- ${cirValidation.errors.join("\n- ")}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const compiledSpec = structuredClone(spec);
|
|
69
|
+
const compiledCir = structuredClone(optimizedCir);
|
|
70
|
+
const nodeMap = new Map(compiledCir.nodes.map(node => [node.id, node]));
|
|
71
|
+
const outgoingTransitions = buildTransitionMap(compiledCir.transitions);
|
|
72
|
+
const incomingTransitions = buildIncomingTransitionMap(compiledCir.transitions);
|
|
73
|
+
validateVerificationSupport(nodeMap);
|
|
74
|
+
const parallelMergePlans = buildParallelMergePlans(compiledCir, nodeMap, outgoingTransitions);
|
|
75
|
+
validateMergeSupport(nodeMap, incomingTransitions, parallelMergePlans);
|
|
76
|
+
|
|
77
|
+
const workflows: RegisteredWorkflow[] = [
|
|
78
|
+
{
|
|
79
|
+
name: compiledCir.name,
|
|
80
|
+
generator: createWorkflowGenerator(compiledCir, nodeMap, outgoingTransitions, parallelMergePlans, compiledSpec, compiledCir),
|
|
81
|
+
options: buildWorkflowOptions(compiledSpec),
|
|
82
|
+
sourceInfo: {
|
|
83
|
+
source: "lasso",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
name: compiledSpec.name,
|
|
90
|
+
spec: compiledSpec,
|
|
91
|
+
cir: compiledCir,
|
|
92
|
+
workflows,
|
|
93
|
+
optimizations: optimizationPasses.length > 0 ? optimizationPasses : undefined,
|
|
94
|
+
register(_pi?: ExtensionAPI) {
|
|
95
|
+
for (const workflow of workflows) {
|
|
96
|
+
registerWorkflow(workflow.name, workflow.generator, workflow.options);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createWorkflowGenerator(
|
|
103
|
+
cir: CirWorkflow,
|
|
104
|
+
nodeMap: Map<string, CirNode>,
|
|
105
|
+
outgoingTransitions: Map<string, CirTransition[]>,
|
|
106
|
+
parallelMergePlans: Map<string, ParallelMergePlan>,
|
|
107
|
+
compiledSpec: HarnessSpec,
|
|
108
|
+
compiledCir: CirWorkflow,
|
|
109
|
+
) {
|
|
110
|
+
return function* compiledHarnessWorkflow(
|
|
111
|
+
ctx: WorkflowContext,
|
|
112
|
+
input: unknown,
|
|
113
|
+
): Generator<YieldItem, CompiledHarnessResult, unknown> {
|
|
114
|
+
const unwrapped = unwrapAdaptiveInput(input);
|
|
115
|
+
const adaptiveMetadata = unwrapped.hasAdaptive ? unwrapped.metadata : undefined;
|
|
116
|
+
const workflowInput = unwrapped.input;
|
|
117
|
+
const effectiveSpec = adaptiveMetadata?.currentVersion.spec ?? compiledSpec;
|
|
118
|
+
const effectiveCir = adaptiveMetadata ? lowerHarnessSpecToCir(effectiveSpec) : compiledCir;
|
|
119
|
+
|
|
120
|
+
if (adaptiveMetadata) {
|
|
121
|
+
const specValidation = validateHarnessSpec(effectiveSpec);
|
|
122
|
+
if (!specValidation.valid) {
|
|
123
|
+
throw new Error(`Adaptive HarnessSpec validation failed:\n- ${specValidation.errors.join("\n- ")}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cirValidation = validateCirWorkflow(effectiveCir);
|
|
127
|
+
if (!cirValidation.valid) {
|
|
128
|
+
throw new Error(`Adaptive CIR validation failed:\n- ${cirValidation.errors.join("\n- ")}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const effectiveNodeMap = adaptiveMetadata ? new Map(effectiveCir.nodes.map(node => [node.id, node])) : nodeMap;
|
|
133
|
+
const effectiveOutgoingTransitions = adaptiveMetadata ? buildTransitionMap(effectiveCir.transitions) : outgoingTransitions;
|
|
134
|
+
const effectiveParallelMergePlans = adaptiveMetadata ? buildParallelMergePlans(effectiveCir, effectiveNodeMap, effectiveOutgoingTransitions) : parallelMergePlans;
|
|
135
|
+
|
|
136
|
+
if (adaptiveMetadata) {
|
|
137
|
+
const adaptiveIncomingTransitions = buildIncomingTransitionMap(effectiveCir.transitions);
|
|
138
|
+
validateVerificationSupport(effectiveNodeMap);
|
|
139
|
+
validateMergeSupport(effectiveNodeMap, adaptiveIncomingTransitions, effectiveParallelMergePlans);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const startTimeMs = Date.now();
|
|
143
|
+
const harnessState = createHarnessState(workflowInput);
|
|
144
|
+
const state: ExecutionState = {
|
|
145
|
+
input: workflowInput,
|
|
146
|
+
outputs: {},
|
|
147
|
+
trace: [],
|
|
148
|
+
harnessState,
|
|
149
|
+
startTimeMs,
|
|
150
|
+
stepCount: 0,
|
|
151
|
+
estimatedCostUsd: 0,
|
|
152
|
+
};
|
|
153
|
+
let currentNodeId = effectiveCir.entryNodeId;
|
|
154
|
+
|
|
155
|
+
while (true) {
|
|
156
|
+
const node = getNode(effectiveNodeMap, currentNodeId);
|
|
157
|
+
|
|
158
|
+
if (node.kind === "condition") {
|
|
159
|
+
const matched = evaluateConditionExpression(node.action.conditionExpr, state);
|
|
160
|
+
recordTrace(ctx, state, node, matched ? "condition-true" : "condition-false");
|
|
161
|
+
currentNodeId = getConditionTransition(node, effectiveOutgoingTransitions, matched).to;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (node.kind === "merge") {
|
|
166
|
+
const mergeOutput = buildMergeOutput(node, state.outputs);
|
|
167
|
+
state.outputs[node.id] = mergeOutput;
|
|
168
|
+
recordTrace(ctx, state, node, "merge", {
|
|
169
|
+
waitFor: [...node.action.join.waitFor],
|
|
170
|
+
strategy: node.action.join.strategy,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const successTransitions = getSuccessTransitions(node.id, effectiveOutgoingTransitions);
|
|
174
|
+
if (successTransitions.length === 0) {
|
|
175
|
+
return yield* buildCompletedResultWithContinuation(ctx, state, node.id, adaptiveMetadata);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
currentNodeId = successTransitions[0].to;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const guardrailResult = checkGuardrails({
|
|
183
|
+
stepCount: state.stepCount,
|
|
184
|
+
estimatedCostUsd: state.estimatedCostUsd,
|
|
185
|
+
maxSteps: effectiveSpec.executionPolicy?.maxSteps,
|
|
186
|
+
costLimitUsd: effectiveSpec.executionPolicy?.costLimitUsd,
|
|
187
|
+
});
|
|
188
|
+
if (!guardrailResult.withinLimits) {
|
|
189
|
+
throw new GuardrailExceededError(guardrailResult.reason!);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const output = yield* executeNodeWithPolicies(ctx, state, node, effectiveNodeMap, effectiveCir.name);
|
|
193
|
+
state.outputs[node.id] = output;
|
|
194
|
+
state.stepCount += 1;
|
|
195
|
+
if (node.kind === "llm") {
|
|
196
|
+
state.estimatedCostUsd += 0.01;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const parallelMergePlan = effectiveParallelMergePlans.get(node.id);
|
|
200
|
+
if (parallelMergePlan) {
|
|
201
|
+
const branchNodes = parallelMergePlan.branchNodeIds.map(branchNodeId => getNode(effectiveNodeMap, branchNodeId));
|
|
202
|
+
const mergeNode = getNode(effectiveNodeMap, parallelMergePlan.mergeNodeId);
|
|
203
|
+
for (const branchNode of branchNodes) {
|
|
204
|
+
recordTrace(ctx, state, branchNode, "enter", {
|
|
205
|
+
parallel: true,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let branchResults: unknown[];
|
|
210
|
+
try {
|
|
211
|
+
branchResults = (yield ctx.all(
|
|
212
|
+
branchNodes.map(branchNode => createActionYieldItem(ctx, branchNode, effectiveCir.name)),
|
|
213
|
+
)) as unknown[];
|
|
214
|
+
} catch (error) {
|
|
215
|
+
state.outputs[mergeNode.id] = {
|
|
216
|
+
status: "failed",
|
|
217
|
+
error: formatUnknownError(error),
|
|
218
|
+
};
|
|
219
|
+
recordTrace(ctx, state, mergeNode, "failure", {
|
|
220
|
+
parallel: true,
|
|
221
|
+
message: formatUnknownError(error),
|
|
222
|
+
});
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
branchNodes.forEach((branchNode, index) => {
|
|
227
|
+
state.outputs[branchNode.id] = branchResults[index];
|
|
228
|
+
recordTrace(ctx, state, branchNode, "success", {
|
|
229
|
+
parallel: true,
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
currentNodeId = parallelMergePlan.mergeNodeId;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const successTransitions = getSuccessTransitions(node.id, effectiveOutgoingTransitions);
|
|
238
|
+
if (successTransitions.length === 0) {
|
|
239
|
+
return yield* buildCompletedResultWithContinuation(ctx, state, node.id, adaptiveMetadata);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
currentNodeId = successTransitions[0].to;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildTransitionMap(transitions: CirTransition[]): Map<string, CirTransition[]> {
|
|
248
|
+
const transitionMap = new Map<string, CirTransition[]>();
|
|
249
|
+
|
|
250
|
+
for (const transition of transitions) {
|
|
251
|
+
const items = transitionMap.get(transition.from) ?? [];
|
|
252
|
+
items.push(transition);
|
|
253
|
+
transitionMap.set(transition.from, items);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return transitionMap;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildIncomingTransitionMap(transitions: CirTransition[]): Map<string, CirTransition[]> {
|
|
260
|
+
const transitionMap = new Map<string, CirTransition[]>();
|
|
261
|
+
|
|
262
|
+
for (const transition of transitions) {
|
|
263
|
+
const items = transitionMap.get(transition.to) ?? [];
|
|
264
|
+
items.push(transition);
|
|
265
|
+
transitionMap.set(transition.to, items);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return transitionMap;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function buildParallelMergePlans(
|
|
272
|
+
cir: CirWorkflow,
|
|
273
|
+
nodeMap: Map<string, CirNode>,
|
|
274
|
+
outgoingTransitions: Map<string, CirTransition[]>,
|
|
275
|
+
): Map<string, ParallelMergePlan> {
|
|
276
|
+
const plans = new Map<string, ParallelMergePlan>();
|
|
277
|
+
|
|
278
|
+
for (const node of cir.nodes) {
|
|
279
|
+
const successTransitions = getSuccessTransitions(node.id, outgoingTransitions);
|
|
280
|
+
if (successTransitions.length <= 1) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const branchNodes = successTransitions.map(transition => getNode(nodeMap, transition.to));
|
|
285
|
+
const branchSuccessTargets = branchNodes.map(branchNode => {
|
|
286
|
+
if (branchNode.kind === "condition" || branchNode.kind === "merge") {
|
|
287
|
+
throw new Error(`Unsupported parallel merge shape at node ${node.id}: branch node ${branchNode.id} is not directly executable`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (branchNode.retry || branchNode.verification || branchNode.failureRouting) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Unsupported parallel merge shape at node ${node.id}: branch node ${branchNode.id} carries retry, verification, or failure-routing metadata`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const branchTransitions = outgoingTransitions.get(branchNode.id) ?? [];
|
|
297
|
+
const directSuccessTransitions = branchTransitions.filter(transition => transition.when === "success");
|
|
298
|
+
const branchConditionTransitions = branchTransitions.filter(transition => transition.when !== "success");
|
|
299
|
+
|
|
300
|
+
if (branchConditionTransitions.length > 0 || directSuccessTransitions.length !== 1) {
|
|
301
|
+
throw new Error(`Unsupported parallel merge shape at node ${node.id}: branch node ${branchNode.id} must transition directly to a single merge node`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return directSuccessTransitions[0].to;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const mergeNodeId = branchSuccessTargets[0];
|
|
308
|
+
if (!branchSuccessTargets.every(targetNodeId => targetNodeId === mergeNodeId)) {
|
|
309
|
+
throw new Error(`Unsupported parallel merge shape at node ${node.id}: branches do not converge on the same merge node`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const mergeNode = getNode(nodeMap, mergeNodeId);
|
|
313
|
+
if (mergeNode.kind !== "merge") {
|
|
314
|
+
throw new Error(`Unsupported parallel merge shape at node ${node.id}: target ${mergeNodeId} is not a merge node`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const waitForSet = new Set(mergeNode.action.join.waitFor);
|
|
318
|
+
const branchNodeIds = branchNodes.map(branchNode => branchNode.id);
|
|
319
|
+
if (
|
|
320
|
+
waitForSet.size !== branchNodeIds.length
|
|
321
|
+
|| branchNodeIds.some(branchNodeId => !waitForSet.has(branchNodeId))
|
|
322
|
+
) {
|
|
323
|
+
throw new Error(`Unsupported parallel merge shape at node ${node.id}: merge node ${mergeNode.id} must wait for the forked branch nodes directly`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
plans.set(node.id, {
|
|
327
|
+
mergeNodeId: mergeNode.id,
|
|
328
|
+
branchNodeIds,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return plans;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function validateMergeSupport(
|
|
336
|
+
nodeMap: Map<string, CirNode>,
|
|
337
|
+
incomingTransitions: Map<string, CirTransition[]>,
|
|
338
|
+
parallelMergePlans: Map<string, ParallelMergePlan>,
|
|
339
|
+
): void {
|
|
340
|
+
const parallelMergeNodeIds = new Set(Array.from(parallelMergePlans.values(), plan => plan.mergeNodeId));
|
|
341
|
+
|
|
342
|
+
for (const node of nodeMap.values()) {
|
|
343
|
+
if (node.kind !== "merge") {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const waitForNodeId of node.action.join.waitFor) {
|
|
348
|
+
const waitForNode = getNode(nodeMap, waitForNodeId);
|
|
349
|
+
if (waitForNode.kind === "condition" || waitForNode.kind === "merge") {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Merge node ${node.id} cannot wait for non-executable node ${waitForNodeId} of kind "${waitForNode.kind}"`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (parallelMergeNodeIds.has(node.id)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const mergeIncomingTransitions = (incomingTransitions.get(node.id) ?? []).filter(transition => transition.when === "success");
|
|
361
|
+
const isDirectSequentialMerge =
|
|
362
|
+
node.action.join.waitFor.length === 1
|
|
363
|
+
&& mergeIncomingTransitions.length === 1
|
|
364
|
+
&& mergeIncomingTransitions[0]?.from === node.action.join.waitFor[0];
|
|
365
|
+
|
|
366
|
+
if (!isDirectSequentialMerge) {
|
|
367
|
+
throw new Error(`Unsupported merge execution shape for merge node ${node.id}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function validateVerificationSupport(nodeMap: Map<string, CirNode>): void {
|
|
373
|
+
for (const node of nodeMap.values()) {
|
|
374
|
+
if (!node.verification || node.verification.length === 0) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const hook of node.verification) {
|
|
379
|
+
const verifierNode = getNode(nodeMap, hook.checkNodeId);
|
|
380
|
+
if (verifierNode.verification && verifierNode.verification.length > 0) {
|
|
381
|
+
throw new Error(`Verifier node ${verifierNode.id} cannot carry nested verification hooks`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildWorkflowOptions(spec: HarnessSpec): WorkflowOptions {
|
|
388
|
+
return {
|
|
389
|
+
description: `Compiled Lasso harness ${spec.name}`,
|
|
390
|
+
...(spec.executionPolicy?.timeout !== undefined
|
|
391
|
+
? { timeoutMs: spec.executionPolicy.timeout * 1000 }
|
|
392
|
+
: {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getSuccessTransitions(
|
|
397
|
+
nodeId: string,
|
|
398
|
+
outgoingTransitions: Map<string, CirTransition[]>,
|
|
399
|
+
): CirTransition[] {
|
|
400
|
+
return (outgoingTransitions.get(nodeId) ?? []).filter(transition => transition.when === "success");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function getConditionTransition(
|
|
404
|
+
node: Extract<CirNode, { kind: "condition" }>,
|
|
405
|
+
outgoingTransitions: Map<string, CirTransition[]>,
|
|
406
|
+
matched: boolean,
|
|
407
|
+
): CirTransition {
|
|
408
|
+
const when = matched ? "condition-true" : "condition-false";
|
|
409
|
+
const transition = (outgoingTransitions.get(node.id) ?? []).find(item => item.when === when);
|
|
410
|
+
if (!transition) {
|
|
411
|
+
throw new Error(`Condition node ${node.id} is missing a ${when} transition`);
|
|
412
|
+
}
|
|
413
|
+
return transition;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getNode(nodeMap: Map<string, CirNode>, nodeId: string): CirNode {
|
|
417
|
+
const node = nodeMap.get(nodeId);
|
|
418
|
+
if (!node) {
|
|
419
|
+
throw new Error(`Compiled workflow is missing node ${nodeId}`);
|
|
420
|
+
}
|
|
421
|
+
return node;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function* executeNodeWithPolicies(
|
|
425
|
+
ctx: WorkflowContext,
|
|
426
|
+
state: ExecutionState,
|
|
427
|
+
node: Exclude<CirNode, { kind: "condition" | "merge" }>,
|
|
428
|
+
nodeMap: Map<string, CirNode>,
|
|
429
|
+
workflowName: string,
|
|
430
|
+
): Generator<YieldItem, unknown, unknown> {
|
|
431
|
+
const verificationRetryCounts = new Map<string, number>();
|
|
432
|
+
|
|
433
|
+
while (true) {
|
|
434
|
+
delete state.outputs[node.id];
|
|
435
|
+
|
|
436
|
+
const output = yield* runWithRetry(ctx, state, node, function* () {
|
|
437
|
+
recordTrace(ctx, state, node, "enter");
|
|
438
|
+
const result = yield createActionYieldItem(ctx, node, workflowName);
|
|
439
|
+
recordTrace(ctx, state, node, "success");
|
|
440
|
+
return result;
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
state.outputs[node.id] = output;
|
|
444
|
+
const verificationReport = yield* runVerification(node.id, node.verification ?? [], nodeMap, state, ctx);
|
|
445
|
+
|
|
446
|
+
if (verificationReport.overallStatus === "pass") {
|
|
447
|
+
return output;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (verificationReport.overallStatus === "block") {
|
|
451
|
+
const retryResult = verificationReport.hookResults.find(r => r.outcome.status === "retry");
|
|
452
|
+
if (retryResult && retryResult.outcome.status === "retry") {
|
|
453
|
+
const retryCount = verificationRetryCounts.get(retryResult.hook.checkNodeId) ?? 0;
|
|
454
|
+
if (retryCount + 1 < retryResult.outcome.maxAttempts) {
|
|
455
|
+
verificationRetryCounts.set(retryResult.hook.checkNodeId, retryCount + 1);
|
|
456
|
+
recordTrace(ctx, state, node, "retry", {
|
|
457
|
+
reason: "verification",
|
|
458
|
+
hook: retryResult.hook.checkNodeId,
|
|
459
|
+
attemptNumber: retryCount + 2,
|
|
460
|
+
});
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const exhaustionMessage = `Verification retry exhausted for node ${node.id} via ${retryResult.hook.checkNodeId}`;
|
|
464
|
+
addFailure(state.harnessState, {
|
|
465
|
+
domainType: "lasso",
|
|
466
|
+
rootCause: "verification_failed",
|
|
467
|
+
nodeId: node.id,
|
|
468
|
+
message: exhaustionMessage,
|
|
469
|
+
});
|
|
470
|
+
throw new Error(exhaustionMessage);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const blockResult = verificationReport.hookResults.find(r => r.outcome.status === "block");
|
|
474
|
+
const message = blockResult?.outcome.status === "block"
|
|
475
|
+
? blockResult.outcome.message
|
|
476
|
+
: `Verification failed for node ${node.id}`;
|
|
477
|
+
addFailure(state.harnessState, {
|
|
478
|
+
domainType: "lasso",
|
|
479
|
+
rootCause: "verification_failed",
|
|
480
|
+
nodeId: node.id,
|
|
481
|
+
message,
|
|
482
|
+
});
|
|
483
|
+
throw new Error(message);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function createActionYieldItem(
|
|
489
|
+
ctx: WorkflowContext,
|
|
490
|
+
node: Exclude<CirNode, { kind: "condition" | "merge" }>,
|
|
491
|
+
workflowName: string,
|
|
492
|
+
): YieldItem {
|
|
493
|
+
switch (node.kind) {
|
|
494
|
+
case "tool":
|
|
495
|
+
return ctx.pi.tool("bash", {
|
|
496
|
+
command: buildShellCommand(node.action.tool, node.action.args, node.action.cwd, node.action.env),
|
|
497
|
+
description: `Lasso tool node ${node.id}`,
|
|
498
|
+
});
|
|
499
|
+
case "llm": {
|
|
500
|
+
const messages = [];
|
|
501
|
+
if (node.action.system) {
|
|
502
|
+
messages.push({
|
|
503
|
+
role: "system",
|
|
504
|
+
content: [{ type: "text", text: node.action.system }],
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
messages.push({
|
|
508
|
+
role: "user",
|
|
509
|
+
content: [{ type: "text", text: node.action.prompt }],
|
|
510
|
+
});
|
|
511
|
+
return ctx.pi.llm(messages, {
|
|
512
|
+
model: node.action.model,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
case "human":
|
|
516
|
+
return ctx.waitForEvent(`lasso:human:${workflowName}:${node.id}`);
|
|
517
|
+
case "subworkflow":
|
|
518
|
+
return ctx.scheduleSubOrchestration(node.action.specRef, node.action.inputs ?? {});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function buildMergeOutput(node: CirMergeNode, outputs: Record<string, unknown>): Record<string, unknown> {
|
|
523
|
+
const missingNodeIds = node.action.join.waitFor.filter(waitForNodeId => !(waitForNodeId in outputs));
|
|
524
|
+
if (missingNodeIds.length > 0) {
|
|
525
|
+
throw new Error(`Merge node ${node.id} is missing outputs for: ${missingNodeIds.join(", ")}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return Object.fromEntries(node.action.join.waitFor.map(waitForNodeId => [waitForNodeId, outputs[waitForNodeId]]));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function buildCompletedResult(
|
|
532
|
+
state: ExecutionState,
|
|
533
|
+
terminalNodeId: string,
|
|
534
|
+
adaptiveMetadata?: AdaptiveRuntimeMetadata,
|
|
535
|
+
): CompiledHarnessResult {
|
|
536
|
+
const endTimeMs = Date.now();
|
|
537
|
+
const durationMs = endTimeMs - state.startTimeMs;
|
|
538
|
+
|
|
539
|
+
updateMetrics(state.harnessState, { durationMs });
|
|
540
|
+
|
|
541
|
+
state.harnessState.outputs = { ...state.outputs };
|
|
542
|
+
|
|
543
|
+
for (const [nodeId, output] of Object.entries(state.outputs)) {
|
|
544
|
+
recordNodeResult(state.harnessState, nodeId, output);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const trace: HarnessExecutionTrace = {
|
|
548
|
+
entries: structuredClone(state.trace),
|
|
549
|
+
totalDurationMs: durationMs,
|
|
550
|
+
nodeCount: new Set(state.trace.map(e => e.nodeId)).size,
|
|
551
|
+
failureCount: state.trace.filter(e => e.phase === "failure").length,
|
|
552
|
+
startTimeMs: state.startTimeMs,
|
|
553
|
+
endTimeMs,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const result: CompiledHarnessResult = {
|
|
557
|
+
status: "completed",
|
|
558
|
+
terminalNodeId,
|
|
559
|
+
result: structuredClone(state.outputs[terminalNodeId]),
|
|
560
|
+
outputs: structuredClone(state.outputs),
|
|
561
|
+
trace,
|
|
562
|
+
harnessState: state.harnessState,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
if (adaptiveMetadata) {
|
|
566
|
+
result.adaptiveMetadata = adaptiveMetadata;
|
|
567
|
+
result.lineage = adaptiveMetadata.lineage;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function* buildCompletedResultWithContinuation(
|
|
574
|
+
ctx: WorkflowContext,
|
|
575
|
+
state: ExecutionState,
|
|
576
|
+
terminalNodeId: string,
|
|
577
|
+
adaptiveMetadata?: AdaptiveRuntimeMetadata,
|
|
578
|
+
): Generator<YieldItem, CompiledHarnessResult, unknown> {
|
|
579
|
+
const result = buildCompletedResult(state, terminalNodeId, adaptiveMetadata);
|
|
580
|
+
|
|
581
|
+
if (adaptiveMetadata) {
|
|
582
|
+
const replanDecision = prepareRuntimeReplan(adaptiveMetadata, state.input, result);
|
|
583
|
+
|
|
584
|
+
if (replanDecision.decision === "continue_as_new") {
|
|
585
|
+
ctx.traceInfo(`Lasso adaptive runtime: continuing as new with version ${replanDecision.nextVersion.version}`);
|
|
586
|
+
yield ctx.continueAsNew(replanDecision.nextInput);
|
|
587
|
+
} else {
|
|
588
|
+
ctx.traceInfo(`Lasso adaptive runtime: ${replanDecision.decision}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return result;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function formatUnknownError(error: unknown): string {
|
|
596
|
+
if (error instanceof Error) {
|
|
597
|
+
return error.message;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return String(error);
|
|
601
|
+
}
|