@rivetkit/workflow-engine 2.1.0-rc.1

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/src/index.ts ADDED
@@ -0,0 +1,907 @@
1
+ import type { Logger } from "pino";
2
+
3
+ // Types
4
+
5
+ // Context
6
+ export {
7
+ DEFAULT_LOOP_COMMIT_INTERVAL,
8
+ DEFAULT_LOOP_HISTORY_EVERY,
9
+ DEFAULT_LOOP_HISTORY_KEEP,
10
+ DEFAULT_MAX_RETRIES,
11
+ DEFAULT_RETRY_BACKOFF_BASE,
12
+ DEFAULT_RETRY_BACKOFF_MAX,
13
+ DEFAULT_STEP_TIMEOUT,
14
+ WorkflowContextImpl,
15
+ } from "./context.js";
16
+ // Driver
17
+ export type { EngineDriver, KVEntry, KVWrite } from "./driver.js";
18
+ // Errors
19
+ export {
20
+ CancelledError,
21
+ CriticalError,
22
+ EntryInProgressError,
23
+ EvictedError,
24
+ HistoryDivergedError,
25
+ JoinError,
26
+ MessageWaitError,
27
+ RaceError,
28
+ RollbackCheckpointError,
29
+ RollbackError,
30
+ SleepError,
31
+ StepExhaustedError,
32
+ StepFailedError,
33
+ } from "./errors.js";
34
+
35
+ // Location utilities
36
+ export {
37
+ appendLoopIteration,
38
+ appendName,
39
+ emptyLocation,
40
+ isLocationPrefix,
41
+ isLoopIterationMarker,
42
+ locationsEqual,
43
+ locationToKey,
44
+ parentLocation,
45
+ registerName,
46
+ resolveName,
47
+ } from "./location.js";
48
+
49
+ // Storage utilities
50
+ export {
51
+ createEntry,
52
+ createHistorySnapshot,
53
+ createStorage,
54
+ deleteEntriesWithPrefix,
55
+ flush,
56
+ generateId,
57
+ getEntry,
58
+ getOrCreateMetadata,
59
+ loadMetadata,
60
+ loadStorage,
61
+ setEntry,
62
+ } from "./storage.js";
63
+ export type {
64
+ BranchConfig,
65
+ BranchOutput,
66
+ BranchStatus,
67
+ BranchStatusType,
68
+ Entry,
69
+ EntryKind,
70
+ EntryKindType,
71
+ EntryMetadata,
72
+ EntryStatus,
73
+ History,
74
+ JoinEntry,
75
+ Location,
76
+ LoopConfig,
77
+ LoopEntry,
78
+ LoopIterationMarker,
79
+ LoopResult,
80
+ Message,
81
+ MessageEntry,
82
+ NameIndex,
83
+ PathSegment,
84
+ RaceEntry,
85
+ RemovedEntry,
86
+ WorkflowEntryMetadataSnapshot,
87
+ WorkflowHistoryEntry,
88
+ WorkflowHistorySnapshot,
89
+ RollbackCheckpointEntry,
90
+ RollbackContextInterface,
91
+ RunWorkflowOptions,
92
+ SleepEntry,
93
+ SleepState,
94
+ StepConfig,
95
+ StepEntry,
96
+ Storage,
97
+ WorkflowContextInterface,
98
+ WorkflowFunction,
99
+ WorkflowHandle,
100
+ WorkflowQueue,
101
+ WorkflowQueueMessage,
102
+ WorkflowQueueNextOptions,
103
+ WorkflowMessageDriver,
104
+ WorkflowResult,
105
+ WorkflowRunMode,
106
+ WorkflowState,
107
+ } from "./types.js";
108
+
109
+ // Loop result helpers
110
+ export const Loop = {
111
+ continue: <S>(state: S): { continue: true; state: S } => ({
112
+ continue: true,
113
+ state,
114
+ }),
115
+ break: <T>(value: T): { break: true; value: T } => ({
116
+ break: true,
117
+ value,
118
+ }),
119
+ };
120
+
121
+ import {
122
+ deserializeEntryMetadata,
123
+ deserializeWorkflowInput,
124
+ deserializeWorkflowOutput,
125
+ deserializeWorkflowState,
126
+ serializeEntryMetadata,
127
+ serializeWorkflowInput,
128
+ serializeWorkflowState,
129
+ } from "../schemas/serde.js";
130
+ import { type RollbackAction, WorkflowContextImpl } from "./context.js";
131
+ // Main workflow runner
132
+ import type { EngineDriver } from "./driver.js";
133
+ import {
134
+ EvictedError,
135
+ MessageWaitError,
136
+ RollbackCheckpointError,
137
+ RollbackStopError,
138
+ SleepError,
139
+ StepFailedError,
140
+ } from "./errors.js";
141
+ import {
142
+ buildEntryMetadataPrefix,
143
+ buildWorkflowErrorKey,
144
+ buildWorkflowInputKey,
145
+ buildWorkflowOutputKey,
146
+ buildWorkflowStateKey,
147
+ } from "./keys.js";
148
+ import {
149
+ createHistorySnapshot,
150
+ flush,
151
+ generateId,
152
+ loadMetadata,
153
+ loadStorage,
154
+ } from "./storage.js";
155
+ import type {
156
+ RollbackContextInterface,
157
+ RunWorkflowOptions,
158
+ Storage,
159
+ WorkflowHistorySnapshot,
160
+ WorkflowFunction,
161
+ WorkflowHandle,
162
+ WorkflowMessageDriver,
163
+ WorkflowResult,
164
+ WorkflowRunMode,
165
+ WorkflowState,
166
+ } from "./types.js";
167
+ import { setLongTimeout } from "./utils.js";
168
+
169
+ /**
170
+ * Run a workflow and return a handle for managing it.
171
+ *
172
+ * The workflow starts executing immediately. Use the returned handle to:
173
+ * - `handle.result` - Await workflow completion (or yield in `yield` mode)
174
+ * - `handle.message()` - Send messages to the workflow
175
+ * - `handle.wake()` - Wake the workflow early
176
+ * - `handle.evict()` - Request graceful shutdown
177
+ * - `handle.getOutput()` / `handle.getState()` - Query status
178
+ */
179
+ interface LiveRuntime {
180
+ sleepWaiter?: () => void;
181
+ isSleeping: boolean;
182
+ }
183
+
184
+ type HistoryNotifier = (() => void) | undefined;
185
+
186
+ function createLiveRuntime(): LiveRuntime {
187
+ return {
188
+ isSleeping: false,
189
+ };
190
+ }
191
+
192
+ function createEvictionWait(signal: AbortSignal): {
193
+ promise: Promise<never>;
194
+ cleanup: () => void;
195
+ } {
196
+ if (signal.aborted) {
197
+ return {
198
+ promise: Promise.reject(new EvictedError()),
199
+ cleanup: () => {},
200
+ };
201
+ }
202
+
203
+ let onAbort: (() => void) | undefined;
204
+ const promise = new Promise<never>((_, reject) => {
205
+ onAbort = () => {
206
+ reject(new EvictedError());
207
+ };
208
+ signal.addEventListener("abort", onAbort, { once: true });
209
+ });
210
+
211
+ return {
212
+ promise,
213
+ cleanup: () => {
214
+ if (onAbort) {
215
+ signal.removeEventListener("abort", onAbort);
216
+ }
217
+ },
218
+ };
219
+ }
220
+
221
+ function createRollbackContext(
222
+ workflowId: string,
223
+ abortController: AbortController,
224
+ ): RollbackContextInterface {
225
+ return {
226
+ workflowId,
227
+ abortSignal: abortController.signal,
228
+ isEvicted: () => abortController.signal.aborted,
229
+ };
230
+ }
231
+
232
+ async function awaitWithEviction<T>(
233
+ promise: Promise<T>,
234
+ abortSignal: AbortSignal,
235
+ ): Promise<T> {
236
+ const { promise: evictionPromise, cleanup } =
237
+ createEvictionWait(abortSignal);
238
+ try {
239
+ return await Promise.race([promise, evictionPromise]);
240
+ } finally {
241
+ cleanup();
242
+ }
243
+ }
244
+
245
+ async function executeRollback<TInput, TOutput>(
246
+ workflowId: string,
247
+ workflowFn: WorkflowFunction<TInput, TOutput>,
248
+ input: TInput,
249
+ driver: EngineDriver,
250
+ messageDriver: WorkflowMessageDriver,
251
+ abortController: AbortController,
252
+ storage: Storage,
253
+ historyNotifier?: HistoryNotifier,
254
+ logger?: Logger,
255
+ ): Promise<void> {
256
+ const rollbackActions: RollbackAction[] = [];
257
+ const ctx = new WorkflowContextImpl(
258
+ workflowId,
259
+ storage,
260
+ driver,
261
+ messageDriver,
262
+ undefined,
263
+ abortController,
264
+ "rollback",
265
+ rollbackActions,
266
+ false,
267
+ historyNotifier,
268
+ logger,
269
+ );
270
+
271
+ try {
272
+ await workflowFn(ctx, input);
273
+ } catch (error) {
274
+ if (error instanceof EvictedError) {
275
+ throw error;
276
+ }
277
+ if (error instanceof RollbackStopError) {
278
+ // Stop replay once we hit incomplete history during rollback.
279
+ } else {
280
+ // Ignore workflow errors during rollback replay.
281
+ }
282
+ }
283
+
284
+ if (rollbackActions.length === 0) {
285
+ return;
286
+ }
287
+
288
+ const rollbackContext = createRollbackContext(workflowId, abortController);
289
+
290
+ for (let i = rollbackActions.length - 1; i >= 0; i--) {
291
+ if (abortController.signal.aborted) {
292
+ throw new EvictedError();
293
+ }
294
+
295
+ const action = rollbackActions[i];
296
+ const metadata = await loadMetadata(storage, driver, action.entryId);
297
+ if (metadata.rollbackCompletedAt !== undefined) {
298
+ continue;
299
+ }
300
+
301
+ try {
302
+ await awaitWithEviction(
303
+ action.rollback(rollbackContext, action.output),
304
+ abortController.signal,
305
+ );
306
+ metadata.rollbackCompletedAt = Date.now();
307
+ metadata.rollbackError = undefined;
308
+ } catch (error) {
309
+ if (error instanceof EvictedError) {
310
+ throw error;
311
+ }
312
+ metadata.rollbackError =
313
+ error instanceof Error ? error.message : String(error);
314
+ throw error;
315
+ } finally {
316
+ metadata.dirty = true;
317
+ await flush(storage, driver, historyNotifier);
318
+ }
319
+ }
320
+ }
321
+
322
+ async function setSleepState<TOutput>(
323
+ storage: Storage,
324
+ driver: EngineDriver,
325
+ workflowId: string,
326
+ deadline: number,
327
+ messageNames?: string[],
328
+ historyNotifier?: HistoryNotifier,
329
+ ): Promise<WorkflowResult<TOutput>> {
330
+ storage.state = "sleeping";
331
+ await flush(storage, driver, historyNotifier);
332
+ await driver.setAlarm(workflowId, deadline);
333
+
334
+ return {
335
+ state: "sleeping",
336
+ sleepUntil: deadline,
337
+ waitingForMessages: messageNames,
338
+ };
339
+ }
340
+
341
+ async function setMessageWaitState<TOutput>(
342
+ storage: Storage,
343
+ driver: EngineDriver,
344
+ messageNames: string[],
345
+ historyNotifier?: HistoryNotifier,
346
+ ): Promise<WorkflowResult<TOutput>> {
347
+ storage.state = "sleeping";
348
+ await flush(storage, driver, historyNotifier);
349
+
350
+ return { state: "sleeping", waitingForMessages: messageNames };
351
+ }
352
+
353
+ async function setEvictedState<TOutput>(
354
+ storage: Storage,
355
+ driver: EngineDriver,
356
+ historyNotifier?: HistoryNotifier,
357
+ ): Promise<WorkflowResult<TOutput>> {
358
+ await flush(storage, driver, historyNotifier);
359
+ return { state: storage.state };
360
+ }
361
+
362
+ async function setRetryState<TOutput>(
363
+ storage: Storage,
364
+ driver: EngineDriver,
365
+ workflowId: string,
366
+ historyNotifier?: HistoryNotifier,
367
+ ): Promise<WorkflowResult<TOutput>> {
368
+ storage.state = "sleeping";
369
+ await flush(storage, driver, historyNotifier);
370
+
371
+ const retryAt = Date.now() + 100;
372
+ await driver.setAlarm(workflowId, retryAt);
373
+
374
+ return { state: "sleeping", sleepUntil: retryAt };
375
+ }
376
+
377
+ async function setFailedState(
378
+ storage: Storage,
379
+ driver: EngineDriver,
380
+ error: unknown,
381
+ historyNotifier?: HistoryNotifier,
382
+ ): Promise<void> {
383
+ storage.state = "failed";
384
+ storage.error = extractErrorInfo(error);
385
+ await flush(storage, driver, historyNotifier);
386
+ }
387
+
388
+ async function waitForSleep(
389
+ runtime: LiveRuntime,
390
+ deadline: number,
391
+ abortSignal: AbortSignal,
392
+ ): Promise<void> {
393
+ while (true) {
394
+ const remaining = deadline - Date.now();
395
+ if (remaining <= 0) {
396
+ return;
397
+ }
398
+
399
+ let timeoutHandle: ReturnType<typeof setLongTimeout> | undefined;
400
+ const timeoutPromise = new Promise<void>((resolve) => {
401
+ timeoutHandle = setLongTimeout(resolve, remaining);
402
+ });
403
+
404
+ const wakePromise = new Promise<void>((resolve) => {
405
+ runtime.sleepWaiter = resolve;
406
+ });
407
+ runtime.isSleeping = true;
408
+
409
+ try {
410
+ await awaitWithEviction(
411
+ Promise.race([timeoutPromise, wakePromise]),
412
+ abortSignal,
413
+ );
414
+ } finally {
415
+ runtime.isSleeping = false;
416
+ runtime.sleepWaiter = undefined;
417
+ timeoutHandle?.abort();
418
+ }
419
+
420
+ if (abortSignal.aborted) {
421
+ throw new EvictedError();
422
+ }
423
+
424
+ if (Date.now() >= deadline) {
425
+ return;
426
+ }
427
+ }
428
+ }
429
+
430
+ async function executeLiveWorkflow<TInput, TOutput>(
431
+ workflowId: string,
432
+ workflowFn: WorkflowFunction<TInput, TOutput>,
433
+ input: TInput,
434
+ driver: EngineDriver,
435
+ messageDriver: WorkflowMessageDriver,
436
+ abortController: AbortController,
437
+ runtime: LiveRuntime,
438
+ onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
439
+ logger?: Logger,
440
+ ): Promise<WorkflowResult<TOutput>> {
441
+ let lastResult: WorkflowResult<TOutput> | undefined;
442
+
443
+ while (true) {
444
+ const result = await executeWorkflow(
445
+ workflowId,
446
+ workflowFn,
447
+ input,
448
+ driver,
449
+ messageDriver,
450
+ abortController,
451
+ onHistoryUpdated,
452
+ logger,
453
+ );
454
+ lastResult = result;
455
+
456
+ if (result.state !== "sleeping") {
457
+ return result;
458
+ }
459
+
460
+ const hasMessages = result.waitingForMessages !== undefined;
461
+ const hasDeadline = result.sleepUntil !== undefined;
462
+
463
+ if (hasMessages && hasDeadline) {
464
+ // Wait for EITHER a message OR the deadline (for queue.next timeout)
465
+ try {
466
+ const messagePromise = awaitWithEviction(
467
+ driver.waitForMessages(
468
+ result.waitingForMessages!,
469
+ abortController.signal,
470
+ ),
471
+ abortController.signal,
472
+ );
473
+ const sleepPromise = waitForSleep(
474
+ runtime,
475
+ result.sleepUntil!,
476
+ abortController.signal,
477
+ );
478
+ await Promise.race([messagePromise, sleepPromise]);
479
+ } catch (error) {
480
+ if (error instanceof EvictedError) {
481
+ return lastResult;
482
+ }
483
+ throw error;
484
+ }
485
+ continue;
486
+ }
487
+
488
+ if (hasMessages) {
489
+ try {
490
+ await awaitWithEviction(
491
+ driver.waitForMessages(
492
+ result.waitingForMessages!,
493
+ abortController.signal,
494
+ ),
495
+ abortController.signal,
496
+ );
497
+ } catch (error) {
498
+ if (error instanceof EvictedError) {
499
+ return lastResult;
500
+ }
501
+ throw error;
502
+ }
503
+ continue;
504
+ }
505
+
506
+ if (hasDeadline) {
507
+ try {
508
+ await waitForSleep(
509
+ runtime,
510
+ result.sleepUntil!,
511
+ abortController.signal,
512
+ );
513
+ } catch (error) {
514
+ if (error instanceof EvictedError) {
515
+ return lastResult;
516
+ }
517
+ throw error;
518
+ }
519
+ continue;
520
+ }
521
+
522
+ return result;
523
+ }
524
+ }
525
+
526
+ export function runWorkflow<TInput, TOutput>(
527
+ workflowId: string,
528
+ workflowFn: WorkflowFunction<TInput, TOutput>,
529
+ input: TInput,
530
+ driver: EngineDriver,
531
+ options: RunWorkflowOptions = {},
532
+ ): WorkflowHandle<TOutput> {
533
+ const messageDriver = driver.messageDriver;
534
+ const abortController = new AbortController();
535
+ const mode: WorkflowRunMode = options.mode ?? "yield";
536
+ const liveRuntime = mode === "live" ? createLiveRuntime() : undefined;
537
+
538
+ const logger = options.logger;
539
+
540
+ const resultPromise =
541
+ mode === "live" && liveRuntime
542
+ ? executeLiveWorkflow(
543
+ workflowId,
544
+ workflowFn,
545
+ input,
546
+ driver,
547
+ messageDriver,
548
+ abortController,
549
+ liveRuntime,
550
+ options.onHistoryUpdated,
551
+ logger,
552
+ )
553
+ : executeWorkflow(
554
+ workflowId,
555
+ workflowFn,
556
+ input,
557
+ driver,
558
+ messageDriver,
559
+ abortController,
560
+ options.onHistoryUpdated,
561
+ logger,
562
+ );
563
+
564
+ return {
565
+ workflowId,
566
+ result: resultPromise,
567
+
568
+ async message(name: string, data: unknown): Promise<void> {
569
+ const messageId = generateId();
570
+ await messageDriver.addMessage({
571
+ id: messageId,
572
+ name,
573
+ data,
574
+ sentAt: Date.now(),
575
+ });
576
+ },
577
+
578
+ async wake(): Promise<void> {
579
+ if (liveRuntime) {
580
+ if (liveRuntime.isSleeping && liveRuntime.sleepWaiter) {
581
+ liveRuntime.sleepWaiter();
582
+ }
583
+ return;
584
+ }
585
+ await driver.setAlarm(workflowId, Date.now());
586
+ },
587
+
588
+ async recover(): Promise<void> {
589
+ const stateValue = await driver.get(buildWorkflowStateKey());
590
+ const state = stateValue
591
+ ? deserializeWorkflowState(stateValue)
592
+ : "pending";
593
+
594
+ if (state !== "failed") {
595
+ return;
596
+ }
597
+
598
+ const metadataEntries = await driver.list(
599
+ buildEntryMetadataPrefix(),
600
+ );
601
+ const writes: { key: Uint8Array; value: Uint8Array }[] = [];
602
+
603
+ for (const entry of metadataEntries) {
604
+ const metadata = deserializeEntryMetadata(entry.value);
605
+ if (
606
+ metadata.status !== "failed" &&
607
+ metadata.status !== "exhausted"
608
+ ) {
609
+ continue;
610
+ }
611
+
612
+ metadata.status = "pending";
613
+ metadata.attempts = 0;
614
+ metadata.lastAttemptAt = 0;
615
+ metadata.error = undefined;
616
+ metadata.dirty = false;
617
+
618
+ writes.push({
619
+ key: entry.key,
620
+ value: serializeEntryMetadata(metadata),
621
+ });
622
+ }
623
+
624
+ if (writes.length > 0) {
625
+ await driver.batch(writes);
626
+ }
627
+
628
+ await driver.delete(buildWorkflowErrorKey());
629
+ await driver.set(
630
+ buildWorkflowStateKey(),
631
+ serializeWorkflowState("sleeping"),
632
+ );
633
+
634
+ if (liveRuntime) {
635
+ if (liveRuntime.isSleeping && liveRuntime.sleepWaiter) {
636
+ liveRuntime.sleepWaiter();
637
+ }
638
+ return;
639
+ }
640
+
641
+ await driver.setAlarm(workflowId, Date.now());
642
+ },
643
+
644
+ evict(): void {
645
+ abortController.abort(new EvictedError());
646
+ },
647
+
648
+ async cancel(): Promise<void> {
649
+ abortController.abort(new EvictedError());
650
+
651
+ await driver.set(
652
+ buildWorkflowStateKey(),
653
+ serializeWorkflowState("cancelled"),
654
+ );
655
+
656
+ await driver.clearAlarm(workflowId);
657
+ },
658
+
659
+ async getOutput(): Promise<TOutput | undefined> {
660
+ const value = await driver.get(buildWorkflowOutputKey());
661
+ if (!value) {
662
+ return undefined;
663
+ }
664
+ return deserializeWorkflowOutput<TOutput>(value);
665
+ },
666
+
667
+ async getState(): Promise<WorkflowState> {
668
+ const value = await driver.get(buildWorkflowStateKey());
669
+ if (!value) {
670
+ return "pending";
671
+ }
672
+ return deserializeWorkflowState(value);
673
+ },
674
+ };
675
+ }
676
+
677
+ /**
678
+ * Internal: Execute the workflow and return the result.
679
+ */
680
+ async function executeWorkflow<TInput, TOutput>(
681
+ workflowId: string,
682
+ workflowFn: WorkflowFunction<TInput, TOutput>,
683
+ input: TInput,
684
+ driver: EngineDriver,
685
+ messageDriver: WorkflowMessageDriver,
686
+ abortController: AbortController,
687
+ onHistoryUpdated?: (history: WorkflowHistorySnapshot) => void,
688
+ logger?: Logger,
689
+ ): Promise<WorkflowResult<TOutput>> {
690
+ const storage = await loadStorage(driver);
691
+ const historyNotifier: HistoryNotifier = onHistoryUpdated
692
+ ? () => onHistoryUpdated(createHistorySnapshot(storage))
693
+ : undefined;
694
+ if (historyNotifier) {
695
+ historyNotifier();
696
+ }
697
+
698
+ if (logger) {
699
+ const entryKeys = Array.from(storage.history.entries.keys());
700
+ logger.debug({
701
+ msg: "loaded workflow storage",
702
+ state: storage.state,
703
+ entryCount: entryKeys.length,
704
+ entries: entryKeys.slice(0, 10),
705
+ nameRegistry: storage.nameRegistry,
706
+ });
707
+ }
708
+
709
+ // Check if workflow was cancelled
710
+ if (storage.state === "cancelled") {
711
+ throw new EvictedError();
712
+ }
713
+
714
+ // Input persistence: store on first run, use stored input on resume
715
+ const storedInputBytes = await driver.get(buildWorkflowInputKey());
716
+ let effectiveInput: TInput;
717
+
718
+ if (storedInputBytes) {
719
+ // Resume: use stored input for deterministic replay
720
+ effectiveInput = deserializeWorkflowInput<TInput>(storedInputBytes);
721
+ } else {
722
+ // First run: store the input
723
+ effectiveInput = input;
724
+ await driver.set(
725
+ buildWorkflowInputKey(),
726
+ serializeWorkflowInput(input),
727
+ );
728
+ }
729
+
730
+ if (storage.state === "rolling_back") {
731
+ try {
732
+ await executeRollback(
733
+ workflowId,
734
+ workflowFn,
735
+ effectiveInput,
736
+ driver,
737
+ messageDriver,
738
+ abortController,
739
+ storage,
740
+ historyNotifier,
741
+ logger,
742
+ );
743
+ } catch (error) {
744
+ if (error instanceof EvictedError) {
745
+ return { state: storage.state };
746
+ }
747
+ throw error;
748
+ }
749
+
750
+ storage.state = "failed";
751
+ await flush(storage, driver, historyNotifier);
752
+
753
+ const storedError = storage.error
754
+ ? new Error(storage.error.message)
755
+ : new Error("Workflow failed");
756
+ if (storage.error?.name) {
757
+ storedError.name = storage.error.name;
758
+ }
759
+ throw storedError;
760
+ }
761
+
762
+ const ctx = new WorkflowContextImpl(
763
+ workflowId,
764
+ storage,
765
+ driver,
766
+ messageDriver,
767
+ undefined,
768
+ abortController,
769
+ "forward",
770
+ undefined,
771
+ false,
772
+ historyNotifier,
773
+ logger,
774
+ );
775
+
776
+ storage.state = "running";
777
+
778
+ try {
779
+ const output = await workflowFn(ctx, effectiveInput);
780
+
781
+ storage.state = "completed";
782
+ storage.output = output;
783
+ await flush(storage, driver, historyNotifier);
784
+ await driver.clearAlarm(workflowId);
785
+
786
+ return { state: "completed", output };
787
+ } catch (error) {
788
+ if (error instanceof SleepError) {
789
+ return await setSleepState(
790
+ storage,
791
+ driver,
792
+ workflowId,
793
+ error.deadline,
794
+ error.messageNames,
795
+ historyNotifier,
796
+ );
797
+ }
798
+
799
+ if (error instanceof MessageWaitError) {
800
+ return await setMessageWaitState(
801
+ storage,
802
+ driver,
803
+ error.messageNames,
804
+ historyNotifier,
805
+ );
806
+ }
807
+
808
+ if (error instanceof EvictedError) {
809
+ return await setEvictedState(storage, driver, historyNotifier);
810
+ }
811
+
812
+ if (error instanceof StepFailedError) {
813
+ return await setRetryState(
814
+ storage,
815
+ driver,
816
+ workflowId,
817
+ historyNotifier,
818
+ );
819
+ }
820
+
821
+ if (error instanceof RollbackCheckpointError) {
822
+ await setFailedState(storage, driver, error, historyNotifier);
823
+ throw error;
824
+ }
825
+
826
+ // Unrecoverable error
827
+ storage.error = extractErrorInfo(error);
828
+ storage.state = "rolling_back";
829
+ await flush(storage, driver, historyNotifier);
830
+
831
+ try {
832
+ await executeRollback(
833
+ workflowId,
834
+ workflowFn,
835
+ effectiveInput,
836
+ driver,
837
+ messageDriver,
838
+ abortController,
839
+ storage,
840
+ historyNotifier,
841
+ logger,
842
+ );
843
+ } catch (rollbackError) {
844
+ if (rollbackError instanceof EvictedError) {
845
+ return { state: storage.state };
846
+ }
847
+ throw rollbackError;
848
+ }
849
+
850
+ storage.state = "failed";
851
+ await flush(storage, driver, historyNotifier);
852
+
853
+ throw error;
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Extract structured error information from an error.
859
+ */
860
+ function extractErrorInfo(error: unknown): {
861
+ name: string;
862
+ message: string;
863
+ stack?: string;
864
+ metadata?: Record<string, unknown>;
865
+ } {
866
+ if (error instanceof Error) {
867
+ const result: {
868
+ name: string;
869
+ message: string;
870
+ stack?: string;
871
+ metadata?: Record<string, unknown>;
872
+ } = {
873
+ name: error.name,
874
+ message: error.message,
875
+ stack: error.stack,
876
+ };
877
+
878
+ // Extract custom properties from error
879
+ const metadata: Record<string, unknown> = {};
880
+ for (const key of Object.keys(error)) {
881
+ if (key !== "name" && key !== "message" && key !== "stack") {
882
+ const value = (error as unknown as Record<string, unknown>)[
883
+ key
884
+ ];
885
+ // Only include serializable values
886
+ if (
887
+ typeof value === "string" ||
888
+ typeof value === "number" ||
889
+ typeof value === "boolean" ||
890
+ value === null
891
+ ) {
892
+ metadata[key] = value;
893
+ }
894
+ }
895
+ }
896
+ if (Object.keys(metadata).length > 0) {
897
+ result.metadata = metadata;
898
+ }
899
+
900
+ return result;
901
+ }
902
+
903
+ return {
904
+ name: "Error",
905
+ message: String(error),
906
+ };
907
+ }