stageflow 0.9.0 → 0.12.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 +1 -0
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +301 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +6 -3
- package/dist/agent/piAdapter.js +129 -21
- package/dist/agent/port.d.ts +50 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +10 -1
- package/dist/config/loadStage.js +138 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +49 -6
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +16 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +79 -1
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +29 -0
- package/dist/runtime/verifiedStageExecution.js +119 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +114 -0
- package/dist/server/operatorResults.js +2 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +39 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +13 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-CXGz4nL7.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +2 -1
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findUnhandledFailedStage, } from "../runstore/port.js";
|
|
1
2
|
import { retryRun, } from "./pipelineScheduler.js";
|
|
2
3
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
3
4
|
import { syncRunStatusFromStages } from "./stageRecovery.js";
|
|
@@ -19,6 +20,20 @@ export class RetryRootWaitTimeoutError extends Error {
|
|
|
19
20
|
this.name = "RetryRootWaitTimeoutError";
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
export function succeededStageRetryBlocker(stageSnap) {
|
|
24
|
+
if (stageSnap.definition_id !== undefined &&
|
|
25
|
+
stageSnap.definition_id !== stageSnap.stage_id) {
|
|
26
|
+
return `Cannot re-run a succeeded clone instance stage (stage=${stageSnap.stage_id}, definition=${stageSnap.definition_id}): clone instances are created by upstream fan-out and cannot be rebuilt in place. Use rerun to start a new run.`;
|
|
27
|
+
}
|
|
28
|
+
const forks = stageSnap.envelope?.clone_forks;
|
|
29
|
+
if (Array.isArray(forks) && forks.length > 0) {
|
|
30
|
+
return `Cannot re-run a succeeded clonable-fanout stage (stage=${stageSnap.stage_id}): its envelope declared clone_forks and existing clone instances cannot be pruned. Use rerun to start a new run.`;
|
|
31
|
+
}
|
|
32
|
+
if (stageSnap.envelope?.fork_choice !== undefined) {
|
|
33
|
+
return `Cannot re-run a succeeded fork stage (stage=${stageSnap.stage_id}): its envelope recorded a fork_choice and skipped branches cannot be restored. Use rerun to start a new run.`;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
22
37
|
export function assertStageRetryEligible(detail, stageId, opts) {
|
|
23
38
|
const stageSnap = detail.stages.find((s) => s.stage_id === stageId);
|
|
24
39
|
if (!stageSnap) {
|
|
@@ -35,23 +50,34 @@ export function assertStageRetryEligible(detail, stageId, opts) {
|
|
|
35
50
|
status: 409,
|
|
36
51
|
};
|
|
37
52
|
}
|
|
53
|
+
const runStatus = opts?.persistedStatus ?? detail.status;
|
|
38
54
|
const recoveryActive = opts?.recoveryActive === true;
|
|
39
|
-
if (
|
|
40
|
-
|
|
55
|
+
if (runStatus !== "failed" && runStatus !== "succeeded") {
|
|
56
|
+
const acceptedPendingRetry = runStatus === "running" &&
|
|
57
|
+
stageSnap.status === "failed" &&
|
|
58
|
+
findUnhandledFailedStage(detail.stages, opts?.dag) === undefined &&
|
|
59
|
+
detail.stages.some((stage) => stage.status === "pending");
|
|
60
|
+
if (!(runStatus === "running" && recoveryActive) && !acceptedPendingRetry) {
|
|
41
61
|
return {
|
|
42
62
|
ok: false,
|
|
43
|
-
reason: `Run is not failed (status=${
|
|
63
|
+
reason: `Run is not failed or succeeded (status=${runStatus})`,
|
|
44
64
|
status: 409,
|
|
45
65
|
};
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
|
-
if (stageSnap.status !== "failed") {
|
|
68
|
+
if (stageSnap.status !== "failed" && stageSnap.status !== "succeeded") {
|
|
49
69
|
return {
|
|
50
70
|
ok: false,
|
|
51
|
-
reason: `Stage is not failed (status=${stageSnap.status})`,
|
|
71
|
+
reason: `Stage is not failed or succeeded (status=${stageSnap.status})`,
|
|
52
72
|
status: 409,
|
|
53
73
|
};
|
|
54
74
|
}
|
|
75
|
+
if (stageSnap.status === "succeeded") {
|
|
76
|
+
const blocker = succeededStageRetryBlocker(stageSnap);
|
|
77
|
+
if (blocker !== undefined) {
|
|
78
|
+
return { ok: false, reason: blocker, status: 409 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
55
81
|
return { ok: true };
|
|
56
82
|
}
|
|
57
83
|
function mutationKey(stageId, attempt) {
|
|
@@ -60,7 +86,7 @@ function mutationKey(stageId, attempt) {
|
|
|
60
86
|
function rootWaitKey(runId, stageId, attempt) {
|
|
61
87
|
return `${runId}\0${stageId}\0${attempt}`;
|
|
62
88
|
}
|
|
63
|
-
function readRetryRootWaitTimeoutMs() {
|
|
89
|
+
export function readRetryRootWaitTimeoutMs() {
|
|
64
90
|
const raw = process.env.STAGEFLOW_RETRY_ROOT_WAIT_TIMEOUT_MS;
|
|
65
91
|
if (raw !== undefined && raw !== "") {
|
|
66
92
|
const parsed = Number(raw);
|
|
@@ -225,8 +251,13 @@ export class RunRetryCoordinator {
|
|
|
225
251
|
async retryStage(req) {
|
|
226
252
|
const { runId, stageId, store, tracking } = req;
|
|
227
253
|
let detail;
|
|
254
|
+
let metaStatus;
|
|
255
|
+
let dag;
|
|
228
256
|
try {
|
|
229
257
|
detail = await store.readRun(runId);
|
|
258
|
+
const meta = await store.readRunMeta(runId);
|
|
259
|
+
metaStatus = meta.status ?? detail.status;
|
|
260
|
+
dag = meta.pipeline_dag;
|
|
230
261
|
}
|
|
231
262
|
catch {
|
|
232
263
|
return { ok: false, reason: `Run not found: ${runId}`, status: 404 };
|
|
@@ -234,6 +265,8 @@ export class RunRetryCoordinator {
|
|
|
234
265
|
const recoveryActive = this.isActive(runId);
|
|
235
266
|
const eligibility = assertStageRetryEligible(detail, stageId, {
|
|
236
267
|
recoveryActive,
|
|
268
|
+
persistedStatus: metaStatus,
|
|
269
|
+
...(dag !== undefined ? { dag } : {}),
|
|
237
270
|
});
|
|
238
271
|
if (!eligibility.ok) {
|
|
239
272
|
return {
|
|
@@ -258,7 +291,7 @@ export class RunRetryCoordinator {
|
|
|
258
291
|
status: 409,
|
|
259
292
|
};
|
|
260
293
|
}
|
|
261
|
-
const priorRunStatus =
|
|
294
|
+
const priorRunStatus = metaStatus;
|
|
262
295
|
let insertedForResume = false;
|
|
263
296
|
let bumpedRunning = false;
|
|
264
297
|
try {
|
|
@@ -272,7 +305,7 @@ export class RunRetryCoordinator {
|
|
|
272
305
|
};
|
|
273
306
|
}
|
|
274
307
|
insertedForResume = tracked.insertedForResume === true;
|
|
275
|
-
if (priorRunStatus === "failed") {
|
|
308
|
+
if (priorRunStatus === "failed" || priorRunStatus === "succeeded") {
|
|
276
309
|
await store.updateRunStatus(runId, "running");
|
|
277
310
|
bumpedRunning = true;
|
|
278
311
|
}
|
|
@@ -286,6 +319,7 @@ export class RunRetryCoordinator {
|
|
|
286
319
|
}
|
|
287
320
|
}
|
|
288
321
|
const execution = await store.createStageExecution(runId, stageId);
|
|
322
|
+
await req.beforeAttemptStart?.(execution.attempt);
|
|
289
323
|
const { meta, task, loaded, workspaceDir } = await loadRunContext(store, runId, req.cwd);
|
|
290
324
|
const prepared = {
|
|
291
325
|
task,
|
|
@@ -298,7 +332,7 @@ export class RunRetryCoordinator {
|
|
|
298
332
|
hitl: req.hitl,
|
|
299
333
|
operatorCatalog: req.operatorCatalog,
|
|
300
334
|
};
|
|
301
|
-
if (
|
|
335
|
+
if (this.isActive(runId)) {
|
|
302
336
|
await this.addRoot(runId, stageId, execution.attempt);
|
|
303
337
|
}
|
|
304
338
|
else {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentPort, StageHandle, StageRunInput } from "../agent/port.js";
|
|
1
|
+
import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunInput, StageSessionMode } from "../agent/port.js";
|
|
2
2
|
import type { RunStore } from "../runstore/port.js";
|
|
3
3
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
4
4
|
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
@@ -25,9 +25,11 @@ export type StageAttemptOpenInput = {
|
|
|
25
25
|
onActivity?: StageRunInput["onActivity"];
|
|
26
26
|
roots?: StageRoots;
|
|
27
27
|
resumeToken?: string;
|
|
28
|
+
sessionMode?: StageSessionMode;
|
|
28
29
|
workerRoots?: boolean;
|
|
29
30
|
completedEnvelopes?: Map<string, StageEnvelope>;
|
|
30
31
|
stageId?: string;
|
|
32
|
+
feedbackLoopContext?: FeedbackLoopContext;
|
|
31
33
|
};
|
|
32
34
|
export type StageAttemptOpenResult = {
|
|
33
35
|
ok: true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveSkillByName } from "../config/listSkills.js";
|
|
2
2
|
import { resolveCloneEmitContext, resolveForkEmitContext } from "../config/resolveForkEmitContext.js";
|
|
3
|
+
import { createAttemptQaTrailReader } from "../hitl/qaTrail.js";
|
|
3
4
|
import { buildCompletedEnvelopesFromRun, resolvePriorEnvelope, } from "./envelopeRouting.js";
|
|
4
5
|
import { resumeSessionFilePath } from "./stageAttemptContext.js";
|
|
5
6
|
import { buildStageRoots, rootsForStageWorker, withResolvedAuthPath, } from "./stageRoots.js";
|
|
@@ -48,6 +49,52 @@ function resolveAttemptRoots(input, stageId) {
|
|
|
48
49
|
? withResolvedAuthPath(baseRoots, input.factoryCwd)
|
|
49
50
|
: baseRoots;
|
|
50
51
|
}
|
|
52
|
+
function evidencePreview(evidence) {
|
|
53
|
+
if (evidence === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const text = JSON.stringify(evidence);
|
|
57
|
+
return text.length <= 4_000 ? text : `${text.slice(0, 4_000)}…`;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "Verification evidence could not be serialized.";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function repairContextForAttempt(input, stageId, attempt) {
|
|
64
|
+
if (attempt <= 1)
|
|
65
|
+
return undefined;
|
|
66
|
+
const recovery = input.dag.nodes.find((node) => node.id === stageId)?.recovery;
|
|
67
|
+
if (recovery?.mode !== "repair" && recovery?.mode !== "manual") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const events = await input.store.listStageEvents(input.runId, stageId, attempt);
|
|
71
|
+
const guidance = events
|
|
72
|
+
.slice()
|
|
73
|
+
.reverse()
|
|
74
|
+
.find((event) => event.event === "manual_recovery_requested")?.guidance;
|
|
75
|
+
if (recovery.mode === "manual" && typeof guidance !== "string") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const failed = (await input.store.listVerificationCheckResults(input.runId, stageId, attempt - 1)).filter((check) => check.status === "failed");
|
|
79
|
+
return {
|
|
80
|
+
prior_attempt: attempt - 1,
|
|
81
|
+
...(typeof guidance === "string" && guidance.trim() !== ""
|
|
82
|
+
? { operator_guidance: guidance.trim() }
|
|
83
|
+
: {}),
|
|
84
|
+
...(recovery.include_failed_checks !== false
|
|
85
|
+
? {
|
|
86
|
+
failed_checks: failed.map((check) => {
|
|
87
|
+
const preview = evidencePreview(check.evidence);
|
|
88
|
+
return {
|
|
89
|
+
id: check.check_id,
|
|
90
|
+
type: check.check_type,
|
|
91
|
+
...(preview !== undefined ? { evidence_preview: preview } : {}),
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
51
98
|
export async function openStageAttempt(input) {
|
|
52
99
|
const stageId = input.stageId ?? input.stage.id;
|
|
53
100
|
const definitionId = input.stage.id;
|
|
@@ -67,7 +114,24 @@ export async function openStageAttempt(input) {
|
|
|
67
114
|
const resumeToken = input.resumeToken ??
|
|
68
115
|
resumeSessionFilePath(input.workspaceDir, stageId, attempt);
|
|
69
116
|
const forkEmitContext = resolveForkEmitContext(input.dag, definitionId);
|
|
70
|
-
const
|
|
117
|
+
const snapshot = input.dag;
|
|
118
|
+
const cloneEmitContext = resolveCloneEmitContext(input.dag, definitionId, {
|
|
119
|
+
...(input.stage.clone_actions !== undefined
|
|
120
|
+
? { allowedActions: input.stage.clone_actions }
|
|
121
|
+
: {}),
|
|
122
|
+
...(snapshot.clone_input_schema !== undefined
|
|
123
|
+
? { successorCloneInputSchemas: snapshot.clone_input_schema }
|
|
124
|
+
: {}),
|
|
125
|
+
});
|
|
126
|
+
const completionContract = input.dag.nodes.find((node) => node.id === stageId)?.completion;
|
|
127
|
+
const runtimeNode = input.dag.nodes.find((node) => node.id === stageId);
|
|
128
|
+
const isDynamicCloneInstance = runtimeNode?.definition_id !== undefined &&
|
|
129
|
+
runtimeNode.definition_id !== runtimeNode.id;
|
|
130
|
+
const feedbackLoopEmitContext = isDynamicCloneInstance || runtimeNode?.clonable === true
|
|
131
|
+
? undefined
|
|
132
|
+
: runtimeNode?.feedback_loop;
|
|
133
|
+
const repairContext = await repairContextForAttempt(input, stageId, attempt);
|
|
134
|
+
const readQaTrail = createAttemptQaTrailReader(input.store, input.runId, stageId, attempt);
|
|
71
135
|
const opened = await openStageWithOperatorCatalog(input.agent, {
|
|
72
136
|
roots,
|
|
73
137
|
stage: input.stage,
|
|
@@ -77,10 +141,24 @@ export async function openStageAttempt(input) {
|
|
|
77
141
|
...(priorResult.joinPriors !== undefined
|
|
78
142
|
? { priorEnvelopes: priorResult.joinPriors }
|
|
79
143
|
: {}),
|
|
144
|
+
...(priorResult.priorEnvelopesByStage !== undefined
|
|
145
|
+
? { priorEnvelopesByStage: priorResult.priorEnvelopesByStage }
|
|
146
|
+
: {}),
|
|
80
147
|
resumeToken,
|
|
148
|
+
...(input.sessionMode !== undefined ? { sessionMode: input.sessionMode } : {}),
|
|
81
149
|
onActivity: input.onActivity,
|
|
82
150
|
forkEmitContext,
|
|
83
151
|
cloneEmitContext,
|
|
152
|
+
...(feedbackLoopEmitContext !== undefined ? { feedbackLoopEmitContext } : {}),
|
|
153
|
+
...(input.feedbackLoopContext !== undefined
|
|
154
|
+
? { feedbackLoopContext: input.feedbackLoopContext }
|
|
155
|
+
: {}),
|
|
156
|
+
...(completionContract !== undefined ? { completionContract } : {}),
|
|
157
|
+
...(repairContext !== undefined ? { repairContext } : {}),
|
|
158
|
+
readQaTrail,
|
|
159
|
+
...(input.stage.timeout_ms !== undefined
|
|
160
|
+
? { timeoutMs: input.stage.timeout_ms }
|
|
161
|
+
: {}),
|
|
84
162
|
}, input.operatorCatalog);
|
|
85
163
|
if (!opened.ok)
|
|
86
164
|
return opened;
|
|
@@ -38,7 +38,7 @@ export async function syncRunStatusFromStages(store, runId) {
|
|
|
38
38
|
if (meta.status === "succeeded")
|
|
39
39
|
return;
|
|
40
40
|
const run = await store.readRun(runId);
|
|
41
|
-
const derived = deriveStatusFromStages(run.stages);
|
|
41
|
+
const derived = deriveStatusFromStages(run.stages, meta.pipeline_dag);
|
|
42
42
|
if ((meta.status ?? "created") !== derived) {
|
|
43
43
|
await store.updateRunStatus(runId, derived);
|
|
44
44
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentPort, StageHandle, StageRunResult } from "../agent/port.js";
|
|
1
|
+
import type { AgentPort, FeedbackLoopContext, StageHandle, StageRunResult, StageSessionMode } from "../agent/port.js";
|
|
2
2
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
3
3
|
import type { StageConfig } from "../types/stage.js";
|
|
4
4
|
import type { TaskFile } from "../types/task.js";
|
|
@@ -45,6 +45,9 @@ export type RunStageOptions = {
|
|
|
45
45
|
completedEnvelopes?: Map<string, StageEnvelope>;
|
|
46
46
|
skipGates?: boolean;
|
|
47
47
|
stageId?: string;
|
|
48
|
+
sessionMode?: StageSessionMode;
|
|
49
|
+
feedbackLoopContext?: FeedbackLoopContext;
|
|
50
|
+
resumeToken?: string;
|
|
48
51
|
};
|
|
49
52
|
/**
|
|
50
53
|
* Yield loop: next() until completed; on wait, coordinator enterWait (KTD6).
|
|
@@ -2,6 +2,7 @@ import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
|
|
|
2
2
|
import { buildStageRoots, withResolvedAuthPath, } from "./stageRoots.js";
|
|
3
3
|
import { createDefaultHitlQaHooks, recordWaitEnter, } from "./stageHitl.js";
|
|
4
4
|
import { openStageAttempt, } from "./stageAttemptBootstrap.js";
|
|
5
|
+
import { createVerifiedStageExecution } from "./verifiedStageExecution.js";
|
|
5
6
|
export function isRunStageWaiting(outcome) {
|
|
6
7
|
return "waiting" in outcome && outcome.waiting === true;
|
|
7
8
|
}
|
|
@@ -113,7 +114,7 @@ export async function runStageYieldLoop(options) {
|
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
export async function runStage(options) {
|
|
116
|
-
const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, } = options;
|
|
117
|
+
const { agent, store, runId, stage, task, dag, checkoutRoot, workspaceDir, hitl, skipStarted, existingHandle, workerMode, roots: rootsOverride, attemptCtx, factoryCwd, operatorCatalog, completedEnvelopes, skipGates, sessionMode, feedbackLoopContext, resumeToken, } = options;
|
|
117
118
|
const stageId = options.stageId ?? stage.id;
|
|
118
119
|
const attemptOpt = attemptCtx?.eventOptions();
|
|
119
120
|
const baseRoots = rootsOverride ??
|
|
@@ -133,6 +134,16 @@ export async function runStage(options) {
|
|
|
133
134
|
attemptCtx,
|
|
134
135
|
});
|
|
135
136
|
}
|
|
137
|
+
const verifiedExecution = createVerifiedStageExecution({
|
|
138
|
+
store,
|
|
139
|
+
runId,
|
|
140
|
+
stageId,
|
|
141
|
+
attempt,
|
|
142
|
+
stage,
|
|
143
|
+
dag,
|
|
144
|
+
roots,
|
|
145
|
+
});
|
|
146
|
+
await verifiedExecution.prepare();
|
|
136
147
|
console.error(`Running stage ${stageId} (${stage.model})...`);
|
|
137
148
|
let activityChain = Promise.resolve();
|
|
138
149
|
const enqueueActivity = (event) => {
|
|
@@ -152,7 +163,7 @@ export async function runStage(options) {
|
|
|
152
163
|
task,
|
|
153
164
|
dag: dag ?? {
|
|
154
165
|
nodes: [
|
|
155
|
-
{ id: stageId, needs: null, ancestors: [], stageIndex: 0 },
|
|
166
|
+
{ id: stageId, needs: null, needsEdges: [], ancestors: [], stageIndex: 0 },
|
|
156
167
|
],
|
|
157
168
|
roots: [stageId],
|
|
158
169
|
childrenOf: {},
|
|
@@ -164,6 +175,9 @@ export async function runStage(options) {
|
|
|
164
175
|
operatorCatalog,
|
|
165
176
|
roots,
|
|
166
177
|
completedEnvelopes,
|
|
178
|
+
...(sessionMode !== undefined ? { sessionMode } : {}),
|
|
179
|
+
...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
|
|
180
|
+
...(resumeToken !== undefined ? { resumeToken } : {}),
|
|
167
181
|
onActivity: (event) => {
|
|
168
182
|
enqueueActivity(event);
|
|
169
183
|
},
|
|
@@ -215,11 +229,23 @@ export async function runStage(options) {
|
|
|
215
229
|
return { waiting: true };
|
|
216
230
|
}
|
|
217
231
|
try {
|
|
232
|
+
let result = outcome;
|
|
233
|
+
if (outcome.ok) {
|
|
234
|
+
try {
|
|
235
|
+
result = await verifiedExecution.verify(outcome);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
result = {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: `Completion verification errored: ${error instanceof Error ? error.message : String(error)}`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
218
244
|
return await finalizeStageResult({
|
|
219
245
|
store,
|
|
220
246
|
runId,
|
|
221
247
|
stageId,
|
|
222
|
-
result
|
|
248
|
+
result,
|
|
223
249
|
activityChain,
|
|
224
250
|
attemptCtx,
|
|
225
251
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveAgentPort } from "../agent/resolveAgentPort.js";
|
|
2
|
+
import { asAgentBackendId } from "../agent/agentBackend.js";
|
|
3
|
+
import { loadStageflowManifestOutcome } from "../config/loadStageflowManifest.js";
|
|
2
4
|
import { definitionIdForInstance } from "../runstore/stageInstanceId.js";
|
|
3
5
|
import { createRunStore } from "../runstore/createStore.js";
|
|
4
6
|
import { loadRunContext } from "./resumeReconstruct.js";
|
|
@@ -6,6 +8,7 @@ import { bindPiAgentDirEnv, rootsForStageWorker, withResolvedAuthPath, } from ".
|
|
|
6
8
|
import { runStage, isRunStageWaiting, } from "./stageRunner.js";
|
|
7
9
|
import { openStageAttempt } from "./stageAttemptBootstrap.js";
|
|
8
10
|
import { attemptContext } from "./stageAttemptContext.js";
|
|
11
|
+
import { loadActiveFeedbackLoopContext } from "./feedbackLoopCoordinator.js";
|
|
9
12
|
import { outcomeToWorkerResult, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
|
|
10
13
|
export async function runStageWorker(input) {
|
|
11
14
|
const store = createRunStore({ rootDir: input.rootDir });
|
|
@@ -19,7 +22,14 @@ export async function runStageWorker(input) {
|
|
|
19
22
|
};
|
|
20
23
|
}
|
|
21
24
|
const dag = meta.pipeline_dag ?? loaded.dag;
|
|
22
|
-
const
|
|
25
|
+
const manifestOutcome = await loadStageflowManifestOutcome(input.rootDir);
|
|
26
|
+
const agent = resolveAgentPort({
|
|
27
|
+
global: manifestOutcome.ok
|
|
28
|
+
? asAgentBackendId(manifestOutcome.value.manifest.agent)
|
|
29
|
+
: undefined,
|
|
30
|
+
pipeline: asAgentBackendId(loaded.pipeline.agent),
|
|
31
|
+
stage: asAgentBackendId(stage.agent),
|
|
32
|
+
});
|
|
23
33
|
const workspaceDir = store.getWorkspaceDir(input.runId);
|
|
24
34
|
const checkoutRoot = meta.checkout_root;
|
|
25
35
|
const mode = input.mode ?? "run";
|
|
@@ -45,6 +55,7 @@ export async function runStageWorker(input) {
|
|
|
45
55
|
operatorCatalog: input.operatorCatalog,
|
|
46
56
|
roots,
|
|
47
57
|
resumeToken: input.sessionFilePath,
|
|
58
|
+
sessionMode: "waiting_resume",
|
|
48
59
|
onActivity: (event) => {
|
|
49
60
|
void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
|
|
50
61
|
},
|
|
@@ -74,6 +85,62 @@ export async function runStageWorker(input) {
|
|
|
74
85
|
});
|
|
75
86
|
return outcome;
|
|
76
87
|
}
|
|
88
|
+
if (mode === "feedback_resume" || mode === "new_session") {
|
|
89
|
+
const feedbackLoopContext = await loadActiveFeedbackLoopContext(store, input.runId, input.stageId);
|
|
90
|
+
if (mode === "feedback_resume" && feedbackLoopContext === undefined) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
reason: "feedback_resume requires an active feedback-loop context",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const priorAttempt = feedbackLoopContext?.prior_stage_attempt;
|
|
97
|
+
const resumeToken = input.sessionFilePath ??
|
|
98
|
+
(mode === "feedback_resume" && priorAttempt !== undefined
|
|
99
|
+
? attemptContext(priorAttempt).sessionPath(workspaceDir, input.stageId)
|
|
100
|
+
: undefined);
|
|
101
|
+
const opened = await openStageAttempt({
|
|
102
|
+
agent,
|
|
103
|
+
store,
|
|
104
|
+
runId: input.runId,
|
|
105
|
+
stage,
|
|
106
|
+
stageId: input.stageId,
|
|
107
|
+
task,
|
|
108
|
+
dag,
|
|
109
|
+
checkoutRoot,
|
|
110
|
+
workspaceDir,
|
|
111
|
+
factoryCwd: input.rootDir,
|
|
112
|
+
attemptCtx,
|
|
113
|
+
operatorCatalog: input.operatorCatalog,
|
|
114
|
+
roots,
|
|
115
|
+
...(resumeToken !== undefined ? { resumeToken } : {}),
|
|
116
|
+
sessionMode: mode,
|
|
117
|
+
...(feedbackLoopContext !== undefined ? { feedbackLoopContext } : {}),
|
|
118
|
+
onActivity: (event) => {
|
|
119
|
+
void store.appendStageEvent(input.runId, input.stageId, event, eventOptions);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
if (!opened.ok) {
|
|
123
|
+
return { ok: false, reason: opened.reason };
|
|
124
|
+
}
|
|
125
|
+
return runStage({
|
|
126
|
+
agent,
|
|
127
|
+
store,
|
|
128
|
+
runId: input.runId,
|
|
129
|
+
stage,
|
|
130
|
+
stageId: input.stageId,
|
|
131
|
+
task,
|
|
132
|
+
dag,
|
|
133
|
+
checkoutRoot,
|
|
134
|
+
workspaceDir,
|
|
135
|
+
existingHandle: opened.handle,
|
|
136
|
+
workerMode: true,
|
|
137
|
+
roots,
|
|
138
|
+
attemptCtx,
|
|
139
|
+
factoryCwd: input.rootDir,
|
|
140
|
+
operatorCatalog: input.operatorCatalog,
|
|
141
|
+
skipGates: input.skipGates,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
77
144
|
return runStage({
|
|
78
145
|
agent,
|
|
79
146
|
store,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { StageRunResult } from "../agent/port.js";
|
|
2
|
+
import type { RunStore } from "../runstore/port.js";
|
|
3
|
+
import type { ResolvedPipelineDag } from "../types/pipeline.js";
|
|
4
|
+
import type { StageConfig } from "../types/stage.js";
|
|
5
|
+
import type { StageRoots } from "./stageRoots.js";
|
|
6
|
+
export type VerifiedStageExecution = {
|
|
7
|
+
/** Establish durable observation before agent work begins. */
|
|
8
|
+
prepare(): Promise<void>;
|
|
9
|
+
/** Accept a successful agent result only when its contract passes. */
|
|
10
|
+
verify(candidate: Extract<StageRunResult, {
|
|
11
|
+
ok: true;
|
|
12
|
+
}>): Promise<StageRunResult>;
|
|
13
|
+
};
|
|
14
|
+
export declare function completionVerificationFailureReason(failedCheckIds: readonly string[]): string;
|
|
15
|
+
/**
|
|
16
|
+
* Owns the verification lifecycle for one immutable stage attempt: establish
|
|
17
|
+
* observation before agent work, then persist evidence before accepting a
|
|
18
|
+
* candidate handoff. Agent lifecycle and final stage status stay outside this
|
|
19
|
+
* module in stageRunner.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createVerifiedStageExecution(options: {
|
|
22
|
+
store: RunStore;
|
|
23
|
+
runId: string;
|
|
24
|
+
stageId: string;
|
|
25
|
+
attempt: number;
|
|
26
|
+
stage: StageConfig;
|
|
27
|
+
dag?: ResolvedPipelineDag;
|
|
28
|
+
roots: StageRoots;
|
|
29
|
+
}): VerifiedStageExecution;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { listQaExchanges } from "../hitl/qaTrail.js";
|
|
2
|
+
import { attemptArtifactsDir } from "../runstore/workspaceLayout.js";
|
|
3
|
+
import { COMPLETION_VERIFICATION_FAILURE_PREFIX, } from "../types/completion.js";
|
|
4
|
+
import { runCompletionContract, } from "./completionCheckRunner.js";
|
|
5
|
+
import { readCompletionCheckoutBaseline, writeCompletionCheckoutBaseline, } from "./completionCheckoutBaseline.js";
|
|
6
|
+
import { createGitCheckoutCapability } from "./gitCheckoutCapability.js";
|
|
7
|
+
export function completionVerificationFailureReason(failedCheckIds) {
|
|
8
|
+
return `${COMPLETION_VERIFICATION_FAILURE_PREFIX} ${failedCheckIds.join(", ")}`;
|
|
9
|
+
}
|
|
10
|
+
function gateDecisionsForAttempt(options) {
|
|
11
|
+
const { store, runId, stageId, attempt } = options;
|
|
12
|
+
return {
|
|
13
|
+
async listDecisions(kind) {
|
|
14
|
+
const events = await store.listStageEvents(runId, stageId, attempt);
|
|
15
|
+
return listQaExchanges(events)
|
|
16
|
+
.filter((exchange) => exchange.prompt.kind === kind && exchange.answer !== null)
|
|
17
|
+
.map((exchange) => {
|
|
18
|
+
const answer = exchange.answer;
|
|
19
|
+
if (answer.kind === "confirm" || answer.kind === "artifact_backed") {
|
|
20
|
+
return {
|
|
21
|
+
kind,
|
|
22
|
+
prompt_id: exchange.prompt.id,
|
|
23
|
+
status: answer.decision === "accept" ? "accepted" : "rejected",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { kind, prompt_id: exchange.prompt.id, status: "answered" };
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Owns the verification lifecycle for one immutable stage attempt: establish
|
|
33
|
+
* observation before agent work, then persist evidence before accepting a
|
|
34
|
+
* candidate handoff. Agent lifecycle and final stage status stay outside this
|
|
35
|
+
* module in stageRunner.
|
|
36
|
+
*/
|
|
37
|
+
export function createVerifiedStageExecution(options) {
|
|
38
|
+
const { store, runId, stageId, attempt, stage, dag, roots } = options;
|
|
39
|
+
const contract = dag?.nodes.find((node) => node.id === stageId)?.completion;
|
|
40
|
+
let checkout;
|
|
41
|
+
let checkoutBefore;
|
|
42
|
+
let checkoutError;
|
|
43
|
+
return {
|
|
44
|
+
async prepare() {
|
|
45
|
+
if (!contract?.checks.some((check) => check.type === "checkout_changes")) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (roots.checkoutRoot === undefined) {
|
|
49
|
+
checkoutError = "checkout check requires a bound checkout";
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
checkout = createGitCheckoutCapability(roots.checkoutRoot);
|
|
53
|
+
try {
|
|
54
|
+
checkoutBefore = await readCompletionCheckoutBaseline(roots.runWorkspaceDir, stageId, attempt);
|
|
55
|
+
if (checkoutBefore === undefined) {
|
|
56
|
+
checkoutBefore = await checkout.capture();
|
|
57
|
+
await writeCompletionCheckoutBaseline(roots.runWorkspaceDir, stageId, attempt, checkoutBefore);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
checkoutError = `could not capture checkout baseline: ${error instanceof Error ? error.message : String(error)}`;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async verify(candidate) {
|
|
65
|
+
if (contract === undefined)
|
|
66
|
+
return candidate;
|
|
67
|
+
try {
|
|
68
|
+
const verification = await runCompletionContract({
|
|
69
|
+
contract,
|
|
70
|
+
envelope: candidate.envelope,
|
|
71
|
+
payloadSchema: stage.payload_schema,
|
|
72
|
+
artifactsDir: attemptArtifactsDir(roots.runWorkspaceDir, stageId, attempt),
|
|
73
|
+
commandWorkingDirectory: roots.cwd,
|
|
74
|
+
gates: gateDecisionsForAttempt({ store, runId, stageId, attempt }),
|
|
75
|
+
checkout,
|
|
76
|
+
checkoutBefore,
|
|
77
|
+
checkoutError,
|
|
78
|
+
checklistAttestations: candidate.envelope.checklist_attestations,
|
|
79
|
+
onCheckStart: async (check) => {
|
|
80
|
+
await store.upsertVerificationCheckResult(runId, stageId, {
|
|
81
|
+
check_id: check.id,
|
|
82
|
+
check_type: check.type,
|
|
83
|
+
status: "running",
|
|
84
|
+
started_at: new Date().toISOString(),
|
|
85
|
+
}, { attempt });
|
|
86
|
+
},
|
|
87
|
+
onCheckComplete: async (check) => {
|
|
88
|
+
await store.upsertVerificationCheckResult(runId, stageId, {
|
|
89
|
+
check_id: check.check_id,
|
|
90
|
+
check_type: check.check_type,
|
|
91
|
+
status: check.outcome === "passed" ? "passed" : "failed",
|
|
92
|
+
finished_at: new Date().toISOString(),
|
|
93
|
+
evidence: {
|
|
94
|
+
...check.evidence,
|
|
95
|
+
...(check.message !== undefined ? { message: check.message } : {}),
|
|
96
|
+
...(check.outcome === "error" ? { runner_outcome: "error" } : {}),
|
|
97
|
+
},
|
|
98
|
+
}, { attempt });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
await store.updateStageExecution(runId, stageId, attempt, {
|
|
102
|
+
verification_outcome: verification.outcome,
|
|
103
|
+
});
|
|
104
|
+
if (verification.outcome === "passed")
|
|
105
|
+
return candidate;
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: completionVerificationFailureReason(verification.failed_check_ids),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
await store.updateStageExecution(runId, stageId, attempt, {
|
|
113
|
+
verification_outcome: "error",
|
|
114
|
+
});
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -37,6 +37,7 @@ export async function bootstrapStageflowHost(options) {
|
|
|
37
37
|
});
|
|
38
38
|
await manager.attachWaitingStages();
|
|
39
39
|
await manager.reconcileOrphanedStages();
|
|
40
|
+
await manager.resumeStalledSchedules();
|
|
40
41
|
const mcpStateless = resolveMcpStateless({
|
|
41
42
|
mcpStateless: options.mcpStateless,
|
|
42
43
|
});
|