211 lines
8.0 KiB
TypeScript
211 lines
8.0 KiB
TypeScript
import {
|
|
BUILTIN_CODING_WORKFLOW_IR,
|
|
WORKFLOW_INTERPRETER_AUTHORITATIVE_FLAG,
|
|
evaluateInterpreterCutoverReadiness,
|
|
isExperimentalFeatureEnabled,
|
|
type Settings,
|
|
type Task,
|
|
type TaskDetail,
|
|
type WorkflowDefinition,
|
|
type WorkflowParitySummary,
|
|
} from "@fusion/core";
|
|
|
|
import type { TaskExecutor } from "./executor.js";
|
|
import { executorLog } from "./logger.js";
|
|
import { WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG } from "./workflow-parity-observer.js";
|
|
import { WorkflowGraphTaskRunner, type WorkflowGraphTaskRunResult } from "./workflow-graph-task-runner.js";
|
|
import type { WorkflowLegacySeams } from "./workflow-node-handlers.js";
|
|
import type { PreparedWorktree, WorkflowRuntimePrimitives } from "./runtime-primitives.js";
|
|
|
|
const AUTHORITATIVE_WORKFLOW_ID = "workflow-interpreter-authoritative";
|
|
|
|
export interface WorkflowAuthoritativeDriverStore {
|
|
getSettings(): Promise<Settings>;
|
|
getTask(taskId: string): Promise<TaskDetail>;
|
|
getTaskWorkflowSelection?(taskId: string): { workflowId: string; stepIds: string[] } | undefined;
|
|
getWorkflowParitySummary?(options?: { since?: string; limit?: number }): WorkflowParitySummary;
|
|
}
|
|
|
|
export interface WorkflowAuthoritativeDriverDeps {
|
|
store: WorkflowAuthoritativeDriverStore;
|
|
executor: Pick<TaskExecutor, "createAuthoritativeWorkflowSeams"> & Partial<Pick<TaskExecutor, "createAuthoritativeWorkflowPrimitives">>;
|
|
minimumObservedRuns?: number;
|
|
}
|
|
|
|
export interface WorkflowAuthoritativeDriverResult {
|
|
handled: boolean;
|
|
disposition: "completed" | "failed" | "fell-back";
|
|
reason?: string;
|
|
readinessReasons: string[];
|
|
graphResult?: WorkflowGraphTaskRunResult;
|
|
}
|
|
|
|
function buildAuthoritativeSettings(settings: Settings): Settings {
|
|
return {
|
|
...settings,
|
|
experimentalFeatures: {
|
|
...(settings.experimentalFeatures ?? {}),
|
|
workflowGraphExecutor: true,
|
|
[WORKFLOW_INTERPRETER_AUTHORITATIVE_FLAG]: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function primitivesFromLegacySeams(seams: WorkflowLegacySeams): WorkflowRuntimePrimitives {
|
|
const prepared: PreparedWorktree = { worktreePath: "" };
|
|
return {
|
|
prepareWorktree: async () => ({ outcome: "success", data: prepared }),
|
|
readArtifact: async () => undefined,
|
|
writeArtifact: async (_ctx, _task, key) => ({ outcome: "success", data: { key } }),
|
|
runPlanningSession: async (ctx, task) => {
|
|
const result = await seams.planning(task, ctx.node.context ?? {});
|
|
return { ...result, data: { approved: result.outcome === "success", artifactKeys: [] } };
|
|
},
|
|
runCodingSession: async (ctx, task) => {
|
|
const result = await seams.execute(task, ctx.node.context ?? {});
|
|
return {
|
|
...result,
|
|
data: { taskDone: result.outcome === "success", modifiedFiles: [] },
|
|
};
|
|
},
|
|
runTaskStep: async (ctx, task) => {
|
|
const result = await seams.stepExecute?.(task, ctx.node.context ?? {});
|
|
return { outcome: result?.outcome ?? "failure" };
|
|
},
|
|
resetTaskStep: async () => ({ ok: true }),
|
|
runReview: async (ctx, task, input) => {
|
|
if (typeof input.stepIndex === "number") {
|
|
const result = await seams.stepReview?.(task, ctx.node.context ?? {}, { type: input.type });
|
|
return {
|
|
outcome: "success",
|
|
value: result?.verdict === "APPROVE" ? "approve" : result?.verdict === "REVISE" ? "revise" : result?.verdict === "RETHINK" ? "rethink" : "unavailable",
|
|
data: result ?? { verdict: "UNAVAILABLE" },
|
|
};
|
|
}
|
|
const result = await seams.review(task, ctx.node.context ?? {});
|
|
return { ...result, data: { verdict: result.outcome === "success" ? "APPROVE" : "REVISE" } };
|
|
},
|
|
runVerification: async () => ({ outcome: "success", data: { verdict: "skipped" } }),
|
|
runWorkflowStep: async () => ({ outcome: "success", data: { allPassed: true } }),
|
|
updateSteps: async (_ctx, _task, steps) => ({ outcome: "success", data: { count: steps.length } }),
|
|
transitionTask: async (ctx, task) => seams.schedule(task, ctx.node.context ?? {}),
|
|
requestMerge: async (ctx, task) => {
|
|
const result = await seams.merge(task, ctx.node.context ?? {});
|
|
return {
|
|
...result,
|
|
data: result.outcome === "success"
|
|
? { status: "merged" as const }
|
|
: { status: "failed" as const, reason: result.value ?? "merge-failed" },
|
|
};
|
|
},
|
|
abortRun: async () => ({ outcome: "success" }),
|
|
audit: () => undefined,
|
|
};
|
|
}
|
|
|
|
export class WorkflowAuthoritativeDriver {
|
|
public constructor(private readonly deps: WorkflowAuthoritativeDriverDeps) {}
|
|
|
|
public async maybeRun(task: Task): Promise<WorkflowAuthoritativeDriverResult> {
|
|
let settings: Settings;
|
|
let paritySummary: WorkflowParitySummary | undefined;
|
|
try {
|
|
settings = await this.deps.store.getSettings();
|
|
paritySummary = this.deps.store.getWorkflowParitySummary?.();
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
executorLog.warn(`[workflow-authoritative] ${task.id}: readiness probe failed — falling back to legacy (${message})`);
|
|
return {
|
|
handled: false,
|
|
disposition: "fell-back",
|
|
reason: `store-unavailable: ${message}`,
|
|
readinessReasons: ["workflow parity readiness probe unavailable"],
|
|
};
|
|
}
|
|
const authoritativeFlagEnabled = isExperimentalFeatureEnabled(
|
|
settings,
|
|
WORKFLOW_INTERPRETER_AUTHORITATIVE_FLAG,
|
|
);
|
|
const dualObserveEnabled = isExperimentalFeatureEnabled(
|
|
settings,
|
|
WORKFLOW_INTERPRETER_DUAL_OBSERVE_FLAG,
|
|
);
|
|
const readiness = evaluateInterpreterCutoverReadiness({
|
|
authoritativeFlagEnabled,
|
|
dualObserveEnabled,
|
|
paritySummary,
|
|
minimumObservedRuns: this.deps.minimumObservedRuns,
|
|
});
|
|
if (!readiness.ready) {
|
|
return {
|
|
handled: false,
|
|
disposition: "fell-back",
|
|
reason: readiness.reasons.join("; "),
|
|
readinessReasons: readiness.reasons,
|
|
};
|
|
}
|
|
|
|
const existingSelection = this.deps.store.getTaskWorkflowSelection?.(task.id);
|
|
if (existingSelection) {
|
|
return {
|
|
handled: false,
|
|
disposition: "fell-back",
|
|
reason: `workflow selection already present (${existingSelection.workflowId})`,
|
|
readinessReasons: [],
|
|
};
|
|
}
|
|
|
|
let liveTask: TaskDetail;
|
|
try {
|
|
liveTask = await this.deps.store.getTask(task.id);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
executorLog.warn(`[workflow-authoritative] ${task.id}: failed to load live task — falling back to legacy (${message})`);
|
|
return {
|
|
handled: false,
|
|
disposition: "fell-back",
|
|
reason: `task-load-failed: ${message}`,
|
|
readinessReasons: [],
|
|
};
|
|
}
|
|
const seams = this.deps.executor.createAuthoritativeWorkflowSeams(settings);
|
|
const runner = new WorkflowGraphTaskRunner({
|
|
store: {
|
|
getTaskWorkflowSelection: () => ({ workflowId: AUTHORITATIVE_WORKFLOW_ID, stepIds: [] }),
|
|
getWorkflowDefinition: async () => ({
|
|
id: AUTHORITATIVE_WORKFLOW_ID,
|
|
name: "Workflow interpreter authoritative cutover",
|
|
ir: BUILTIN_CODING_WORKFLOW_IR,
|
|
} satisfies Pick<WorkflowDefinition, "id" | "name" | "ir"> as WorkflowDefinition),
|
|
},
|
|
primitives: this.deps.executor.createAuthoritativeWorkflowPrimitives?.(settings) ?? primitivesFromLegacySeams(seams),
|
|
seams,
|
|
runCustomNode: async (node) => {
|
|
throw new Error(`unexpected custom node in builtin authoritative workflow: ${node.id}`);
|
|
},
|
|
onEvent: (event) => {
|
|
executorLog.log(`[workflow-authoritative] ${event.type} ${event.taskId}: ${event.detail}`);
|
|
},
|
|
});
|
|
|
|
const graphResult = await runner.run(liveTask, buildAuthoritativeSettings(settings));
|
|
if (graphResult.disposition === "fell-back") {
|
|
return {
|
|
handled: false,
|
|
disposition: "fell-back",
|
|
reason: graphResult.reason,
|
|
readinessReasons: [],
|
|
graphResult,
|
|
};
|
|
}
|
|
|
|
return {
|
|
handled: true,
|
|
disposition: graphResult.disposition,
|
|
reason: graphResult.reason,
|
|
readinessReasons: [],
|
|
graphResult,
|
|
};
|
|
}
|
|
}
|