feat(FN-000): run workflow work items through runtime
Fusion-Task-Id: FN-000
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Settings, TaskDetail, WorkflowIr, WorkflowIrNode } from "@fusion/core";
|
||||
import type { Settings, TaskDetail, WorkflowIr, WorkflowIrNode, WorkflowWorkItem, WorkflowWorkItemState } from "@fusion/core";
|
||||
import {
|
||||
BUILTIN_CODING_WORKFLOW_IR,
|
||||
getBuiltinWorkflow,
|
||||
@@ -31,7 +31,14 @@ export interface WorkflowTaskRuntimeResult {
|
||||
}
|
||||
|
||||
export interface WorkflowTaskRuntimeDeps extends Omit<WorkflowGraphExecutorDeps, "seams" | "runCustomNode"> {
|
||||
store: WorkflowIrResolverStore;
|
||||
store: WorkflowIrResolverStore & {
|
||||
getTask?: (taskId: string) => Promise<TaskDetail>;
|
||||
transitionWorkflowWorkItem?: (
|
||||
id: string,
|
||||
state: WorkflowWorkItemState,
|
||||
patch?: { now?: string; lastError?: string | null; leaseOwner?: string | null; leaseExpiresAt?: string | null },
|
||||
) => WorkflowWorkItem;
|
||||
};
|
||||
primitives: WorkflowRuntimePrimitives;
|
||||
runCustomNode: WorkflowCustomNodeRunner;
|
||||
onEvent?: (event: { type: "start" | "terminal"; taskId: string; detail: string }) => void;
|
||||
@@ -114,6 +121,95 @@ export class WorkflowTaskRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
public async runWorkItem(
|
||||
workItem: WorkflowWorkItem,
|
||||
settings: (Pick<Settings, "experimentalFeatures"> & Partial<Settings>) | undefined,
|
||||
): Promise<WorkflowTaskRuntimeResult> {
|
||||
if (workItem.state !== "running") {
|
||||
return this.failWorkItem(workItem, `workflow-work-item-not-running:${workItem.state}`);
|
||||
}
|
||||
if (!this.deps.store.getTask || !this.deps.store.transitionWorkflowWorkItem) {
|
||||
return this.failWorkItem(workItem, "workflow-work-item-store-unwired");
|
||||
}
|
||||
|
||||
let task: TaskDetail;
|
||||
try {
|
||||
task = await this.deps.store.getTask(workItem.taskId);
|
||||
} catch (err) {
|
||||
return this.failWorkItem(workItem, `workflow-work-item-task-missing:${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
let target: WorkflowRuntimeTarget;
|
||||
try {
|
||||
target = await this.resolveRuntimeTarget(workItem.taskId);
|
||||
} catch (err) {
|
||||
return this.failWorkItem(workItem, `workflow-resolution-error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
const node = target.ir.nodes.find((candidate) => candidate.id === workItem.nodeId);
|
||||
if (!node) {
|
||||
return this.failWorkItem(workItem, `workflow-work-item-node-missing:${workItem.nodeId}`);
|
||||
}
|
||||
|
||||
const invoked: string[] = [];
|
||||
const handler = this.recordingHandlers(invoked)[node.kind];
|
||||
if (!handler && node.kind !== "start" && node.kind !== "end") {
|
||||
return this.failWorkItem(workItem, `workflow-work-item-node-unhandled:${node.kind}`);
|
||||
}
|
||||
|
||||
const runtimeSettings = forceWorkflowGraphExecutor(settings);
|
||||
let outcome: WorkflowNodeOutcome = "success";
|
||||
let reason: string | undefined;
|
||||
let context: Record<string, unknown> = {
|
||||
"workflow:work-item-id": workItem.id,
|
||||
"workflow:work-item-kind": workItem.kind,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = handler
|
||||
? await handler(node, { task, settings: runtimeSettings, context })
|
||||
: { outcome: "success" as const };
|
||||
outcome = result.outcome;
|
||||
if (result.value !== undefined) context[`node:${node.id}:value`] = result.value;
|
||||
context = { ...context, ...(result.contextPatch ?? {}) };
|
||||
reason = result.outcome === "failure" ? result.value ?? "workflow-work-item-node-failed" : undefined;
|
||||
} catch (err) {
|
||||
outcome = "failure";
|
||||
reason = `workflow-work-item-node-error:${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
|
||||
const disposition: WorkflowTaskRuntimeDisposition = outcome === "success" ? "completed" : "failed";
|
||||
this.deps.store.transitionWorkflowWorkItem(workItem.id, disposition === "completed" ? "succeeded" : "failed", {
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
lastError: reason ?? null,
|
||||
});
|
||||
this.emit("terminal", workItem.taskId, `work-item:${disposition}`);
|
||||
return {
|
||||
disposition,
|
||||
outcome,
|
||||
visitedNodeIds: invoked.length > 0 ? invoked : [node.id],
|
||||
context,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
private failWorkItem(workItem: WorkflowWorkItem, reason: string): WorkflowTaskRuntimeResult {
|
||||
this.deps.store.transitionWorkflowWorkItem?.(workItem.id, "failed", {
|
||||
leaseOwner: null,
|
||||
leaseExpiresAt: null,
|
||||
lastError: reason,
|
||||
});
|
||||
this.emit("terminal", workItem.taskId, `work-item:failed:${reason}`);
|
||||
return {
|
||||
disposition: "failed",
|
||||
outcome: "failure",
|
||||
visitedNodeIds: [],
|
||||
context: {},
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveRuntimeTarget(taskId: string): Promise<WorkflowRuntimeTarget> {
|
||||
let workflowId: string | undefined;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user