@devflow-tools/workflow-engine 0.16.17 → 0.16.19
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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/steps/builtin.d.ts.map +1 -1
- package/dist/steps/builtin.js +30 -13
- package/dist/steps/builtin.js.map +1 -1
- package/dist/types.d.ts +67 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/workflow-engine.d.ts +23 -20
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +741 -328
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +3 -3
package/dist/workflow-engine.js
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
|
+
import { resolveCapabilityRollout } from "@devflow-tools/sdk/capability-rollout";
|
|
1
2
|
import { resolveStepRef } from "@devflow-tools/sdk/step-templates";
|
|
2
3
|
import { getLogWriter } from "@devflow-tools/telemetry";
|
|
3
|
-
import { topologicalSort
|
|
4
|
+
import { topologicalSort } from "./dag.js";
|
|
4
5
|
import { StepResolver } from "./step-resolver.js";
|
|
5
6
|
import { BUILTIN_STEPS } from "./steps/builtin.js";
|
|
6
7
|
import { classifyWorkflowFailure } from "./failure-classifier.js";
|
|
7
8
|
export class WorkflowEngine {
|
|
8
9
|
definitions = new Map();
|
|
9
10
|
runs = new Map();
|
|
11
|
+
runtimes = new Map();
|
|
12
|
+
advancing = new Map();
|
|
10
13
|
resolver = new StepResolver();
|
|
11
14
|
eventListeners = new Map();
|
|
12
15
|
data;
|
|
13
16
|
options;
|
|
17
|
+
truthfulWorkflow;
|
|
14
18
|
pluginStepHandlers = new Map();
|
|
15
19
|
constructor(data, options = {}) {
|
|
16
20
|
this.data = data;
|
|
17
21
|
this.options = options;
|
|
22
|
+
this.truthfulWorkflow = options.truthfulWorkflow
|
|
23
|
+
?? resolveCapabilityRollout(options.projectRoot ?? ".").config.truthfulWorkflow;
|
|
18
24
|
for (const [id, handler] of Object.entries(BUILTIN_STEPS)) {
|
|
19
25
|
this.resolver.register(id, handler);
|
|
20
26
|
}
|
|
21
27
|
this.registerDefaults();
|
|
22
28
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* concrete `StepInstruction` objects. Non-ref steps pass through unchanged.
|
|
27
|
-
*/
|
|
29
|
+
now() {
|
|
30
|
+
return this.options.now?.() ?? Date.now();
|
|
31
|
+
}
|
|
28
32
|
resolveStepRefs(definition) {
|
|
29
33
|
return definition.steps.map((step) => {
|
|
30
|
-
if ("stepTemplate" in step)
|
|
34
|
+
if ("stepTemplate" in step)
|
|
31
35
|
return resolveStepRef(step);
|
|
32
|
-
}
|
|
33
36
|
return step;
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
|
-
// --- Registration ---
|
|
37
39
|
register(definition) {
|
|
38
40
|
this.definitions.set(definition.name, definition);
|
|
39
41
|
}
|
|
@@ -52,286 +54,516 @@ export class WorkflowEngine {
|
|
|
52
54
|
this.pluginStepHandlers.set(workflowName, handlerId);
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
async runPluginWorkflow(pluginName, templateName, input) {
|
|
56
|
-
return this.run(this.pluginWorkflowName(pluginName, templateName), input);
|
|
57
|
+
async runPluginWorkflow(pluginName, templateName, input, execution) {
|
|
58
|
+
return this.run(this.pluginWorkflowName(pluginName, templateName), input, execution);
|
|
57
59
|
}
|
|
58
60
|
pluginWorkflowName(pluginName, templateName) {
|
|
59
61
|
return `${pluginName}:${templateName}`;
|
|
60
62
|
}
|
|
61
63
|
async list() {
|
|
62
|
-
return Array.from(this.definitions.values()).map((
|
|
63
|
-
name:
|
|
64
|
-
description:
|
|
65
|
-
version:
|
|
66
|
-
stepCount:
|
|
64
|
+
return Array.from(this.definitions.values()).map((definition) => ({
|
|
65
|
+
name: definition.name,
|
|
66
|
+
description: definition.description,
|
|
67
|
+
version: definition.version,
|
|
68
|
+
stepCount: definition.steps.length,
|
|
67
69
|
}));
|
|
68
70
|
}
|
|
69
71
|
getDefinition(name) {
|
|
70
72
|
return this.definitions.get(name);
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!def)
|
|
74
|
+
async run(name, input, execution = {}) {
|
|
75
|
+
const definition = this.definitions.get(name);
|
|
76
|
+
if (!definition)
|
|
76
77
|
throw new Error(`Workflow "${name}" not registered`);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const stepStates = new Map();
|
|
85
|
-
for (const step of resolvedSteps) {
|
|
86
|
-
stepStates.set(step.id, {
|
|
87
|
-
stepId: step.id,
|
|
88
|
-
status: "pending",
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
const runState = {
|
|
92
|
-
runId,
|
|
78
|
+
const startedAt = this.now();
|
|
79
|
+
const engineRunId = `run:${startedAt}:${Math.random().toString(36).slice(2, 7)}`;
|
|
80
|
+
const telemetryRunId = execution.telemetryRunId ?? engineRunId;
|
|
81
|
+
const resolvedSteps = this.resolveStepRefs(definition);
|
|
82
|
+
const dependencyIssue = validateWorkflowDependencies(resolvedSteps);
|
|
83
|
+
const run = {
|
|
84
|
+
runId: engineRunId,
|
|
93
85
|
workflowName: name,
|
|
94
86
|
input,
|
|
95
87
|
status: "running",
|
|
96
|
-
steps:
|
|
88
|
+
steps: resolvedSteps.map((step) => ({ stepId: step.id, status: "pending" })),
|
|
97
89
|
startedAt,
|
|
98
90
|
failureCategory: null,
|
|
91
|
+
metadata: {
|
|
92
|
+
engineRunId,
|
|
93
|
+
telemetryRunId,
|
|
94
|
+
projectRoot: this.options.projectRoot ?? ".",
|
|
95
|
+
sessionId: execution.sessionId ?? this.options.sessionId ?? "default",
|
|
96
|
+
executionId: execution.executionId,
|
|
97
|
+
contextReceipt: execution.contextReceipt,
|
|
98
|
+
truthfulWorkflow: this.truthfulWorkflow,
|
|
99
|
+
},
|
|
99
100
|
};
|
|
100
|
-
this.runs.set(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
this.runs.set(engineRunId, run);
|
|
102
|
+
this.runtimes.set(engineRunId, {
|
|
103
|
+
steps: resolvedSteps,
|
|
104
|
+
outputs: new Map(),
|
|
105
|
+
loggedEnded: false,
|
|
106
|
+
});
|
|
107
|
+
getLogWriter().startRun({ name, agent: "workflow-engine" });
|
|
108
|
+
if (dependencyIssue) {
|
|
109
|
+
run.status = "failed";
|
|
110
|
+
run.finishedAt = this.now();
|
|
111
|
+
this.setRunFailure(run, classifyWorkflowFailure({
|
|
112
|
+
workflowDesignIssue: dependencyIssue,
|
|
113
|
+
}));
|
|
114
|
+
await this.persistRun(run);
|
|
115
|
+
return this.runResult(run);
|
|
116
|
+
}
|
|
117
|
+
await this.advanceRun(engineRunId);
|
|
118
|
+
return this.runResult(run);
|
|
119
|
+
}
|
|
120
|
+
async advanceRun(runId) {
|
|
121
|
+
const current = this.advancing.get(runId);
|
|
122
|
+
if (current)
|
|
123
|
+
return current;
|
|
124
|
+
const advancement = this.advanceRunInternal(runId);
|
|
125
|
+
this.advancing.set(runId, advancement);
|
|
105
126
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
await advancement;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.advancing.delete(runId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async advanceRunInternal(runId) {
|
|
134
|
+
const run = this.runs.get(runId);
|
|
135
|
+
const runtime = this.runtimes.get(runId);
|
|
136
|
+
if (!run || !runtime)
|
|
137
|
+
throw new Error(`Run "${runId}" not found`);
|
|
138
|
+
if (run.status === "completed" || run.status === "canceled")
|
|
139
|
+
return;
|
|
140
|
+
while (true) {
|
|
141
|
+
this.skipFailedDescendants(run, runtime.steps);
|
|
142
|
+
const eligible = runtime.steps.filter((definition) => {
|
|
143
|
+
const state = this.stepState(run, definition.id);
|
|
144
|
+
if (state.status !== "pending")
|
|
145
|
+
return false;
|
|
146
|
+
return (definition.depends ?? []).every((dependency) => this.stepState(run, dependency).status === "success");
|
|
147
|
+
});
|
|
148
|
+
if (eligible.length === 0)
|
|
149
|
+
break;
|
|
150
|
+
await Promise.all(eligible.map((definition) => this.executeStep(run, runtime, definition)));
|
|
151
|
+
}
|
|
152
|
+
this.recomputeRunStatus(run);
|
|
153
|
+
await this.persistRun(run);
|
|
154
|
+
this.finishLogIfTerminal(run, runtime);
|
|
155
|
+
}
|
|
156
|
+
skipFailedDescendants(run, definitions) {
|
|
157
|
+
let changed = true;
|
|
158
|
+
while (changed) {
|
|
159
|
+
changed = false;
|
|
160
|
+
for (const definition of definitions) {
|
|
161
|
+
const state = this.stepState(run, definition.id);
|
|
162
|
+
if (state.status !== "pending")
|
|
163
|
+
continue;
|
|
164
|
+
const failed = (definition.depends ?? []).filter((dependency) => {
|
|
165
|
+
const dependencyStatus = this.stepState(run, dependency).status;
|
|
166
|
+
return dependencyStatus === "failed" || dependencyStatus === "skipped";
|
|
114
167
|
});
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
168
|
+
if (failed.length === 0)
|
|
169
|
+
continue;
|
|
170
|
+
state.status = "skipped";
|
|
171
|
+
state.error = `Dependencies failed: ${failed.join(", ")}`;
|
|
172
|
+
state.finishedAt = this.now();
|
|
173
|
+
changed = true;
|
|
174
|
+
if (!run.failureCategory) {
|
|
175
|
+
this.setRunFailure(run, classifyWorkflowFailure({
|
|
176
|
+
stepId: definition.id,
|
|
177
|
+
dependencyFailed: failed,
|
|
178
|
+
}));
|
|
126
179
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
output = await this.executeSimple(wfStep, state);
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
if (stepDef.requiresUserConfirmation) {
|
|
163
|
-
state.status = "awaiting_approval";
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const handlerId = this.pluginStepHandlers.get(name);
|
|
167
|
-
const handler = handlerId ? this.resolver.resolve(handlerId) : undefined;
|
|
168
|
-
output = handler
|
|
169
|
-
? await handler.run({
|
|
170
|
-
workflowName: name,
|
|
171
|
-
stepId: stepDef.id,
|
|
172
|
-
title: stepDef.title,
|
|
173
|
-
instruction: stepDef.instruction,
|
|
174
|
-
suggestedTools: stepDef.suggestedTools ?? [],
|
|
175
|
-
expectedOutput: stepDef.output,
|
|
176
|
-
input: state.input,
|
|
177
|
-
}, this.stepContext())
|
|
178
|
-
: {
|
|
179
|
-
status: "host_action_required",
|
|
180
|
-
workflowName: name,
|
|
181
|
-
stepId: stepDef.id,
|
|
182
|
-
instruction: stepDef.instruction,
|
|
183
|
-
suggestedTools: stepDef.suggestedTools ?? [],
|
|
184
|
-
expectedOutput: stepDef.output,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
if (output) {
|
|
188
|
-
const outputRecord = output;
|
|
189
|
-
const semanticFailure = classifyWorkflowFailure({
|
|
190
|
-
stepId: stepDef.id,
|
|
191
|
-
toolName: typeof outputRecord.retrievalTool === "string"
|
|
192
|
-
? outputRecord.retrievalTool
|
|
193
|
-
: "type" in stepDef ? wfStep.run : undefined,
|
|
194
|
-
output,
|
|
195
|
-
});
|
|
196
|
-
if (semanticFailure.category === "context_insufficient"
|
|
197
|
-
|| semanticFailure.category === "knowledge_missing") {
|
|
198
|
-
if (!runState.failureCategory)
|
|
199
|
-
this.setRunFailure(runState, semanticFailure);
|
|
200
|
-
throw new Error(semanticFailure.reason);
|
|
201
|
-
}
|
|
202
|
-
if ("type" in stepDef && wfStep.outputSchema) {
|
|
203
|
-
const errors = validateOutputSchema(output, wfStep.outputSchema);
|
|
204
|
-
if (errors.length > 0) {
|
|
205
|
-
throw new WorkflowDesignError(`Step ${JSON.stringify(stepDef.id)} output contract failed: ${errors.join("; ")}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
outputs.set(stepDef.id, output);
|
|
209
|
-
state.output = output;
|
|
210
|
-
state.status = "success";
|
|
211
|
-
state.finishedAt = Date.now();
|
|
212
|
-
logWriter.endStep(stepLogId, { duration: state.finishedAt - (state.startedAt ?? state.finishedAt) });
|
|
213
|
-
}
|
|
214
|
-
this.emit(runId, {
|
|
215
|
-
type: "step-done",
|
|
216
|
-
stepId: stepDef.id,
|
|
217
|
-
duration: (state.finishedAt ?? Date.now()) - (state.startedAt ?? Date.now()),
|
|
218
|
-
output,
|
|
219
|
-
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async executeStep(run, runtime, definition) {
|
|
184
|
+
const state = this.stepState(run, definition.id);
|
|
185
|
+
if (state.status !== "pending")
|
|
186
|
+
return;
|
|
187
|
+
state.status = "running";
|
|
188
|
+
state.startedAt = this.now();
|
|
189
|
+
const logWriter = getLogWriter();
|
|
190
|
+
const stepLogId = logWriter.startStep(run.runId, { name: definition.id });
|
|
191
|
+
const declaredInputs = isWorkflowStepDef(definition) ? definition.inputs ?? {} : {};
|
|
192
|
+
state.input = Object.keys(declaredInputs).length > 0
|
|
193
|
+
? resolveInputs(declaredInputs, runtime.outputs, run.input)
|
|
194
|
+
: { ...run.input };
|
|
195
|
+
this.emit(run.runId, {
|
|
196
|
+
type: "step-start",
|
|
197
|
+
stepId: definition.id,
|
|
198
|
+
time: new Date(this.now()).toISOString(),
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
if (this.isManualStep(definition)) {
|
|
202
|
+
await this.prepareManualStep(run, definition, state);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const { output, handlerId } = await this.invokeStep(run, definition, state);
|
|
206
|
+
const hostAction = normalizeHostActionRequest(output);
|
|
207
|
+
if (hostAction) {
|
|
208
|
+
if (run.metadata.truthfulWorkflow === "shadow") {
|
|
209
|
+
state.shadowCandidate = { ...hostAction };
|
|
220
210
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
state.finishedAt = Date.now();
|
|
225
|
-
const failure = classifyWorkflowFailure({
|
|
226
|
-
stepId: stepDef.id,
|
|
227
|
-
toolName: "type" in stepDef ? wfStep.run : undefined,
|
|
228
|
-
error: err,
|
|
229
|
-
workflowDesignIssue: err instanceof WorkflowDesignError ? err.message : undefined,
|
|
230
|
-
});
|
|
231
|
-
if (!runState.failureCategory)
|
|
232
|
-
this.setRunFailure(runState, failure);
|
|
233
|
-
logWriter.toolCall({
|
|
234
|
-
tool: "workflow-step",
|
|
235
|
-
runId,
|
|
236
|
-
stepId: stepLogId,
|
|
237
|
-
input: state.input ?? {},
|
|
238
|
-
output: null,
|
|
239
|
-
duration: state.finishedAt - (state.startedAt ?? state.finishedAt),
|
|
240
|
-
success: false,
|
|
241
|
-
error: state.error,
|
|
242
|
-
});
|
|
243
|
-
this.emit(runId, { type: "step-error", stepId: stepDef.id, error: state.error });
|
|
211
|
+
if (run.metadata.truthfulWorkflow === "on") {
|
|
212
|
+
await this.requestHostAction(run, state, hostAction);
|
|
213
|
+
return;
|
|
244
214
|
}
|
|
215
|
+
}
|
|
216
|
+
const authoritativeOutput = hostAction && isLegacyBuiltinHostAction(handlerId)
|
|
217
|
+
? legacyBuiltinOutput(handlerId, state.input ?? {})
|
|
218
|
+
: output;
|
|
219
|
+
this.validateSuccessfulOutput(run, definition, authoritativeOutput, handlerId);
|
|
220
|
+
runtime.outputs.set(definition.id, authoritativeOutput);
|
|
221
|
+
state.output = authoritativeOutput;
|
|
222
|
+
state.status = "success";
|
|
223
|
+
state.finishedAt = this.now();
|
|
224
|
+
logWriter.endStep(stepLogId, {
|
|
225
|
+
duration: state.finishedAt - (state.startedAt ?? state.finishedAt),
|
|
226
|
+
});
|
|
227
|
+
this.emit(run.runId, {
|
|
228
|
+
type: "step-done",
|
|
229
|
+
stepId: definition.id,
|
|
230
|
+
duration: state.finishedAt - (state.startedAt ?? state.finishedAt),
|
|
231
|
+
output: authoritativeOutput,
|
|
245
232
|
});
|
|
246
233
|
}
|
|
247
234
|
catch (error) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.
|
|
235
|
+
state.status = "failed";
|
|
236
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
237
|
+
state.finishedAt = this.now();
|
|
238
|
+
const failure = classifyWorkflowFailure({
|
|
239
|
+
stepId: definition.id,
|
|
240
|
+
toolName: isWorkflowStepDef(definition) ? definition.run : undefined,
|
|
251
241
|
error,
|
|
252
242
|
workflowDesignIssue: error instanceof WorkflowDesignError ? error.message : undefined,
|
|
253
|
-
}));
|
|
254
|
-
await this.persistRun(runState);
|
|
255
|
-
return this.runResult(runState);
|
|
256
|
-
}
|
|
257
|
-
// Check if any manual steps are awaiting approval
|
|
258
|
-
const hasAwaitingApproval = stepStates.has(Array.from(stepStates.values()).find((s) => s.status === "awaiting_approval")?.stepId ?? "_none");
|
|
259
|
-
// Wait — re-check correctly
|
|
260
|
-
const awaiting = Array.from(stepStates.values()).some((s) => s.status === "awaiting_approval");
|
|
261
|
-
if (!awaiting) {
|
|
262
|
-
const allSuccess = Array.from(stepStates.values()).every((s) => s.status === "success" || s.status === "skipped");
|
|
263
|
-
runState.status = allSuccess ? "completed" : "failed";
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
runState.status = "running"; // Stay running until manual steps are approved
|
|
267
|
-
}
|
|
268
|
-
runState.finishedAt = runState.status !== "running" ? Date.now() : undefined;
|
|
269
|
-
runState.steps = Array.from(stepStates.values());
|
|
270
|
-
await this.persistRun(runState);
|
|
271
|
-
const completed = Array.from(stepStates.values()).filter(s => s.status === "success" || s.status === "failed");
|
|
272
|
-
logWriter.endRun(runId, {
|
|
273
|
-
steps: stepStates.size,
|
|
274
|
-
tools: completed.length,
|
|
275
|
-
errors: Array.from(stepStates.values()).filter(s => s.status === "failed").length,
|
|
276
|
-
tokens: 0,
|
|
277
|
-
});
|
|
278
|
-
return this.runResult(runState);
|
|
279
|
-
}
|
|
280
|
-
async executeSimple(step, state) {
|
|
281
|
-
const handler = step.run ? this.resolver.resolve(step.run) : undefined;
|
|
282
|
-
if (handler) {
|
|
283
|
-
return await handler.run(state.input ?? {}, {
|
|
284
|
-
...this.stepContext(),
|
|
285
243
|
});
|
|
244
|
+
if (!run.failureCategory)
|
|
245
|
+
this.setRunFailure(run, failure);
|
|
246
|
+
logWriter.toolCall({
|
|
247
|
+
tool: "workflow-step",
|
|
248
|
+
runId: run.runId,
|
|
249
|
+
stepId: stepLogId,
|
|
250
|
+
input: state.input ?? {},
|
|
251
|
+
output: null,
|
|
252
|
+
duration: state.finishedAt - (state.startedAt ?? state.finishedAt),
|
|
253
|
+
success: false,
|
|
254
|
+
error: state.error,
|
|
255
|
+
});
|
|
256
|
+
this.emit(run.runId, { type: "step-error", stepId: definition.id, error: state.error });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
isManualStep(definition) {
|
|
260
|
+
return isWorkflowStepDef(definition)
|
|
261
|
+
? definition.type === "manual"
|
|
262
|
+
: definition.requiresUserConfirmation === true;
|
|
263
|
+
}
|
|
264
|
+
async prepareManualStep(run, definition, state) {
|
|
265
|
+
if (isWorkflowStepDef(definition) && definition.run) {
|
|
266
|
+
const handler = this.resolver.resolve(definition.run);
|
|
267
|
+
if (handler) {
|
|
268
|
+
const candidate = await handler.run(state.input ?? {}, this.stepContext(run));
|
|
269
|
+
let readiness = candidate;
|
|
270
|
+
if (definition.run === "builtin:commit") {
|
|
271
|
+
if (run.metadata.truthfulWorkflow === "shadow")
|
|
272
|
+
state.shadowCandidate = candidate;
|
|
273
|
+
if (run.metadata.truthfulWorkflow !== "on") {
|
|
274
|
+
readiness = legacyBuiltinOutput(definition.run, state.input ?? {});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (readiness.ready === false) {
|
|
278
|
+
const reason = typeof readiness.reason === "string"
|
|
279
|
+
? readiness.reason
|
|
280
|
+
: "commit_not_ready";
|
|
281
|
+
throw new Error(`Manual step is not ready: ${reason}`);
|
|
282
|
+
}
|
|
283
|
+
if (run.metadata.truthfulWorkflow === "on")
|
|
284
|
+
state.output = candidate;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
state.status = "awaiting_approval";
|
|
288
|
+
}
|
|
289
|
+
async invokeStep(run, definition, state) {
|
|
290
|
+
if (isWorkflowStepDef(definition)) {
|
|
291
|
+
const handlerId = definition.run;
|
|
292
|
+
if (definition.type === "stateful") {
|
|
293
|
+
return {
|
|
294
|
+
output: await this.executeStateful(definition, state, run),
|
|
295
|
+
handlerId,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const handler = handlerId ? this.resolver.resolve(handlerId) : undefined;
|
|
299
|
+
return {
|
|
300
|
+
output: handler
|
|
301
|
+
? await handler.run(state.input ?? {}, this.stepContext(run))
|
|
302
|
+
: { ok: true },
|
|
303
|
+
handlerId,
|
|
304
|
+
};
|
|
286
305
|
}
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
const handlerId = this.pluginStepHandlers.get(run.workflowName);
|
|
307
|
+
const handler = handlerId ? this.resolver.resolve(handlerId) : undefined;
|
|
308
|
+
const output = handler
|
|
309
|
+
? await handler.run({
|
|
310
|
+
workflowName: run.workflowName,
|
|
311
|
+
stepId: definition.id,
|
|
312
|
+
title: definition.title,
|
|
313
|
+
instruction: definition.instruction,
|
|
314
|
+
suggestedTools: definition.suggestedTools ?? [],
|
|
315
|
+
expectedOutput: definition.output,
|
|
316
|
+
input: state.input,
|
|
317
|
+
}, this.stepContext(run))
|
|
318
|
+
: {
|
|
319
|
+
status: "host_action_required",
|
|
320
|
+
workflowName: run.workflowName,
|
|
321
|
+
stepId: definition.id,
|
|
322
|
+
instruction: definition.instruction,
|
|
323
|
+
suggestedTools: definition.suggestedTools ?? [],
|
|
324
|
+
expectedOutput: definition.output,
|
|
325
|
+
};
|
|
326
|
+
return { output, handlerId };
|
|
289
327
|
}
|
|
290
|
-
async executeStateful(
|
|
291
|
-
|
|
292
|
-
const
|
|
328
|
+
async executeStateful(definition, state, run) {
|
|
329
|
+
const maxRetries = definition.retry ?? 3;
|
|
330
|
+
const handler = definition.run ? this.resolver.resolve(definition.run) : undefined;
|
|
293
331
|
let attempts = 0;
|
|
294
332
|
let lastOutput = {};
|
|
295
333
|
while (attempts < maxRetries) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
lastOutput = await handler.run(state.input ?? {}, {
|
|
300
|
-
...this.stepContext(),
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
// Validate — default to pass if no output validator
|
|
304
|
-
const isValid = lastOutput?.valid !== false;
|
|
305
|
-
if (isValid) {
|
|
334
|
+
if (handler)
|
|
335
|
+
lastOutput = await handler.run(state.input ?? {}, this.stepContext(run));
|
|
336
|
+
if (normalizeHostActionRequest(lastOutput) || lastOutput.valid !== false)
|
|
306
337
|
return lastOutput;
|
|
307
|
-
}
|
|
308
338
|
attempts++;
|
|
309
339
|
}
|
|
310
|
-
// All retries exhausted — return best effort
|
|
311
340
|
return { ...lastOutput, validated: false, attempts };
|
|
312
341
|
}
|
|
313
|
-
|
|
342
|
+
validateSuccessfulOutput(run, definition, output, handlerId) {
|
|
343
|
+
const semanticFailure = classifyWorkflowFailure({
|
|
344
|
+
stepId: definition.id,
|
|
345
|
+
toolName: typeof output.retrievalTool === "string" ? output.retrievalTool : handlerId,
|
|
346
|
+
output,
|
|
347
|
+
});
|
|
348
|
+
if (semanticFailure.category === "context_insufficient"
|
|
349
|
+
|| semanticFailure.category === "knowledge_missing") {
|
|
350
|
+
if (!run.failureCategory)
|
|
351
|
+
this.setRunFailure(run, semanticFailure);
|
|
352
|
+
throw new Error(semanticFailure.reason);
|
|
353
|
+
}
|
|
354
|
+
if (isWorkflowStepDef(definition) && definition.outputSchema) {
|
|
355
|
+
const errors = validateOutputSchema(output, definition.outputSchema);
|
|
356
|
+
if (errors.length > 0) {
|
|
357
|
+
throw new WorkflowDesignError(`Step ${JSON.stringify(definition.id)} output contract failed: ${errors.join("; ")}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async requestHostAction(run, state, request) {
|
|
362
|
+
const store = this.options.hostActionStore;
|
|
363
|
+
if (!store)
|
|
364
|
+
throw new Error("Truthful workflow requires a HostActionStore");
|
|
365
|
+
const actionId = `action:${run.runId}:${state.stepId}`;
|
|
366
|
+
const record = await store.requestHostAction({
|
|
367
|
+
actionId,
|
|
368
|
+
runId: run.metadata.telemetryRunId,
|
|
369
|
+
engineRunId: run.runId,
|
|
370
|
+
stepId: state.stepId,
|
|
371
|
+
projectRoot: run.metadata.projectRoot,
|
|
372
|
+
sessionId: run.metadata.sessionId,
|
|
373
|
+
executionId: run.metadata.executionId,
|
|
374
|
+
contextReceipt: run.metadata.contextReceipt,
|
|
375
|
+
});
|
|
376
|
+
this.validateRequestedRecord(run, state, actionId, record);
|
|
377
|
+
const priorActionIds = stringArray(state.input?.actionIds);
|
|
378
|
+
const priorVerifiedActionIds = stringArray(state.input?.verifiedActionIds);
|
|
379
|
+
state.actionId = record.actionId;
|
|
380
|
+
state.actionKind = request.actionKind;
|
|
381
|
+
state.actionIds = uniqueStrings([...priorActionIds, record.actionId]);
|
|
382
|
+
state.verifiedActionIds = priorVerifiedActionIds;
|
|
383
|
+
state.output = {
|
|
384
|
+
...request,
|
|
385
|
+
actionId: record.actionId,
|
|
386
|
+
actionIds: state.actionIds,
|
|
387
|
+
verifiedActionIds: state.verifiedActionIds,
|
|
388
|
+
};
|
|
389
|
+
state.status = "waiting_host";
|
|
390
|
+
}
|
|
391
|
+
validateRequestedRecord(run, state, actionId, record) {
|
|
392
|
+
if (record.actionId !== actionId
|
|
393
|
+
|| record.runId !== run.metadata.telemetryRunId
|
|
394
|
+
|| record.engineRunId !== run.runId
|
|
395
|
+
|| record.stepId !== state.stepId
|
|
396
|
+
|| record.projectRoot !== run.metadata.projectRoot
|
|
397
|
+
|| record.sessionId !== optionalString(run.metadata.sessionId)
|
|
398
|
+
|| record.executionId !== run.metadata.executionId
|
|
399
|
+
|| record.contextReceipt !== run.metadata.contextReceipt
|
|
400
|
+
|| record.state !== "waiting") {
|
|
401
|
+
throw new Error("HostActionStore returned a mismatched action record");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async resumeHostAction(record) {
|
|
405
|
+
if (record.state === "waiting" || record.state === "running") {
|
|
406
|
+
throw new Error(`Host action record is not resumable (state: ${record.state})`);
|
|
407
|
+
}
|
|
408
|
+
const run = this.runs.get(record.engineRunId);
|
|
409
|
+
if (!run)
|
|
410
|
+
throw new Error(`Engine run "${record.engineRunId}" not found`);
|
|
411
|
+
if (run.metadata.truthfulWorkflow !== "on") {
|
|
412
|
+
throw new Error("Host action resume requires truthful workflow mode on");
|
|
413
|
+
}
|
|
414
|
+
if (record.projectRoot !== run.metadata.projectRoot)
|
|
415
|
+
throw new Error("Host action project mismatch");
|
|
416
|
+
if (record.runId !== run.metadata.telemetryRunId)
|
|
417
|
+
throw new Error("Host action telemetry run mismatch");
|
|
418
|
+
if (record.engineRunId !== run.runId)
|
|
419
|
+
throw new Error("Host action engine run mismatch");
|
|
420
|
+
const state = run.steps.find((step) => step.stepId === record.stepId);
|
|
421
|
+
if (!state)
|
|
422
|
+
throw new Error(`Host action step "${record.stepId}" not found`);
|
|
423
|
+
if (!state.actionId || state.actionId !== record.actionId)
|
|
424
|
+
throw new Error("Host action ID mismatch");
|
|
425
|
+
if (record.sessionId !== run.metadata.sessionId)
|
|
426
|
+
throw new Error("Host action session mismatch");
|
|
427
|
+
if (record.executionId !== run.metadata.executionId)
|
|
428
|
+
throw new Error("Host action execution mismatch");
|
|
429
|
+
if (record.contextReceipt !== run.metadata.contextReceipt)
|
|
430
|
+
throw new Error("Host action context receipt mismatch");
|
|
431
|
+
const persisted = await this.options.hostActionStore?.getHostAction(record.projectRoot, record.actionId);
|
|
432
|
+
if (!persisted || !sameHostActionRecord(persisted, record)) {
|
|
433
|
+
throw new Error("Host action record does not match persisted evidence");
|
|
434
|
+
}
|
|
435
|
+
if (record.state === "reported") {
|
|
436
|
+
if (state.status === "verifying")
|
|
437
|
+
return this.publicRun(run);
|
|
438
|
+
if (state.status !== "waiting_host") {
|
|
439
|
+
throw new Error(`Host action step is not waiting (status: ${state.status})`);
|
|
440
|
+
}
|
|
441
|
+
state.status = "verifying";
|
|
442
|
+
state.output = { ...state.output, report: record.report, state: "reported" };
|
|
443
|
+
run.status = "waiting_host";
|
|
444
|
+
await this.persistRun(run);
|
|
445
|
+
return this.publicRun(run);
|
|
446
|
+
}
|
|
447
|
+
if (record.state === "verified") {
|
|
448
|
+
if (!record.evidenceHash)
|
|
449
|
+
throw new Error("Verified host action requires an evidence hash");
|
|
450
|
+
if (state.status === "success") {
|
|
451
|
+
if (state.evidenceHash === record.evidenceHash)
|
|
452
|
+
return this.publicRun(run);
|
|
453
|
+
throw new Error("Host action terminal evidence mismatch");
|
|
454
|
+
}
|
|
455
|
+
if (state.status !== "waiting_host" && state.status !== "verifying") {
|
|
456
|
+
throw new Error(`Host action step is not resumable (status: ${state.status})`);
|
|
457
|
+
}
|
|
458
|
+
const runtime = this.runtimes.get(run.runId);
|
|
459
|
+
if (!runtime)
|
|
460
|
+
throw new Error(`Run runtime "${run.runId}" not found`);
|
|
461
|
+
const priorActionIds = stringArray(state.input?.actionIds);
|
|
462
|
+
const priorVerifiedActionIds = stringArray(state.input?.verifiedActionIds);
|
|
463
|
+
state.actionIds = uniqueStrings([...priorActionIds, record.actionId]);
|
|
464
|
+
state.verifiedActionIds = uniqueStrings([...priorVerifiedActionIds, ...state.actionIds]);
|
|
465
|
+
state.evidenceHash = record.evidenceHash;
|
|
466
|
+
state.output = {
|
|
467
|
+
...record.report,
|
|
468
|
+
state: "verified",
|
|
469
|
+
actionId: record.actionId,
|
|
470
|
+
actionIds: state.actionIds,
|
|
471
|
+
verifiedActionIds: state.verifiedActionIds,
|
|
472
|
+
evidenceHash: record.evidenceHash,
|
|
473
|
+
};
|
|
474
|
+
state.status = "success";
|
|
475
|
+
state.finishedAt = record.finishedAt ?? this.now();
|
|
476
|
+
runtime.outputs.set(state.stepId, state.output);
|
|
477
|
+
await this.advanceRun(run.runId);
|
|
478
|
+
return this.publicRun(run);
|
|
479
|
+
}
|
|
480
|
+
if (state.status === "failed" && state.output?.state === record.state) {
|
|
481
|
+
return this.publicRun(run);
|
|
482
|
+
}
|
|
483
|
+
if (state.status !== "waiting_host" && state.status !== "verifying") {
|
|
484
|
+
throw new Error(`Host action step is not resumable (status: ${state.status})`);
|
|
485
|
+
}
|
|
486
|
+
state.status = "failed";
|
|
487
|
+
state.error = `Host action ${record.state}`;
|
|
488
|
+
state.output = {
|
|
489
|
+
...record.report,
|
|
490
|
+
state: record.state,
|
|
491
|
+
actionId: record.actionId,
|
|
492
|
+
evidenceHash: record.evidenceHash,
|
|
493
|
+
};
|
|
494
|
+
state.evidenceHash = record.evidenceHash;
|
|
495
|
+
state.finishedAt = record.finishedAt ?? this.now();
|
|
496
|
+
run.status = "failed";
|
|
497
|
+
run.finishedAt = state.finishedAt;
|
|
498
|
+
this.setRunFailure(run, {
|
|
499
|
+
category: "tool_error",
|
|
500
|
+
reason: state.error,
|
|
501
|
+
evidence: { actionId: record.actionId, state: record.state },
|
|
502
|
+
});
|
|
503
|
+
await this.advanceRun(run.runId);
|
|
504
|
+
return this.publicRun(run);
|
|
505
|
+
}
|
|
506
|
+
async reconcileTimedOutActions(now = this.now()) {
|
|
507
|
+
const reconciled = [];
|
|
508
|
+
for (const run of this.runs.values()) {
|
|
509
|
+
if (run.metadata.truthfulWorkflow !== "on")
|
|
510
|
+
continue;
|
|
511
|
+
const runtime = this.runtimes.get(run.runId);
|
|
512
|
+
if (!runtime)
|
|
513
|
+
continue;
|
|
514
|
+
for (const state of run.steps) {
|
|
515
|
+
if (state.status !== "waiting_host" && state.status !== "verifying")
|
|
516
|
+
continue;
|
|
517
|
+
const definition = runtime.steps.find((step) => step.id === state.stepId);
|
|
518
|
+
if (!definition || state.startedAt === undefined || now - state.startedAt < stepTimeout(definition)) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (!state.actionId || !this.options.hostActionStore) {
|
|
522
|
+
throw new Error("Timed out host action has no durable store identity");
|
|
523
|
+
}
|
|
524
|
+
const existing = await this.options.hostActionStore.getHostAction(run.metadata.projectRoot, state.actionId);
|
|
525
|
+
if (!existing)
|
|
526
|
+
throw new Error(`Host action "${state.actionId}" not found during timeout`);
|
|
527
|
+
const terminal = existing.state === "verified"
|
|
528
|
+
|| existing.state === "failed"
|
|
529
|
+
|| existing.state === "cancelled"
|
|
530
|
+
|| existing.state === "degraded";
|
|
531
|
+
const record = terminal
|
|
532
|
+
? existing
|
|
533
|
+
: await this.options.hostActionStore.failHostAction({
|
|
534
|
+
actionId: state.actionId,
|
|
535
|
+
projectRoot: run.metadata.projectRoot,
|
|
536
|
+
outcome: "degraded",
|
|
537
|
+
report: { reason: "step_timeout", timedOutAt: now },
|
|
538
|
+
});
|
|
539
|
+
await this.resumeHostAction(record);
|
|
540
|
+
reconciled.push(state.actionId);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return reconciled;
|
|
544
|
+
}
|
|
314
545
|
async approve(runId, stepId) {
|
|
315
546
|
const run = this.runs.get(runId);
|
|
316
547
|
if (!run)
|
|
317
548
|
throw new Error(`Run "${runId}" not found`);
|
|
318
|
-
const step = run.steps.find((
|
|
549
|
+
const step = run.steps.find((candidate) => candidate.stepId === stepId);
|
|
319
550
|
if (!step)
|
|
320
551
|
throw new Error(`Step "${stepId}" not found in run "${runId}"`);
|
|
321
552
|
if (step.status !== "awaiting_approval") {
|
|
322
553
|
throw new Error(`Step "${stepId}" is not awaiting approval (status: ${step.status})`);
|
|
323
554
|
}
|
|
324
555
|
step.status = "success";
|
|
325
|
-
step.finishedAt =
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
556
|
+
step.finishedAt = this.now();
|
|
557
|
+
const runtime = this.runtimes.get(runId);
|
|
558
|
+
if (runtime && step.output)
|
|
559
|
+
runtime.outputs.set(stepId, step.output);
|
|
560
|
+
await this.advanceRun(runId);
|
|
329
561
|
}
|
|
330
562
|
async reject(runId, stepId, rejection) {
|
|
331
563
|
const run = this.runs.get(runId);
|
|
332
564
|
if (!run)
|
|
333
565
|
throw new Error(`Run "${runId}" not found`);
|
|
334
|
-
const step = run.steps.find((
|
|
566
|
+
const step = run.steps.find((candidate) => candidate.stepId === stepId);
|
|
335
567
|
if (!step)
|
|
336
568
|
throw new Error(`Step "${stepId}" not found in run "${runId}"`);
|
|
337
569
|
if (step.status !== "awaiting_approval") {
|
|
@@ -342,45 +574,23 @@ export class WorkflowEngine {
|
|
|
342
574
|
: rejection;
|
|
343
575
|
step.status = "failed";
|
|
344
576
|
step.error = normalized.reason;
|
|
345
|
-
step.finishedAt =
|
|
577
|
+
step.finishedAt = this.now();
|
|
346
578
|
run.status = "failed";
|
|
347
|
-
run.finishedAt =
|
|
579
|
+
run.finishedAt = this.now();
|
|
348
580
|
run.failureCategory = normalized.category;
|
|
349
581
|
run.failureReason = normalized.reason;
|
|
350
582
|
run.failureEvidence = normalized.evidence;
|
|
351
|
-
await this.
|
|
583
|
+
await this.advanceRun(runId);
|
|
352
584
|
}
|
|
353
|
-
maybeCompleteRun(run) {
|
|
354
|
-
const allDone = run.steps.every((s) => s.status === "success" || s.status === "failed" || s.status === "skipped");
|
|
355
|
-
if (allDone) {
|
|
356
|
-
const allSuccess = run.steps.every((s) => s.status === "success" || s.status === "skipped");
|
|
357
|
-
run.status = allSuccess ? "completed" : "failed";
|
|
358
|
-
run.finishedAt = Date.now();
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
// --- Plan Rendering ---
|
|
362
|
-
/**
|
|
363
|
-
* Render a markdown execution plan from a telemetry run and workflow template.
|
|
364
|
-
*
|
|
365
|
-
* The output is a human-readable plan that Claude Code (or another agent)
|
|
366
|
-
* can follow step-by-step. Each step includes the instruction, suggested
|
|
367
|
-
* tools, expected output, and a `devflow_report_step` call template.
|
|
368
|
-
*
|
|
369
|
-
* Steps may be in the new `StepInstruction` format (with `instruction`,
|
|
370
|
-
* `title`, `suggestedTools`) or the legacy `WorkflowStepDef` format (with
|
|
371
|
-
* only `description`). Both are handled gracefully.
|
|
372
|
-
*/
|
|
373
585
|
renderPlan(run, template) {
|
|
374
|
-
const
|
|
375
|
-
const steps = resolvedSteps
|
|
586
|
+
const steps = this.resolveStepRefs(template)
|
|
376
587
|
.map((step, index) => {
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
? stepAny.suggestedTools.join(", ")
|
|
588
|
+
const title = isWorkflowStepDef(step) ? step.description : step.title;
|
|
589
|
+
const instruction = isWorkflowStepDef(step) ? step.description : step.instruction;
|
|
590
|
+
const suggestedTools = !isWorkflowStepDef(step) && Array.isArray(step.suggestedTools)
|
|
591
|
+
? step.suggestedTools.join(", ")
|
|
382
592
|
: "N/A";
|
|
383
|
-
const output =
|
|
593
|
+
const output = !isWorkflowStepDef(step) ? step.output ?? "N/A" : "N/A";
|
|
384
594
|
return `---
|
|
385
595
|
## Step ${index + 1}: ${title} (\`${step.id}\`)
|
|
386
596
|
|
|
@@ -412,51 +622,59 @@ Follow these steps strictly in order. After each step, call \`devflow_report_ste
|
|
|
412
622
|
${steps}
|
|
413
623
|
`;
|
|
414
624
|
}
|
|
415
|
-
// --- Queries ---
|
|
416
625
|
getRun(runId) {
|
|
417
626
|
const run = this.runs.get(runId);
|
|
418
|
-
|
|
419
|
-
|
|
627
|
+
return run ? this.publicRun(run) : undefined;
|
|
628
|
+
}
|
|
629
|
+
async listRuns() {
|
|
630
|
+
return Array.from(this.runs.values()).map((run) => this.publicRun(run, true));
|
|
631
|
+
}
|
|
632
|
+
publicRun(run, compact = false) {
|
|
633
|
+
const actionIds = uniqueStrings(run.steps.flatMap((step) => step.actionIds ?? []));
|
|
420
634
|
return {
|
|
421
635
|
runId: run.runId,
|
|
422
636
|
type: run.workflowName,
|
|
423
|
-
status: run.status,
|
|
637
|
+
status: publicRunStatus(run.status),
|
|
424
638
|
startedAt: run.startedAt,
|
|
425
639
|
finishedAt: run.finishedAt,
|
|
426
|
-
steps: run.steps.map((
|
|
427
|
-
stepId:
|
|
428
|
-
status:
|
|
429
|
-
input:
|
|
430
|
-
output:
|
|
431
|
-
duration:
|
|
432
|
-
|
|
640
|
+
steps: run.steps.map((step) => ({
|
|
641
|
+
stepId: step.stepId,
|
|
642
|
+
status: publicStepStatus(step.status),
|
|
643
|
+
input: compact ? undefined : step.input,
|
|
644
|
+
output: compact ? undefined : step.output,
|
|
645
|
+
duration: step.finishedAt !== undefined && step.startedAt !== undefined
|
|
646
|
+
? step.finishedAt - step.startedAt
|
|
647
|
+
: undefined,
|
|
648
|
+
error: step.error,
|
|
649
|
+
metadata: {
|
|
650
|
+
canonicalStatus: step.status,
|
|
651
|
+
actionId: step.actionId,
|
|
652
|
+
actionKind: step.actionKind,
|
|
653
|
+
actionIds: step.actionIds,
|
|
654
|
+
verifiedActionIds: step.verifiedActionIds,
|
|
655
|
+
evidenceHash: step.evidenceHash,
|
|
656
|
+
shadowCandidate: step.shadowCandidate,
|
|
657
|
+
},
|
|
433
658
|
})),
|
|
434
659
|
failureCategory: run.failureCategory,
|
|
435
660
|
failureReason: run.failureReason,
|
|
436
661
|
failureEvidence: run.failureEvidence,
|
|
662
|
+
metadata: {
|
|
663
|
+
canonicalStatus: run.status,
|
|
664
|
+
engineRunId: run.metadata.engineRunId,
|
|
665
|
+
telemetryRunId: run.metadata.telemetryRunId,
|
|
666
|
+
projectRoot: run.metadata.projectRoot,
|
|
667
|
+
sessionId: run.metadata.sessionId,
|
|
668
|
+
executionId: run.metadata.executionId,
|
|
669
|
+
contextReceipt: run.metadata.contextReceipt,
|
|
670
|
+
actionIds,
|
|
671
|
+
},
|
|
437
672
|
};
|
|
438
673
|
}
|
|
439
|
-
|
|
440
|
-
return Array.from(this.runs.values()).map((r) => ({
|
|
441
|
-
runId: r.runId,
|
|
442
|
-
type: r.workflowName,
|
|
443
|
-
status: r.status,
|
|
444
|
-
startedAt: r.startedAt,
|
|
445
|
-
finishedAt: r.finishedAt,
|
|
446
|
-
steps: r.steps.map((s) => ({
|
|
447
|
-
stepId: s.stepId,
|
|
448
|
-
status: s.status,
|
|
449
|
-
error: s.error,
|
|
450
|
-
})),
|
|
451
|
-
failureCategory: r.failureCategory,
|
|
452
|
-
failureReason: r.failureReason,
|
|
453
|
-
failureEvidence: r.failureEvidence,
|
|
454
|
-
}));
|
|
455
|
-
}
|
|
456
|
-
stepContext() {
|
|
674
|
+
stepContext(run) {
|
|
457
675
|
return {
|
|
458
|
-
sessionId:
|
|
459
|
-
projectRoot:
|
|
676
|
+
sessionId: run.metadata.sessionId,
|
|
677
|
+
projectRoot: run.metadata.projectRoot,
|
|
460
678
|
context: this.options.context,
|
|
461
679
|
memory: this.options.memory,
|
|
462
680
|
knowledge: this.options.knowledge,
|
|
@@ -464,24 +682,46 @@ ${steps}
|
|
|
464
682
|
executeInstruction: this.options.executeInstruction,
|
|
465
683
|
};
|
|
466
684
|
}
|
|
685
|
+
stepState(run, stepId) {
|
|
686
|
+
const state = run.steps.find((step) => step.stepId === stepId);
|
|
687
|
+
if (!state)
|
|
688
|
+
throw new WorkflowDesignError(`Step ${JSON.stringify(stepId)} not found`);
|
|
689
|
+
return state;
|
|
690
|
+
}
|
|
691
|
+
recomputeRunStatus(run) {
|
|
692
|
+
const hasFailed = run.steps.some((step) => step.status === "failed");
|
|
693
|
+
const allTerminal = run.steps.every((step) => step.status === "success" || step.status === "skipped");
|
|
694
|
+
if (hasFailed)
|
|
695
|
+
run.status = "failed";
|
|
696
|
+
else if (allTerminal)
|
|
697
|
+
run.status = "completed";
|
|
698
|
+
else if (run.steps.some((step) => step.status === "waiting_host" || step.status === "verifying")) {
|
|
699
|
+
run.status = "waiting_host";
|
|
700
|
+
}
|
|
701
|
+
else
|
|
702
|
+
run.status = "running";
|
|
703
|
+
run.finishedAt = run.status === "completed" || run.status === "failed" ? this.now() : undefined;
|
|
704
|
+
}
|
|
467
705
|
setRunFailure(run, failure) {
|
|
468
706
|
run.failureCategory = failure.category;
|
|
469
707
|
run.failureReason = failure.reason;
|
|
470
708
|
run.failureEvidence = failure.evidence;
|
|
471
709
|
}
|
|
472
710
|
runResult(run) {
|
|
711
|
+
const publicRun = this.publicRun(run, true);
|
|
473
712
|
return {
|
|
474
713
|
runId: run.runId,
|
|
475
|
-
status:
|
|
476
|
-
duration: (run.finishedAt ??
|
|
714
|
+
status: publicRun.status,
|
|
715
|
+
duration: (run.finishedAt ?? this.now()) - run.startedAt,
|
|
477
716
|
failureCategory: run.failureCategory,
|
|
478
717
|
failureReason: run.failureReason,
|
|
718
|
+
metadata: publicRun.metadata,
|
|
479
719
|
};
|
|
480
720
|
}
|
|
481
721
|
async persistRun(run) {
|
|
482
722
|
if (!this.data)
|
|
483
723
|
return;
|
|
484
|
-
|
|
724
|
+
const persist = async () => {
|
|
485
725
|
await this.data.sqlExecute(`INSERT OR REPLACE INTO devflow_workflow_runs
|
|
486
726
|
(run_id, type, status, input, steps, started_at, finished_at,
|
|
487
727
|
failure_category, failure_reason, failure_evidence)
|
|
@@ -497,23 +737,81 @@ ${steps}
|
|
|
497
737
|
run.failureReason ?? null,
|
|
498
738
|
run.failureEvidence ? JSON.stringify(run.failureEvidence) : null,
|
|
499
739
|
]);
|
|
740
|
+
await this.data.kvSet(`workflow:run-metadata:${run.runId}`, {
|
|
741
|
+
...run.metadata,
|
|
742
|
+
canonicalStatus: run.status,
|
|
743
|
+
actionIds: uniqueStrings(run.steps.flatMap((step) => step.actionIds ?? [])),
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
if (run.metadata.truthfulWorkflow === "on") {
|
|
747
|
+
await persist();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
await persist();
|
|
500
752
|
}
|
|
501
753
|
catch {
|
|
502
|
-
//
|
|
754
|
+
// Legacy off/shadow embedders may not have the current persistence schema.
|
|
503
755
|
}
|
|
504
756
|
}
|
|
505
|
-
|
|
757
|
+
finishLogIfTerminal(run, runtime) {
|
|
758
|
+
if (runtime.loggedEnded || (run.status !== "completed" && run.status !== "failed"))
|
|
759
|
+
return;
|
|
760
|
+
runtime.loggedEnded = true;
|
|
761
|
+
getLogWriter().endRun(run.runId, {
|
|
762
|
+
steps: run.steps.length,
|
|
763
|
+
tools: run.steps.filter((step) => step.status === "success" || step.status === "failed").length,
|
|
764
|
+
errors: run.steps.filter((step) => step.status === "failed").length,
|
|
765
|
+
tokens: 0,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
506
768
|
onEvent(runId, listener) {
|
|
507
769
|
if (!this.eventListeners.has(runId))
|
|
508
770
|
this.eventListeners.set(runId, []);
|
|
509
771
|
this.eventListeners.get(runId).push(listener);
|
|
510
772
|
}
|
|
511
773
|
emit(runId, event) {
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
fn(event);
|
|
774
|
+
for (const listener of this.eventListeners.get(runId) ?? [])
|
|
775
|
+
listener(event);
|
|
515
776
|
}
|
|
516
777
|
registerDefaults() {
|
|
778
|
+
const actions = (executeTimeout) => [
|
|
779
|
+
{
|
|
780
|
+
id: "execute",
|
|
781
|
+
description: "Execute changes",
|
|
782
|
+
type: "stateful",
|
|
783
|
+
run: "builtin:execute",
|
|
784
|
+
depends: ["plan"],
|
|
785
|
+
inputs: { plan: "${plan.plan}" },
|
|
786
|
+
retry: 3,
|
|
787
|
+
timeout: executeTimeout,
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
id: "verify",
|
|
791
|
+
description: "Verify changes",
|
|
792
|
+
type: "simple",
|
|
793
|
+
run: "builtin:verify",
|
|
794
|
+
depends: ["execute"],
|
|
795
|
+
inputs: {
|
|
796
|
+
actionIds: "${execute.actionIds}",
|
|
797
|
+
verifiedActionIds: "${execute.verifiedActionIds}",
|
|
798
|
+
},
|
|
799
|
+
timeout: 60000,
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
id: "commit",
|
|
803
|
+
description: "Generate commit message",
|
|
804
|
+
type: "manual",
|
|
805
|
+
run: "builtin:commit",
|
|
806
|
+
depends: ["verify"],
|
|
807
|
+
inputs: {
|
|
808
|
+
summary: "${trigger.input}",
|
|
809
|
+
actionIds: "${verify.actionIds}",
|
|
810
|
+
verifiedActionIds: "${verify.verifiedActionIds}",
|
|
811
|
+
},
|
|
812
|
+
timeout: 30000,
|
|
813
|
+
},
|
|
814
|
+
];
|
|
517
815
|
this.register({
|
|
518
816
|
name: "bugfix",
|
|
519
817
|
description: "Standard bug fix workflow: analyze → plan → execute → verify → commit",
|
|
@@ -522,9 +820,7 @@ ${steps}
|
|
|
522
820
|
{ id: "analyze", description: "Analyze bug description", type: "simple", run: "builtin:analyze", timeout: 30000 },
|
|
523
821
|
{ id: "retrieve", description: "Retrieve relevant context", type: "simple", run: "builtin:retrieve", depends: ["analyze"], timeout: 30000 },
|
|
524
822
|
{ id: "plan", description: "Generate fix plan", type: "simple", run: "builtin:plan", depends: ["retrieve"], timeout: 30000 },
|
|
525
|
-
|
|
526
|
-
{ id: "verify", description: "Verify fix", type: "simple", run: "builtin:verify", depends: ["execute"], timeout: 60000 },
|
|
527
|
-
{ id: "commit", description: "Generate commit message", type: "manual", run: "builtin:commit", depends: ["verify"], timeout: 30000 },
|
|
823
|
+
...actions(120000),
|
|
528
824
|
],
|
|
529
825
|
triggers: [{ type: "mcp", tool: "run_workflow" }, { type: "cli", command: "bugfix" }],
|
|
530
826
|
});
|
|
@@ -536,9 +832,7 @@ ${steps}
|
|
|
536
832
|
{ id: "analyze", description: "Analyze feature requirements", type: "simple", run: "builtin:analyze", timeout: 30000 },
|
|
537
833
|
{ id: "retrieve", description: "Retrieve context", type: "simple", run: "builtin:retrieve", depends: ["analyze"], timeout: 30000 },
|
|
538
834
|
{ id: "plan", description: "Generate implementation plan", type: "simple", run: "builtin:plan", depends: ["retrieve"], timeout: 30000 },
|
|
539
|
-
|
|
540
|
-
{ id: "verify", description: "Verify implementation", type: "simple", run: "builtin:verify", depends: ["execute"], timeout: 60000 },
|
|
541
|
-
{ id: "commit", description: "Generate commit message", type: "manual", run: "builtin:commit", depends: ["verify"], timeout: 30000 },
|
|
835
|
+
...actions(300000),
|
|
542
836
|
],
|
|
543
837
|
triggers: [{ type: "mcp", tool: "run_workflow" }, { type: "cli", command: "feature" }],
|
|
544
838
|
});
|
|
@@ -549,35 +843,154 @@ ${steps}
|
|
|
549
843
|
steps: [
|
|
550
844
|
{ id: "analyze", description: "Analyze review scope", type: "simple", run: "builtin:analyze", timeout: 30000 },
|
|
551
845
|
{ id: "plan", description: "Generate review checklist", type: "simple", run: "builtin:plan", depends: ["analyze"], timeout: 30000 },
|
|
552
|
-
{
|
|
553
|
-
|
|
846
|
+
{
|
|
847
|
+
id: "verify",
|
|
848
|
+
description: "Run review checks",
|
|
849
|
+
type: "simple",
|
|
850
|
+
run: "builtin:verify",
|
|
851
|
+
depends: ["plan"],
|
|
852
|
+
timeout: 60000,
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: "commit",
|
|
856
|
+
description: "Post review summary",
|
|
857
|
+
type: "manual",
|
|
858
|
+
run: "builtin:commit",
|
|
859
|
+
depends: ["verify"],
|
|
860
|
+
inputs: {
|
|
861
|
+
summary: "${trigger.input}",
|
|
862
|
+
actionIds: "${verify.actionIds}",
|
|
863
|
+
verifiedActionIds: "${verify.verifiedActionIds}",
|
|
864
|
+
},
|
|
865
|
+
timeout: 30000,
|
|
866
|
+
},
|
|
554
867
|
],
|
|
555
868
|
triggers: [{ type: "mcp", tool: "run_workflow" }, { type: "cli", command: "review" }],
|
|
556
869
|
});
|
|
557
870
|
}
|
|
558
871
|
}
|
|
559
|
-
/**
|
|
560
|
-
* Resolve ${variable} references in input values.
|
|
561
|
-
* ${trigger.X} comes from the top-level input.
|
|
562
|
-
* ${stepId.field} comes from previous step outputs.
|
|
563
|
-
*/
|
|
564
872
|
function resolveInputs(inputs, outputs, triggerInput) {
|
|
565
873
|
const resolved = {};
|
|
566
874
|
for (const [key, value] of Object.entries(inputs)) {
|
|
875
|
+
const exact = /^\$\{([^}]+)\}$/.exec(value);
|
|
876
|
+
if (exact) {
|
|
877
|
+
const referenced = resolveReference(exact[1], outputs, triggerInput);
|
|
878
|
+
resolved[key] = referenced !== null && typeof referenced === "object"
|
|
879
|
+
? referenced
|
|
880
|
+
: String(referenced ?? "");
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
567
883
|
resolved[key] = value.replace(/\$\{([^}]+)\}/g, (_match, path) => {
|
|
568
|
-
|
|
569
|
-
if (parts[0] === "trigger") {
|
|
570
|
-
return String(triggerInput[parts.slice(1).join(".")] ?? "");
|
|
571
|
-
}
|
|
572
|
-
const stepOutput = outputs.get(parts[0]);
|
|
573
|
-
if (stepOutput && parts.length > 1) {
|
|
574
|
-
return String(stepOutput[parts.slice(1).join(".")] ?? "");
|
|
575
|
-
}
|
|
576
|
-
return String(stepOutput ?? "");
|
|
884
|
+
return String(resolveReference(path, outputs, triggerInput) ?? "");
|
|
577
885
|
});
|
|
578
886
|
}
|
|
579
887
|
return resolved;
|
|
580
888
|
}
|
|
889
|
+
function resolveReference(path, outputs, triggerInput) {
|
|
890
|
+
const parts = path.split(".");
|
|
891
|
+
if (parts[0] === "trigger")
|
|
892
|
+
return readPath(triggerInput, parts.slice(1));
|
|
893
|
+
const output = outputs.get(parts[0]);
|
|
894
|
+
if (!output)
|
|
895
|
+
return "";
|
|
896
|
+
return parts.length === 1 ? output : readPath(output, parts.slice(1));
|
|
897
|
+
}
|
|
898
|
+
function readPath(value, parts) {
|
|
899
|
+
let current = value;
|
|
900
|
+
for (const part of parts) {
|
|
901
|
+
if (typeof current !== "object" || current === null)
|
|
902
|
+
return "";
|
|
903
|
+
current = current[part];
|
|
904
|
+
}
|
|
905
|
+
return current ?? "";
|
|
906
|
+
}
|
|
907
|
+
function normalizeHostActionRequest(output) {
|
|
908
|
+
const canonical = output.actionRequired === true && output.state === "waiting";
|
|
909
|
+
const legacy = output.status === "host_action_required";
|
|
910
|
+
if (!canonical && !legacy)
|
|
911
|
+
return null;
|
|
912
|
+
return {
|
|
913
|
+
actionRequired: true,
|
|
914
|
+
actionKind: output.actionKind === "verify" ? "verify" : "execute",
|
|
915
|
+
state: "waiting",
|
|
916
|
+
requiredEvidence: ["context_receipt", "tool_or_diff", "report_step"],
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function legacyBuiltinOutput(handlerId, input) {
|
|
920
|
+
if (handlerId === "builtin:execute") {
|
|
921
|
+
const plan = Array.isArray(input.plan) ? input.plan : [];
|
|
922
|
+
return {
|
|
923
|
+
changes: plan.map((step, index) => ({
|
|
924
|
+
step: index + 1,
|
|
925
|
+
description: String(step),
|
|
926
|
+
status: "done",
|
|
927
|
+
})),
|
|
928
|
+
totalChanges: plan.length,
|
|
929
|
+
valid: true,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
if (handlerId === "builtin:verify") {
|
|
933
|
+
const changes = Array.isArray(input.changes) ? input.changes : [];
|
|
934
|
+
return { verified: changes.length, passed: changes.length, failed: 0, valid: true };
|
|
935
|
+
}
|
|
936
|
+
const summary = typeof input.summary === "string" ? input.summary : "Apply changes";
|
|
937
|
+
const verified = typeof input.verified === "number" ? input.verified : 0;
|
|
938
|
+
return { message: `feat: ${summary}\n\nVerified ${verified} changes.`, ready: true };
|
|
939
|
+
}
|
|
940
|
+
function isLegacyBuiltinHostAction(handlerId) {
|
|
941
|
+
return handlerId === "builtin:execute" || handlerId === "builtin:verify";
|
|
942
|
+
}
|
|
943
|
+
function isWorkflowStepDef(step) {
|
|
944
|
+
return "type" in step;
|
|
945
|
+
}
|
|
946
|
+
function stepTimeout(step) {
|
|
947
|
+
return isWorkflowStepDef(step) ? step.timeout : 60000;
|
|
948
|
+
}
|
|
949
|
+
function publicRunStatus(status) {
|
|
950
|
+
return status === "waiting_host" ? "running" : status;
|
|
951
|
+
}
|
|
952
|
+
function publicStepStatus(status) {
|
|
953
|
+
if (status === "waiting_host"
|
|
954
|
+
|| status === "verifying"
|
|
955
|
+
|| status === "awaiting_approval")
|
|
956
|
+
return "running";
|
|
957
|
+
return status;
|
|
958
|
+
}
|
|
959
|
+
function sameHostActionRecord(left, right) {
|
|
960
|
+
return left.actionId === right.actionId
|
|
961
|
+
&& left.runId === right.runId
|
|
962
|
+
&& left.engineRunId === right.engineRunId
|
|
963
|
+
&& left.stepId === right.stepId
|
|
964
|
+
&& left.projectRoot === right.projectRoot
|
|
965
|
+
&& left.sessionId === right.sessionId
|
|
966
|
+
&& left.executionId === right.executionId
|
|
967
|
+
&& left.contextReceipt === right.contextReceipt
|
|
968
|
+
&& left.state === right.state
|
|
969
|
+
&& left.evidenceHash === right.evidenceHash
|
|
970
|
+
&& stableJson(left.report) === stableJson(right.report);
|
|
971
|
+
}
|
|
972
|
+
function stableJson(value) {
|
|
973
|
+
if (Array.isArray(value))
|
|
974
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
975
|
+
if (value && typeof value === "object") {
|
|
976
|
+
return `{${Object.entries(value)
|
|
977
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
978
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
979
|
+
.join(",")}}`;
|
|
980
|
+
}
|
|
981
|
+
return JSON.stringify(value);
|
|
982
|
+
}
|
|
983
|
+
function stringArray(value) {
|
|
984
|
+
return Array.isArray(value)
|
|
985
|
+
? value.filter((item) => typeof item === "string" && item.length > 0)
|
|
986
|
+
: [];
|
|
987
|
+
}
|
|
988
|
+
function uniqueStrings(values) {
|
|
989
|
+
return Array.from(new Set(values));
|
|
990
|
+
}
|
|
991
|
+
function optionalString(value) {
|
|
992
|
+
return value;
|
|
993
|
+
}
|
|
581
994
|
class WorkflowDesignError extends Error {
|
|
582
995
|
constructor(message) {
|
|
583
996
|
super(message);
|