@donkeylabs/server 2.0.20 → 2.0.21
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/package.json +1 -1
- package/src/core/index.ts +6 -0
- package/src/core/workflow-executor.ts +104 -334
- package/src/core/workflow-socket.ts +2 -0
- package/src/core/workflow-state-machine.ts +593 -0
- package/src/core/workflows.test.ts +343 -0
- package/src/core/workflows.ts +234 -887
package/src/core/workflows.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type ProxyRequest,
|
|
23
23
|
} from "./workflow-socket";
|
|
24
24
|
import { isProcessAlive } from "./external-jobs";
|
|
25
|
+
import { WorkflowStateMachine, type StateMachineEvents } from "./workflow-state-machine";
|
|
25
26
|
|
|
26
27
|
// Type helper for Zod schema inference
|
|
27
28
|
type ZodSchema = z.ZodTypeAny;
|
|
@@ -655,7 +656,7 @@ export interface Workflows {
|
|
|
655
656
|
}
|
|
656
657
|
|
|
657
658
|
// ============================================
|
|
658
|
-
// Workflow Service Implementation
|
|
659
|
+
// Workflow Service Implementation (Supervisor)
|
|
659
660
|
// ============================================
|
|
660
661
|
|
|
661
662
|
interface IsolatedProcessInfo {
|
|
@@ -667,13 +668,13 @@ interface IsolatedProcessInfo {
|
|
|
667
668
|
|
|
668
669
|
class WorkflowsImpl implements Workflows {
|
|
669
670
|
private adapter: WorkflowAdapter;
|
|
670
|
-
private
|
|
671
|
+
private eventsService?: Events;
|
|
671
672
|
private jobs?: Jobs;
|
|
672
673
|
private sse?: SSE;
|
|
673
674
|
private core?: CoreServices;
|
|
674
675
|
private plugins: Record<string, any> = {};
|
|
675
676
|
private definitions = new Map<string, WorkflowDefinition>();
|
|
676
|
-
private running = new Map<string, { timeout?: ReturnType<typeof setTimeout
|
|
677
|
+
private running = new Map<string, { timeout?: ReturnType<typeof setTimeout>; sm?: WorkflowStateMachine }>();
|
|
677
678
|
private pollInterval: number;
|
|
678
679
|
|
|
679
680
|
// Isolated execution state
|
|
@@ -687,7 +688,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
687
688
|
|
|
688
689
|
constructor(config: WorkflowsConfig = {}) {
|
|
689
690
|
this.adapter = config.adapter ?? new MemoryWorkflowAdapter();
|
|
690
|
-
this.
|
|
691
|
+
this.eventsService = config.events;
|
|
691
692
|
this.jobs = config.jobs;
|
|
692
693
|
this.sse = config.sse;
|
|
693
694
|
this.core = config.core;
|
|
@@ -760,18 +761,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
760
761
|
throw new Error(`Workflow "${definition.name}" is already registered`);
|
|
761
762
|
}
|
|
762
763
|
|
|
763
|
-
// Validate isolated workflows don't use unsupported step types
|
|
764
|
-
if (definition.isolated !== false) {
|
|
765
|
-
for (const [stepName, step] of definition.steps) {
|
|
766
|
-
if (step.type === "choice" || step.type === "parallel") {
|
|
767
|
-
throw new Error(
|
|
768
|
-
`Workflow "${definition.name}" uses ${step.type} step "${stepName}" ` +
|
|
769
|
-
`which is not supported in isolated mode. Use .isolated(false) to run inline.`
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
764
|
// Store module path for isolated workflows
|
|
776
765
|
if (options?.modulePath) {
|
|
777
766
|
this.workflowModulePaths.set(definition.name, options.modulePath);
|
|
@@ -829,13 +818,13 @@ class WorkflowsImpl implements Workflows {
|
|
|
829
818
|
// Execute in isolated subprocess
|
|
830
819
|
this.executeIsolatedWorkflow(instance.id, definition, input, modulePath);
|
|
831
820
|
} else {
|
|
832
|
-
// Execute inline
|
|
821
|
+
// Execute inline using state machine
|
|
833
822
|
if (isIsolated && !modulePath) {
|
|
834
823
|
console.warn(
|
|
835
824
|
`[Workflows] Workflow "${workflowName}" falling back to inline execution (no modulePath)`
|
|
836
825
|
);
|
|
837
826
|
}
|
|
838
|
-
this.
|
|
827
|
+
this.startInlineWorkflow(instance.id, definition);
|
|
839
828
|
}
|
|
840
829
|
|
|
841
830
|
return instance.id;
|
|
@@ -865,8 +854,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
865
854
|
await this.getSocketServer().closeSocket(instanceId);
|
|
866
855
|
}
|
|
867
856
|
|
|
868
|
-
//
|
|
857
|
+
// Cancel inline state machine if running
|
|
869
858
|
const runInfo = this.running.get(instanceId);
|
|
859
|
+
if (runInfo?.sm) {
|
|
860
|
+
runInfo.sm.cancel(instanceId);
|
|
861
|
+
}
|
|
870
862
|
if (runInfo?.timeout) {
|
|
871
863
|
clearTimeout(runInfo.timeout);
|
|
872
864
|
}
|
|
@@ -918,7 +910,7 @@ class WorkflowsImpl implements Workflows {
|
|
|
918
910
|
if (isIsolated && modulePath && this.dbPath) {
|
|
919
911
|
this.executeIsolatedWorkflow(instance.id, definition, instance.input, modulePath);
|
|
920
912
|
} else {
|
|
921
|
-
this.
|
|
913
|
+
this.startInlineWorkflow(instance.id, definition);
|
|
922
914
|
}
|
|
923
915
|
}
|
|
924
916
|
}
|
|
@@ -942,8 +934,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
942
934
|
this.socketServer = undefined;
|
|
943
935
|
}
|
|
944
936
|
|
|
945
|
-
// Clear all inline timeouts
|
|
937
|
+
// Clear all inline timeouts and cancel state machines
|
|
946
938
|
for (const [instanceId, runInfo] of this.running) {
|
|
939
|
+
if (runInfo.sm) {
|
|
940
|
+
runInfo.sm.cancel(instanceId);
|
|
941
|
+
}
|
|
947
942
|
if (runInfo.timeout) {
|
|
948
943
|
clearTimeout(runInfo.timeout);
|
|
949
944
|
}
|
|
@@ -957,761 +952,177 @@ class WorkflowsImpl implements Workflows {
|
|
|
957
952
|
}
|
|
958
953
|
|
|
959
954
|
// ============================================
|
|
960
|
-
// Execution
|
|
955
|
+
// Inline Execution via State Machine
|
|
961
956
|
// ============================================
|
|
962
957
|
|
|
963
|
-
private
|
|
958
|
+
private startInlineWorkflow(
|
|
964
959
|
instanceId: string,
|
|
965
|
-
definition: WorkflowDefinition
|
|
966
|
-
):
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
});
|
|
976
|
-
}
|
|
960
|
+
definition: WorkflowDefinition,
|
|
961
|
+
): void {
|
|
962
|
+
const sm = new WorkflowStateMachine({
|
|
963
|
+
adapter: this.adapter,
|
|
964
|
+
core: this.core,
|
|
965
|
+
plugins: this.plugins,
|
|
966
|
+
events: this.createInlineEventHandler(instanceId),
|
|
967
|
+
jobs: this.jobs,
|
|
968
|
+
pollInterval: this.pollInterval,
|
|
969
|
+
});
|
|
977
970
|
|
|
978
971
|
// Set up workflow timeout
|
|
972
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
979
973
|
if (definition.timeout) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const stepName = instance.currentStep;
|
|
1000
|
-
if (!stepName) {
|
|
1001
|
-
await this.completeWorkflow(instanceId);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const step = definition.steps.get(stepName);
|
|
1006
|
-
if (!step) {
|
|
1007
|
-
await this.failWorkflow(instanceId, `Step "${stepName}" not found`);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
// Build context
|
|
1012
|
-
const ctx = this.buildContext(instance, definition);
|
|
1013
|
-
|
|
1014
|
-
// Emit step started event
|
|
1015
|
-
await this.emitEvent("workflow.step.started", {
|
|
1016
|
-
instanceId,
|
|
1017
|
-
workflowName: instance.workflowName,
|
|
1018
|
-
stepName,
|
|
1019
|
-
stepType: step.type,
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
// Broadcast via SSE
|
|
1023
|
-
if (this.sse) {
|
|
1024
|
-
this.sse.broadcast(`workflow:${instanceId}`, "step.started", { stepName });
|
|
1025
|
-
this.sse.broadcast("workflows:all", "workflow.step.started", {
|
|
1026
|
-
instanceId,
|
|
1027
|
-
workflowName: instance.workflowName,
|
|
1028
|
-
stepName,
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// Update step result as running
|
|
1033
|
-
const stepResult: StepResult = {
|
|
1034
|
-
stepName,
|
|
1035
|
-
status: "running",
|
|
1036
|
-
startedAt: new Date(),
|
|
1037
|
-
attempts: (instance.stepResults[stepName]?.attempts ?? 0) + 1,
|
|
1038
|
-
};
|
|
1039
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1040
|
-
stepResults: { ...instance.stepResults, [stepName]: stepResult },
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
try {
|
|
1044
|
-
let output: any;
|
|
1045
|
-
|
|
1046
|
-
switch (step.type) {
|
|
1047
|
-
case "task":
|
|
1048
|
-
output = await this.executeTaskStep(instanceId, step, ctx, definition);
|
|
1049
|
-
break;
|
|
1050
|
-
case "parallel":
|
|
1051
|
-
output = await this.executeParallelStep(instanceId, step, ctx, definition);
|
|
1052
|
-
break;
|
|
1053
|
-
case "choice":
|
|
1054
|
-
output = await this.executeChoiceStep(instanceId, step, ctx, definition);
|
|
1055
|
-
break;
|
|
1056
|
-
case "pass":
|
|
1057
|
-
output = await this.executePassStep(instanceId, step, ctx);
|
|
1058
|
-
break;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Step completed successfully
|
|
1062
|
-
await this.completeStep(instanceId, stepName, output, step, definition);
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1065
|
-
await this.handleStepError(instanceId, stepName, errorMsg, step, definition);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
private async executeTaskStep(
|
|
1070
|
-
instanceId: string,
|
|
1071
|
-
step: TaskStepDefinition,
|
|
1072
|
-
ctx: WorkflowContext,
|
|
1073
|
-
definition: WorkflowDefinition
|
|
1074
|
-
): Promise<any> {
|
|
1075
|
-
// Determine which API is being used
|
|
1076
|
-
const useInlineHandler = !!step.handler;
|
|
1077
|
-
|
|
1078
|
-
if (useInlineHandler) {
|
|
1079
|
-
// === NEW API: Inline handler with Zod schemas ===
|
|
1080
|
-
let input: any;
|
|
1081
|
-
|
|
1082
|
-
if (step.inputSchema) {
|
|
1083
|
-
if (typeof step.inputSchema === "function") {
|
|
1084
|
-
// inputSchema is a mapper function: (prev, workflowInput) => input
|
|
1085
|
-
input = step.inputSchema(ctx.prev, ctx.input);
|
|
1086
|
-
} else {
|
|
1087
|
-
// inputSchema is a Zod schema - validate workflow input
|
|
1088
|
-
const parseResult = step.inputSchema.safeParse(ctx.input);
|
|
1089
|
-
if (!parseResult.success) {
|
|
1090
|
-
throw new Error(`Input validation failed: ${parseResult.error.message}`);
|
|
1091
|
-
}
|
|
1092
|
-
input = parseResult.data;
|
|
1093
|
-
}
|
|
1094
|
-
} else {
|
|
1095
|
-
// No input schema, use workflow input directly
|
|
1096
|
-
input = ctx.input;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
// Update step with input
|
|
1100
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1101
|
-
if (instance) {
|
|
1102
|
-
const stepResult = instance.stepResults[step.name];
|
|
1103
|
-
if (stepResult) {
|
|
1104
|
-
stepResult.input = input;
|
|
1105
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1106
|
-
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Execute the inline handler
|
|
1112
|
-
let result = await step.handler!(input, ctx);
|
|
1113
|
-
|
|
1114
|
-
// Validate output if schema provided
|
|
1115
|
-
if (step.outputSchema) {
|
|
1116
|
-
const parseResult = step.outputSchema.safeParse(result);
|
|
1117
|
-
if (!parseResult.success) {
|
|
1118
|
-
throw new Error(`Output validation failed: ${parseResult.error.message}`);
|
|
1119
|
-
}
|
|
1120
|
-
result = parseResult.data;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
return result;
|
|
1124
|
-
} else {
|
|
1125
|
-
// === LEGACY API: Job-based execution ===
|
|
1126
|
-
if (!this.jobs) {
|
|
1127
|
-
throw new Error("Jobs service not configured");
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
if (!step.job) {
|
|
1131
|
-
throw new Error("Task step requires either 'handler' or 'job'");
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// Prepare job input
|
|
1135
|
-
const jobInput = step.input ? step.input(ctx) : ctx.input;
|
|
1136
|
-
|
|
1137
|
-
// Update step with input
|
|
1138
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1139
|
-
if (instance) {
|
|
1140
|
-
const stepResult = instance.stepResults[step.name];
|
|
1141
|
-
if (stepResult) {
|
|
1142
|
-
stepResult.input = jobInput;
|
|
1143
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1144
|
-
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
974
|
+
timeout = setTimeout(async () => {
|
|
975
|
+
sm.cancel(instanceId);
|
|
976
|
+
await this.adapter.updateInstance(instanceId, {
|
|
977
|
+
status: "failed",
|
|
978
|
+
error: "Workflow timed out",
|
|
979
|
+
completedAt: new Date(),
|
|
980
|
+
});
|
|
981
|
+
await this.emitEvent("workflow.failed", {
|
|
982
|
+
instanceId,
|
|
983
|
+
workflowName: definition.name,
|
|
984
|
+
error: "Workflow timed out",
|
|
985
|
+
});
|
|
986
|
+
if (this.sse) {
|
|
987
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: "Workflow timed out" });
|
|
988
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
989
|
+
instanceId,
|
|
990
|
+
workflowName: definition.name,
|
|
991
|
+
error: "Workflow timed out",
|
|
1145
992
|
});
|
|
1146
993
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
// Enqueue the job
|
|
1150
|
-
const jobId = await this.jobs.enqueue(step.job, {
|
|
1151
|
-
...jobInput,
|
|
1152
|
-
_workflowInstanceId: instanceId,
|
|
1153
|
-
_workflowStepName: step.name,
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
// Wait for job completion
|
|
1157
|
-
const result = await this.waitForJob(jobId, step.timeout);
|
|
1158
|
-
|
|
1159
|
-
// Transform output if needed
|
|
1160
|
-
return step.output ? step.output(result, ctx) : result;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
private async waitForJob(jobId: string, timeout?: number): Promise<any> {
|
|
1165
|
-
if (!this.jobs) {
|
|
1166
|
-
throw new Error("Jobs service not configured");
|
|
994
|
+
this.running.delete(instanceId);
|
|
995
|
+
}, definition.timeout);
|
|
1167
996
|
}
|
|
1168
997
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
while (true) {
|
|
1172
|
-
const job = await this.jobs.get(jobId);
|
|
1173
|
-
|
|
1174
|
-
if (!job) {
|
|
1175
|
-
throw new Error(`Job ${jobId} not found`);
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
if (job.status === "completed") {
|
|
1179
|
-
return job.result;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
if (job.status === "failed") {
|
|
1183
|
-
throw new Error(job.error ?? "Job failed");
|
|
1184
|
-
}
|
|
998
|
+
this.running.set(instanceId, { timeout, sm });
|
|
1185
999
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1000
|
+
// Run the state machine (fire and forget - events handle communication)
|
|
1001
|
+
sm.run(instanceId, definition).then(() => {
|
|
1002
|
+
// Clean up timeout on completion
|
|
1003
|
+
const runInfo = this.running.get(instanceId);
|
|
1004
|
+
if (runInfo?.timeout) {
|
|
1005
|
+
clearTimeout(runInfo.timeout);
|
|
1189
1006
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
private async executeParallelStep(
|
|
1197
|
-
instanceId: string,
|
|
1198
|
-
step: ParallelStepDefinition,
|
|
1199
|
-
ctx: WorkflowContext,
|
|
1200
|
-
definition: WorkflowDefinition
|
|
1201
|
-
): Promise<any> {
|
|
1202
|
-
const branchPromises: Promise<{ name: string; result: any }>[] = [];
|
|
1203
|
-
const branchInstanceIds: string[] = [];
|
|
1204
|
-
|
|
1205
|
-
for (const branchDef of step.branches) {
|
|
1206
|
-
// Register branch workflow if not already
|
|
1207
|
-
if (!this.definitions.has(branchDef.name)) {
|
|
1208
|
-
this.definitions.set(branchDef.name, branchDef);
|
|
1007
|
+
this.running.delete(instanceId);
|
|
1008
|
+
}).catch(() => {
|
|
1009
|
+
// State machine already persisted the failure - just clean up
|
|
1010
|
+
const runInfo = this.running.get(instanceId);
|
|
1011
|
+
if (runInfo?.timeout) {
|
|
1012
|
+
clearTimeout(runInfo.timeout);
|
|
1209
1013
|
}
|
|
1210
|
-
|
|
1211
|
-
// Start branch as sub-workflow
|
|
1212
|
-
const branchInstanceId = await this.adapter.createInstance({
|
|
1213
|
-
workflowName: branchDef.name,
|
|
1214
|
-
status: "pending",
|
|
1215
|
-
currentStep: branchDef.startAt,
|
|
1216
|
-
input: ctx.input,
|
|
1217
|
-
stepResults: {},
|
|
1218
|
-
createdAt: new Date(),
|
|
1219
|
-
parentId: instanceId,
|
|
1220
|
-
branchName: branchDef.name,
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
branchInstanceIds.push(branchInstanceId.id);
|
|
1224
|
-
|
|
1225
|
-
// Execute branch
|
|
1226
|
-
const branchPromise = (async () => {
|
|
1227
|
-
await this.executeWorkflow(branchInstanceId.id, branchDef);
|
|
1228
|
-
|
|
1229
|
-
// Wait for branch completion
|
|
1230
|
-
while (true) {
|
|
1231
|
-
const branchInstance = await this.adapter.getInstance(branchInstanceId.id);
|
|
1232
|
-
if (!branchInstance) {
|
|
1233
|
-
throw new Error(`Branch instance ${branchInstanceId.id} not found`);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
if (branchInstance.status === "completed") {
|
|
1237
|
-
return { name: branchDef.name, result: branchInstance.output };
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
if (branchInstance.status === "failed") {
|
|
1241
|
-
throw new Error(branchInstance.error ?? `Branch ${branchDef.name} failed`);
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
|
|
1245
|
-
}
|
|
1246
|
-
})();
|
|
1247
|
-
|
|
1248
|
-
branchPromises.push(branchPromise);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
// Track branch instances
|
|
1252
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1253
|
-
branchInstances: {
|
|
1254
|
-
...((await this.adapter.getInstance(instanceId))?.branchInstances ?? {}),
|
|
1255
|
-
[step.name]: branchInstanceIds,
|
|
1256
|
-
},
|
|
1014
|
+
this.running.delete(instanceId);
|
|
1257
1015
|
});
|
|
1258
|
-
|
|
1259
|
-
// Wait for all branches
|
|
1260
|
-
if (step.onError === "wait-all") {
|
|
1261
|
-
const results = await Promise.allSettled(branchPromises);
|
|
1262
|
-
const output: Record<string, any> = {};
|
|
1263
|
-
const errors: string[] = [];
|
|
1264
|
-
|
|
1265
|
-
for (const result of results) {
|
|
1266
|
-
if (result.status === "fulfilled") {
|
|
1267
|
-
output[result.value.name] = result.value.result;
|
|
1268
|
-
} else {
|
|
1269
|
-
errors.push(result.reason?.message ?? "Branch failed");
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
if (errors.length > 0) {
|
|
1274
|
-
throw new Error(`Parallel branches failed: ${errors.join(", ")}`);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
return output;
|
|
1278
|
-
} else {
|
|
1279
|
-
// fail-fast (default)
|
|
1280
|
-
const results = await Promise.all(branchPromises);
|
|
1281
|
-
const output: Record<string, any> = {};
|
|
1282
|
-
for (const result of results) {
|
|
1283
|
-
output[result.name] = result.result;
|
|
1284
|
-
}
|
|
1285
|
-
return output;
|
|
1286
|
-
}
|
|
1287
1016
|
}
|
|
1288
1017
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1306
|
-
if (instance) {
|
|
1307
|
-
const stepResult = instance.stepResults[step.name];
|
|
1308
|
-
if (stepResult) {
|
|
1309
|
-
stepResult.status = "completed";
|
|
1310
|
-
stepResult.output = { chosen: choice.next };
|
|
1311
|
-
stepResult.completedAt = new Date();
|
|
1312
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1313
|
-
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Emit progress
|
|
1319
|
-
await this.emitEvent("workflow.step.completed", {
|
|
1320
|
-
instanceId,
|
|
1321
|
-
workflowName: (await this.adapter.getInstance(instanceId))?.workflowName,
|
|
1322
|
-
stepName: step.name,
|
|
1323
|
-
output: { chosen: choice.next },
|
|
1018
|
+
/**
|
|
1019
|
+
* Create an event handler that bridges state machine events to Events service + SSE
|
|
1020
|
+
*/
|
|
1021
|
+
private createInlineEventHandler(instanceId: string): StateMachineEvents {
|
|
1022
|
+
return {
|
|
1023
|
+
onStepStarted: (id, stepName, stepType) => {
|
|
1024
|
+
this.emitEvent("workflow.step.started", {
|
|
1025
|
+
instanceId: id,
|
|
1026
|
+
stepName,
|
|
1027
|
+
stepType,
|
|
1028
|
+
});
|
|
1029
|
+
if (this.sse) {
|
|
1030
|
+
this.sse.broadcast(`workflow:${id}`, "step.started", { stepName });
|
|
1031
|
+
this.sse.broadcast("workflows:all", "workflow.step.started", {
|
|
1032
|
+
instanceId: id,
|
|
1033
|
+
stepName,
|
|
1324
1034
|
});
|
|
1325
|
-
|
|
1326
|
-
// Execute next step
|
|
1327
|
-
await this.executeStep(instanceId, definition);
|
|
1328
|
-
return choice.next;
|
|
1329
1035
|
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1343
|
-
if (instance) {
|
|
1344
|
-
const stepResult = instance.stepResults[step.name];
|
|
1345
|
-
if (stepResult) {
|
|
1346
|
-
stepResult.status = "completed";
|
|
1347
|
-
stepResult.output = { chosen: step.default };
|
|
1348
|
-
stepResult.completedAt = new Date();
|
|
1349
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1350
|
-
stepResults: { ...instance.stepResults, [step.name]: stepResult },
|
|
1036
|
+
},
|
|
1037
|
+
onStepCompleted: (id, stepName, output, nextStep) => {
|
|
1038
|
+
this.emitEvent("workflow.step.completed", {
|
|
1039
|
+
instanceId: id,
|
|
1040
|
+
stepName,
|
|
1041
|
+
output,
|
|
1042
|
+
});
|
|
1043
|
+
if (this.sse) {
|
|
1044
|
+
this.sse.broadcast(`workflow:${id}`, "step.completed", { stepName, output });
|
|
1045
|
+
this.sse.broadcast("workflows:all", "workflow.step.completed", {
|
|
1046
|
+
instanceId: id,
|
|
1047
|
+
stepName,
|
|
1351
1048
|
});
|
|
1352
1049
|
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
private async executePassStep(
|
|
1370
|
-
instanceId: string,
|
|
1371
|
-
step: PassStepDefinition,
|
|
1372
|
-
ctx: WorkflowContext
|
|
1373
|
-
): Promise<any> {
|
|
1374
|
-
if (step.result !== undefined) {
|
|
1375
|
-
return step.result;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
if (step.transform) {
|
|
1379
|
-
return step.transform(ctx);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
return ctx.input;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
private buildContext(instance: WorkflowInstance, definition: WorkflowDefinition): WorkflowContext {
|
|
1386
|
-
// Build steps object with outputs
|
|
1387
|
-
const steps: Record<string, any> = {};
|
|
1388
|
-
for (const [name, result] of Object.entries(instance.stepResults)) {
|
|
1389
|
-
if (result.status === "completed" && result.output !== undefined) {
|
|
1390
|
-
steps[name] = result.output;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// Find the previous step's output by tracing the workflow path
|
|
1395
|
-
let prev: any = undefined;
|
|
1396
|
-
if (instance.currentStep) {
|
|
1397
|
-
// Find which step comes before current step
|
|
1398
|
-
for (const [stepName, stepDef] of definition.steps) {
|
|
1399
|
-
if (stepDef.next === instance.currentStep && steps[stepName] !== undefined) {
|
|
1400
|
-
prev = steps[stepName];
|
|
1401
|
-
break;
|
|
1050
|
+
},
|
|
1051
|
+
onStepFailed: (id, stepName, error, attempts) => {
|
|
1052
|
+
this.emitEvent("workflow.step.failed", {
|
|
1053
|
+
instanceId: id,
|
|
1054
|
+
stepName,
|
|
1055
|
+
error,
|
|
1056
|
+
attempts,
|
|
1057
|
+
});
|
|
1058
|
+
if (this.sse) {
|
|
1059
|
+
this.sse.broadcast(`workflow:${id}`, "step.failed", { stepName, error });
|
|
1060
|
+
this.sse.broadcast("workflows:all", "workflow.step.failed", {
|
|
1061
|
+
instanceId: id,
|
|
1062
|
+
stepName,
|
|
1063
|
+
error,
|
|
1064
|
+
});
|
|
1402
1065
|
}
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1066
|
+
},
|
|
1067
|
+
onStepRetry: (id, stepName, attempt, max, delayMs) => {
|
|
1068
|
+
this.emitEvent("workflow.step.retry", {
|
|
1069
|
+
instanceId: id,
|
|
1070
|
+
stepName,
|
|
1071
|
+
attempt,
|
|
1072
|
+
maxAttempts: max,
|
|
1073
|
+
delay: delayMs,
|
|
1074
|
+
});
|
|
1075
|
+
},
|
|
1076
|
+
onProgress: (id, progress, currentStep, completed, total) => {
|
|
1077
|
+
this.emitEvent("workflow.progress", {
|
|
1078
|
+
instanceId: id,
|
|
1079
|
+
progress,
|
|
1080
|
+
currentStep,
|
|
1081
|
+
completedSteps: completed,
|
|
1082
|
+
totalSteps: total,
|
|
1083
|
+
});
|
|
1084
|
+
if (this.sse) {
|
|
1085
|
+
this.sse.broadcast(`workflow:${id}`, "progress", {
|
|
1086
|
+
progress,
|
|
1087
|
+
currentStep,
|
|
1088
|
+
completedSteps: completed,
|
|
1089
|
+
totalSteps: total,
|
|
1090
|
+
});
|
|
1091
|
+
this.sse.broadcast("workflows:all", "workflow.progress", {
|
|
1092
|
+
instanceId: id,
|
|
1093
|
+
progress,
|
|
1094
|
+
currentStep,
|
|
1412
1095
|
});
|
|
1413
|
-
if (completedSteps.length > 0) {
|
|
1414
|
-
prev = completedSteps[0][1].output;
|
|
1415
1096
|
}
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// Metadata snapshot (mutable reference for setMetadata updates)
|
|
1420
|
-
const metadata = { ...(instance.metadata ?? {}) };
|
|
1421
|
-
|
|
1422
|
-
return {
|
|
1423
|
-
input: instance.input,
|
|
1424
|
-
steps,
|
|
1425
|
-
prev,
|
|
1426
|
-
instance,
|
|
1427
|
-
getStepResult: <T = any>(stepName: string): T | undefined => {
|
|
1428
|
-
return steps[stepName] as T | undefined;
|
|
1429
1097
|
},
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
// Update local snapshot
|
|
1435
|
-
metadata[key] = value;
|
|
1436
|
-
// Persist to database
|
|
1437
|
-
await this.adapter.updateInstance(instance.id, {
|
|
1438
|
-
metadata: { ...metadata },
|
|
1098
|
+
onCompleted: (id, output) => {
|
|
1099
|
+
this.emitEvent("workflow.completed", {
|
|
1100
|
+
instanceId: id,
|
|
1101
|
+
output,
|
|
1439
1102
|
});
|
|
1103
|
+
if (this.sse) {
|
|
1104
|
+
this.sse.broadcast(`workflow:${id}`, "completed", { output });
|
|
1105
|
+
this.sse.broadcast("workflows:all", "workflow.completed", {
|
|
1106
|
+
instanceId: id,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1440
1109
|
},
|
|
1441
|
-
|
|
1442
|
-
|
|
1110
|
+
onFailed: (id, error) => {
|
|
1111
|
+
this.emitEvent("workflow.failed", {
|
|
1112
|
+
instanceId: id,
|
|
1113
|
+
error,
|
|
1114
|
+
});
|
|
1115
|
+
if (this.sse) {
|
|
1116
|
+
this.sse.broadcast(`workflow:${id}`, "failed", { error });
|
|
1117
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1118
|
+
instanceId: id,
|
|
1119
|
+
error,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1443
1122
|
},
|
|
1444
1123
|
};
|
|
1445
1124
|
}
|
|
1446
1125
|
|
|
1447
|
-
private async completeStep(
|
|
1448
|
-
instanceId: string,
|
|
1449
|
-
stepName: string,
|
|
1450
|
-
output: any,
|
|
1451
|
-
step: StepDefinition,
|
|
1452
|
-
definition: WorkflowDefinition
|
|
1453
|
-
): Promise<void> {
|
|
1454
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1455
|
-
if (!instance) return;
|
|
1456
|
-
|
|
1457
|
-
// Check if workflow is still running (not cancelled/failed/timed out)
|
|
1458
|
-
if (instance.status !== "running") {
|
|
1459
|
-
console.log(`[Workflows] Ignoring step completion for ${instanceId}, status is ${instance.status}`);
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// Update step result
|
|
1464
|
-
const stepResult = instance.stepResults[stepName] ?? {
|
|
1465
|
-
stepName,
|
|
1466
|
-
status: "pending",
|
|
1467
|
-
attempts: 0,
|
|
1468
|
-
};
|
|
1469
|
-
stepResult.status = "completed";
|
|
1470
|
-
stepResult.output = output;
|
|
1471
|
-
stepResult.completedAt = new Date();
|
|
1472
|
-
|
|
1473
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1474
|
-
stepResults: { ...instance.stepResults, [stepName]: stepResult },
|
|
1475
|
-
});
|
|
1476
|
-
|
|
1477
|
-
// Emit step completed event
|
|
1478
|
-
await this.emitEvent("workflow.step.completed", {
|
|
1479
|
-
instanceId,
|
|
1480
|
-
workflowName: instance.workflowName,
|
|
1481
|
-
stepName,
|
|
1482
|
-
output,
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
// Broadcast step completed via SSE
|
|
1486
|
-
if (this.sse) {
|
|
1487
|
-
this.sse.broadcast(`workflow:${instanceId}`, "step.completed", {
|
|
1488
|
-
stepName,
|
|
1489
|
-
output,
|
|
1490
|
-
});
|
|
1491
|
-
this.sse.broadcast("workflows:all", "workflow.step.completed", {
|
|
1492
|
-
instanceId,
|
|
1493
|
-
workflowName: instance.workflowName,
|
|
1494
|
-
stepName,
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// Calculate and emit progress
|
|
1499
|
-
const totalSteps = definition.steps.size;
|
|
1500
|
-
const completedSteps = Object.values(instance.stepResults).filter(
|
|
1501
|
-
(r) => r.status === "completed"
|
|
1502
|
-
).length + 1; // +1 for current step
|
|
1503
|
-
const progress = Math.round((completedSteps / totalSteps) * 100);
|
|
1504
|
-
|
|
1505
|
-
await this.emitEvent("workflow.progress", {
|
|
1506
|
-
instanceId,
|
|
1507
|
-
workflowName: instance.workflowName,
|
|
1508
|
-
progress,
|
|
1509
|
-
currentStep: stepName,
|
|
1510
|
-
completedSteps,
|
|
1511
|
-
totalSteps,
|
|
1512
|
-
});
|
|
1513
|
-
|
|
1514
|
-
// Broadcast progress via SSE
|
|
1515
|
-
if (this.sse) {
|
|
1516
|
-
this.sse.broadcast(`workflow:${instanceId}`, "progress", {
|
|
1517
|
-
progress,
|
|
1518
|
-
currentStep: stepName,
|
|
1519
|
-
completedSteps,
|
|
1520
|
-
totalSteps,
|
|
1521
|
-
});
|
|
1522
|
-
this.sse.broadcast("workflows:all", "workflow.progress", {
|
|
1523
|
-
instanceId,
|
|
1524
|
-
workflowName: instance.workflowName,
|
|
1525
|
-
progress,
|
|
1526
|
-
currentStep: stepName,
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
// Move to next step or complete
|
|
1531
|
-
if (step.end) {
|
|
1532
|
-
await this.completeWorkflow(instanceId, output);
|
|
1533
|
-
} else if (step.next) {
|
|
1534
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1535
|
-
currentStep: step.next,
|
|
1536
|
-
});
|
|
1537
|
-
await this.executeStep(instanceId, definition);
|
|
1538
|
-
} else {
|
|
1539
|
-
// No next step, complete
|
|
1540
|
-
await this.completeWorkflow(instanceId, output);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
private async handleStepError(
|
|
1545
|
-
instanceId: string,
|
|
1546
|
-
stepName: string,
|
|
1547
|
-
error: string,
|
|
1548
|
-
step: StepDefinition,
|
|
1549
|
-
definition: WorkflowDefinition
|
|
1550
|
-
): Promise<void> {
|
|
1551
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1552
|
-
if (!instance) return;
|
|
1553
|
-
|
|
1554
|
-
const stepResult = instance.stepResults[stepName] ?? {
|
|
1555
|
-
stepName,
|
|
1556
|
-
status: "pending",
|
|
1557
|
-
attempts: 0,
|
|
1558
|
-
};
|
|
1559
|
-
|
|
1560
|
-
// Check retry config
|
|
1561
|
-
const retry = step.retry ?? definition.defaultRetry;
|
|
1562
|
-
if (retry && stepResult.attempts < retry.maxAttempts) {
|
|
1563
|
-
// Retry with backoff
|
|
1564
|
-
const backoffRate = retry.backoffRate ?? 2;
|
|
1565
|
-
const intervalMs = retry.intervalMs ?? 1000;
|
|
1566
|
-
const maxIntervalMs = retry.maxIntervalMs ?? 30000;
|
|
1567
|
-
const delay = Math.min(
|
|
1568
|
-
intervalMs * Math.pow(backoffRate, stepResult.attempts - 1),
|
|
1569
|
-
maxIntervalMs
|
|
1570
|
-
);
|
|
1571
|
-
|
|
1572
|
-
console.log(
|
|
1573
|
-
`[Workflows] Retrying step ${stepName} in ${delay}ms (attempt ${stepResult.attempts}/${retry.maxAttempts})`
|
|
1574
|
-
);
|
|
1575
|
-
|
|
1576
|
-
await this.emitEvent("workflow.step.retry", {
|
|
1577
|
-
instanceId,
|
|
1578
|
-
workflowName: instance.workflowName,
|
|
1579
|
-
stepName,
|
|
1580
|
-
attempt: stepResult.attempts,
|
|
1581
|
-
maxAttempts: retry.maxAttempts,
|
|
1582
|
-
delay,
|
|
1583
|
-
error,
|
|
1584
|
-
});
|
|
1585
|
-
|
|
1586
|
-
// Update step result
|
|
1587
|
-
stepResult.error = error;
|
|
1588
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1589
|
-
stepResults: { ...instance.stepResults, [stepName]: stepResult },
|
|
1590
|
-
});
|
|
1591
|
-
|
|
1592
|
-
// Retry after delay
|
|
1593
|
-
setTimeout(() => {
|
|
1594
|
-
this.executeStep(instanceId, definition);
|
|
1595
|
-
}, delay);
|
|
1596
|
-
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
// No more retries, fail the step
|
|
1601
|
-
stepResult.status = "failed";
|
|
1602
|
-
stepResult.error = error;
|
|
1603
|
-
stepResult.completedAt = new Date();
|
|
1604
|
-
|
|
1605
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1606
|
-
stepResults: { ...instance.stepResults, [stepName]: stepResult },
|
|
1607
|
-
});
|
|
1608
|
-
|
|
1609
|
-
await this.emitEvent("workflow.step.failed", {
|
|
1610
|
-
instanceId,
|
|
1611
|
-
workflowName: instance.workflowName,
|
|
1612
|
-
stepName,
|
|
1613
|
-
error,
|
|
1614
|
-
attempts: stepResult.attempts,
|
|
1615
|
-
});
|
|
1616
|
-
|
|
1617
|
-
// Broadcast step failed via SSE
|
|
1618
|
-
if (this.sse) {
|
|
1619
|
-
this.sse.broadcast(`workflow:${instanceId}`, "step.failed", {
|
|
1620
|
-
stepName,
|
|
1621
|
-
error,
|
|
1622
|
-
});
|
|
1623
|
-
this.sse.broadcast("workflows:all", "workflow.step.failed", {
|
|
1624
|
-
instanceId,
|
|
1625
|
-
workflowName: instance.workflowName,
|
|
1626
|
-
stepName,
|
|
1627
|
-
error,
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
// Fail the workflow
|
|
1632
|
-
await this.failWorkflow(instanceId, `Step "${stepName}" failed: ${error}`);
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
private async completeWorkflow(instanceId: string, output?: any): Promise<void> {
|
|
1636
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1637
|
-
if (!instance) return;
|
|
1638
|
-
|
|
1639
|
-
// Check if workflow is still running (not cancelled/failed/timed out)
|
|
1640
|
-
if (instance.status !== "running") {
|
|
1641
|
-
console.log(`[Workflows] Ignoring workflow completion for ${instanceId}, status is ${instance.status}`);
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
// Clear timeout
|
|
1646
|
-
const runInfo = this.running.get(instanceId);
|
|
1647
|
-
if (runInfo?.timeout) {
|
|
1648
|
-
clearTimeout(runInfo.timeout);
|
|
1649
|
-
}
|
|
1650
|
-
this.running.delete(instanceId);
|
|
1651
|
-
|
|
1652
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1653
|
-
status: "completed",
|
|
1654
|
-
output,
|
|
1655
|
-
completedAt: new Date(),
|
|
1656
|
-
currentStep: undefined,
|
|
1657
|
-
});
|
|
1658
|
-
|
|
1659
|
-
await this.emitEvent("workflow.completed", {
|
|
1660
|
-
instanceId,
|
|
1661
|
-
workflowName: instance.workflowName,
|
|
1662
|
-
output,
|
|
1663
|
-
});
|
|
1664
|
-
|
|
1665
|
-
// Broadcast via SSE
|
|
1666
|
-
if (this.sse) {
|
|
1667
|
-
this.sse.broadcast(`workflow:${instanceId}`, "completed", { output });
|
|
1668
|
-
this.sse.broadcast("workflows:all", "workflow.completed", {
|
|
1669
|
-
instanceId,
|
|
1670
|
-
workflowName: instance.workflowName,
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
private async failWorkflow(instanceId: string, error: string): Promise<void> {
|
|
1676
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1677
|
-
if (!instance) return;
|
|
1678
|
-
|
|
1679
|
-
// Clear timeout
|
|
1680
|
-
const runInfo = this.running.get(instanceId);
|
|
1681
|
-
if (runInfo?.timeout) {
|
|
1682
|
-
clearTimeout(runInfo.timeout);
|
|
1683
|
-
}
|
|
1684
|
-
this.running.delete(instanceId);
|
|
1685
|
-
|
|
1686
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1687
|
-
status: "failed",
|
|
1688
|
-
error,
|
|
1689
|
-
completedAt: new Date(),
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
|
-
await this.emitEvent("workflow.failed", {
|
|
1693
|
-
instanceId,
|
|
1694
|
-
workflowName: instance.workflowName,
|
|
1695
|
-
error,
|
|
1696
|
-
});
|
|
1697
|
-
|
|
1698
|
-
// Broadcast via SSE
|
|
1699
|
-
if (this.sse) {
|
|
1700
|
-
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error });
|
|
1701
|
-
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1702
|
-
instanceId,
|
|
1703
|
-
workflowName: instance.workflowName,
|
|
1704
|
-
error,
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
private async emitEvent(event: string, data: any): Promise<void> {
|
|
1710
|
-
if (this.events) {
|
|
1711
|
-
await this.events.emit(event, data);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
1126
|
// ============================================
|
|
1716
1127
|
// Isolated Execution Engine
|
|
1717
1128
|
// ============================================
|
|
@@ -1799,13 +1210,33 @@ class WorkflowsImpl implements Workflows {
|
|
|
1799
1210
|
const instance = await this.adapter.getInstance(instanceId);
|
|
1800
1211
|
if (instance && instance.status === "running") {
|
|
1801
1212
|
console.error(`[Workflows] Isolated workflow ${instanceId} crashed with exit code ${exitCode}`);
|
|
1802
|
-
await this.
|
|
1213
|
+
await this.adapter.updateInstance(instanceId, {
|
|
1214
|
+
status: "failed",
|
|
1215
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1216
|
+
completedAt: new Date(),
|
|
1217
|
+
});
|
|
1218
|
+
await this.emitEvent("workflow.failed", {
|
|
1219
|
+
instanceId,
|
|
1220
|
+
workflowName: instance.workflowName,
|
|
1221
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1222
|
+
});
|
|
1223
|
+
if (this.sse) {
|
|
1224
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", {
|
|
1225
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1226
|
+
});
|
|
1227
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1228
|
+
instanceId,
|
|
1229
|
+
workflowName: instance.workflowName,
|
|
1230
|
+
error: `Subprocess crashed with exit code ${exitCode}`,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1803
1233
|
}
|
|
1804
1234
|
});
|
|
1805
1235
|
}
|
|
1806
1236
|
|
|
1807
1237
|
/**
|
|
1808
|
-
* Handle events from isolated workflow subprocess
|
|
1238
|
+
* Handle events from isolated workflow subprocess.
|
|
1239
|
+
* The subprocess owns persistence via its own adapter - we only forward events to SSE/Events.
|
|
1809
1240
|
*/
|
|
1810
1241
|
private async handleIsolatedEvent(event: WorkflowEvent): Promise<void> {
|
|
1811
1242
|
const { instanceId, type } = event;
|
|
@@ -1819,42 +1250,22 @@ class WorkflowsImpl implements Workflows {
|
|
|
1819
1250
|
|
|
1820
1251
|
switch (type) {
|
|
1821
1252
|
case "started":
|
|
1822
|
-
// Already marked as running in executeIsolatedWorkflow
|
|
1823
|
-
break;
|
|
1824
|
-
|
|
1825
1253
|
case "heartbeat":
|
|
1826
|
-
//
|
|
1254
|
+
// No-op: heartbeat handled above, started is handled by executeIsolatedWorkflow
|
|
1827
1255
|
break;
|
|
1828
1256
|
|
|
1829
1257
|
case "step.started": {
|
|
1830
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1831
|
-
if (!instance) break;
|
|
1832
|
-
|
|
1833
|
-
// Update current step and step results in DB
|
|
1834
|
-
const stepResult = {
|
|
1835
|
-
stepName: event.stepName!,
|
|
1836
|
-
status: "running" as const,
|
|
1837
|
-
startedAt: new Date(),
|
|
1838
|
-
attempts: (instance.stepResults[event.stepName!]?.attempts ?? 0) + 1,
|
|
1839
|
-
};
|
|
1840
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1841
|
-
currentStep: event.stepName,
|
|
1842
|
-
stepResults: { ...instance.stepResults, [event.stepName!]: stepResult },
|
|
1843
|
-
});
|
|
1844
|
-
|
|
1845
1258
|
await this.emitEvent("workflow.step.started", {
|
|
1846
1259
|
instanceId,
|
|
1847
|
-
workflowName: instance?.workflowName,
|
|
1848
1260
|
stepName: event.stepName,
|
|
1261
|
+
stepType: event.stepType,
|
|
1849
1262
|
});
|
|
1850
|
-
// Broadcast via SSE
|
|
1851
1263
|
if (this.sse) {
|
|
1852
1264
|
this.sse.broadcast(`workflow:${instanceId}`, "step.started", {
|
|
1853
1265
|
stepName: event.stepName,
|
|
1854
1266
|
});
|
|
1855
1267
|
this.sse.broadcast("workflows:all", "workflow.step.started", {
|
|
1856
1268
|
instanceId,
|
|
1857
|
-
workflowName: instance?.workflowName,
|
|
1858
1269
|
stepName: event.stepName,
|
|
1859
1270
|
});
|
|
1860
1271
|
}
|
|
@@ -1862,32 +1273,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
1862
1273
|
}
|
|
1863
1274
|
|
|
1864
1275
|
case "step.completed": {
|
|
1865
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1866
|
-
if (!instance) break;
|
|
1867
|
-
|
|
1868
|
-
// Update step results in DB
|
|
1869
|
-
const stepResult = instance.stepResults[event.stepName!] ?? {
|
|
1870
|
-
stepName: event.stepName!,
|
|
1871
|
-
status: "pending" as const,
|
|
1872
|
-
startedAt: new Date(),
|
|
1873
|
-
attempts: 0,
|
|
1874
|
-
};
|
|
1875
|
-
stepResult.status = "completed";
|
|
1876
|
-
stepResult.output = event.output;
|
|
1877
|
-
stepResult.completedAt = new Date();
|
|
1878
|
-
|
|
1879
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1880
|
-
stepResults: { ...instance.stepResults, [event.stepName!]: stepResult },
|
|
1881
|
-
currentStep: event.nextStep,
|
|
1882
|
-
});
|
|
1883
|
-
|
|
1884
1276
|
await this.emitEvent("workflow.step.completed", {
|
|
1885
1277
|
instanceId,
|
|
1886
|
-
workflowName: instance?.workflowName,
|
|
1887
1278
|
stepName: event.stepName,
|
|
1888
1279
|
output: event.output,
|
|
1889
1280
|
});
|
|
1890
|
-
// Broadcast via SSE
|
|
1891
1281
|
if (this.sse) {
|
|
1892
1282
|
this.sse.broadcast(`workflow:${instanceId}`, "step.completed", {
|
|
1893
1283
|
stepName: event.stepName,
|
|
@@ -1895,7 +1285,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
1895
1285
|
});
|
|
1896
1286
|
this.sse.broadcast("workflows:all", "workflow.step.completed", {
|
|
1897
1287
|
instanceId,
|
|
1898
|
-
workflowName: instance?.workflowName,
|
|
1899
1288
|
stepName: event.stepName,
|
|
1900
1289
|
output: event.output,
|
|
1901
1290
|
});
|
|
@@ -1904,31 +1293,11 @@ class WorkflowsImpl implements Workflows {
|
|
|
1904
1293
|
}
|
|
1905
1294
|
|
|
1906
1295
|
case "step.failed": {
|
|
1907
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1908
|
-
if (!instance) break;
|
|
1909
|
-
|
|
1910
|
-
// Update step results in DB
|
|
1911
|
-
const stepResult = instance.stepResults[event.stepName!] ?? {
|
|
1912
|
-
stepName: event.stepName!,
|
|
1913
|
-
status: "pending" as const,
|
|
1914
|
-
startedAt: new Date(),
|
|
1915
|
-
attempts: 0,
|
|
1916
|
-
};
|
|
1917
|
-
stepResult.status = "failed";
|
|
1918
|
-
stepResult.error = event.error;
|
|
1919
|
-
stepResult.completedAt = new Date();
|
|
1920
|
-
|
|
1921
|
-
await this.adapter.updateInstance(instanceId, {
|
|
1922
|
-
stepResults: { ...instance.stepResults, [event.stepName!]: stepResult },
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
1296
|
await this.emitEvent("workflow.step.failed", {
|
|
1926
1297
|
instanceId,
|
|
1927
|
-
workflowName: instance?.workflowName,
|
|
1928
1298
|
stepName: event.stepName,
|
|
1929
1299
|
error: event.error,
|
|
1930
1300
|
});
|
|
1931
|
-
// Broadcast via SSE
|
|
1932
1301
|
if (this.sse) {
|
|
1933
1302
|
this.sse.broadcast(`workflow:${instanceId}`, "step.failed", {
|
|
1934
1303
|
stepName: event.stepName,
|
|
@@ -1936,7 +1305,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
1936
1305
|
});
|
|
1937
1306
|
this.sse.broadcast("workflows:all", "workflow.step.failed", {
|
|
1938
1307
|
instanceId,
|
|
1939
|
-
workflowName: instance?.workflowName,
|
|
1940
1308
|
stepName: event.stepName,
|
|
1941
1309
|
error: event.error,
|
|
1942
1310
|
});
|
|
@@ -1945,15 +1313,12 @@ class WorkflowsImpl implements Workflows {
|
|
|
1945
1313
|
}
|
|
1946
1314
|
|
|
1947
1315
|
case "progress": {
|
|
1948
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
1949
1316
|
await this.emitEvent("workflow.progress", {
|
|
1950
1317
|
instanceId,
|
|
1951
|
-
workflowName: instance?.workflowName,
|
|
1952
1318
|
progress: event.progress,
|
|
1953
1319
|
completedSteps: event.completedSteps,
|
|
1954
1320
|
totalSteps: event.totalSteps,
|
|
1955
1321
|
});
|
|
1956
|
-
// Broadcast via SSE
|
|
1957
1322
|
if (this.sse) {
|
|
1958
1323
|
this.sse.broadcast(`workflow:${instanceId}`, "progress", {
|
|
1959
1324
|
progress: event.progress,
|
|
@@ -1962,7 +1327,6 @@ class WorkflowsImpl implements Workflows {
|
|
|
1962
1327
|
});
|
|
1963
1328
|
this.sse.broadcast("workflows:all", "workflow.progress", {
|
|
1964
1329
|
instanceId,
|
|
1965
|
-
workflowName: instance?.workflowName,
|
|
1966
1330
|
progress: event.progress,
|
|
1967
1331
|
completedSteps: event.completedSteps,
|
|
1968
1332
|
totalSteps: event.totalSteps,
|
|
@@ -1971,13 +1335,40 @@ class WorkflowsImpl implements Workflows {
|
|
|
1971
1335
|
break;
|
|
1972
1336
|
}
|
|
1973
1337
|
|
|
1974
|
-
case "completed":
|
|
1975
|
-
|
|
1338
|
+
case "completed": {
|
|
1339
|
+
// Clean up isolated process tracking
|
|
1340
|
+
this.cleanupIsolatedProcess(instanceId);
|
|
1341
|
+
|
|
1342
|
+
// Subprocess already persisted state - just emit events
|
|
1343
|
+
await this.emitEvent("workflow.completed", {
|
|
1344
|
+
instanceId,
|
|
1345
|
+
output: event.output,
|
|
1346
|
+
});
|
|
1347
|
+
if (this.sse) {
|
|
1348
|
+
this.sse.broadcast(`workflow:${instanceId}`, "completed", { output: event.output });
|
|
1349
|
+
this.sse.broadcast("workflows:all", "workflow.completed", { instanceId });
|
|
1350
|
+
}
|
|
1976
1351
|
break;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
case "failed": {
|
|
1355
|
+
// Clean up isolated process tracking
|
|
1356
|
+
this.cleanupIsolatedProcess(instanceId);
|
|
1977
1357
|
|
|
1978
|
-
|
|
1979
|
-
await this.
|
|
1358
|
+
// Subprocess already persisted state - just emit events
|
|
1359
|
+
await this.emitEvent("workflow.failed", {
|
|
1360
|
+
instanceId,
|
|
1361
|
+
error: event.error,
|
|
1362
|
+
});
|
|
1363
|
+
if (this.sse) {
|
|
1364
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: event.error });
|
|
1365
|
+
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
1366
|
+
instanceId,
|
|
1367
|
+
error: event.error,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1980
1370
|
break;
|
|
1371
|
+
}
|
|
1981
1372
|
}
|
|
1982
1373
|
}
|
|
1983
1374
|
|
|
@@ -2015,6 +1406,18 @@ class WorkflowsImpl implements Workflows {
|
|
|
2015
1406
|
}
|
|
2016
1407
|
}
|
|
2017
1408
|
|
|
1409
|
+
/**
|
|
1410
|
+
* Clean up isolated process tracking
|
|
1411
|
+
*/
|
|
1412
|
+
private cleanupIsolatedProcess(instanceId: string): void {
|
|
1413
|
+
const info = this.isolatedProcesses.get(instanceId);
|
|
1414
|
+
if (info) {
|
|
1415
|
+
if (info.timeout) clearTimeout(info.timeout);
|
|
1416
|
+
if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
|
|
1417
|
+
this.isolatedProcesses.delete(instanceId);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
2018
1421
|
/**
|
|
2019
1422
|
* Reset heartbeat timeout for an isolated workflow
|
|
2020
1423
|
*/
|
|
@@ -2060,85 +1463,29 @@ class WorkflowsImpl implements Workflows {
|
|
|
2060
1463
|
await this.getSocketServer().closeSocket(instanceId);
|
|
2061
1464
|
|
|
2062
1465
|
// Fail the workflow
|
|
2063
|
-
await this.failWorkflow(instanceId, "Workflow timed out");
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
/**
|
|
2067
|
-
* Complete an isolated workflow (called from event handler)
|
|
2068
|
-
*/
|
|
2069
|
-
private async completeWorkflowIsolated(instanceId: string, output?: any): Promise<void> {
|
|
2070
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
2071
|
-
if (!instance) return;
|
|
2072
|
-
|
|
2073
|
-
// Clean up isolated process tracking (process should have exited)
|
|
2074
|
-
const info = this.isolatedProcesses.get(instanceId);
|
|
2075
|
-
if (info) {
|
|
2076
|
-
if (info.timeout) clearTimeout(info.timeout);
|
|
2077
|
-
if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
|
|
2078
|
-
this.isolatedProcesses.delete(instanceId);
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
await this.adapter.updateInstance(instanceId, {
|
|
2082
|
-
status: "completed",
|
|
2083
|
-
output,
|
|
2084
|
-
completedAt: new Date(),
|
|
2085
|
-
currentStep: undefined,
|
|
2086
|
-
});
|
|
2087
|
-
|
|
2088
|
-
await this.emitEvent("workflow.completed", {
|
|
2089
|
-
instanceId,
|
|
2090
|
-
workflowName: instance.workflowName,
|
|
2091
|
-
output,
|
|
2092
|
-
});
|
|
2093
|
-
|
|
2094
|
-
// Broadcast via SSE
|
|
2095
|
-
if (this.sse) {
|
|
2096
|
-
this.sse.broadcast(`workflow:${instanceId}`, "completed", { output });
|
|
2097
|
-
this.sse.broadcast("workflows:all", "workflow.completed", {
|
|
2098
|
-
instanceId,
|
|
2099
|
-
workflowName: instance.workflowName,
|
|
2100
|
-
output,
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
/**
|
|
2106
|
-
* Fail an isolated workflow (called from event handler)
|
|
2107
|
-
*/
|
|
2108
|
-
private async failWorkflowIsolated(instanceId: string, error: string): Promise<void> {
|
|
2109
|
-
const instance = await this.adapter.getInstance(instanceId);
|
|
2110
|
-
if (!instance) return;
|
|
2111
|
-
|
|
2112
|
-
// Clean up isolated process tracking
|
|
2113
|
-
const info = this.isolatedProcesses.get(instanceId);
|
|
2114
|
-
if (info) {
|
|
2115
|
-
if (info.timeout) clearTimeout(info.timeout);
|
|
2116
|
-
if (info.heartbeatTimeout) clearTimeout(info.heartbeatTimeout);
|
|
2117
|
-
this.isolatedProcesses.delete(instanceId);
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
1466
|
await this.adapter.updateInstance(instanceId, {
|
|
2121
1467
|
status: "failed",
|
|
2122
|
-
error,
|
|
1468
|
+
error: "Workflow timed out",
|
|
2123
1469
|
completedAt: new Date(),
|
|
2124
1470
|
});
|
|
2125
|
-
|
|
2126
1471
|
await this.emitEvent("workflow.failed", {
|
|
2127
1472
|
instanceId,
|
|
2128
|
-
|
|
2129
|
-
error,
|
|
1473
|
+
error: "Workflow timed out",
|
|
2130
1474
|
});
|
|
2131
|
-
|
|
2132
|
-
// Broadcast via SSE
|
|
2133
1475
|
if (this.sse) {
|
|
2134
|
-
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error });
|
|
1476
|
+
this.sse.broadcast(`workflow:${instanceId}`, "failed", { error: "Workflow timed out" });
|
|
2135
1477
|
this.sse.broadcast("workflows:all", "workflow.failed", {
|
|
2136
1478
|
instanceId,
|
|
2137
|
-
|
|
2138
|
-
error,
|
|
1479
|
+
error: "Workflow timed out",
|
|
2139
1480
|
});
|
|
2140
1481
|
}
|
|
2141
1482
|
}
|
|
1483
|
+
|
|
1484
|
+
private async emitEvent(event: string, data: any): Promise<void> {
|
|
1485
|
+
if (this.eventsService) {
|
|
1486
|
+
await this.eventsService.emit(event, data);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
2142
1489
|
}
|
|
2143
1490
|
|
|
2144
1491
|
// ============================================
|