@donkeylabs/server 0.4.7 → 0.4.8

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.
@@ -0,0 +1,1173 @@
1
+ // Core Workflows Service
2
+ // Step function / state machine orchestration built on Jobs
3
+ //
4
+ // Supports:
5
+ // - task: Execute a job (sync or async)
6
+ // - parallel: Run multiple branches concurrently
7
+ // - choice: Conditional branching
8
+ // - pass: Transform data / no-op
9
+
10
+ import type { Events } from "./events";
11
+ import type { Jobs } from "./jobs";
12
+ import type { SSE } from "./sse";
13
+
14
+ // ============================================
15
+ // Step Types
16
+ // ============================================
17
+
18
+ export type StepType = "task" | "parallel" | "choice" | "pass";
19
+
20
+ export interface BaseStepDefinition {
21
+ name: string;
22
+ type: StepType;
23
+ next?: string;
24
+ end?: boolean;
25
+ /** Retry configuration for this step */
26
+ retry?: RetryConfig;
27
+ /** Timeout for this step in milliseconds */
28
+ timeout?: number;
29
+ }
30
+
31
+ export interface RetryConfig {
32
+ maxAttempts: number;
33
+ /** Backoff multiplier (default: 2) */
34
+ backoffRate?: number;
35
+ /** Initial delay in ms (default: 1000) */
36
+ intervalMs?: number;
37
+ /** Max delay in ms (default: 30000) */
38
+ maxIntervalMs?: number;
39
+ /** Errors to retry on (default: all) */
40
+ retryOn?: string[];
41
+ }
42
+
43
+ // Task Step: Execute a job
44
+ export interface TaskStepDefinition extends BaseStepDefinition {
45
+ type: "task";
46
+ /** Job name to execute */
47
+ job: string;
48
+ /** Transform workflow context to job input */
49
+ input?: (ctx: WorkflowContext) => any;
50
+ /** Transform job result to step output */
51
+ output?: (result: any, ctx: WorkflowContext) => any;
52
+ }
53
+
54
+ // Parallel Step: Run branches concurrently
55
+ export interface ParallelStepDefinition extends BaseStepDefinition {
56
+ type: "parallel";
57
+ /** Branches to execute in parallel */
58
+ branches: WorkflowDefinition[];
59
+ /** How to handle branch failures */
60
+ onError?: "fail-fast" | "wait-all";
61
+ }
62
+
63
+ // Choice Step: Conditional branching
64
+ export interface ChoiceCondition {
65
+ /** Condition function - return true to take this branch */
66
+ condition: (ctx: WorkflowContext) => boolean;
67
+ /** Step to go to if condition is true */
68
+ next: string;
69
+ }
70
+
71
+ export interface ChoiceStepDefinition extends BaseStepDefinition {
72
+ type: "choice";
73
+ /** Conditions evaluated in order */
74
+ choices: ChoiceCondition[];
75
+ /** Default step if no conditions match */
76
+ default?: string;
77
+ }
78
+
79
+ // Pass Step: Transform data or no-op
80
+ export interface PassStepDefinition extends BaseStepDefinition {
81
+ type: "pass";
82
+ /** Transform input to output */
83
+ transform?: (ctx: WorkflowContext) => any;
84
+ /** Static result to use */
85
+ result?: any;
86
+ }
87
+
88
+ export type StepDefinition =
89
+ | TaskStepDefinition
90
+ | ParallelStepDefinition
91
+ | ChoiceStepDefinition
92
+ | PassStepDefinition;
93
+
94
+ // ============================================
95
+ // Workflow Definition
96
+ // ============================================
97
+
98
+ export interface WorkflowDefinition {
99
+ name: string;
100
+ steps: Map<string, StepDefinition>;
101
+ startAt: string;
102
+ /** Default timeout for the entire workflow in ms */
103
+ timeout?: number;
104
+ /** Default retry config for all steps */
105
+ defaultRetry?: RetryConfig;
106
+ }
107
+
108
+ // ============================================
109
+ // Workflow Instance (Runtime State)
110
+ // ============================================
111
+
112
+ export type WorkflowStatus =
113
+ | "pending"
114
+ | "running"
115
+ | "completed"
116
+ | "failed"
117
+ | "cancelled"
118
+ | "timed_out";
119
+
120
+ export type StepStatus =
121
+ | "pending"
122
+ | "running"
123
+ | "completed"
124
+ | "failed"
125
+ | "skipped";
126
+
127
+ export interface StepResult {
128
+ stepName: string;
129
+ status: StepStatus;
130
+ input?: any;
131
+ output?: any;
132
+ error?: string;
133
+ startedAt?: Date;
134
+ completedAt?: Date;
135
+ attempts: number;
136
+ }
137
+
138
+ export interface WorkflowInstance {
139
+ id: string;
140
+ workflowName: string;
141
+ status: WorkflowStatus;
142
+ currentStep?: string;
143
+ input: any;
144
+ output?: any;
145
+ error?: string;
146
+ stepResults: Record<string, StepResult>;
147
+ /** For parallel steps, track branch instances */
148
+ branchInstances?: Record<string, string[]>;
149
+ createdAt: Date;
150
+ startedAt?: Date;
151
+ completedAt?: Date;
152
+ /** Parent workflow instance ID (for branches) */
153
+ parentId?: string;
154
+ /** Branch name if this is a branch instance */
155
+ branchName?: string;
156
+ }
157
+
158
+ // ============================================
159
+ // Workflow Context (Available to steps)
160
+ // ============================================
161
+
162
+ export interface WorkflowContext {
163
+ /** Original workflow input */
164
+ input: any;
165
+ /** Results from completed steps */
166
+ steps: Record<string, any>;
167
+ /** Current workflow instance */
168
+ instance: WorkflowInstance;
169
+ /** Get a step result with type safety */
170
+ getStepResult<T = any>(stepName: string): T | undefined;
171
+ }
172
+
173
+ // ============================================
174
+ // Workflow Adapter (Persistence)
175
+ // ============================================
176
+
177
+ export interface WorkflowAdapter {
178
+ createInstance(instance: Omit<WorkflowInstance, "id">): Promise<WorkflowInstance>;
179
+ getInstance(instanceId: string): Promise<WorkflowInstance | null>;
180
+ updateInstance(instanceId: string, updates: Partial<WorkflowInstance>): Promise<void>;
181
+ deleteInstance(instanceId: string): Promise<boolean>;
182
+ getInstancesByWorkflow(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
183
+ getRunningInstances(): Promise<WorkflowInstance[]>;
184
+ }
185
+
186
+ // In-memory adapter
187
+ export class MemoryWorkflowAdapter implements WorkflowAdapter {
188
+ private instances = new Map<string, WorkflowInstance>();
189
+ private counter = 0;
190
+
191
+ async createInstance(instance: Omit<WorkflowInstance, "id">): Promise<WorkflowInstance> {
192
+ const id = `wf_${++this.counter}_${Date.now()}`;
193
+ const fullInstance: WorkflowInstance = { ...instance, id };
194
+ this.instances.set(id, fullInstance);
195
+ return fullInstance;
196
+ }
197
+
198
+ async getInstance(instanceId: string): Promise<WorkflowInstance | null> {
199
+ return this.instances.get(instanceId) ?? null;
200
+ }
201
+
202
+ async updateInstance(instanceId: string, updates: Partial<WorkflowInstance>): Promise<void> {
203
+ const instance = this.instances.get(instanceId);
204
+ if (instance) {
205
+ Object.assign(instance, updates);
206
+ }
207
+ }
208
+
209
+ async deleteInstance(instanceId: string): Promise<boolean> {
210
+ return this.instances.delete(instanceId);
211
+ }
212
+
213
+ async getInstancesByWorkflow(
214
+ workflowName: string,
215
+ status?: WorkflowStatus
216
+ ): Promise<WorkflowInstance[]> {
217
+ const results: WorkflowInstance[] = [];
218
+ for (const instance of this.instances.values()) {
219
+ if (instance.workflowName === workflowName) {
220
+ if (!status || instance.status === status) {
221
+ results.push(instance);
222
+ }
223
+ }
224
+ }
225
+ return results;
226
+ }
227
+
228
+ async getRunningInstances(): Promise<WorkflowInstance[]> {
229
+ const results: WorkflowInstance[] = [];
230
+ for (const instance of this.instances.values()) {
231
+ if (instance.status === "running") {
232
+ results.push(instance);
233
+ }
234
+ }
235
+ return results;
236
+ }
237
+ }
238
+
239
+ // ============================================
240
+ // Workflow Builder (Fluent API)
241
+ // ============================================
242
+
243
+ export class WorkflowBuilder {
244
+ private _name: string;
245
+ private _steps = new Map<string, StepDefinition>();
246
+ private _startAt?: string;
247
+ private _timeout?: number;
248
+ private _defaultRetry?: RetryConfig;
249
+ private _lastStep?: string;
250
+
251
+ constructor(name: string) {
252
+ this._name = name;
253
+ }
254
+
255
+ /** Set the starting step explicitly */
256
+ startAt(stepName: string): this {
257
+ this._startAt = stepName;
258
+ return this;
259
+ }
260
+
261
+ /** Set default timeout for the workflow */
262
+ timeout(ms: number): this {
263
+ this._timeout = ms;
264
+ return this;
265
+ }
266
+
267
+ /** Set default retry config for all steps */
268
+ defaultRetry(config: RetryConfig): this {
269
+ this._defaultRetry = config;
270
+ return this;
271
+ }
272
+
273
+ /** Add a task step that executes a job */
274
+ task(
275
+ name: string,
276
+ config: {
277
+ job: string;
278
+ input?: (ctx: WorkflowContext) => any;
279
+ output?: (result: any, ctx: WorkflowContext) => any;
280
+ retry?: RetryConfig;
281
+ timeout?: number;
282
+ next?: string;
283
+ end?: boolean;
284
+ }
285
+ ): this {
286
+ const step: TaskStepDefinition = {
287
+ name,
288
+ type: "task",
289
+ job: config.job,
290
+ input: config.input,
291
+ output: config.output,
292
+ retry: config.retry,
293
+ timeout: config.timeout,
294
+ next: config.next,
295
+ end: config.end,
296
+ };
297
+
298
+ this.addStep(step);
299
+ return this;
300
+ }
301
+
302
+ /** Add a parallel step that runs branches concurrently */
303
+ parallel(
304
+ name: string,
305
+ config: {
306
+ branches: WorkflowDefinition[];
307
+ onError?: "fail-fast" | "wait-all";
308
+ next?: string;
309
+ end?: boolean;
310
+ }
311
+ ): this {
312
+ const step: ParallelStepDefinition = {
313
+ name,
314
+ type: "parallel",
315
+ branches: config.branches,
316
+ onError: config.onError,
317
+ next: config.next,
318
+ end: config.end,
319
+ };
320
+
321
+ this.addStep(step);
322
+ return this;
323
+ }
324
+
325
+ /** Add a choice step for conditional branching */
326
+ choice(
327
+ name: string,
328
+ config: {
329
+ choices: ChoiceCondition[];
330
+ default?: string;
331
+ }
332
+ ): this {
333
+ const step: ChoiceStepDefinition = {
334
+ name,
335
+ type: "choice",
336
+ choices: config.choices,
337
+ default: config.default,
338
+ };
339
+
340
+ this.addStep(step);
341
+ return this;
342
+ }
343
+
344
+ /** Add a pass step for data transformation */
345
+ pass(
346
+ name: string,
347
+ config?: {
348
+ transform?: (ctx: WorkflowContext) => any;
349
+ result?: any;
350
+ next?: string;
351
+ end?: boolean;
352
+ }
353
+ ): this {
354
+ const step: PassStepDefinition = {
355
+ name,
356
+ type: "pass",
357
+ transform: config?.transform,
358
+ result: config?.result,
359
+ next: config?.next,
360
+ end: config?.end ?? (!config?.next),
361
+ };
362
+
363
+ this.addStep(step);
364
+ return this;
365
+ }
366
+
367
+ /** Add an end step (shorthand for pass with end: true) */
368
+ end(name: string = "end"): this {
369
+ return this.pass(name, { end: true });
370
+ }
371
+
372
+ private addStep(step: StepDefinition): void {
373
+ // Auto-link previous step to this one
374
+ if (this._lastStep && !this._steps.get(this._lastStep)?.next && !this._steps.get(this._lastStep)?.end) {
375
+ const lastStep = this._steps.get(this._lastStep)!;
376
+ if (lastStep.type !== "choice") {
377
+ lastStep.next = step.name;
378
+ }
379
+ }
380
+
381
+ // First step is the start
382
+ if (!this._startAt) {
383
+ this._startAt = step.name;
384
+ }
385
+
386
+ this._steps.set(step.name, step);
387
+ this._lastStep = step.name;
388
+ }
389
+
390
+ /** Build the workflow definition */
391
+ build(): WorkflowDefinition {
392
+ if (!this._startAt) {
393
+ throw new Error("Workflow must have at least one step");
394
+ }
395
+
396
+ // Validate: mark last step as end if not already
397
+ const lastStep = this._steps.get(this._lastStep!);
398
+ if (lastStep && !lastStep.next && !lastStep.end && lastStep.type !== "choice") {
399
+ lastStep.end = true;
400
+ }
401
+
402
+ return {
403
+ name: this._name,
404
+ steps: this._steps,
405
+ startAt: this._startAt,
406
+ timeout: this._timeout,
407
+ defaultRetry: this._defaultRetry,
408
+ };
409
+ }
410
+ }
411
+
412
+ /** Create a workflow builder */
413
+ export function workflow(name: string): WorkflowBuilder {
414
+ return new WorkflowBuilder(name);
415
+ }
416
+
417
+ /** Create a branch for parallel steps */
418
+ workflow.branch = function (name: string): WorkflowBuilder {
419
+ return new WorkflowBuilder(name);
420
+ };
421
+
422
+ // ============================================
423
+ // Workflow Service Interface
424
+ // ============================================
425
+
426
+ export interface WorkflowsConfig {
427
+ adapter?: WorkflowAdapter;
428
+ events?: Events;
429
+ jobs?: Jobs;
430
+ sse?: SSE;
431
+ /** Poll interval for checking job completion (ms) */
432
+ pollInterval?: number;
433
+ }
434
+
435
+ export interface Workflows {
436
+ /** Register a workflow definition */
437
+ register(definition: WorkflowDefinition): void;
438
+ /** Start a new workflow instance */
439
+ start<T = any>(workflowName: string, input: T): Promise<string>;
440
+ /** Get a workflow instance by ID */
441
+ getInstance(instanceId: string): Promise<WorkflowInstance | null>;
442
+ /** Cancel a running workflow */
443
+ cancel(instanceId: string): Promise<boolean>;
444
+ /** Get all instances of a workflow */
445
+ getInstances(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]>;
446
+ /** Resume workflows after server restart */
447
+ resume(): Promise<void>;
448
+ /** Stop the workflow service */
449
+ stop(): Promise<void>;
450
+ }
451
+
452
+ // ============================================
453
+ // Workflow Service Implementation
454
+ // ============================================
455
+
456
+ class WorkflowsImpl implements Workflows {
457
+ private adapter: WorkflowAdapter;
458
+ private events?: Events;
459
+ private jobs?: Jobs;
460
+ private sse?: SSE;
461
+ private definitions = new Map<string, WorkflowDefinition>();
462
+ private running = new Map<string, { timeout?: ReturnType<typeof setTimeout> }>();
463
+ private pollInterval: number;
464
+
465
+ constructor(config: WorkflowsConfig = {}) {
466
+ this.adapter = config.adapter ?? new MemoryWorkflowAdapter();
467
+ this.events = config.events;
468
+ this.jobs = config.jobs;
469
+ this.sse = config.sse;
470
+ this.pollInterval = config.pollInterval ?? 1000;
471
+ }
472
+
473
+ register(definition: WorkflowDefinition): void {
474
+ if (this.definitions.has(definition.name)) {
475
+ throw new Error(`Workflow "${definition.name}" is already registered`);
476
+ }
477
+ this.definitions.set(definition.name, definition);
478
+ }
479
+
480
+ async start<T = any>(workflowName: string, input: T): Promise<string> {
481
+ const definition = this.definitions.get(workflowName);
482
+ if (!definition) {
483
+ throw new Error(`Workflow "${workflowName}" is not registered`);
484
+ }
485
+
486
+ const instance = await this.adapter.createInstance({
487
+ workflowName,
488
+ status: "pending",
489
+ currentStep: definition.startAt,
490
+ input,
491
+ stepResults: {},
492
+ createdAt: new Date(),
493
+ });
494
+
495
+ // Emit start event
496
+ await this.emitEvent("workflow.started", {
497
+ instanceId: instance.id,
498
+ workflowName,
499
+ input,
500
+ });
501
+
502
+ // Start execution
503
+ this.executeWorkflow(instance.id, definition);
504
+
505
+ return instance.id;
506
+ }
507
+
508
+ async getInstance(instanceId: string): Promise<WorkflowInstance | null> {
509
+ return this.adapter.getInstance(instanceId);
510
+ }
511
+
512
+ async cancel(instanceId: string): Promise<boolean> {
513
+ const instance = await this.adapter.getInstance(instanceId);
514
+ if (!instance || instance.status !== "running") {
515
+ return false;
516
+ }
517
+
518
+ // Clear timeout
519
+ const runInfo = this.running.get(instanceId);
520
+ if (runInfo?.timeout) {
521
+ clearTimeout(runInfo.timeout);
522
+ }
523
+ this.running.delete(instanceId);
524
+
525
+ // Update status
526
+ await this.adapter.updateInstance(instanceId, {
527
+ status: "cancelled",
528
+ completedAt: new Date(),
529
+ });
530
+
531
+ await this.emitEvent("workflow.cancelled", {
532
+ instanceId,
533
+ workflowName: instance.workflowName,
534
+ });
535
+
536
+ return true;
537
+ }
538
+
539
+ async getInstances(workflowName: string, status?: WorkflowStatus): Promise<WorkflowInstance[]> {
540
+ return this.adapter.getInstancesByWorkflow(workflowName, status);
541
+ }
542
+
543
+ async resume(): Promise<void> {
544
+ const running = await this.adapter.getRunningInstances();
545
+
546
+ for (const instance of running) {
547
+ const definition = this.definitions.get(instance.workflowName);
548
+ if (!definition) {
549
+ // Workflow no longer registered, mark as failed
550
+ await this.adapter.updateInstance(instance.id, {
551
+ status: "failed",
552
+ error: "Workflow definition not found after restart",
553
+ completedAt: new Date(),
554
+ });
555
+ continue;
556
+ }
557
+
558
+ console.log(`[Workflows] Resuming workflow instance ${instance.id}`);
559
+ this.executeWorkflow(instance.id, definition);
560
+ }
561
+ }
562
+
563
+ async stop(): Promise<void> {
564
+ // Clear all timeouts
565
+ for (const [instanceId, runInfo] of this.running) {
566
+ if (runInfo.timeout) {
567
+ clearTimeout(runInfo.timeout);
568
+ }
569
+ }
570
+ this.running.clear();
571
+ }
572
+
573
+ // ============================================
574
+ // Execution Engine
575
+ // ============================================
576
+
577
+ private async executeWorkflow(
578
+ instanceId: string,
579
+ definition: WorkflowDefinition
580
+ ): Promise<void> {
581
+ const instance = await this.adapter.getInstance(instanceId);
582
+ if (!instance) return;
583
+
584
+ // Mark as running
585
+ if (instance.status === "pending") {
586
+ await this.adapter.updateInstance(instanceId, {
587
+ status: "running",
588
+ startedAt: new Date(),
589
+ });
590
+ }
591
+
592
+ // Set up workflow timeout
593
+ if (definition.timeout) {
594
+ const timeout = setTimeout(async () => {
595
+ await this.failWorkflow(instanceId, "Workflow timed out");
596
+ }, definition.timeout);
597
+ this.running.set(instanceId, { timeout });
598
+ } else {
599
+ this.running.set(instanceId, {});
600
+ }
601
+
602
+ // Execute current step
603
+ await this.executeStep(instanceId, definition);
604
+ }
605
+
606
+ private async executeStep(
607
+ instanceId: string,
608
+ definition: WorkflowDefinition
609
+ ): Promise<void> {
610
+ const instance = await this.adapter.getInstance(instanceId);
611
+ if (!instance || instance.status !== "running") return;
612
+
613
+ const stepName = instance.currentStep;
614
+ if (!stepName) {
615
+ await this.completeWorkflow(instanceId);
616
+ return;
617
+ }
618
+
619
+ const step = definition.steps.get(stepName);
620
+ if (!step) {
621
+ await this.failWorkflow(instanceId, `Step "${stepName}" not found`);
622
+ return;
623
+ }
624
+
625
+ // Build context
626
+ const ctx = this.buildContext(instance);
627
+
628
+ // Emit step started event
629
+ await this.emitEvent("workflow.step.started", {
630
+ instanceId,
631
+ workflowName: instance.workflowName,
632
+ stepName,
633
+ stepType: step.type,
634
+ });
635
+
636
+ // Update step result as running
637
+ const stepResult: StepResult = {
638
+ stepName,
639
+ status: "running",
640
+ startedAt: new Date(),
641
+ attempts: (instance.stepResults[stepName]?.attempts ?? 0) + 1,
642
+ };
643
+ await this.adapter.updateInstance(instanceId, {
644
+ stepResults: { ...instance.stepResults, [stepName]: stepResult },
645
+ });
646
+
647
+ try {
648
+ let output: any;
649
+
650
+ switch (step.type) {
651
+ case "task":
652
+ output = await this.executeTaskStep(instanceId, step, ctx, definition);
653
+ break;
654
+ case "parallel":
655
+ output = await this.executeParallelStep(instanceId, step, ctx, definition);
656
+ break;
657
+ case "choice":
658
+ output = await this.executeChoiceStep(instanceId, step, ctx, definition);
659
+ break;
660
+ case "pass":
661
+ output = await this.executePassStep(instanceId, step, ctx);
662
+ break;
663
+ }
664
+
665
+ // Step completed successfully
666
+ await this.completeStep(instanceId, stepName, output, step, definition);
667
+ } catch (error) {
668
+ const errorMsg = error instanceof Error ? error.message : String(error);
669
+ await this.handleStepError(instanceId, stepName, errorMsg, step, definition);
670
+ }
671
+ }
672
+
673
+ private async executeTaskStep(
674
+ instanceId: string,
675
+ step: TaskStepDefinition,
676
+ ctx: WorkflowContext,
677
+ definition: WorkflowDefinition
678
+ ): Promise<any> {
679
+ if (!this.jobs) {
680
+ throw new Error("Jobs service not configured");
681
+ }
682
+
683
+ // Prepare job input
684
+ const jobInput = step.input ? step.input(ctx) : ctx.input;
685
+
686
+ // Update step with input
687
+ const instance = await this.adapter.getInstance(instanceId);
688
+ if (instance) {
689
+ const stepResult = instance.stepResults[step.name];
690
+ if (stepResult) {
691
+ stepResult.input = jobInput;
692
+ await this.adapter.updateInstance(instanceId, {
693
+ stepResults: { ...instance.stepResults, [step.name]: stepResult },
694
+ });
695
+ }
696
+ }
697
+
698
+ // Enqueue the job
699
+ const jobId = await this.jobs.enqueue(step.job, {
700
+ ...jobInput,
701
+ _workflowInstanceId: instanceId,
702
+ _workflowStepName: step.name,
703
+ });
704
+
705
+ // Wait for job completion
706
+ const result = await this.waitForJob(jobId, step.timeout);
707
+
708
+ // Transform output if needed
709
+ return step.output ? step.output(result, ctx) : result;
710
+ }
711
+
712
+ private async waitForJob(jobId: string, timeout?: number): Promise<any> {
713
+ if (!this.jobs) {
714
+ throw new Error("Jobs service not configured");
715
+ }
716
+
717
+ const startTime = Date.now();
718
+
719
+ while (true) {
720
+ const job = await this.jobs.get(jobId);
721
+
722
+ if (!job) {
723
+ throw new Error(`Job ${jobId} not found`);
724
+ }
725
+
726
+ if (job.status === "completed") {
727
+ return job.result;
728
+ }
729
+
730
+ if (job.status === "failed") {
731
+ throw new Error(job.error ?? "Job failed");
732
+ }
733
+
734
+ // Check timeout
735
+ if (timeout && Date.now() - startTime > timeout) {
736
+ throw new Error("Job timed out");
737
+ }
738
+
739
+ // Wait before polling again
740
+ await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
741
+ }
742
+ }
743
+
744
+ private async executeParallelStep(
745
+ instanceId: string,
746
+ step: ParallelStepDefinition,
747
+ ctx: WorkflowContext,
748
+ definition: WorkflowDefinition
749
+ ): Promise<any> {
750
+ const branchPromises: Promise<{ name: string; result: any }>[] = [];
751
+ const branchInstanceIds: string[] = [];
752
+
753
+ for (const branchDef of step.branches) {
754
+ // Register branch workflow if not already
755
+ if (!this.definitions.has(branchDef.name)) {
756
+ this.definitions.set(branchDef.name, branchDef);
757
+ }
758
+
759
+ // Start branch as sub-workflow
760
+ const branchInstanceId = await this.adapter.createInstance({
761
+ workflowName: branchDef.name,
762
+ status: "pending",
763
+ currentStep: branchDef.startAt,
764
+ input: ctx.input,
765
+ stepResults: {},
766
+ createdAt: new Date(),
767
+ parentId: instanceId,
768
+ branchName: branchDef.name,
769
+ });
770
+
771
+ branchInstanceIds.push(branchInstanceId.id);
772
+
773
+ // Execute branch
774
+ const branchPromise = (async () => {
775
+ await this.executeWorkflow(branchInstanceId.id, branchDef);
776
+
777
+ // Wait for branch completion
778
+ while (true) {
779
+ const branchInstance = await this.adapter.getInstance(branchInstanceId.id);
780
+ if (!branchInstance) {
781
+ throw new Error(`Branch instance ${branchInstanceId.id} not found`);
782
+ }
783
+
784
+ if (branchInstance.status === "completed") {
785
+ return { name: branchDef.name, result: branchInstance.output };
786
+ }
787
+
788
+ if (branchInstance.status === "failed") {
789
+ throw new Error(branchInstance.error ?? `Branch ${branchDef.name} failed`);
790
+ }
791
+
792
+ await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
793
+ }
794
+ })();
795
+
796
+ branchPromises.push(branchPromise);
797
+ }
798
+
799
+ // Track branch instances
800
+ await this.adapter.updateInstance(instanceId, {
801
+ branchInstances: {
802
+ ...((await this.adapter.getInstance(instanceId))?.branchInstances ?? {}),
803
+ [step.name]: branchInstanceIds,
804
+ },
805
+ });
806
+
807
+ // Wait for all branches
808
+ if (step.onError === "wait-all") {
809
+ const results = await Promise.allSettled(branchPromises);
810
+ const output: Record<string, any> = {};
811
+ const errors: string[] = [];
812
+
813
+ for (const result of results) {
814
+ if (result.status === "fulfilled") {
815
+ output[result.value.name] = result.value.result;
816
+ } else {
817
+ errors.push(result.reason?.message ?? "Branch failed");
818
+ }
819
+ }
820
+
821
+ if (errors.length > 0) {
822
+ throw new Error(`Parallel branches failed: ${errors.join(", ")}`);
823
+ }
824
+
825
+ return output;
826
+ } else {
827
+ // fail-fast (default)
828
+ const results = await Promise.all(branchPromises);
829
+ const output: Record<string, any> = {};
830
+ for (const result of results) {
831
+ output[result.name] = result.result;
832
+ }
833
+ return output;
834
+ }
835
+ }
836
+
837
+ private async executeChoiceStep(
838
+ instanceId: string,
839
+ step: ChoiceStepDefinition,
840
+ ctx: WorkflowContext,
841
+ definition: WorkflowDefinition
842
+ ): Promise<string> {
843
+ // Evaluate conditions in order
844
+ for (const choice of step.choices) {
845
+ try {
846
+ if (choice.condition(ctx)) {
847
+ // Update current step and continue
848
+ await this.adapter.updateInstance(instanceId, {
849
+ currentStep: choice.next,
850
+ });
851
+
852
+ // Mark choice step as complete
853
+ const instance = await this.adapter.getInstance(instanceId);
854
+ if (instance) {
855
+ const stepResult = instance.stepResults[step.name];
856
+ if (stepResult) {
857
+ stepResult.status = "completed";
858
+ stepResult.output = { chosen: choice.next };
859
+ stepResult.completedAt = new Date();
860
+ await this.adapter.updateInstance(instanceId, {
861
+ stepResults: { ...instance.stepResults, [step.name]: stepResult },
862
+ });
863
+ }
864
+ }
865
+
866
+ // Emit progress
867
+ await this.emitEvent("workflow.step.completed", {
868
+ instanceId,
869
+ workflowName: (await this.adapter.getInstance(instanceId))?.workflowName,
870
+ stepName: step.name,
871
+ output: { chosen: choice.next },
872
+ });
873
+
874
+ // Execute next step
875
+ await this.executeStep(instanceId, definition);
876
+ return choice.next;
877
+ }
878
+ } catch {
879
+ // Condition threw, try next
880
+ }
881
+ }
882
+
883
+ // No condition matched, use default
884
+ if (step.default) {
885
+ await this.adapter.updateInstance(instanceId, {
886
+ currentStep: step.default,
887
+ });
888
+
889
+ // Mark choice step as complete
890
+ const instance = await this.adapter.getInstance(instanceId);
891
+ if (instance) {
892
+ const stepResult = instance.stepResults[step.name];
893
+ if (stepResult) {
894
+ stepResult.status = "completed";
895
+ stepResult.output = { chosen: step.default };
896
+ stepResult.completedAt = new Date();
897
+ await this.adapter.updateInstance(instanceId, {
898
+ stepResults: { ...instance.stepResults, [step.name]: stepResult },
899
+ });
900
+ }
901
+ }
902
+
903
+ await this.emitEvent("workflow.step.completed", {
904
+ instanceId,
905
+ workflowName: instance?.workflowName,
906
+ stepName: step.name,
907
+ output: { chosen: step.default },
908
+ });
909
+
910
+ await this.executeStep(instanceId, definition);
911
+ return step.default;
912
+ }
913
+
914
+ throw new Error("No choice condition matched and no default specified");
915
+ }
916
+
917
+ private async executePassStep(
918
+ instanceId: string,
919
+ step: PassStepDefinition,
920
+ ctx: WorkflowContext
921
+ ): Promise<any> {
922
+ if (step.result !== undefined) {
923
+ return step.result;
924
+ }
925
+
926
+ if (step.transform) {
927
+ return step.transform(ctx);
928
+ }
929
+
930
+ return ctx.input;
931
+ }
932
+
933
+ private buildContext(instance: WorkflowInstance): WorkflowContext {
934
+ // Build steps object with outputs
935
+ const steps: Record<string, any> = {};
936
+ for (const [name, result] of Object.entries(instance.stepResults)) {
937
+ if (result.status === "completed" && result.output !== undefined) {
938
+ steps[name] = result.output;
939
+ }
940
+ }
941
+
942
+ return {
943
+ input: instance.input,
944
+ steps,
945
+ instance,
946
+ getStepResult: <T = any>(stepName: string): T | undefined => {
947
+ return steps[stepName] as T | undefined;
948
+ },
949
+ };
950
+ }
951
+
952
+ private async completeStep(
953
+ instanceId: string,
954
+ stepName: string,
955
+ output: any,
956
+ step: StepDefinition,
957
+ definition: WorkflowDefinition
958
+ ): Promise<void> {
959
+ const instance = await this.adapter.getInstance(instanceId);
960
+ if (!instance) return;
961
+
962
+ // Update step result
963
+ const stepResult = instance.stepResults[stepName] ?? {
964
+ stepName,
965
+ status: "pending",
966
+ attempts: 0,
967
+ };
968
+ stepResult.status = "completed";
969
+ stepResult.output = output;
970
+ stepResult.completedAt = new Date();
971
+
972
+ await this.adapter.updateInstance(instanceId, {
973
+ stepResults: { ...instance.stepResults, [stepName]: stepResult },
974
+ });
975
+
976
+ // Emit step completed event
977
+ await this.emitEvent("workflow.step.completed", {
978
+ instanceId,
979
+ workflowName: instance.workflowName,
980
+ stepName,
981
+ output,
982
+ });
983
+
984
+ // Calculate and emit progress
985
+ const totalSteps = definition.steps.size;
986
+ const completedSteps = Object.values(instance.stepResults).filter(
987
+ (r) => r.status === "completed"
988
+ ).length + 1; // +1 for current step
989
+ const progress = Math.round((completedSteps / totalSteps) * 100);
990
+
991
+ await this.emitEvent("workflow.progress", {
992
+ instanceId,
993
+ workflowName: instance.workflowName,
994
+ progress,
995
+ currentStep: stepName,
996
+ completedSteps,
997
+ totalSteps,
998
+ });
999
+
1000
+ // Broadcast via SSE
1001
+ if (this.sse) {
1002
+ this.sse.broadcast(`workflow:${instanceId}`, "progress", {
1003
+ progress,
1004
+ currentStep: stepName,
1005
+ completedSteps,
1006
+ totalSteps,
1007
+ });
1008
+ }
1009
+
1010
+ // Move to next step or complete
1011
+ if (step.end) {
1012
+ await this.completeWorkflow(instanceId, output);
1013
+ } else if (step.next) {
1014
+ await this.adapter.updateInstance(instanceId, {
1015
+ currentStep: step.next,
1016
+ });
1017
+ await this.executeStep(instanceId, definition);
1018
+ } else {
1019
+ // No next step, complete
1020
+ await this.completeWorkflow(instanceId, output);
1021
+ }
1022
+ }
1023
+
1024
+ private async handleStepError(
1025
+ instanceId: string,
1026
+ stepName: string,
1027
+ error: string,
1028
+ step: StepDefinition,
1029
+ definition: WorkflowDefinition
1030
+ ): Promise<void> {
1031
+ const instance = await this.adapter.getInstance(instanceId);
1032
+ if (!instance) return;
1033
+
1034
+ const stepResult = instance.stepResults[stepName] ?? {
1035
+ stepName,
1036
+ status: "pending",
1037
+ attempts: 0,
1038
+ };
1039
+
1040
+ // Check retry config
1041
+ const retry = step.retry ?? definition.defaultRetry;
1042
+ if (retry && stepResult.attempts < retry.maxAttempts) {
1043
+ // Retry with backoff
1044
+ const backoffRate = retry.backoffRate ?? 2;
1045
+ const intervalMs = retry.intervalMs ?? 1000;
1046
+ const maxIntervalMs = retry.maxIntervalMs ?? 30000;
1047
+ const delay = Math.min(
1048
+ intervalMs * Math.pow(backoffRate, stepResult.attempts - 1),
1049
+ maxIntervalMs
1050
+ );
1051
+
1052
+ console.log(
1053
+ `[Workflows] Retrying step ${stepName} in ${delay}ms (attempt ${stepResult.attempts}/${retry.maxAttempts})`
1054
+ );
1055
+
1056
+ await this.emitEvent("workflow.step.retry", {
1057
+ instanceId,
1058
+ workflowName: instance.workflowName,
1059
+ stepName,
1060
+ attempt: stepResult.attempts,
1061
+ maxAttempts: retry.maxAttempts,
1062
+ delay,
1063
+ error,
1064
+ });
1065
+
1066
+ // Update step result
1067
+ stepResult.error = error;
1068
+ await this.adapter.updateInstance(instanceId, {
1069
+ stepResults: { ...instance.stepResults, [stepName]: stepResult },
1070
+ });
1071
+
1072
+ // Retry after delay
1073
+ setTimeout(() => {
1074
+ this.executeStep(instanceId, definition);
1075
+ }, delay);
1076
+
1077
+ return;
1078
+ }
1079
+
1080
+ // No more retries, fail the step
1081
+ stepResult.status = "failed";
1082
+ stepResult.error = error;
1083
+ stepResult.completedAt = new Date();
1084
+
1085
+ await this.adapter.updateInstance(instanceId, {
1086
+ stepResults: { ...instance.stepResults, [stepName]: stepResult },
1087
+ });
1088
+
1089
+ await this.emitEvent("workflow.step.failed", {
1090
+ instanceId,
1091
+ workflowName: instance.workflowName,
1092
+ stepName,
1093
+ error,
1094
+ attempts: stepResult.attempts,
1095
+ });
1096
+
1097
+ // Fail the workflow
1098
+ await this.failWorkflow(instanceId, `Step "${stepName}" failed: ${error}`);
1099
+ }
1100
+
1101
+ private async completeWorkflow(instanceId: string, output?: any): Promise<void> {
1102
+ const instance = await this.adapter.getInstance(instanceId);
1103
+ if (!instance) return;
1104
+
1105
+ // Clear timeout
1106
+ const runInfo = this.running.get(instanceId);
1107
+ if (runInfo?.timeout) {
1108
+ clearTimeout(runInfo.timeout);
1109
+ }
1110
+ this.running.delete(instanceId);
1111
+
1112
+ await this.adapter.updateInstance(instanceId, {
1113
+ status: "completed",
1114
+ output,
1115
+ completedAt: new Date(),
1116
+ currentStep: undefined,
1117
+ });
1118
+
1119
+ await this.emitEvent("workflow.completed", {
1120
+ instanceId,
1121
+ workflowName: instance.workflowName,
1122
+ output,
1123
+ });
1124
+
1125
+ // Broadcast via SSE
1126
+ if (this.sse) {
1127
+ this.sse.broadcast(`workflow:${instanceId}`, "completed", { output });
1128
+ }
1129
+ }
1130
+
1131
+ private async failWorkflow(instanceId: string, error: string): Promise<void> {
1132
+ const instance = await this.adapter.getInstance(instanceId);
1133
+ if (!instance) return;
1134
+
1135
+ // Clear timeout
1136
+ const runInfo = this.running.get(instanceId);
1137
+ if (runInfo?.timeout) {
1138
+ clearTimeout(runInfo.timeout);
1139
+ }
1140
+ this.running.delete(instanceId);
1141
+
1142
+ await this.adapter.updateInstance(instanceId, {
1143
+ status: "failed",
1144
+ error,
1145
+ completedAt: new Date(),
1146
+ });
1147
+
1148
+ await this.emitEvent("workflow.failed", {
1149
+ instanceId,
1150
+ workflowName: instance.workflowName,
1151
+ error,
1152
+ });
1153
+
1154
+ // Broadcast via SSE
1155
+ if (this.sse) {
1156
+ this.sse.broadcast(`workflow:${instanceId}`, "failed", { error });
1157
+ }
1158
+ }
1159
+
1160
+ private async emitEvent(event: string, data: any): Promise<void> {
1161
+ if (this.events) {
1162
+ await this.events.emit(event, data);
1163
+ }
1164
+ }
1165
+ }
1166
+
1167
+ // ============================================
1168
+ // Factory Function
1169
+ // ============================================
1170
+
1171
+ export function createWorkflows(config?: WorkflowsConfig): Workflows {
1172
+ return new WorkflowsImpl(config);
1173
+ }