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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { FeedbackLoopContext, StageSessionMode } from "../agent/port.js";
|
|
2
|
+
import type { DeferredFeedbackSendBack, FeedbackLoopRecord, FeedbackReplayRecord, RunStore } from "../runstore/port.js";
|
|
3
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
4
|
+
import type { ResolvedPipelineDag, ResolvedPipelineStageNode } from "../types/pipeline.js";
|
|
5
|
+
import type { FeedbackScheduleState } from "./feedbackLoopSchedule.js";
|
|
6
|
+
/** Launch/schedule payload consumed only by ReplayLifecycle. */
|
|
7
|
+
export type FeedbackSendBackLaunchMaps = {
|
|
8
|
+
contextsByStageId: Map<string, FeedbackLoopContext>;
|
|
9
|
+
sessionModeByStageId: Map<string, StageSessionMode>;
|
|
10
|
+
priorAttemptByStageId: Map<string, number>;
|
|
11
|
+
launchAttemptByStageId: Map<string, number>;
|
|
12
|
+
supersededCloneIds: ReadonlySet<string>;
|
|
13
|
+
};
|
|
14
|
+
export type FeedbackSendBackAccepted = {
|
|
15
|
+
kind: "accepted";
|
|
16
|
+
loop: FeedbackLoopRecord;
|
|
17
|
+
replay: FeedbackReplayRecord;
|
|
18
|
+
routeStageIds: string[];
|
|
19
|
+
launch: FeedbackSendBackLaunchMaps;
|
|
20
|
+
};
|
|
21
|
+
export type FeedbackSendBackIdempotent = {
|
|
22
|
+
kind: "idempotent";
|
|
23
|
+
loop: FeedbackLoopRecord;
|
|
24
|
+
replay: FeedbackReplayRecord;
|
|
25
|
+
routeStageIds: string[];
|
|
26
|
+
launch: FeedbackSendBackLaunchMaps;
|
|
27
|
+
};
|
|
28
|
+
export type FeedbackSendBackWaitingForHuman = {
|
|
29
|
+
kind: "waiting_for_human";
|
|
30
|
+
loop: FeedbackLoopRecord;
|
|
31
|
+
deferred: DeferredFeedbackSendBack;
|
|
32
|
+
sourceStageId: string;
|
|
33
|
+
sourceAttempt: number;
|
|
34
|
+
};
|
|
35
|
+
export type FeedbackSendBackRejected = {
|
|
36
|
+
kind: "rejected";
|
|
37
|
+
reason: string;
|
|
38
|
+
};
|
|
39
|
+
export type FeedbackSendBackResult = FeedbackSendBackAccepted | FeedbackSendBackIdempotent | FeedbackSendBackWaitingForHuman | FeedbackSendBackRejected;
|
|
40
|
+
export declare function terminalizeCurrentFeedbackReplay(store: RunStore, runId: string, loop: FeedbackLoopRecord, status: Extract<FeedbackReplayRecord["status"], "completed" | "failed" | "waiting_for_human">): Promise<void>;
|
|
41
|
+
export declare function acceptFeedbackSendBack(options: {
|
|
42
|
+
store: RunStore;
|
|
43
|
+
runId: string;
|
|
44
|
+
dag: ResolvedPipelineDag;
|
|
45
|
+
sourceNode: ResolvedPipelineStageNode;
|
|
46
|
+
sourceAttempt: number;
|
|
47
|
+
envelope: StageEnvelope;
|
|
48
|
+
}): Promise<FeedbackSendBackResult>;
|
|
49
|
+
export declare function markFeedbackLoopContinued(options: {
|
|
50
|
+
store: RunStore;
|
|
51
|
+
runId: string;
|
|
52
|
+
sourceStageId: string;
|
|
53
|
+
envelope: StageEnvelope;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
export declare function markFeedbackRouteStageSucceeded(options: {
|
|
56
|
+
store: RunStore;
|
|
57
|
+
runId: string;
|
|
58
|
+
replayId: string | undefined;
|
|
59
|
+
stageId: string;
|
|
60
|
+
envelope: StageEnvelope;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
export declare function markFeedbackRouteStageFailed(store: RunStore, runId: string, replayId: string | undefined, stageId: string): Promise<void>;
|
|
63
|
+
export declare function markFeedbackRouteStageRunning(options: {
|
|
64
|
+
store: RunStore;
|
|
65
|
+
runId: string;
|
|
66
|
+
replayId: string | undefined;
|
|
67
|
+
stageId: string;
|
|
68
|
+
}): Promise<void>;
|
|
69
|
+
export declare function hydrateActiveFeedbackScheduleFromStore(store: RunStore, runId: string, dag: ResolvedPipelineDag, feedback: FeedbackScheduleState): Promise<{
|
|
70
|
+
loop: FeedbackLoopRecord;
|
|
71
|
+
replay: FeedbackReplayRecord;
|
|
72
|
+
} | undefined>;
|
|
73
|
+
export declare function rebindFeedbackRoutePassAttempts(store: RunStore, runId: string, replayId: string, attemptOverrides: ReadonlyMap<string, number>): Promise<void>;
|
|
74
|
+
export declare function loadActiveFeedbackLoopContext(store: RunStore, runId: string, stageId: string, dag?: ResolvedPipelineDag): Promise<FeedbackLoopContext | undefined>;
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { instancesOfDefinition } from "../runstore/pipelineDagSnapshot.js";
|
|
3
|
+
import { feedbackRouteStageIds, postSourceStageIds } from "./feedbackLoopRoute.js";
|
|
4
|
+
import { collectSupersededCloneIdsForRoute, retireCohortsForRoute, } from "./forkGeneration.js";
|
|
5
|
+
function toStageSessionMode(replaySession) {
|
|
6
|
+
return replaySession === "resume" ? "feedback_resume" : "new_session";
|
|
7
|
+
}
|
|
8
|
+
export async function terminalizeCurrentFeedbackReplay(store, runId, loop, status) {
|
|
9
|
+
if (loop.current_replay_id === undefined)
|
|
10
|
+
return;
|
|
11
|
+
await store.updateFeedbackReplay(runId, loop.current_replay_id, { status });
|
|
12
|
+
}
|
|
13
|
+
async function resolveResumeSessionAttempt(store, runId, loopId, stageId, fallbackAttempt) {
|
|
14
|
+
const replays = await store.listFeedbackReplays(runId, loopId);
|
|
15
|
+
for (const replay of replays) {
|
|
16
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replay.replay_id);
|
|
17
|
+
for (const pass of passes) {
|
|
18
|
+
if (pass.stage_id !== stageId)
|
|
19
|
+
continue;
|
|
20
|
+
if (pass.session_origin_attempt !== undefined) {
|
|
21
|
+
return pass.session_origin_attempt;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return fallbackAttempt;
|
|
26
|
+
}
|
|
27
|
+
function buildContext(options) {
|
|
28
|
+
const { loop, replay, stageId, priorAttempt } = options;
|
|
29
|
+
const remaining = Math.max(0, replay.max_replays - replay.replay_number);
|
|
30
|
+
return {
|
|
31
|
+
loop_id: loop.loop_id,
|
|
32
|
+
replay_id: replay.replay_id,
|
|
33
|
+
source_stage_id: replay.source_stage_id,
|
|
34
|
+
target_stage_id: replay.target_stage_id,
|
|
35
|
+
feedback_envelope: replay.feedback_envelope,
|
|
36
|
+
replay_number: replay.replay_number,
|
|
37
|
+
max_replays: replay.max_replays,
|
|
38
|
+
remaining_replays: remaining,
|
|
39
|
+
is_final_replay: remaining === 0,
|
|
40
|
+
replay_session: replay.replay_session,
|
|
41
|
+
route_stage_ids: replay.route_stage_ids,
|
|
42
|
+
...(priorAttempt !== undefined
|
|
43
|
+
? { prior_stage_attempt: priorAttempt }
|
|
44
|
+
: {}),
|
|
45
|
+
...(options.activeForkGenerationId !== undefined
|
|
46
|
+
? { active_fork_generation_id: options.activeForkGenerationId }
|
|
47
|
+
: {}),
|
|
48
|
+
...(options.activeForkCloneStageIds !== undefined
|
|
49
|
+
? { active_fork_clone_stage_ids: options.activeForkCloneStageIds }
|
|
50
|
+
: {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function attachActiveForkContext(store, runId, dag, contextsByStageId) {
|
|
54
|
+
for (const [stageId, ctx] of contextsByStageId) {
|
|
55
|
+
const node = dag.nodes.find((n) => n.id === stageId);
|
|
56
|
+
if (node === undefined || node.needs === null)
|
|
57
|
+
continue;
|
|
58
|
+
const snapshot = dag;
|
|
59
|
+
const instances = Array.isArray(snapshot.stage_ids)
|
|
60
|
+
? instancesOfDefinition(snapshot, node.needs).filter((id) => id !== node.needs)
|
|
61
|
+
: [];
|
|
62
|
+
if (instances.length === 0)
|
|
63
|
+
continue;
|
|
64
|
+
const sample = dag.nodes.find((n) => n.id === instances[0]);
|
|
65
|
+
const forkParentId = sample?.needs;
|
|
66
|
+
if (forkParentId === undefined || forkParentId === null)
|
|
67
|
+
continue;
|
|
68
|
+
const gens = await store.listForkGenerations(runId, {
|
|
69
|
+
forkParentStageId: forkParentId,
|
|
70
|
+
});
|
|
71
|
+
const active = gens.find((g) => g.status === "active");
|
|
72
|
+
if (active === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
contextsByStageId.set(stageId, {
|
|
75
|
+
...ctx,
|
|
76
|
+
active_fork_generation_id: active.generation_id,
|
|
77
|
+
active_fork_clone_stage_ids: [...active.clone_stage_ids],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function rebuildLaunchMaps(store, runId, loop, replay, dag) {
|
|
82
|
+
const sessionMode = toStageSessionMode(replay.replay_session);
|
|
83
|
+
const contextsByStageId = new Map();
|
|
84
|
+
const sessionModeByStageId = new Map();
|
|
85
|
+
const priorAttemptByStageId = new Map();
|
|
86
|
+
const launchAttemptByStageId = new Map();
|
|
87
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replay.replay_id);
|
|
88
|
+
for (const pass of passes) {
|
|
89
|
+
const prior = replay.replay_session === "resume"
|
|
90
|
+
? (pass.session_origin_attempt ??
|
|
91
|
+
(await resolveResumeSessionAttempt(store, runId, loop.loop_id, pass.stage_id, pass.stage_attempt > 1 ? pass.stage_attempt - 1 : undefined)))
|
|
92
|
+
: pass.stage_attempt > 1
|
|
93
|
+
? pass.stage_attempt - 1
|
|
94
|
+
: undefined;
|
|
95
|
+
contextsByStageId.set(pass.stage_id, buildContext({
|
|
96
|
+
loop,
|
|
97
|
+
replay,
|
|
98
|
+
stageId: pass.stage_id,
|
|
99
|
+
...(prior !== undefined ? { priorAttempt: prior } : {}),
|
|
100
|
+
}));
|
|
101
|
+
sessionModeByStageId.set(pass.stage_id, sessionMode);
|
|
102
|
+
if (prior !== undefined) {
|
|
103
|
+
priorAttemptByStageId.set(pass.stage_id, prior);
|
|
104
|
+
}
|
|
105
|
+
launchAttemptByStageId.set(pass.stage_id, pass.stage_attempt);
|
|
106
|
+
}
|
|
107
|
+
if (dag !== undefined) {
|
|
108
|
+
await attachActiveForkContext(store, runId, dag, contextsByStageId);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
contextsByStageId,
|
|
112
|
+
sessionModeByStageId,
|
|
113
|
+
priorAttemptByStageId,
|
|
114
|
+
launchAttemptByStageId,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export async function acceptFeedbackSendBack(options) {
|
|
118
|
+
const { store, runId, dag, sourceNode, sourceAttempt, envelope } = options;
|
|
119
|
+
const action = envelope.feedback_loop;
|
|
120
|
+
if (action === undefined || action.action !== "send_back") {
|
|
121
|
+
return { kind: "rejected", reason: "envelope is not a feedback_loop send_back" };
|
|
122
|
+
}
|
|
123
|
+
const nodePolicy = sourceNode.feedback_loop;
|
|
124
|
+
if (nodePolicy === undefined) {
|
|
125
|
+
return {
|
|
126
|
+
kind: "rejected",
|
|
127
|
+
reason: `stage "${sourceNode.id}" has no feedback_loop policy`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const loops = await store.listFeedbackLoops(runId);
|
|
131
|
+
const otherActive = loops.find((loop) => (loop.state === "active" || loop.state === "waiting_for_human") &&
|
|
132
|
+
loop.source_stage_id !== sourceNode.id);
|
|
133
|
+
if (otherActive !== undefined) {
|
|
134
|
+
return {
|
|
135
|
+
kind: "rejected",
|
|
136
|
+
reason: `run already has an active feedback loop from "${otherActive.source_stage_id}"`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
let loop = loops.find((entry) => entry.source_stage_id === sourceNode.id &&
|
|
140
|
+
(entry.state === "active" || entry.state === "waiting_for_human")) ??
|
|
141
|
+
loops.find((entry) => entry.source_stage_id === sourceNode.id);
|
|
142
|
+
if (loop !== undefined && loop.state !== "active" && loop.state !== "waiting_for_human") {
|
|
143
|
+
loop = undefined;
|
|
144
|
+
}
|
|
145
|
+
if (loop === undefined) {
|
|
146
|
+
loop = await store.createFeedbackLoop(runId, {
|
|
147
|
+
loop_id: randomUUID(),
|
|
148
|
+
source_stage_id: sourceNode.id,
|
|
149
|
+
source_attempt: sourceAttempt,
|
|
150
|
+
policy: nodePolicy,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const existingReplays = await store.listFeedbackReplays(runId, loop.loop_id);
|
|
154
|
+
const duplicate = existingReplays.find((replay) => replay.source_attempt === sourceAttempt &&
|
|
155
|
+
replay.target_stage_id === action.target);
|
|
156
|
+
if (duplicate !== undefined) {
|
|
157
|
+
const maps = await rebuildLaunchMaps(store, runId, loop, duplicate, dag);
|
|
158
|
+
const supersededCloneIds = await collectSupersededCloneIdsForRoute({
|
|
159
|
+
store,
|
|
160
|
+
runId,
|
|
161
|
+
dag,
|
|
162
|
+
routeStageIds: duplicate.route_stage_ids,
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
kind: "idempotent",
|
|
166
|
+
loop,
|
|
167
|
+
replay: duplicate,
|
|
168
|
+
routeStageIds: duplicate.route_stage_ids,
|
|
169
|
+
launch: {
|
|
170
|
+
...maps,
|
|
171
|
+
supersededCloneIds,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const nextReplayNumber = (loop.current_replay_number ?? 0) + 1;
|
|
176
|
+
const policy = loop.policy;
|
|
177
|
+
if (nextReplayNumber > policy.max_replays) {
|
|
178
|
+
if (policy.on_max_replays === "wait_for_human") {
|
|
179
|
+
const deferred = {
|
|
180
|
+
target: action.target,
|
|
181
|
+
feedback_envelope: envelope,
|
|
182
|
+
source_attempt: sourceAttempt,
|
|
183
|
+
};
|
|
184
|
+
await terminalizeCurrentFeedbackReplay(store, runId, loop, "waiting_for_human");
|
|
185
|
+
await store.updateFeedbackLoop(runId, loop.loop_id, {
|
|
186
|
+
state: "waiting_for_human",
|
|
187
|
+
deferred_send_back: deferred,
|
|
188
|
+
});
|
|
189
|
+
const waitingLoop = await store.getFeedbackLoop(runId, loop.loop_id);
|
|
190
|
+
return {
|
|
191
|
+
kind: "waiting_for_human",
|
|
192
|
+
loop: waitingLoop,
|
|
193
|
+
deferred,
|
|
194
|
+
sourceStageId: sourceNode.id,
|
|
195
|
+
sourceAttempt,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
await terminalizeCurrentFeedbackReplay(store, runId, loop, "failed");
|
|
199
|
+
await store.updateFeedbackLoop(runId, loop.loop_id, {
|
|
200
|
+
state: "completed",
|
|
201
|
+
});
|
|
202
|
+
return {
|
|
203
|
+
kind: "rejected",
|
|
204
|
+
reason: `feedback loop exceeded max_replays (${policy.max_replays}); require_continue`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (loop.current_replay_id !== undefined) {
|
|
208
|
+
const currentPasses = await store.listFeedbackReplayStagePasses(runId, loop.current_replay_id);
|
|
209
|
+
const sourcePass = currentPasses.find((p) => p.stage_id === sourceNode.id);
|
|
210
|
+
if (sourcePass !== undefined && sourcePass.status !== "succeeded") {
|
|
211
|
+
await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceNode.id, {
|
|
212
|
+
status: "succeeded",
|
|
213
|
+
finished_at: new Date().toISOString(),
|
|
214
|
+
emitted_envelope: envelope,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
await store.updateFeedbackReplay(runId, loop.current_replay_id, {
|
|
218
|
+
status: "completed",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const routeStageIds = feedbackRouteStageIds(sourceNode, action.target);
|
|
222
|
+
const { supersededCloneIds } = await retireCohortsForRoute({
|
|
223
|
+
store,
|
|
224
|
+
runId,
|
|
225
|
+
dag,
|
|
226
|
+
routeStageIds,
|
|
227
|
+
});
|
|
228
|
+
const replayId = randomUUID();
|
|
229
|
+
const replay = await store.createFeedbackReplay(runId, {
|
|
230
|
+
replay_id: replayId,
|
|
231
|
+
loop_id: loop.loop_id,
|
|
232
|
+
source_stage_id: sourceNode.id,
|
|
233
|
+
source_attempt: sourceAttempt,
|
|
234
|
+
target_stage_id: action.target,
|
|
235
|
+
replay_number: nextReplayNumber,
|
|
236
|
+
max_replays: policy.max_replays,
|
|
237
|
+
replay_session: policy.replay_session,
|
|
238
|
+
route_stage_ids: routeStageIds,
|
|
239
|
+
feedback_envelope: envelope,
|
|
240
|
+
status: "active",
|
|
241
|
+
});
|
|
242
|
+
const sessionMode = toStageSessionMode(policy.replay_session);
|
|
243
|
+
const contextsByStageId = new Map();
|
|
244
|
+
const sessionModeByStageId = new Map();
|
|
245
|
+
const priorAttemptByStageId = new Map();
|
|
246
|
+
const launchAttemptByStageId = new Map();
|
|
247
|
+
for (const stageId of routeStageIds) {
|
|
248
|
+
const latest = await store.getLatestStageExecution(runId, stageId);
|
|
249
|
+
const priorAttempt = policy.replay_session === "resume"
|
|
250
|
+
? await resolveResumeSessionAttempt(store, runId, loop.loop_id, stageId, latest?.attempt)
|
|
251
|
+
: latest?.attempt;
|
|
252
|
+
const nextExecution = await store.createStageExecution(runId, stageId);
|
|
253
|
+
await store.createFeedbackReplayStagePass(runId, {
|
|
254
|
+
replay_id: replayId,
|
|
255
|
+
stage_id: stageId,
|
|
256
|
+
stage_attempt: nextExecution.attempt,
|
|
257
|
+
...(policy.replay_session === "resume" && priorAttempt !== undefined
|
|
258
|
+
? { session_origin_attempt: priorAttempt }
|
|
259
|
+
: {}),
|
|
260
|
+
session_mode: policy.replay_session,
|
|
261
|
+
status: "pending",
|
|
262
|
+
});
|
|
263
|
+
contextsByStageId.set(stageId, buildContext({
|
|
264
|
+
loop,
|
|
265
|
+
replay,
|
|
266
|
+
stageId,
|
|
267
|
+
...(priorAttempt !== undefined ? { priorAttempt } : {}),
|
|
268
|
+
}));
|
|
269
|
+
sessionModeByStageId.set(stageId, sessionMode);
|
|
270
|
+
if (priorAttempt !== undefined) {
|
|
271
|
+
priorAttemptByStageId.set(stageId, priorAttempt);
|
|
272
|
+
}
|
|
273
|
+
launchAttemptByStageId.set(stageId, nextExecution.attempt);
|
|
274
|
+
}
|
|
275
|
+
await attachActiveForkContext(store, runId, dag, contextsByStageId);
|
|
276
|
+
await store.updateFeedbackLoop(runId, loop.loop_id, {
|
|
277
|
+
state: "active",
|
|
278
|
+
current_replay_id: replayId,
|
|
279
|
+
current_replay_number: nextReplayNumber,
|
|
280
|
+
deferred_send_back: null,
|
|
281
|
+
});
|
|
282
|
+
const updatedLoop = await store.getFeedbackLoop(runId, loop.loop_id);
|
|
283
|
+
return {
|
|
284
|
+
kind: "accepted",
|
|
285
|
+
loop: updatedLoop,
|
|
286
|
+
replay,
|
|
287
|
+
routeStageIds,
|
|
288
|
+
launch: {
|
|
289
|
+
contextsByStageId,
|
|
290
|
+
sessionModeByStageId,
|
|
291
|
+
priorAttemptByStageId,
|
|
292
|
+
launchAttemptByStageId,
|
|
293
|
+
supersededCloneIds,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export async function markFeedbackLoopContinued(options) {
|
|
298
|
+
const { store, runId, sourceStageId, envelope } = options;
|
|
299
|
+
const loops = await store.listFeedbackLoops(runId);
|
|
300
|
+
const loop = loops.find((entry) => entry.source_stage_id === sourceStageId &&
|
|
301
|
+
(entry.state === "active" || entry.state === "waiting_for_human"));
|
|
302
|
+
if (loop === undefined)
|
|
303
|
+
return;
|
|
304
|
+
if (loop.current_replay_id !== undefined) {
|
|
305
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, loop.current_replay_id);
|
|
306
|
+
const sourcePass = passes.find((p) => p.stage_id === sourceStageId);
|
|
307
|
+
if (sourcePass !== undefined && sourcePass.status !== "succeeded") {
|
|
308
|
+
await store.updateFeedbackReplayStagePass(runId, loop.current_replay_id, sourceStageId, {
|
|
309
|
+
status: "succeeded",
|
|
310
|
+
finished_at: new Date().toISOString(),
|
|
311
|
+
emitted_envelope: envelope,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
await terminalizeCurrentFeedbackReplay(store, runId, loop, "completed");
|
|
316
|
+
await store.updateFeedbackLoop(runId, loop.loop_id, {
|
|
317
|
+
state: "continued",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
export async function markFeedbackRouteStageSucceeded(options) {
|
|
321
|
+
const { store, runId, replayId, stageId, envelope } = options;
|
|
322
|
+
if (replayId === undefined)
|
|
323
|
+
return;
|
|
324
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replayId);
|
|
325
|
+
const pass = passes.find((p) => p.stage_id === stageId);
|
|
326
|
+
if (pass === undefined)
|
|
327
|
+
return;
|
|
328
|
+
await store.updateFeedbackReplayStagePass(runId, replayId, stageId, {
|
|
329
|
+
status: "succeeded",
|
|
330
|
+
finished_at: new Date().toISOString(),
|
|
331
|
+
emitted_envelope: envelope,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
export async function markFeedbackRouteStageFailed(store, runId, replayId, stageId) {
|
|
335
|
+
if (replayId === undefined)
|
|
336
|
+
return;
|
|
337
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replayId);
|
|
338
|
+
const pass = passes.find((p) => p.stage_id === stageId);
|
|
339
|
+
if (pass === undefined || pass.status === "succeeded")
|
|
340
|
+
return;
|
|
341
|
+
await store.updateFeedbackReplayStagePass(runId, replayId, stageId, {
|
|
342
|
+
status: "failed",
|
|
343
|
+
finished_at: new Date().toISOString(),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
export async function markFeedbackRouteStageRunning(options) {
|
|
347
|
+
const { store, runId, replayId, stageId } = options;
|
|
348
|
+
if (replayId === undefined)
|
|
349
|
+
return;
|
|
350
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replayId);
|
|
351
|
+
const pass = passes.find((p) => p.stage_id === stageId);
|
|
352
|
+
if (pass === undefined || pass.status !== "pending")
|
|
353
|
+
return;
|
|
354
|
+
await store.updateFeedbackReplayStagePass(runId, replayId, stageId, {
|
|
355
|
+
status: "running",
|
|
356
|
+
started_at: new Date().toISOString(),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
export async function hydrateActiveFeedbackScheduleFromStore(store, runId, dag, feedback) {
|
|
360
|
+
const loops = await store.listFeedbackLoops(runId);
|
|
361
|
+
const loop = loops.find((entry) => entry.state === "active" && entry.current_replay_id !== undefined);
|
|
362
|
+
if (loop === undefined || loop.current_replay_id === undefined) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
const replay = await store.getFeedbackReplay(runId, loop.current_replay_id);
|
|
366
|
+
if (replay.status !== "active")
|
|
367
|
+
return undefined;
|
|
368
|
+
const maps = await rebuildLaunchMaps(store, runId, loop, replay, dag);
|
|
369
|
+
feedback.contextsByStageId = maps.contextsByStageId;
|
|
370
|
+
feedback.sessionModeByStageId = maps.sessionModeByStageId;
|
|
371
|
+
feedback.priorAttemptByStageId = maps.priorAttemptByStageId;
|
|
372
|
+
feedback.launchAttemptByStageId = maps.launchAttemptByStageId;
|
|
373
|
+
feedback.activeLoopId = loop.loop_id;
|
|
374
|
+
feedback.activeReplayId = replay.replay_id;
|
|
375
|
+
feedback.sourceStageId = replay.source_stage_id;
|
|
376
|
+
feedback.activeHoldStageIds = new Set(postSourceStageIds(dag, replay.source_stage_id));
|
|
377
|
+
feedback.activeCloneIdsByForkParent = new Map();
|
|
378
|
+
const supersededCloneIds = await collectSupersededCloneIdsForRoute({
|
|
379
|
+
store,
|
|
380
|
+
runId,
|
|
381
|
+
dag,
|
|
382
|
+
routeStageIds: replay.route_stage_ids,
|
|
383
|
+
});
|
|
384
|
+
for (const cloneId of supersededCloneIds) {
|
|
385
|
+
const node = dag.nodes.find((n) => n.id === cloneId);
|
|
386
|
+
const forkParentId = node?.needs;
|
|
387
|
+
if (forkParentId !== undefined && forkParentId !== null) {
|
|
388
|
+
feedback.activeCloneIdsByForkParent.set(forkParentId, new Set());
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const gens = await store.listForkGenerations(runId, {
|
|
392
|
+
replayId: replay.replay_id,
|
|
393
|
+
});
|
|
394
|
+
for (const gen of gens) {
|
|
395
|
+
if (gen.status !== "active")
|
|
396
|
+
continue;
|
|
397
|
+
feedback.activeCloneIdsByForkParent.set(gen.fork_parent_stage_id, new Set(gen.clone_stage_ids));
|
|
398
|
+
}
|
|
399
|
+
return { loop, replay };
|
|
400
|
+
}
|
|
401
|
+
export async function rebindFeedbackRoutePassAttempts(store, runId, replayId, attemptOverrides) {
|
|
402
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replayId);
|
|
403
|
+
const passStages = new Set(passes.map((p) => p.stage_id));
|
|
404
|
+
for (const [stageId, stageAttempt] of attemptOverrides) {
|
|
405
|
+
if (!passStages.has(stageId))
|
|
406
|
+
continue;
|
|
407
|
+
await store.updateFeedbackReplayStagePass(runId, replayId, stageId, {
|
|
408
|
+
stage_attempt: stageAttempt,
|
|
409
|
+
status: "pending",
|
|
410
|
+
started_at: null,
|
|
411
|
+
finished_at: null,
|
|
412
|
+
emitted_envelope: null,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
export async function loadActiveFeedbackLoopContext(store, runId, stageId, dag) {
|
|
417
|
+
const loops = await store.listFeedbackLoops(runId);
|
|
418
|
+
const active = loops
|
|
419
|
+
.filter((l) => l.state === "active" || l.state === "waiting_for_human")
|
|
420
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))[0];
|
|
421
|
+
if (active?.current_replay_id === undefined)
|
|
422
|
+
return undefined;
|
|
423
|
+
const replay = await store.getFeedbackReplay(runId, active.current_replay_id);
|
|
424
|
+
if (!replay.route_stage_ids.includes(stageId))
|
|
425
|
+
return undefined;
|
|
426
|
+
const passes = await store.listFeedbackReplayStagePasses(runId, replay.replay_id);
|
|
427
|
+
const pass = passes.find((p) => p.stage_id === stageId);
|
|
428
|
+
const fallbackPrior = pass !== undefined && pass.stage_attempt > 1
|
|
429
|
+
? pass.stage_attempt - 1
|
|
430
|
+
: undefined;
|
|
431
|
+
const priorAttempt = replay.replay_session === "resume"
|
|
432
|
+
? (pass?.session_origin_attempt ??
|
|
433
|
+
(await resolveResumeSessionAttempt(store, runId, active.loop_id, stageId, fallbackPrior)))
|
|
434
|
+
: fallbackPrior;
|
|
435
|
+
const ctx = buildContext({
|
|
436
|
+
loop: active,
|
|
437
|
+
replay,
|
|
438
|
+
stageId,
|
|
439
|
+
...(priorAttempt !== undefined ? { priorAttempt } : {}),
|
|
440
|
+
});
|
|
441
|
+
if (dag === undefined)
|
|
442
|
+
return ctx;
|
|
443
|
+
const contexts = new Map([[stageId, ctx]]);
|
|
444
|
+
await attachActiveForkContext(store, runId, dag, contexts);
|
|
445
|
+
return contexts.get(stageId);
|
|
446
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FeedbackLoopRecord, RunStore } from "../runstore/port.js";
|
|
2
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
3
|
+
import type { ResolvedPipelineDag, ResolvedPipelineStageNode } from "../types/pipeline.js";
|
|
4
|
+
import type { FeedbackScheduleState } from "./feedbackLoopSchedule.js";
|
|
5
|
+
export type FeedbackLoopDecisionKind = "extend" | "continue" | "abandon";
|
|
6
|
+
export type FeedbackLoopDecisionInput = {
|
|
7
|
+
decision: FeedbackLoopDecisionKind;
|
|
8
|
+
reason?: string;
|
|
9
|
+
};
|
|
10
|
+
type StageScheduleState = "pending" | "active" | "waiting" | "succeeded" | "failed" | "skipped";
|
|
11
|
+
export type FeedbackLoopDecisionScheduleContext = {
|
|
12
|
+
dag: ResolvedPipelineDag;
|
|
13
|
+
sourceNode: ResolvedPipelineStageNode;
|
|
14
|
+
states: Map<string, StageScheduleState>;
|
|
15
|
+
completedEnvelopes: Map<string, StageEnvelope>;
|
|
16
|
+
feedback: FeedbackScheduleState;
|
|
17
|
+
};
|
|
18
|
+
export type ResolveFeedbackLoopDecisionResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
effect: "extended";
|
|
21
|
+
loop: FeedbackLoopRecord;
|
|
22
|
+
replayId: string;
|
|
23
|
+
} | {
|
|
24
|
+
ok: true;
|
|
25
|
+
effect: "continued";
|
|
26
|
+
loop: FeedbackLoopRecord;
|
|
27
|
+
} | {
|
|
28
|
+
ok: true;
|
|
29
|
+
effect: "abandoned";
|
|
30
|
+
loop: FeedbackLoopRecord;
|
|
31
|
+
reason: string;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function resolveFeedbackLoopDecision(options: {
|
|
37
|
+
store: RunStore;
|
|
38
|
+
runId: string;
|
|
39
|
+
decision: FeedbackLoopDecisionKind;
|
|
40
|
+
loopId?: string;
|
|
41
|
+
reason?: string;
|
|
42
|
+
schedule?: FeedbackLoopDecisionScheduleContext;
|
|
43
|
+
}): Promise<ResolveFeedbackLoopDecisionResult>;
|
|
44
|
+
export {};
|