stageflow 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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { collectDownstreamClosure, collectDownstreamStageIds, } from "./dagTraversal.js";
|
|
2
|
+
import { buildCompletedEnvelopesFromRun, buildStageConfigById, } from "./envelopeRouting.js";
|
|
3
|
+
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
4
|
+
import { runStage, isRunStageWaiting } from "./stageRunner.js";
|
|
5
|
+
import { attemptContext } from "./stageAttemptContext.js";
|
|
6
|
+
export function snapshotToScheduleState(snap, overrides, stageId, executionMode) {
|
|
7
|
+
const override = overrides.get(stageId);
|
|
8
|
+
if (override !== undefined)
|
|
9
|
+
return override;
|
|
10
|
+
switch (snap.status) {
|
|
11
|
+
case "succeeded":
|
|
12
|
+
return "succeeded";
|
|
13
|
+
case "failed":
|
|
14
|
+
return "failed";
|
|
15
|
+
case "running":
|
|
16
|
+
return "active";
|
|
17
|
+
case "waiting_for_input":
|
|
18
|
+
return executionMode === "process" ? "waiting" : "active";
|
|
19
|
+
default:
|
|
20
|
+
return "pending";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function hydrateScheduleFromStore(store, runId, dag, executionMode = "process") {
|
|
24
|
+
const states = new Map();
|
|
25
|
+
for (const node of dag.nodes) {
|
|
26
|
+
states.set(node.id, "pending");
|
|
27
|
+
}
|
|
28
|
+
const runDetail = await store.readRun(runId);
|
|
29
|
+
const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages);
|
|
30
|
+
let schedulingHalted = false;
|
|
31
|
+
const overrides = new Map();
|
|
32
|
+
for (const snap of runDetail.stages) {
|
|
33
|
+
const state = snapshotToScheduleState(snap, overrides, snap.stage_id, executionMode);
|
|
34
|
+
states.set(snap.stage_id, state);
|
|
35
|
+
if (state === "failed") {
|
|
36
|
+
schedulingHalted = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
states,
|
|
41
|
+
completedEnvelopes,
|
|
42
|
+
schedulingHalted,
|
|
43
|
+
firstFailureReason: undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function hydrateScheduleForRetryRoots(store, runId, dag, retryStageIds) {
|
|
47
|
+
const runDetail = await store.readRun(runId);
|
|
48
|
+
const downstream = collectDownstreamClosure(dag, retryStageIds);
|
|
49
|
+
const retryRootSet = new Set(retryStageIds);
|
|
50
|
+
const completedEnvelopes = await buildCompletedEnvelopesFromRun(store, runId, runDetail.stages);
|
|
51
|
+
const states = new Map();
|
|
52
|
+
for (const node of dag.nodes) {
|
|
53
|
+
states.set(node.id, "pending");
|
|
54
|
+
}
|
|
55
|
+
const overrides = new Map();
|
|
56
|
+
for (const rootId of retryStageIds) {
|
|
57
|
+
overrides.set(rootId, "pending");
|
|
58
|
+
}
|
|
59
|
+
for (const id of downstream) {
|
|
60
|
+
overrides.set(id, "pending");
|
|
61
|
+
}
|
|
62
|
+
for (const snap of runDetail.stages) {
|
|
63
|
+
const id = snap.stage_id;
|
|
64
|
+
const state = snapshotToScheduleState(snap, overrides, id, "process");
|
|
65
|
+
states.set(id, state);
|
|
66
|
+
if (retryRootSet.has(id) || downstream.has(id)) {
|
|
67
|
+
completedEnvelopes.delete(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
states,
|
|
72
|
+
completedEnvelopes,
|
|
73
|
+
schedulingHalted: false,
|
|
74
|
+
firstFailureReason: undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function hydrateScheduleForRetry(store, runId, dag, retryStageId) {
|
|
78
|
+
return hydrateScheduleForRetryRoots(store, runId, dag, [retryStageId]);
|
|
79
|
+
}
|
|
80
|
+
export async function resumeRun(options) {
|
|
81
|
+
const { prepared, resumeFromStageId, initialPrior } = options;
|
|
82
|
+
const dag = prepared.loaded.dag;
|
|
83
|
+
const downstream = dag.childrenOf[resumeFromStageId];
|
|
84
|
+
if (downstream?.length) {
|
|
85
|
+
return runPipelineDag(options);
|
|
86
|
+
}
|
|
87
|
+
const executionMode = options.executionMode ?? "process";
|
|
88
|
+
const hydrated = await hydrateScheduleFromStore(prepared.store, prepared.run.runId, dag, executionMode);
|
|
89
|
+
hydrated.states.set(resumeFromStageId, "succeeded");
|
|
90
|
+
if (initialPrior !== undefined && initialPrior !== null) {
|
|
91
|
+
hydrated.completedEnvelopes.set(resumeFromStageId, initialPrior);
|
|
92
|
+
}
|
|
93
|
+
const hasActive = [...hydrated.states.values()].some((s) => s === "active");
|
|
94
|
+
if (hasActive) {
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
runDir: prepared.run.workspaceDir,
|
|
98
|
+
runId: prepared.run.runId,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const allTerminal = dag.nodes.every((node) => {
|
|
102
|
+
const state = hydrated.states.get(node.id);
|
|
103
|
+
return state !== undefined && terminalState(state);
|
|
104
|
+
});
|
|
105
|
+
if (allTerminal) {
|
|
106
|
+
const allSucceeded = dag.nodes.every((node) => hydrated.states.get(node.id) === "succeeded");
|
|
107
|
+
await prepared.store.updateRunStatus(prepared.run.runId, allSucceeded ? "succeeded" : "failed");
|
|
108
|
+
return {
|
|
109
|
+
ok: allSucceeded,
|
|
110
|
+
runDir: prepared.run.workspaceDir,
|
|
111
|
+
runId: prepared.run.runId,
|
|
112
|
+
...(allSucceeded
|
|
113
|
+
? {}
|
|
114
|
+
: { reason: hydrated.firstFailureReason ?? "pipeline incomplete" }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return runPipelineDag(options);
|
|
118
|
+
}
|
|
119
|
+
export async function retryRun(options) {
|
|
120
|
+
const { prepared, retryRoots } = options;
|
|
121
|
+
const hydrated = await hydrateScheduleForRetryRoots(prepared.store, prepared.run.runId, prepared.loaded.dag, [...retryRoots.keys()]);
|
|
122
|
+
return runPipelineDag({
|
|
123
|
+
prepared: options.prepared,
|
|
124
|
+
maxActiveStagesPerRun: options.maxActiveStagesPerRun,
|
|
125
|
+
executionMode: options.executionMode,
|
|
126
|
+
stageProcessLauncher: options.stageProcessLauncher,
|
|
127
|
+
initialSchedule: hydrated,
|
|
128
|
+
retryContext: { retryRoots },
|
|
129
|
+
mutationQueue: options.mutationQueue,
|
|
130
|
+
onLoopTick: options.onLoopTick,
|
|
131
|
+
onRetryRootTerminal: options.onRetryRootTerminal,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt) {
|
|
135
|
+
retryRoots.set(stageId, attempt);
|
|
136
|
+
const downstream = collectDownstreamClosure(dag, [stageId]);
|
|
137
|
+
const resetIds = new Set([stageId, ...downstream]);
|
|
138
|
+
for (const id of resetIds) {
|
|
139
|
+
const state = states.get(id);
|
|
140
|
+
if (state === "active" || state === "succeeded") {
|
|
141
|
+
completedEnvelopes.delete(id);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
states.set(id, "pending");
|
|
145
|
+
completedEnvelopes.delete(id);
|
|
146
|
+
}
|
|
147
|
+
clearSchedulingHalt?.();
|
|
148
|
+
}
|
|
149
|
+
export function hasWorkOutsideBranch(dag, branchRootId, states, ctx) {
|
|
150
|
+
const branchIds = new Set([
|
|
151
|
+
branchRootId,
|
|
152
|
+
...collectDownstreamStageIds(dag, branchRootId),
|
|
153
|
+
]);
|
|
154
|
+
for (const node of dag.nodes) {
|
|
155
|
+
if (branchIds.has(node.id))
|
|
156
|
+
continue;
|
|
157
|
+
const state = states.get(node.id);
|
|
158
|
+
if (state === "pending" || state === "active" || state === "waiting") {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (ctx?.pendingDeltaStageIds !== undefined) {
|
|
163
|
+
for (const stageId of ctx.pendingDeltaStageIds) {
|
|
164
|
+
if (!branchIds.has(stageId)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (ctx?.retryRoots !== undefined) {
|
|
170
|
+
for (const [rootId] of ctx.retryRoots) {
|
|
171
|
+
if (branchIds.has(rootId))
|
|
172
|
+
continue;
|
|
173
|
+
const state = states.get(rootId);
|
|
174
|
+
if (state === "pending" || state === "active" || state === "waiting") {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (state === "failed") {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
function terminalState(status) {
|
|
185
|
+
return status === "succeeded" || status === "failed" || status === "skipped";
|
|
186
|
+
}
|
|
187
|
+
function sortRunnableIds(dag, ids) {
|
|
188
|
+
const indexById = new Map(dag.nodes.map((node) => [node.id, node.stageIndex]));
|
|
189
|
+
return ids.slice().sort((a, b) => (indexById.get(a) ?? 0) - (indexById.get(b) ?? 0));
|
|
190
|
+
}
|
|
191
|
+
function isRunnable(dag, stageId, states, schedulingHalted) {
|
|
192
|
+
if (schedulingHalted)
|
|
193
|
+
return false;
|
|
194
|
+
const state = states.get(stageId);
|
|
195
|
+
if (state !== "pending")
|
|
196
|
+
return false;
|
|
197
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
198
|
+
if (!node)
|
|
199
|
+
return false;
|
|
200
|
+
if (node.needs === null)
|
|
201
|
+
return true;
|
|
202
|
+
return states.get(node.needs) === "succeeded";
|
|
203
|
+
}
|
|
204
|
+
export async function runPipelineDag(options) {
|
|
205
|
+
const { prepared, maxActiveStagesPerRun } = options;
|
|
206
|
+
const executionMode = options.executionMode ?? "process";
|
|
207
|
+
const { agent, task, loaded, run, store, checkoutRoot, hitl, cwd, } = prepared;
|
|
208
|
+
const dag = loaded.dag;
|
|
209
|
+
const stageById = buildStageConfigById(loaded);
|
|
210
|
+
let { states, completedEnvelopes, schedulingHalted, firstFailureReason } = options.initialSchedule ??
|
|
211
|
+
(await hydrateScheduleFromStore(store, run.runId, dag, executionMode));
|
|
212
|
+
const retryContext = options.retryContext;
|
|
213
|
+
const resolveLaunchAttempt = async (stageId) => {
|
|
214
|
+
if (retryContext) {
|
|
215
|
+
const preCreatedAttempt = retryContext.retryRoots.get(stageId);
|
|
216
|
+
if (preCreatedAttempt !== undefined) {
|
|
217
|
+
return preCreatedAttempt;
|
|
218
|
+
}
|
|
219
|
+
const execution = await store.createStageExecution(run.runId, stageId);
|
|
220
|
+
return execution.attempt;
|
|
221
|
+
}
|
|
222
|
+
const attemptCount = await store.countStageAttempts(run.runId, stageId);
|
|
223
|
+
if (attemptCount === 0) {
|
|
224
|
+
await store.createStageExecution(run.runId, stageId);
|
|
225
|
+
}
|
|
226
|
+
return 1;
|
|
227
|
+
};
|
|
228
|
+
if (options.resumeFromStageId !== undefined) {
|
|
229
|
+
const resumedId = options.resumeFromStageId;
|
|
230
|
+
states.set(resumedId, "succeeded");
|
|
231
|
+
if (options.initialPrior !== undefined && options.initialPrior !== null) {
|
|
232
|
+
completedEnvelopes.set(resumedId, options.initialPrior);
|
|
233
|
+
}
|
|
234
|
+
else if (!completedEnvelopes.has(resumedId)) {
|
|
235
|
+
try {
|
|
236
|
+
const envelope = await store.readEnvelope(run.runId, resumedId);
|
|
237
|
+
completedEnvelopes.set(resumedId, envelope);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// prior resolution will fail for downstream if envelope missing
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (options.startAtStageIndex !== undefined) {
|
|
245
|
+
const startAt = options.startAtStageIndex;
|
|
246
|
+
for (const node of dag.nodes) {
|
|
247
|
+
if (node.stageIndex < startAt) {
|
|
248
|
+
states.set(node.id, "succeeded");
|
|
249
|
+
if (!completedEnvelopes.has(node.id)) {
|
|
250
|
+
try {
|
|
251
|
+
const envelope = await store.readEnvelope(run.runId, node.id);
|
|
252
|
+
completedEnvelopes.set(node.id, envelope);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// linear shim: upstream envelopes expected on disk
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
let activeCount = 0;
|
|
262
|
+
const inFlight = new Set();
|
|
263
|
+
const notifyRetryRootTerminal = (stageId, outcome) => {
|
|
264
|
+
if (retryContext === undefined)
|
|
265
|
+
return;
|
|
266
|
+
const attempt = retryContext.retryRoots.get(stageId);
|
|
267
|
+
if (attempt === undefined)
|
|
268
|
+
return;
|
|
269
|
+
options.onRetryRootTerminal?.(stageId, attempt, outcome);
|
|
270
|
+
};
|
|
271
|
+
const markSkippedPending = () => {
|
|
272
|
+
for (const node of dag.nodes) {
|
|
273
|
+
if (states.get(node.id) === "pending") {
|
|
274
|
+
states.set(node.id, "skipped");
|
|
275
|
+
notifyRetryRootTerminal(node.id, "skipped");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const markBranchDownstreamSkipped = (stageId) => {
|
|
280
|
+
for (const id of collectDownstreamStageIds(dag, stageId)) {
|
|
281
|
+
if (states.get(id) === "pending") {
|
|
282
|
+
states.set(id, "skipped");
|
|
283
|
+
notifyRetryRootTerminal(id, "skipped");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const outsideWorkContext = () => ({
|
|
288
|
+
retryRoots: retryContext?.retryRoots,
|
|
289
|
+
pendingDeltaStageIds: options.mutationQueue?.pendingStageIds(),
|
|
290
|
+
});
|
|
291
|
+
const drainRetryMutations = () => {
|
|
292
|
+
if (options.mutationQueue === undefined || retryContext === undefined) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
options.mutationQueue.drainPending((stageId, attempt) => {
|
|
296
|
+
applyRetryRootDelta(dag, states, completedEnvelopes, retryContext.retryRoots, stageId, attempt, () => {
|
|
297
|
+
schedulingHalted = false;
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
const onStageFailure = async (stageId, reason) => {
|
|
302
|
+
states.set(stageId, "failed");
|
|
303
|
+
notifyRetryRootTerminal(stageId, "failed");
|
|
304
|
+
if (retryContext) {
|
|
305
|
+
markBranchDownstreamSkipped(stageId);
|
|
306
|
+
drainRetryMutations();
|
|
307
|
+
if (!hasWorkOutsideBranch(dag, stageId, states, outsideWorkContext())) {
|
|
308
|
+
schedulingHalted = true;
|
|
309
|
+
}
|
|
310
|
+
if (firstFailureReason === undefined) {
|
|
311
|
+
firstFailureReason = reason;
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (!schedulingHalted) {
|
|
316
|
+
schedulingHalted = true;
|
|
317
|
+
firstFailureReason = reason;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const onStageSuccess = (stageId, envelope) => {
|
|
321
|
+
states.set(stageId, "succeeded");
|
|
322
|
+
completedEnvelopes.set(stageId, envelope);
|
|
323
|
+
notifyRetryRootTerminal(stageId, "succeeded");
|
|
324
|
+
};
|
|
325
|
+
const launchStage = async (stageId) => {
|
|
326
|
+
const stage = stageById.get(stageId);
|
|
327
|
+
if (!stage) {
|
|
328
|
+
await onStageFailure(stageId, `stage config missing for "${stageId}"`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const attempt = await resolveLaunchAttempt(stageId);
|
|
332
|
+
const attemptCtx = attemptContext(attempt);
|
|
333
|
+
if (executionMode === "process") {
|
|
334
|
+
const launcher = options.stageProcessLauncher;
|
|
335
|
+
if (!launcher) {
|
|
336
|
+
await onStageFailure(stageId, "stage process launcher is not configured");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const launchResult = await launcher.launch({
|
|
340
|
+
runId: run.runId,
|
|
341
|
+
stageId,
|
|
342
|
+
rootDir: cwd,
|
|
343
|
+
attempt,
|
|
344
|
+
...(prepared.operatorCatalog !== undefined
|
|
345
|
+
? { operatorCatalog: prepared.operatorCatalog }
|
|
346
|
+
: {}),
|
|
347
|
+
});
|
|
348
|
+
if (launchResult.type === "succeeded") {
|
|
349
|
+
try {
|
|
350
|
+
const envelope = await store.readEnvelope(run.runId, stageId);
|
|
351
|
+
onStageSuccess(stageId, envelope);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
states.set(stageId, "succeeded");
|
|
355
|
+
notifyRetryRootTerminal(stageId, "succeeded");
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (launchResult.type === "waiting") {
|
|
360
|
+
states.set(stageId, "waiting");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
await onStageFailure(stageId, launchResult.reason);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const result = await runStage({
|
|
367
|
+
agent,
|
|
368
|
+
store,
|
|
369
|
+
runId: run.runId,
|
|
370
|
+
stage,
|
|
371
|
+
task,
|
|
372
|
+
dag,
|
|
373
|
+
checkoutRoot,
|
|
374
|
+
workspaceDir: run.workspaceDir,
|
|
375
|
+
hitl,
|
|
376
|
+
attemptCtx,
|
|
377
|
+
factoryCwd: cwd,
|
|
378
|
+
operatorCatalog: prepared.operatorCatalog,
|
|
379
|
+
completedEnvelopes,
|
|
380
|
+
});
|
|
381
|
+
if (isRunStageWaiting(result)) {
|
|
382
|
+
await onStageFailure(stageId, WAIT_WITHOUT_WORKER_DISPATCH);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!result.ok) {
|
|
386
|
+
await onStageFailure(stageId, result.reason ?? "stage failed");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (result.envelope) {
|
|
390
|
+
onStageSuccess(stageId, result.envelope);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
states.set(stageId, "succeeded");
|
|
394
|
+
notifyRetryRootTerminal(stageId, "succeeded");
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const startStage = (stageId) => {
|
|
398
|
+
if (states.get(stageId) !== "pending")
|
|
399
|
+
return;
|
|
400
|
+
states.set(stageId, "active");
|
|
401
|
+
activeCount += 1;
|
|
402
|
+
const taskPromise = launchStage(stageId).finally(() => {
|
|
403
|
+
activeCount -= 1;
|
|
404
|
+
inFlight.delete(taskPromise);
|
|
405
|
+
});
|
|
406
|
+
inFlight.add(taskPromise);
|
|
407
|
+
};
|
|
408
|
+
const pickRunnable = () => {
|
|
409
|
+
const ids = [];
|
|
410
|
+
for (const node of dag.nodes) {
|
|
411
|
+
if (isRunnable(dag, node.id, states, schedulingHalted)) {
|
|
412
|
+
ids.push(node.id);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return sortRunnableIds(dag, ids);
|
|
416
|
+
};
|
|
417
|
+
const allTerminal = () => {
|
|
418
|
+
for (const node of dag.nodes) {
|
|
419
|
+
const state = states.get(node.id);
|
|
420
|
+
if (state === undefined || !terminalState(state)) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
};
|
|
426
|
+
while (!allTerminal()) {
|
|
427
|
+
drainRetryMutations();
|
|
428
|
+
if (options.onLoopTick !== undefined) {
|
|
429
|
+
await options.onLoopTick();
|
|
430
|
+
}
|
|
431
|
+
if (!schedulingHalted) {
|
|
432
|
+
const runnable = pickRunnable();
|
|
433
|
+
for (const stageId of runnable) {
|
|
434
|
+
if (activeCount >= maxActiveStagesPerRun)
|
|
435
|
+
break;
|
|
436
|
+
startStage(stageId);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (inFlight.size === 0) {
|
|
440
|
+
if (schedulingHalted) {
|
|
441
|
+
markSkippedPending();
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
const runnable = pickRunnable();
|
|
445
|
+
if (runnable.length === 0) {
|
|
446
|
+
const hasWaiting = [...states.values()].some((s) => s === "waiting");
|
|
447
|
+
if (hasWaiting) {
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
await Promise.race([
|
|
455
|
+
...inFlight,
|
|
456
|
+
new Promise((resolve) => setTimeout(resolve, 25)),
|
|
457
|
+
]);
|
|
458
|
+
}
|
|
459
|
+
if (inFlight.size > 0) {
|
|
460
|
+
await Promise.all(inFlight);
|
|
461
|
+
}
|
|
462
|
+
const hasWaiting = [...states.values()].some((s) => s === "waiting");
|
|
463
|
+
if (hasWaiting && !schedulingHalted) {
|
|
464
|
+
return { ok: true, runDir: run.workspaceDir, runId: run.runId };
|
|
465
|
+
}
|
|
466
|
+
if (schedulingHalted) {
|
|
467
|
+
markSkippedPending();
|
|
468
|
+
if (retryContext === undefined) {
|
|
469
|
+
await store.updateRunStatus(run.runId, "failed");
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
runDir: run.workspaceDir,
|
|
474
|
+
runId: run.runId,
|
|
475
|
+
reason: firstFailureReason,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const allSucceeded = dag.nodes.every((node) => states.get(node.id) === "succeeded");
|
|
479
|
+
if (!allSucceeded) {
|
|
480
|
+
if (retryContext === undefined) {
|
|
481
|
+
await store.updateRunStatus(run.runId, "failed");
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
runDir: run.workspaceDir,
|
|
486
|
+
runId: run.runId,
|
|
487
|
+
reason: firstFailureReason ?? "pipeline incomplete",
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (retryContext === undefined) {
|
|
491
|
+
await store.updateRunStatus(run.runId, "succeeded");
|
|
492
|
+
}
|
|
493
|
+
return { ok: true, runDir: run.workspaceDir, runId: run.runId };
|
|
494
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AgentPort, OpaqueAnswer } from "../agent/port.js";
|
|
2
|
+
import type { RunStore, RunMeta } from "../runstore/port.js";
|
|
3
|
+
import type { LoadedPipeline } from "../types/pipeline.js";
|
|
4
|
+
import type { TaskFile } from "../types/task.js";
|
|
5
|
+
import type { StageExecutionMode } from "./stageConcurrency.js";
|
|
6
|
+
import type { StageProcessLauncher } from "./stageProcessLauncher.js";
|
|
7
|
+
import { StageHitlController } from "./stageHitl.js";
|
|
8
|
+
import { type OperatorCatalog } from "./stageAttemptBootstrap.js";
|
|
9
|
+
export type PreparedResumeContext = {
|
|
10
|
+
runId: string;
|
|
11
|
+
stageId: string;
|
|
12
|
+
opaqueAnswer: OpaqueAnswer;
|
|
13
|
+
agent: AgentPort;
|
|
14
|
+
store: RunStore;
|
|
15
|
+
hitl: StageHitlController;
|
|
16
|
+
executionMode: StageExecutionMode;
|
|
17
|
+
stageProcessLauncher?: StageProcessLauncher;
|
|
18
|
+
cwd: string;
|
|
19
|
+
maxActiveStagesPerRun: number;
|
|
20
|
+
operatorCatalog?: OperatorCatalog;
|
|
21
|
+
};
|
|
22
|
+
export type LoadedRunContext = {
|
|
23
|
+
meta: RunMeta;
|
|
24
|
+
taskYaml: string;
|
|
25
|
+
task: TaskFile;
|
|
26
|
+
loaded: LoadedPipeline;
|
|
27
|
+
workspaceDir: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function loadRunContext(store: RunStore, runId: string, cwd: string): Promise<LoadedRunContext>;
|
|
30
|
+
export declare function reconstructAndContinue(ctx: PreparedResumeContext): Promise<{
|
|
31
|
+
ok: boolean;
|
|
32
|
+
reason?: string;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { fakeHitlResumePath } from "../agent/fakeAgent.js";
|
|
3
|
+
import { loadPipeline } from "../config/loadPipeline.js";
|
|
4
|
+
import { loadTaskFromYaml } from "../config/loadTask.js";
|
|
5
|
+
import { WAIT_WITHOUT_WORKER_DISPATCH } from "./answerResume.js";
|
|
6
|
+
import { resumeRun } from "./pipelineScheduler.js";
|
|
7
|
+
import { buildStageRoots, withResolvedAuthPath } from "./stageRoots.js";
|
|
8
|
+
import { runStage, isRunStageWaiting } from "./stageRunner.js";
|
|
9
|
+
import { attemptContext } from "./stageAttemptContext.js";
|
|
10
|
+
import { openStageAttempt, } from "./stageAttemptBootstrap.js";
|
|
11
|
+
export async function loadRunContext(store, runId, cwd) {
|
|
12
|
+
const meta = await store.readRunMeta(runId);
|
|
13
|
+
const taskYaml = await store.readTaskYaml(runId);
|
|
14
|
+
const task = loadTaskFromYaml(taskYaml, `run ${runId} task`);
|
|
15
|
+
const loaded = await loadPipeline(meta.pipeline_id, { cwd });
|
|
16
|
+
const workspaceDir = store.getWorkspaceDir(runId);
|
|
17
|
+
return { meta, taskYaml, task, loaded, workspaceDir };
|
|
18
|
+
}
|
|
19
|
+
export async function reconstructAndContinue(ctx) {
|
|
20
|
+
const { runId, stageId, opaqueAnswer, agent, store, cwd } = ctx;
|
|
21
|
+
try {
|
|
22
|
+
const latestExecution = await store.getLatestStageExecution(runId, stageId);
|
|
23
|
+
const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
|
|
24
|
+
const attemptOpt = attemptCtx.eventOptions();
|
|
25
|
+
const { meta, task, loaded } = await loadRunContext(store, runId, cwd);
|
|
26
|
+
const stageIndex = loaded.stages.findIndex((s) => s.id === stageId);
|
|
27
|
+
if (stageIndex < 0) {
|
|
28
|
+
const reason = `Stage ${stageId} not in pipeline ${meta.pipeline_id}`;
|
|
29
|
+
await store.appendStageEvent(runId, stageId, {
|
|
30
|
+
event: "failed",
|
|
31
|
+
reason,
|
|
32
|
+
}, attemptOpt);
|
|
33
|
+
await store.updateRunStatus(runId, "failed");
|
|
34
|
+
return { ok: false, reason };
|
|
35
|
+
}
|
|
36
|
+
const stage = loaded.stages[stageIndex];
|
|
37
|
+
const workspaceDir = store.getWorkspaceDir(runId);
|
|
38
|
+
const checkoutRoot = meta.checkout_root;
|
|
39
|
+
const roots = withResolvedAuthPath(buildStageRoots(workspaceDir, stageId, checkoutRoot, attemptCtx), cwd);
|
|
40
|
+
const fakeResume = fakeHitlResumePath(roots, stageId);
|
|
41
|
+
const piSession = attemptCtx.sessionPath(workspaceDir, stageId);
|
|
42
|
+
if (!existsSync(fakeResume) && !existsSync(piSession)) {
|
|
43
|
+
const reason = "missing resume context for waiting stage";
|
|
44
|
+
await store.appendStageEvent(runId, stageId, {
|
|
45
|
+
event: "failed",
|
|
46
|
+
reason,
|
|
47
|
+
}, attemptOpt);
|
|
48
|
+
await store.updateRunStatus(runId, "failed");
|
|
49
|
+
return { ok: false, reason };
|
|
50
|
+
}
|
|
51
|
+
const opened = await openStageAttempt({
|
|
52
|
+
agent,
|
|
53
|
+
store,
|
|
54
|
+
runId,
|
|
55
|
+
stage,
|
|
56
|
+
task,
|
|
57
|
+
dag: loaded.dag,
|
|
58
|
+
checkoutRoot,
|
|
59
|
+
workspaceDir,
|
|
60
|
+
factoryCwd: cwd,
|
|
61
|
+
attemptCtx,
|
|
62
|
+
operatorCatalog: ctx.operatorCatalog,
|
|
63
|
+
onActivity: (event) => {
|
|
64
|
+
void store.appendStageEvent(runId, stageId, event, attemptOpt);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (!opened.ok) {
|
|
68
|
+
await store.appendStageEvent(runId, stageId, {
|
|
69
|
+
event: "failed",
|
|
70
|
+
reason: opened.reason,
|
|
71
|
+
}, attemptOpt);
|
|
72
|
+
await store.updateRunStatus(runId, "failed");
|
|
73
|
+
return { ok: false, reason: opened.reason };
|
|
74
|
+
}
|
|
75
|
+
opened.handle.deliverAnswer(opaqueAnswer);
|
|
76
|
+
const result = await runStage({
|
|
77
|
+
agent,
|
|
78
|
+
store,
|
|
79
|
+
runId,
|
|
80
|
+
stage,
|
|
81
|
+
task,
|
|
82
|
+
dag: loaded.dag,
|
|
83
|
+
checkoutRoot,
|
|
84
|
+
workspaceDir,
|
|
85
|
+
hitl: ctx.hitl,
|
|
86
|
+
skipStarted: true,
|
|
87
|
+
existingHandle: opened.handle,
|
|
88
|
+
attemptCtx,
|
|
89
|
+
factoryCwd: cwd,
|
|
90
|
+
operatorCatalog: ctx.operatorCatalog,
|
|
91
|
+
});
|
|
92
|
+
if (isRunStageWaiting(result)) {
|
|
93
|
+
await store.updateRunStatus(runId, "failed");
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: WAIT_WITHOUT_WORKER_DISPATCH,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
await store.updateRunStatus(runId, "failed");
|
|
101
|
+
return { ok: false, reason: result.reason };
|
|
102
|
+
}
|
|
103
|
+
const rest = await resumeRun({
|
|
104
|
+
prepared: {
|
|
105
|
+
task,
|
|
106
|
+
loaded,
|
|
107
|
+
run: { runId, workspaceDir },
|
|
108
|
+
agent,
|
|
109
|
+
store,
|
|
110
|
+
cwd,
|
|
111
|
+
checkoutRoot,
|
|
112
|
+
hitl: ctx.hitl,
|
|
113
|
+
operatorCatalog: ctx.operatorCatalog,
|
|
114
|
+
},
|
|
115
|
+
maxActiveStagesPerRun: ctx.maxActiveStagesPerRun,
|
|
116
|
+
resumeFromStageId: stageId,
|
|
117
|
+
initialPrior: result.envelope ?? null,
|
|
118
|
+
executionMode: ctx.executionMode,
|
|
119
|
+
stageProcessLauncher: ctx.stageProcessLauncher,
|
|
120
|
+
});
|
|
121
|
+
return rest.ok
|
|
122
|
+
? { ok: true }
|
|
123
|
+
: { ok: false, reason: rest.reason };
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const reason = err instanceof Error
|
|
127
|
+
? err.message
|
|
128
|
+
: `resume failed: ${String(err)}`;
|
|
129
|
+
try {
|
|
130
|
+
const latestExecution = await store.getLatestStageExecution(runId, stageId);
|
|
131
|
+
const attemptCtx = attemptContext(latestExecution?.attempt ?? 1);
|
|
132
|
+
const attemptOpt = attemptCtx.eventOptions();
|
|
133
|
+
await store.appendStageEvent(runId, stageId, {
|
|
134
|
+
event: "failed",
|
|
135
|
+
reason,
|
|
136
|
+
}, attemptOpt);
|
|
137
|
+
await store.updateRunStatus(runId, "failed");
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// ignore secondary failures
|
|
141
|
+
}
|
|
142
|
+
return { ok: false, reason };
|
|
143
|
+
}
|
|
144
|
+
}
|