@workflow-manager/runner 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -12
- package/dist/adapters.d.ts +4 -0
- package/dist/adapters.js +11 -0
- package/dist/claudeCodeExecutor.d.ts +4 -0
- package/dist/claudeCodeExecutor.js +173 -0
- package/dist/cliRunRenderer.d.ts +36 -0
- package/dist/cliRunRenderer.js +286 -0
- package/dist/engine.d.ts +4 -2
- package/dist/engine.js +622 -44
- package/dist/events.d.ts +1 -1
- package/dist/events.js +4 -2
- package/dist/index.js +371 -41
- package/dist/manPage.d.ts +1 -0
- package/dist/manPage.js +147 -0
- package/dist/mockExecutor.d.ts +2 -2
- package/dist/mockExecutor.js +62 -13
- package/dist/opencodeExecutor.d.ts +2 -2
- package/dist/opencodeExecutor.js +101 -138
- package/dist/parser.js +98 -2
- package/dist/piAgentExecutor.d.ts +4 -0
- package/dist/piAgentExecutor.js +298 -0
- package/dist/remote/api.d.ts +1 -0
- package/dist/remote/commands.d.ts +2 -0
- package/dist/remote/commands.js +76 -4
- package/dist/runnerApi.d.ts +7 -0
- package/dist/runnerApi.js +221 -0
- package/dist/runnerSession.d.ts +62 -0
- package/dist/runnerSession.js +260 -0
- package/dist/runtimePreflight.d.ts +16 -0
- package/dist/runtimePreflight.js +189 -0
- package/dist/skillResolver.d.ts +6 -0
- package/dist/skillResolver.js +75 -0
- package/dist/types.d.ts +148 -2
- package/man/wfm.1 +54 -4
- package/package.json +28 -4
- package/skills/commit-discipline/SKILL.md +109 -0
- package/skills/doc-sync/SKILL.md +83 -0
- package/skills/repo-hygiene/SKILL.md +70 -0
- package/skills/spec-driven-development/SKILL.md +33 -0
- package/skills/workflow-manager-cli/SKILL.md +14 -9
package/dist/engine.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { resolveTaskAdapter } from "./adapters.js";
|
|
2
4
|
import { EventLog } from "./events.js";
|
|
5
|
+
import { executeClaudeCodeStep, shouldUseRealClaudeCode } from "./claudeCodeExecutor.js";
|
|
3
6
|
import { executeMockStep } from "./mockExecutor.js";
|
|
4
7
|
import { executeOpencodeStep, shouldUseRealOpencode } from "./opencodeExecutor.js";
|
|
8
|
+
import { executePiAgentStep } from "./piAgentExecutor.js";
|
|
9
|
+
import { validateRuntimeRequirements } from "./runtimePreflight.js";
|
|
5
10
|
function nodeType(step) {
|
|
6
11
|
if (step.kind === "approval")
|
|
7
12
|
return "HUMAN";
|
|
@@ -9,9 +14,92 @@ function nodeType(step) {
|
|
|
9
14
|
return "SYSTEM";
|
|
10
15
|
return "AGENT";
|
|
11
16
|
}
|
|
17
|
+
function stepAdapter(step) {
|
|
18
|
+
return step.kind === "task" ? resolveTaskAdapter(step.taskSpec?.adapterKey) : "approval";
|
|
19
|
+
}
|
|
12
20
|
function stepObjective(step, workflowObjective) {
|
|
13
21
|
return step.objective ?? `${workflowObjective} :: ${step.key}`;
|
|
14
22
|
}
|
|
23
|
+
function stepLabel(step) {
|
|
24
|
+
return step.title ?? step.objective ?? step.key;
|
|
25
|
+
}
|
|
26
|
+
const EXECUTION_STATUSES = ["SUCCESS", "QA_REJECTED", "YIELD_EXTERNAL", "FAILED"];
|
|
27
|
+
const QA_ACTIONS = ["PROCEED", "RETRY_CURRENT", "ROLLBACK_PREVIOUS", "RESTART_ALL"];
|
|
28
|
+
function isExecutionStatus(value) {
|
|
29
|
+
return typeof value === "string" && EXECUTION_STATUSES.includes(value);
|
|
30
|
+
}
|
|
31
|
+
function isQaAction(value) {
|
|
32
|
+
return typeof value === "string" && QA_ACTIONS.includes(value);
|
|
33
|
+
}
|
|
34
|
+
function validatedExecutorOutput(step, input, attempt, output) {
|
|
35
|
+
if (isExecutionStatus(output.execution_status) && isQaAction(output.qa_routing.action)) {
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
const invalidExecutionStatus = isExecutionStatus(output.execution_status) ? null : String(output.execution_status);
|
|
39
|
+
const invalidQaAction = isQaAction(output.qa_routing.action) ? null : String(output.qa_routing.action);
|
|
40
|
+
const reason = [
|
|
41
|
+
invalidExecutionStatus ? `execution_status=${invalidExecutionStatus}` : null,
|
|
42
|
+
invalidQaAction ? `qa_routing.action=${invalidQaAction}` : null,
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join(", ");
|
|
46
|
+
return {
|
|
47
|
+
step_id: step.key,
|
|
48
|
+
execution_status: "FAILED",
|
|
49
|
+
qa_routing: {
|
|
50
|
+
action: "PROCEED",
|
|
51
|
+
feedback_reason: `Invalid executor output for ${step.key}: ${reason}`,
|
|
52
|
+
},
|
|
53
|
+
mutated_payload: {
|
|
54
|
+
stepKey: step.key,
|
|
55
|
+
attempt,
|
|
56
|
+
adapter: input.priming_configuration.adapter ?? stepAdapter(step),
|
|
57
|
+
invalidExecutionStatus,
|
|
58
|
+
invalidQaAction,
|
|
59
|
+
},
|
|
60
|
+
metadata: {
|
|
61
|
+
execution_time_ms: output.metadata.execution_time_ms,
|
|
62
|
+
external_intervention_required: false,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function orderStepsByDependencies(steps) {
|
|
67
|
+
const byKey = new Map(steps.map((step) => [step.key, step]));
|
|
68
|
+
const ordered = [];
|
|
69
|
+
const visiting = new Set();
|
|
70
|
+
const visited = new Set();
|
|
71
|
+
const visit = (step) => {
|
|
72
|
+
if (visited.has(step.key)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (visiting.has(step.key)) {
|
|
76
|
+
throw new Error(`Circular dependency detected at step ${step.key}`);
|
|
77
|
+
}
|
|
78
|
+
visiting.add(step.key);
|
|
79
|
+
for (const dependency of step.dependsOn ?? []) {
|
|
80
|
+
const dependencyStep = byKey.get(dependency);
|
|
81
|
+
if (dependencyStep) {
|
|
82
|
+
visit(dependencyStep);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
visiting.delete(step.key);
|
|
86
|
+
visited.add(step.key);
|
|
87
|
+
ordered.push(step);
|
|
88
|
+
};
|
|
89
|
+
for (const step of steps) {
|
|
90
|
+
visit(step);
|
|
91
|
+
}
|
|
92
|
+
return ordered;
|
|
93
|
+
}
|
|
94
|
+
function summarizeContext(value) {
|
|
95
|
+
if (typeof value === "string") {
|
|
96
|
+
return { type: "string", length: value.length };
|
|
97
|
+
}
|
|
98
|
+
if (value && typeof value === "object") {
|
|
99
|
+
return { type: "object", keys: Object.keys(value).sort() };
|
|
100
|
+
}
|
|
101
|
+
return { type: "none" };
|
|
102
|
+
}
|
|
15
103
|
function requiresValidation(step) {
|
|
16
104
|
if (step.approvalSpec?.validation?.required)
|
|
17
105
|
return step.approvalSpec.validation.mode ?? "human";
|
|
@@ -21,6 +109,9 @@ function requiresValidation(step) {
|
|
|
21
109
|
return step.approvalSpec?.validation?.mode ?? "human";
|
|
22
110
|
return step.validation?.mode ?? "none";
|
|
23
111
|
}
|
|
112
|
+
export function canUseInteractiveConfirmation(step) {
|
|
113
|
+
return requiresValidation(step) === "human";
|
|
114
|
+
}
|
|
24
115
|
function canConfirm(step, options, output) {
|
|
25
116
|
const mode = requiresValidation(step);
|
|
26
117
|
if (mode === "none" && output.execution_status !== "YIELD_EXTERNAL")
|
|
@@ -36,22 +127,366 @@ function canConfirm(step, options, output) {
|
|
|
36
127
|
return { ok: true };
|
|
37
128
|
return { ok: false, reason: `Missing confirmation for ${step.key} (${mode})` };
|
|
38
129
|
}
|
|
39
|
-
function
|
|
40
|
-
const
|
|
130
|
+
function summarizeText(value, maxLength = 180) {
|
|
131
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
132
|
+
if (normalized.length <= maxLength) {
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
return `${normalized.slice(0, maxLength - 3)}...`;
|
|
136
|
+
}
|
|
137
|
+
function summarizeArtifact(value) {
|
|
138
|
+
if (typeof value === "string") {
|
|
139
|
+
return summarizeText(value);
|
|
140
|
+
}
|
|
141
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
142
|
+
return String(value);
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
const items = value.slice(0, 3).map((entry) => summarizeArtifact(entry)).filter(Boolean);
|
|
146
|
+
return items.length > 0 ? items.join(", ") : "Array value";
|
|
147
|
+
}
|
|
148
|
+
if (!value || typeof value !== "object") {
|
|
149
|
+
return "No review artifact available.";
|
|
150
|
+
}
|
|
151
|
+
const record = value;
|
|
152
|
+
const preferredKeys = [
|
|
153
|
+
"summary",
|
|
154
|
+
"output",
|
|
155
|
+
"storyMarkdown",
|
|
156
|
+
"chapterMarkdown",
|
|
157
|
+
"stdout",
|
|
158
|
+
"stderr",
|
|
159
|
+
"paragraph",
|
|
160
|
+
"objective",
|
|
161
|
+
"feedback_reason",
|
|
162
|
+
"feedbackReason",
|
|
163
|
+
];
|
|
164
|
+
for (const key of preferredKeys) {
|
|
165
|
+
const candidate = record[key];
|
|
166
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
167
|
+
return summarizeText(candidate);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const primitives = Object.entries(record)
|
|
171
|
+
.filter(([, candidate]) => typeof candidate === "string" || typeof candidate === "number" || typeof candidate === "boolean")
|
|
172
|
+
.slice(0, 4)
|
|
173
|
+
.map(([key, candidate]) => `${key}=${summarizeText(String(candidate), 60)}`);
|
|
174
|
+
if (primitives.length > 0) {
|
|
175
|
+
return primitives.join(", ");
|
|
176
|
+
}
|
|
177
|
+
const keys = Object.keys(record).slice(0, 5);
|
|
178
|
+
return keys.length > 0 ? `Object with keys: ${keys.join(", ")}` : "No review artifact available.";
|
|
179
|
+
}
|
|
180
|
+
function buildApprovalPreview(step, stepRuns, previousOutput, currentOutput) {
|
|
181
|
+
const items = [];
|
|
182
|
+
if (currentOutput && Object.keys(currentOutput).length > 0 && step.kind !== "approval") {
|
|
183
|
+
items.push({
|
|
184
|
+
stepKey: step.key,
|
|
185
|
+
title: `Output from ${step.key}`,
|
|
186
|
+
summary: summarizeArtifact(currentOutput),
|
|
187
|
+
source: "current_step",
|
|
188
|
+
status: stepRuns.get(step.key)?.status,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (const dependency of step.dependsOn ?? []) {
|
|
192
|
+
items.push({
|
|
193
|
+
stepKey: dependency,
|
|
194
|
+
title: `Dependency ${dependency}`,
|
|
195
|
+
summary: summarizeArtifact(previousOutput[dependency]),
|
|
196
|
+
source: "dependency",
|
|
197
|
+
status: stepRuns.get(dependency)?.status,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (items.length === 0) {
|
|
201
|
+
items.push({
|
|
202
|
+
stepKey: null,
|
|
203
|
+
title: step.kind === "approval" ? "Manual approval gate" : "No review artifact",
|
|
204
|
+
summary: step.kind === "approval"
|
|
205
|
+
? "This step is a manual checkpoint. Approving it continues the workflow."
|
|
206
|
+
: "This step did not emit a review artifact. Approving it continues the workflow.",
|
|
207
|
+
source: "approval_gate",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const summary = step.kind === "approval"
|
|
211
|
+
? `Approve this gate to continue after ${items
|
|
212
|
+
.filter((item) => item.stepKey)
|
|
213
|
+
.map((item) => item.stepKey)
|
|
214
|
+
.join(", ") || "the previous step"}.`
|
|
215
|
+
: `Approve the results of ${step.key} before the workflow continues.`;
|
|
216
|
+
return {
|
|
217
|
+
stepLabel: stepLabel(step),
|
|
218
|
+
objective: step.objective ?? step.title ?? null,
|
|
219
|
+
summary,
|
|
220
|
+
items,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export async function promptForApprovalDecision(stepKey, reason, validation, preview, actor, signal) {
|
|
224
|
+
if (!process.stdin.isTTY)
|
|
225
|
+
return null;
|
|
226
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
227
|
+
const decisionVerb = validation === "external" ? "Resume" : "Approve";
|
|
228
|
+
const positiveAnswers = new Set(["a", "approve", "y", "yes", "r", "resume"]);
|
|
229
|
+
const negativeAnswers = new Set(["c", "cancel", "n", "no"]);
|
|
230
|
+
const render = () => {
|
|
231
|
+
process.stderr.write(`\n${decisionVerb} required for ${stepKey}\n`);
|
|
232
|
+
process.stderr.write(`- Reason: ${reason}\n`);
|
|
233
|
+
process.stderr.write(`- Validation: ${validation}\n`);
|
|
234
|
+
if (preview) {
|
|
235
|
+
process.stderr.write(`- Step: ${preview.stepLabel}\n`);
|
|
236
|
+
process.stderr.write(`- What you are deciding: ${preview.summary}\n`);
|
|
237
|
+
for (const item of preview.items) {
|
|
238
|
+
const status = item.status ? ` [${item.status}]` : "";
|
|
239
|
+
process.stderr.write(` - ${item.title}${status}: ${item.summary}\n`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
render();
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
let settled = false;
|
|
246
|
+
const finish = (value) => {
|
|
247
|
+
if (settled) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
settled = true;
|
|
251
|
+
signal?.removeEventListener("abort", onAbort);
|
|
252
|
+
rl.close();
|
|
253
|
+
process.stdin.resume();
|
|
254
|
+
resolve(value);
|
|
255
|
+
};
|
|
256
|
+
const onAbort = () => {
|
|
257
|
+
finish(null);
|
|
258
|
+
};
|
|
259
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
260
|
+
const ask = () => {
|
|
261
|
+
rl.question(`${decisionVerb} now? [a]pprove/[r]esume / [c]ancel / [v]iew: `, (answer) => {
|
|
262
|
+
if (signal?.aborted) {
|
|
263
|
+
finish(null);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const normalized = answer.trim().toLowerCase();
|
|
267
|
+
if (positiveAnswers.has(normalized)) {
|
|
268
|
+
finish({ decision: "approved", actor, source: "terminal" });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (negativeAnswers.has(normalized)) {
|
|
272
|
+
finish({ decision: "cancelled", actor, source: "terminal", note: "cancelled in terminal" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (normalized === "v" || normalized === "view" || normalized === "") {
|
|
276
|
+
render();
|
|
277
|
+
ask();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
process.stderr.write("Enter 'a'/'r' to continue, 'c' to cancel, or 'v' to reprint the decision details.\n");
|
|
281
|
+
ask();
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
if (signal?.aborted) {
|
|
285
|
+
finish(null);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
ask();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
async function executeStep(step, input, attempt, workflow, workflowFilePath, hooks) {
|
|
292
|
+
if (step.kind !== "task") {
|
|
293
|
+
return executeMockStep(step, input, attempt, hooks);
|
|
294
|
+
}
|
|
295
|
+
const adapterKey = resolveTaskAdapter(step.taskSpec?.adapterKey);
|
|
296
|
+
if (adapterKey === "pi-agent") {
|
|
297
|
+
return executePiAgentStep(step, input, attempt, workflow, workflowFilePath, hooks);
|
|
298
|
+
}
|
|
41
299
|
if (adapterKey === "opencode" && shouldUseRealOpencode(step)) {
|
|
42
|
-
return executeOpencodeStep(step, input, attempt);
|
|
300
|
+
return executeOpencodeStep(step, input, attempt, hooks);
|
|
43
301
|
}
|
|
44
|
-
|
|
302
|
+
if (adapterKey === "claude-code" && shouldUseRealClaudeCode(step)) {
|
|
303
|
+
return executeClaudeCodeStep(step, input, attempt, workflow, workflowFilePath, hooks);
|
|
304
|
+
}
|
|
305
|
+
return executeMockStep(step, input, attempt, hooks);
|
|
45
306
|
}
|
|
46
|
-
export function runWorkflow(definition, options) {
|
|
47
|
-
const runId = randomUUID();
|
|
307
|
+
export async function runWorkflow(definition, options) {
|
|
308
|
+
const runId = options?.runId ?? randomUUID();
|
|
48
309
|
const actor = options?.actor ?? "cli";
|
|
49
310
|
const primaryObjective = options?.objective ?? definition.title;
|
|
50
311
|
const workflowObjectives = definition.objectives ?? [];
|
|
51
312
|
const globalState = { ...(options?.input ?? {}) };
|
|
313
|
+
const workflowFilePath = options?.workflowFilePath ?? "";
|
|
52
314
|
const eventLog = new EventLog();
|
|
315
|
+
const observer = options?.observer;
|
|
316
|
+
const controller = options?.controller;
|
|
53
317
|
let runStatus = "queued";
|
|
318
|
+
let currentStepKey = null;
|
|
319
|
+
let runStartedAt = null;
|
|
320
|
+
let runUpdatedAt = new Date().toISOString();
|
|
321
|
+
let runEndedAt = null;
|
|
322
|
+
let waitingForApproval = null;
|
|
54
323
|
const stepRuns = new Map();
|
|
324
|
+
const stepRuntime = new Map();
|
|
325
|
+
const emptyExecution = () => ({
|
|
326
|
+
executionStatus: null,
|
|
327
|
+
qaAction: null,
|
|
328
|
+
feedbackReason: null,
|
|
329
|
+
});
|
|
330
|
+
const touchRun = (ended = false) => {
|
|
331
|
+
const now = new Date().toISOString();
|
|
332
|
+
runUpdatedAt = now;
|
|
333
|
+
if (ended) {
|
|
334
|
+
runEndedAt = now;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const touchStep = (stepKey, patch) => {
|
|
338
|
+
const current = stepRuntime.get(stepKey);
|
|
339
|
+
if (!current) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const now = new Date().toISOString();
|
|
343
|
+
if (patch.startedAt !== undefined)
|
|
344
|
+
current.startedAt = patch.startedAt;
|
|
345
|
+
if (patch.finishedAt !== undefined)
|
|
346
|
+
current.finishedAt = patch.finishedAt;
|
|
347
|
+
if (patch.lastExecution)
|
|
348
|
+
current.lastExecution = { ...patch.lastExecution };
|
|
349
|
+
current.updatedAt = patch.updatedAt ?? now;
|
|
350
|
+
touchRun(false);
|
|
351
|
+
};
|
|
352
|
+
const buildStepDetails = () => definition.steps.map((step) => {
|
|
353
|
+
const run = stepRuns.get(step.key);
|
|
354
|
+
const runtime = stepRuntime.get(step.key);
|
|
355
|
+
return {
|
|
356
|
+
stepKey: step.key,
|
|
357
|
+
status: run.status,
|
|
358
|
+
attempt: run.attempt,
|
|
359
|
+
confirmed: run.confirmed,
|
|
360
|
+
adapter: stepAdapter(step),
|
|
361
|
+
startedAt: runtime.startedAt,
|
|
362
|
+
updatedAt: runtime.updatedAt,
|
|
363
|
+
finishedAt: runtime.finishedAt,
|
|
364
|
+
kind: step.kind,
|
|
365
|
+
objective: step.objective ?? step.title ?? null,
|
|
366
|
+
dependsOn: step.dependsOn ?? [],
|
|
367
|
+
config: {
|
|
368
|
+
model: typeof step.taskSpec?.init?.model === "string" ? step.taskSpec.init.model : null,
|
|
369
|
+
skills: step.taskSpec?.init?.skills ?? [],
|
|
370
|
+
mcps: step.taskSpec?.init?.mcps ?? [],
|
|
371
|
+
systemPrompts: step.taskSpec?.init?.systemPrompts ?? [],
|
|
372
|
+
contextSummary: summarizeContext(step.taskSpec?.init?.context),
|
|
373
|
+
},
|
|
374
|
+
lastExecution: { ...runtime.lastExecution },
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
const buildSnapshot = () => ({
|
|
378
|
+
runId,
|
|
379
|
+
workflowKey: definition.key,
|
|
380
|
+
workflowTitle: definition.title,
|
|
381
|
+
status: runStatus,
|
|
382
|
+
currentStepKey,
|
|
383
|
+
startedAt: runStartedAt,
|
|
384
|
+
updatedAt: runUpdatedAt,
|
|
385
|
+
endedAt: runEndedAt,
|
|
386
|
+
objective: primaryObjective,
|
|
387
|
+
objectives: [...workflowObjectives],
|
|
388
|
+
waitingForApproval: waitingForApproval ? { ...waitingForApproval } : null,
|
|
389
|
+
steps: definition.steps.map((step) => {
|
|
390
|
+
const run = stepRuns.get(step.key);
|
|
391
|
+
const runtime = stepRuntime.get(step.key);
|
|
392
|
+
return {
|
|
393
|
+
stepKey: step.key,
|
|
394
|
+
status: run.status,
|
|
395
|
+
attempt: run.attempt,
|
|
396
|
+
confirmed: run.confirmed,
|
|
397
|
+
adapter: stepAdapter(step),
|
|
398
|
+
startedAt: runtime.startedAt,
|
|
399
|
+
updatedAt: runtime.updatedAt,
|
|
400
|
+
finishedAt: runtime.finishedAt,
|
|
401
|
+
};
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
const emitSnapshot = () => {
|
|
405
|
+
observer?.onSnapshot(buildSnapshot(), buildStepDetails());
|
|
406
|
+
};
|
|
407
|
+
const pushEvent = (type, payload = {}, stepKey, eventActor = "system") => {
|
|
408
|
+
const event = eventLog.push(runId, type, payload, stepKey, eventActor);
|
|
409
|
+
observer?.onEvent(event);
|
|
410
|
+
};
|
|
411
|
+
const emitLog = (stepKey, stream, text) => {
|
|
412
|
+
observer?.onLog({
|
|
413
|
+
id: randomUUID(),
|
|
414
|
+
runId,
|
|
415
|
+
stepKey,
|
|
416
|
+
stream,
|
|
417
|
+
text,
|
|
418
|
+
occurredAt: new Date().toISOString(),
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
const waitForDecision = async (step, stepRun, reason, validation, requireConfirmationEvent, preview = null) => {
|
|
422
|
+
stepRun.status = "waiting_for_approval";
|
|
423
|
+
runStatus = "waiting_for_approval";
|
|
424
|
+
waitingForApproval = {
|
|
425
|
+
stepKey: step.key,
|
|
426
|
+
reason,
|
|
427
|
+
validation,
|
|
428
|
+
preview,
|
|
429
|
+
};
|
|
430
|
+
touchStep(step.key, { finishedAt: null });
|
|
431
|
+
pushEvent("step.waiting_for_approval", { reason, validation, preview }, step.key);
|
|
432
|
+
pushEvent("run.waiting_for_approval", { reason, preview }, step.key, actor);
|
|
433
|
+
emitSnapshot();
|
|
434
|
+
const request = { stepKey: step.key, reason, validation };
|
|
435
|
+
let resolution = null;
|
|
436
|
+
if (controller) {
|
|
437
|
+
const promptAbort = new AbortController();
|
|
438
|
+
const promptTask = options?.approvalPrompt
|
|
439
|
+
? options.approvalPrompt({ ...request, preview, signal: promptAbort.signal }).catch((error) => {
|
|
440
|
+
if (promptAbort.signal.aborted) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
throw error;
|
|
444
|
+
})
|
|
445
|
+
: null;
|
|
446
|
+
try {
|
|
447
|
+
resolution = await controller.waitForDecision(request);
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
promptAbort.abort();
|
|
451
|
+
await promptTask;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
else if (options?.approvalPrompt) {
|
|
455
|
+
resolution = await options.approvalPrompt({ ...request, preview });
|
|
456
|
+
}
|
|
457
|
+
else if (options?.interactive && process.stdin.isTTY) {
|
|
458
|
+
resolution = await promptForApprovalDecision(step.key, reason, validation, preview, actor);
|
|
459
|
+
}
|
|
460
|
+
if (!resolution) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
const resolutionActor = resolution.actor?.trim() ? resolution.actor : actor;
|
|
464
|
+
pushEvent("approval.resolved", {
|
|
465
|
+
decision: resolution.decision,
|
|
466
|
+
actor: resolutionActor,
|
|
467
|
+
note: resolution.note ?? null,
|
|
468
|
+
source: resolution.source ?? "api",
|
|
469
|
+
}, step.key, resolutionActor);
|
|
470
|
+
if (resolution.decision === "cancelled") {
|
|
471
|
+
stepRun.status = "cancelled";
|
|
472
|
+
runStatus = "cancelled";
|
|
473
|
+
currentStepKey = step.key;
|
|
474
|
+
waitingForApproval = null;
|
|
475
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
476
|
+
touchRun(true);
|
|
477
|
+
pushEvent("run.cancelled", { stepKey: step.key, reason: resolution.note ?? "cancelled by API", source: resolution.source ?? "api" }, step.key, resolutionActor);
|
|
478
|
+
emitSnapshot();
|
|
479
|
+
return resolution;
|
|
480
|
+
}
|
|
481
|
+
waitingForApproval = null;
|
|
482
|
+
runStatus = "running";
|
|
483
|
+
if (requireConfirmationEvent) {
|
|
484
|
+
stepRun.confirmed = true;
|
|
485
|
+
pushEvent("step.confirmed", { by: resolutionActor, validation, note: resolution.note ?? null, source: resolution.source ?? "api" }, step.key, resolutionActor);
|
|
486
|
+
}
|
|
487
|
+
emitSnapshot();
|
|
488
|
+
return resolution;
|
|
489
|
+
};
|
|
55
490
|
for (const step of definition.steps) {
|
|
56
491
|
stepRuns.set(step.key, {
|
|
57
492
|
stepKey: step.key,
|
|
@@ -59,21 +494,64 @@ export function runWorkflow(definition, options) {
|
|
|
59
494
|
attempt: 0,
|
|
60
495
|
confirmed: false,
|
|
61
496
|
});
|
|
497
|
+
stepRuntime.set(step.key, {
|
|
498
|
+
startedAt: null,
|
|
499
|
+
updatedAt: runUpdatedAt,
|
|
500
|
+
finishedAt: null,
|
|
501
|
+
lastExecution: emptyExecution(),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
emitSnapshot();
|
|
505
|
+
pushEvent("run.created", { workflowKey: definition.key }, undefined, actor);
|
|
506
|
+
const runtimeErrors = validateRuntimeRequirements(definition);
|
|
507
|
+
if (runtimeErrors.length > 0) {
|
|
508
|
+
runStatus = "failed";
|
|
509
|
+
touchRun(true);
|
|
510
|
+
pushEvent("run.failed", { reason: runtimeErrors.join("; "), runtimeErrors }, undefined, actor);
|
|
511
|
+
emitSnapshot();
|
|
512
|
+
return {
|
|
513
|
+
runId,
|
|
514
|
+
status: runStatus,
|
|
515
|
+
outputs: globalState,
|
|
516
|
+
stepRuns: definition.steps.map((step) => stepRuns.get(step.key)),
|
|
517
|
+
events: eventLog.all(),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
let orderedSteps;
|
|
521
|
+
try {
|
|
522
|
+
orderedSteps = orderStepsByDependencies(definition.steps);
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
runStatus = "failed";
|
|
526
|
+
touchRun(true);
|
|
527
|
+
pushEvent("run.failed", { reason: err.message }, undefined, actor);
|
|
528
|
+
emitSnapshot();
|
|
529
|
+
return {
|
|
530
|
+
runId,
|
|
531
|
+
status: runStatus,
|
|
532
|
+
outputs: globalState,
|
|
533
|
+
stepRuns: definition.steps.map((step) => stepRuns.get(step.key)),
|
|
534
|
+
events: eventLog.all(),
|
|
535
|
+
};
|
|
62
536
|
}
|
|
63
|
-
eventLog.push(runId, "run.created", { workflowKey: definition.key }, undefined, actor);
|
|
64
537
|
runStatus = "running";
|
|
65
|
-
|
|
538
|
+
runStartedAt = new Date().toISOString();
|
|
539
|
+
touchRun(false);
|
|
540
|
+
pushEvent("run.started", { objective: primaryObjective, objectives: workflowObjectives }, undefined, actor);
|
|
541
|
+
emitSnapshot();
|
|
66
542
|
let index = 0;
|
|
67
543
|
let guard = 0;
|
|
68
544
|
const maxSteps = Math.max(definition.steps.length * 30, 30);
|
|
69
|
-
while (index <
|
|
545
|
+
while (index < orderedSteps.length) {
|
|
70
546
|
guard += 1;
|
|
71
547
|
if (guard > maxSteps) {
|
|
72
548
|
runStatus = "failed";
|
|
73
|
-
|
|
549
|
+
touchRun(true);
|
|
550
|
+
pushEvent("run.failed", { reason: "Execution guard exceeded" });
|
|
551
|
+
emitSnapshot();
|
|
74
552
|
break;
|
|
75
553
|
}
|
|
76
|
-
const step =
|
|
554
|
+
const step = orderedSteps[index];
|
|
77
555
|
const stepRun = stepRuns.get(step.key);
|
|
78
556
|
if (!stepRun)
|
|
79
557
|
throw new Error(`Missing step run for ${step.key}`);
|
|
@@ -81,15 +559,26 @@ export function runWorkflow(definition, options) {
|
|
|
81
559
|
const depsComplete = dependencies.every((depKey) => stepRuns.get(depKey)?.status === "succeeded");
|
|
82
560
|
if (!depsComplete) {
|
|
83
561
|
runStatus = "failed";
|
|
84
|
-
|
|
562
|
+
currentStepKey = step.key;
|
|
563
|
+
touchRun(true);
|
|
564
|
+
pushEvent("run.failed", { reason: `Dependencies not satisfied for ${step.key}` }, step.key);
|
|
565
|
+
emitSnapshot();
|
|
85
566
|
break;
|
|
86
567
|
}
|
|
87
568
|
stepRun.status = "runnable";
|
|
88
|
-
|
|
569
|
+
currentStepKey = step.key;
|
|
570
|
+
waitingForApproval = null;
|
|
571
|
+
touchStep(step.key, { finishedAt: null });
|
|
572
|
+
pushEvent("step.runnable", { stepKey: step.key }, step.key);
|
|
573
|
+
emitSnapshot();
|
|
89
574
|
stepRun.status = "running";
|
|
90
575
|
stepRun.attempt += 1;
|
|
91
|
-
|
|
92
|
-
|
|
576
|
+
delete stepRun.output;
|
|
577
|
+
delete globalState[step.key];
|
|
578
|
+
touchStep(step.key, { startedAt: new Date().toISOString(), finishedAt: null });
|
|
579
|
+
pushEvent("step.claimed", { attempt: stepRun.attempt }, step.key);
|
|
580
|
+
pushEvent("step.execution_started", { attempt: stepRun.attempt }, step.key);
|
|
581
|
+
emitSnapshot();
|
|
93
582
|
const previousOutput = {};
|
|
94
583
|
for (const dep of dependencies) {
|
|
95
584
|
previousOutput[dep] = stepRuns.get(dep)?.output ?? null;
|
|
@@ -112,106 +601,195 @@ export function runWorkflow(definition, options) {
|
|
|
112
601
|
mcp_endpoints: step.taskSpec?.init?.mcps ?? [],
|
|
113
602
|
system_prompts: step.taskSpec?.init?.systemPrompts ?? [],
|
|
114
603
|
context: step.taskSpec?.init?.context,
|
|
115
|
-
adapter: step.taskSpec?.adapterKey
|
|
604
|
+
adapter: resolveTaskAdapter(step.taskSpec?.adapterKey),
|
|
116
605
|
model: step.taskSpec?.init?.model,
|
|
117
606
|
},
|
|
118
607
|
};
|
|
119
|
-
const
|
|
120
|
-
|
|
608
|
+
const hooks = {
|
|
609
|
+
onStarted: (payload) => {
|
|
610
|
+
pushEvent("agent.started", { attempt: stepRun.attempt, ...(payload ?? {}) }, step.key);
|
|
611
|
+
},
|
|
612
|
+
onStdout: (chunk) => {
|
|
613
|
+
emitLog(step.key, "stdout", chunk);
|
|
614
|
+
pushEvent("agent.stdout", { stream: "stdout", text: chunk }, step.key);
|
|
615
|
+
},
|
|
616
|
+
onStderr: (chunk) => {
|
|
617
|
+
emitLog(step.key, "stderr", chunk);
|
|
618
|
+
pushEvent("agent.stderr", { stream: "stderr", text: chunk }, step.key);
|
|
619
|
+
},
|
|
620
|
+
onFinished: (payload) => {
|
|
621
|
+
pushEvent("agent.finished", { attempt: stepRun.attempt, ...(payload ?? {}) }, step.key);
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
const output = validatedExecutorOutput(step, inputEnvelope, stepRun.attempt, await executeStep(step, inputEnvelope, stepRun.attempt, definition, workflowFilePath, hooks));
|
|
625
|
+
touchStep(step.key, {
|
|
626
|
+
lastExecution: {
|
|
627
|
+
executionStatus: output.execution_status,
|
|
628
|
+
qaAction: output.qa_routing.action,
|
|
629
|
+
feedbackReason: output.qa_routing.feedback_reason,
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
pushEvent("step.execution_finished", {
|
|
121
633
|
status: output.execution_status,
|
|
122
634
|
action: output.qa_routing.action,
|
|
123
|
-
|
|
635
|
+
feedbackReason: output.qa_routing.feedback_reason,
|
|
636
|
+
adapter: resolveTaskAdapter(step.taskSpec?.adapterKey),
|
|
124
637
|
init: {
|
|
125
638
|
skills: step.taskSpec?.init?.skills ?? [],
|
|
126
639
|
mcps: step.taskSpec?.init?.mcps ?? [],
|
|
127
640
|
context: step.taskSpec?.init?.context ?? {},
|
|
641
|
+
systemPrompts: step.taskSpec?.init?.systemPrompts ?? [],
|
|
642
|
+
model: step.taskSpec?.init?.model ?? null,
|
|
128
643
|
},
|
|
129
644
|
}, step.key);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
645
|
+
emitSnapshot();
|
|
646
|
+
const approvalPreview = buildApprovalPreview(step, stepRuns, previousOutput, output.mutated_payload);
|
|
647
|
+
const confirmed = canConfirm(step, options ?? {}, output).ok;
|
|
648
|
+
if (!confirmed) {
|
|
649
|
+
const validation = requiresValidation(step);
|
|
650
|
+
const decision = await waitForDecision(step, stepRun, `confirmation required for ${step.key}`, validation, true, approvalPreview);
|
|
651
|
+
if (decision === null || decision.decision === "cancelled") {
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!stepRun.confirmed) {
|
|
656
|
+
stepRun.confirmed = true;
|
|
657
|
+
waitingForApproval = null;
|
|
658
|
+
pushEvent("step.confirmed", { by: actor, validation: requiresValidation(step) }, step.key, actor);
|
|
659
|
+
emitSnapshot();
|
|
137
660
|
}
|
|
138
|
-
stepRun.confirmed = true;
|
|
139
|
-
eventLog.push(runId, "step.confirmed", { by: actor, validation: requiresValidation(step) }, step.key, actor);
|
|
140
661
|
if (output.execution_status === "YIELD_EXTERNAL") {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
662
|
+
if (step.kind === "approval") {
|
|
663
|
+
stepRun.status = "succeeded";
|
|
664
|
+
runStatus = "running";
|
|
665
|
+
waitingForApproval = null;
|
|
666
|
+
stepRun.output = output.mutated_payload;
|
|
667
|
+
globalState[step.key] = output.mutated_payload;
|
|
668
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
669
|
+
currentStepKey = null;
|
|
670
|
+
emitSnapshot();
|
|
671
|
+
index += 1;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const decision = await waitForDecision(step, stepRun, "external intervention", "external", false, approvalPreview);
|
|
675
|
+
if (decision === null || decision.decision === "cancelled") {
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
145
678
|
stepRun.status = "succeeded";
|
|
146
679
|
runStatus = "running";
|
|
680
|
+
waitingForApproval = null;
|
|
147
681
|
stepRun.output = output.mutated_payload;
|
|
148
682
|
globalState[step.key] = output.mutated_payload;
|
|
683
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
684
|
+
currentStepKey = null;
|
|
685
|
+
emitSnapshot();
|
|
149
686
|
index += 1;
|
|
150
687
|
continue;
|
|
151
688
|
}
|
|
152
689
|
if (output.execution_status === "FAILED") {
|
|
153
690
|
stepRun.status = "failed";
|
|
154
691
|
runStatus = "failed";
|
|
155
|
-
|
|
692
|
+
currentStepKey = step.key;
|
|
693
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
694
|
+
touchRun(true);
|
|
695
|
+
pushEvent("run.failed", { stepKey: step.key, reason: "step failed" }, step.key);
|
|
696
|
+
emitSnapshot();
|
|
156
697
|
break;
|
|
157
698
|
}
|
|
158
699
|
if (output.execution_status === "QA_REJECTED") {
|
|
159
700
|
const retryMax = step.retryPolicy?.maxAttempts ?? definition.defaultRetryPolicy?.maxAttempts ?? 1;
|
|
160
|
-
|
|
701
|
+
const qaAction = String(output.qa_routing.action);
|
|
702
|
+
if (qaAction === "RETRY_CURRENT") {
|
|
161
703
|
if (stepRun.attempt < retryMax) {
|
|
162
704
|
stepRun.status = "pending";
|
|
163
|
-
|
|
705
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
706
|
+
pushEvent("step.retried", { stepKey: step.key, attempt: stepRun.attempt + 1 }, step.key);
|
|
707
|
+
emitSnapshot();
|
|
164
708
|
continue;
|
|
165
709
|
}
|
|
166
710
|
stepRun.status = "failed";
|
|
167
711
|
runStatus = "failed";
|
|
168
|
-
|
|
712
|
+
currentStepKey = step.key;
|
|
713
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
714
|
+
touchRun(true);
|
|
715
|
+
pushEvent("run.failed", { stepKey: step.key, reason: "max retry exceeded" }, step.key);
|
|
716
|
+
emitSnapshot();
|
|
169
717
|
break;
|
|
170
718
|
}
|
|
171
|
-
if (
|
|
719
|
+
if (qaAction === "ROLLBACK_PREVIOUS") {
|
|
172
720
|
if (index === 0) {
|
|
173
721
|
stepRun.status = "failed";
|
|
174
722
|
runStatus = "failed";
|
|
175
|
-
|
|
723
|
+
currentStepKey = step.key;
|
|
724
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
725
|
+
touchRun(true);
|
|
726
|
+
pushEvent("run.failed", { stepKey: step.key, reason: "cannot rollback before first step" }, step.key);
|
|
727
|
+
emitSnapshot();
|
|
176
728
|
break;
|
|
177
729
|
}
|
|
178
|
-
const prevStep =
|
|
730
|
+
const prevStep = orderedSteps[index - 1];
|
|
179
731
|
const prevRun = stepRuns.get(prevStep.key);
|
|
180
732
|
prevRun.status = "pending";
|
|
733
|
+
prevRun.attempt = 0;
|
|
181
734
|
prevRun.confirmed = false;
|
|
182
|
-
|
|
735
|
+
delete prevRun.output;
|
|
736
|
+
delete globalState[prevStep.key];
|
|
737
|
+
delete stepRun.output;
|
|
738
|
+
delete globalState[step.key];
|
|
739
|
+
touchStep(prevStep.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
|
|
740
|
+
pushEvent("step.retried", { stepKey: prevStep.key, via: step.key }, prevStep.key);
|
|
183
741
|
stepRun.status = "pending";
|
|
184
742
|
stepRun.confirmed = false;
|
|
743
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
744
|
+
emitSnapshot();
|
|
185
745
|
index -= 1;
|
|
186
746
|
continue;
|
|
187
747
|
}
|
|
188
|
-
if (
|
|
748
|
+
if (qaAction === "RESTART_ALL") {
|
|
189
749
|
for (const s of definition.steps) {
|
|
190
750
|
const sr = stepRuns.get(s.key);
|
|
191
751
|
sr.status = "pending";
|
|
192
752
|
sr.attempt = 0;
|
|
193
753
|
sr.confirmed = false;
|
|
194
754
|
delete sr.output;
|
|
755
|
+
delete globalState[s.key];
|
|
756
|
+
touchStep(s.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
|
|
195
757
|
}
|
|
196
|
-
|
|
758
|
+
currentStepKey = null;
|
|
759
|
+
pushEvent("step.retried", { mode: "restart_all", triggeredBy: step.key }, step.key);
|
|
760
|
+
emitSnapshot();
|
|
197
761
|
index = 0;
|
|
198
762
|
continue;
|
|
199
763
|
}
|
|
764
|
+
stepRun.status = "failed";
|
|
765
|
+
runStatus = "failed";
|
|
766
|
+
currentStepKey = step.key;
|
|
767
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
768
|
+
touchRun(true);
|
|
769
|
+
pushEvent("run.failed", { stepKey: step.key, reason: `Unknown QA action: ${qaAction}` }, step.key);
|
|
770
|
+
emitSnapshot();
|
|
771
|
+
break;
|
|
200
772
|
}
|
|
201
773
|
stepRun.status = "succeeded";
|
|
202
774
|
stepRun.output = output.mutated_payload;
|
|
203
775
|
globalState[step.key] = output.mutated_payload;
|
|
776
|
+
touchStep(step.key, { finishedAt: new Date().toISOString() });
|
|
777
|
+
currentStepKey = null;
|
|
778
|
+
emitSnapshot();
|
|
204
779
|
index += 1;
|
|
205
780
|
}
|
|
206
781
|
if (runStatus === "running") {
|
|
207
782
|
runStatus = "succeeded";
|
|
208
|
-
|
|
783
|
+
currentStepKey = null;
|
|
784
|
+
touchRun(true);
|
|
785
|
+
pushEvent("run.completed", { steps: definition.steps.length }, undefined, actor);
|
|
786
|
+
emitSnapshot();
|
|
209
787
|
}
|
|
210
788
|
return {
|
|
211
789
|
runId,
|
|
212
790
|
status: runStatus,
|
|
213
791
|
outputs: globalState,
|
|
214
|
-
stepRuns: definition.steps.map((
|
|
792
|
+
stepRuns: definition.steps.map((step) => stepRuns.get(step.key)),
|
|
215
793
|
events: eventLog.all(),
|
|
216
794
|
};
|
|
217
795
|
}
|