@multiplier-labs/stepflow 0.3.3 → 0.3.6

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.
Files changed (84) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-GYPNJBDP.js → chunk-D5HW7VEG.js} +269 -48
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
  5. package/dist/chunk-FMPJBK2N.js.map +1 -0
  6. package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
  7. package/dist/chunk-JPXPB7U5.js.map +1 -0
  8. package/dist/{chunk-4VOKMGP6.js → chunk-JUPZEBR3.js} +519 -522
  9. package/dist/chunk-JUPZEBR3.js.map +1 -0
  10. package/dist/events/index.d.ts +58 -3
  11. package/dist/events/index.js +1 -1
  12. package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
  13. package/dist/index.d.ts +2 -12
  14. package/dist/index.js +764 -485
  15. package/dist/index.js.map +1 -1
  16. package/dist/scheduler/index.d.ts +1 -1
  17. package/dist/scheduler/index.js +2 -2
  18. package/dist/storage/index.d.ts +313 -47
  19. package/dist/storage/index.js +4 -2
  20. package/package.json +27 -13
  21. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  22. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  23. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  24. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  25. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  26. package/.github/CODEOWNERS +0 -15
  27. package/.github/dependabot.yml +0 -23
  28. package/.github/workflows/ci.yml +0 -38
  29. package/.github/workflows/publish.yml +0 -36
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-4VOKMGP6.js.map +0 -1
  34. package/dist/chunk-GYPNJBDP.js.map +0 -1
  35. package/dist/chunk-KHTUWLVZ.js.map +0 -1
  36. package/dist/chunk-ML35PIHX.js.map +0 -1
  37. package/docs/github-packages-setup.md +0 -97
  38. package/docs/stepflow-api-reference.md +0 -1661
  39. package/src/core/engine.test.ts +0 -1365
  40. package/src/core/engine.ts +0 -692
  41. package/src/core/index.ts +0 -7
  42. package/src/core/orchestrator.ts +0 -571
  43. package/src/core/types.ts +0 -220
  44. package/src/events/index.ts +0 -8
  45. package/src/events/memory.test.ts +0 -107
  46. package/src/events/memory.ts +0 -92
  47. package/src/events/socketio.test.ts +0 -378
  48. package/src/events/socketio.ts +0 -262
  49. package/src/events/types.ts +0 -118
  50. package/src/events/webhook.test.ts +0 -657
  51. package/src/events/webhook.ts +0 -564
  52. package/src/index.ts +0 -270
  53. package/src/planning/index.ts +0 -55
  54. package/src/planning/planner.ts +0 -576
  55. package/src/planning/planning.test.ts +0 -727
  56. package/src/planning/registry.test.ts +0 -398
  57. package/src/planning/registry.ts +0 -365
  58. package/src/planning/types.ts +0 -566
  59. package/src/scheduler/cron.test.ts +0 -471
  60. package/src/scheduler/cron.ts +0 -414
  61. package/src/scheduler/index.ts +0 -8
  62. package/src/scheduler/postgres-persistence.ts +0 -518
  63. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  64. package/src/scheduler/sqlite-persistence.ts +0 -261
  65. package/src/scheduler/types.ts +0 -73
  66. package/src/storage/index.ts +0 -27
  67. package/src/storage/memory.test.ts +0 -349
  68. package/src/storage/memory.ts +0 -227
  69. package/src/storage/postgres.ts +0 -1354
  70. package/src/storage/sqlite.test.ts +0 -466
  71. package/src/storage/sqlite.ts +0 -627
  72. package/src/storage/types.ts +0 -343
  73. package/src/utils/errors.test.ts +0 -150
  74. package/src/utils/errors.ts +0 -161
  75. package/src/utils/id.ts +0 -21
  76. package/src/utils/index.ts +0 -8
  77. package/src/utils/logger.test.ts +0 -121
  78. package/src/utils/logger.ts +0 -96
  79. package/src/utils/postgres-deps.ts +0 -63
  80. package/src/utils/retry.test.ts +0 -177
  81. package/src/utils/retry.ts +0 -121
  82. package/tsconfig.json +0 -21
  83. package/tsup.config.ts +0 -14
  84. package/vitest.config.ts +0 -9
@@ -1,571 +0,0 @@
1
- /**
2
- * Workflow Orchestrator
3
- *
4
- * This module is the heart of the workflow execution system.
5
- * It manages the execution of workflow steps, handles errors,
6
- * emits events, and persists state.
7
- */
8
-
9
- import type {
10
- WorkflowDefinition,
11
- WorkflowStep,
12
- WorkflowContext,
13
- RunResult,
14
- Logger,
15
- SpawnChildOptions,
16
- } from './types';
17
- import type { StorageAdapter } from '../storage/types';
18
- import type { EventTransport, WorkflowEvent } from '../events/types';
19
- import { WorkflowEngineError, StepError, StepTimeoutError, WorkflowCanceledError, WorkflowTimeoutError } from '../utils/errors';
20
- import { createScopedLogger } from '../utils/logger';
21
- import { sleep, calculateRetryDelay } from '../utils/retry';
22
-
23
- /**
24
- * Checkpoint data for resuming workflows.
25
- */
26
- export interface WorkflowCheckpoint {
27
- /** Set of step keys that have been completed */
28
- completedStepKeys: Set<string>;
29
- /** Accumulated results from completed steps */
30
- results: Record<string, unknown>;
31
- }
32
-
33
- /**
34
- * Options for executing a workflow.
35
- */
36
- export interface ExecuteOptions {
37
- /** The run ID */
38
- runId: string;
39
- /** The workflow definition */
40
- definition: WorkflowDefinition;
41
- /** Input parameters */
42
- input: Record<string, unknown>;
43
- /** Optional metadata */
44
- metadata?: Record<string, unknown>;
45
- /** Storage adapter */
46
- storage: StorageAdapter;
47
- /** Event transport */
48
- events: EventTransport;
49
- /** Logger instance */
50
- logger: Logger;
51
- /** Abort controller for cancellation */
52
- abortController: AbortController;
53
- /** Function to spawn child workflows */
54
- spawnChild: (options: SpawnChildOptions) => Promise<string>;
55
- /** Optional checkpoint for resuming from a previous execution */
56
- checkpoint?: WorkflowCheckpoint;
57
- }
58
-
59
- /**
60
- * Execute a workflow definition.
61
- * This is the main execution logic that runs all steps sequentially.
62
- */
63
- export async function executeWorkflow(options: ExecuteOptions): Promise<RunResult> {
64
- const {
65
- runId,
66
- definition,
67
- input,
68
- metadata = {},
69
- storage,
70
- events,
71
- logger,
72
- abortController,
73
- spawnChild,
74
- checkpoint,
75
- } = options;
76
-
77
- const startTime = Date.now();
78
- const isResume = !!checkpoint;
79
-
80
- // Update run status to running (if not already)
81
- // On resume, don't overwrite the original startedAt
82
- await storage.updateRun(runId, {
83
- status: 'running',
84
- ...(isResume ? {} : { startedAt: new Date() }),
85
- });
86
-
87
- emitEvent(events, logger, {
88
- runId,
89
- kind: definition.kind,
90
- eventType: isResume ? 'run.resumed' : 'run.started',
91
- timestamp: new Date(),
92
- payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : undefined,
93
- });
94
-
95
- // Track completed step keys for checkpoint persistence
96
- const completedSteps: string[] = checkpoint ? Array.from(checkpoint.completedStepKeys) : [];
97
-
98
- // Build the execution context, using checkpoint results if resuming
99
- const context: WorkflowContext = {
100
- runId,
101
- stepId: '', // Will be set for each step before handler is called
102
- kind: definition.kind,
103
- input,
104
- results: checkpoint?.results ? { ...checkpoint.results } : {},
105
- metadata,
106
- logger: createScopedLogger(logger, runId),
107
- signal: abortController.signal,
108
- spawnChild,
109
- emit: (eventType: string, payload?: unknown) => {
110
- emitEvent(events, logger, {
111
- runId,
112
- kind: definition.kind,
113
- eventType,
114
- timestamp: new Date(),
115
- payload,
116
- });
117
- },
118
- };
119
-
120
- // Set up workflow-level timeout if specified
121
- let workflowTimeoutId: ReturnType<typeof setTimeout> | undefined;
122
- let workflowTimeoutError: WorkflowTimeoutError | undefined;
123
-
124
- if (definition.timeout) {
125
- workflowTimeoutId = setTimeout(() => {
126
- workflowTimeoutError = new WorkflowTimeoutError(runId, definition.timeout!);
127
- abortController.abort();
128
- }, definition.timeout);
129
- }
130
-
131
- try {
132
- // Execute beforeRun hook
133
- if (definition.hooks?.beforeRun) {
134
- await definition.hooks.beforeRun(context);
135
- }
136
-
137
- // Execute each step in sequence
138
- for (const step of definition.steps) {
139
- // Check for cancellation
140
- if (abortController.signal.aborted) {
141
- throw new WorkflowCanceledError(runId);
142
- }
143
-
144
- context.currentStep = step.key;
145
-
146
- // Skip already completed steps when resuming
147
- if (checkpoint?.completedStepKeys.has(step.key)) {
148
- logger.info(`Skipping step "${step.key}" (already completed in previous run)`);
149
- emitEvent(events, logger, {
150
- runId,
151
- kind: definition.kind,
152
- eventType: 'step.skipped',
153
- stepKey: step.key,
154
- timestamp: new Date(),
155
- payload: { reason: 'checkpoint' },
156
- });
157
- continue;
158
- }
159
-
160
- // Check skip condition
161
- if (step.skipIf) {
162
- const shouldSkip = await step.skipIf(context);
163
- if (shouldSkip) {
164
- logger.info(`Skipping step "${step.key}" (condition met)`);
165
-
166
- emitEvent(events, logger, {
167
- runId,
168
- kind: definition.kind,
169
- eventType: 'step.skipped',
170
- stepKey: step.key,
171
- timestamp: new Date(),
172
- });
173
-
174
- continue;
175
- }
176
- }
177
-
178
- // Execute the step
179
- const result = await executeStep(step, context, {
180
- definition,
181
- storage,
182
- events,
183
- logger,
184
- abortController,
185
- });
186
-
187
- // Store result in context
188
- context.results[step.key] = result;
189
-
190
- // Track completed step
191
- completedSteps.push(step.key);
192
-
193
- // Save checkpoint (accumulated results + completed steps)
194
- await storage.updateRun(runId, {
195
- context: { ...context.results },
196
- completedSteps: [...completedSteps],
197
- });
198
- }
199
-
200
- // All steps completed successfully
201
- // Clear workflow timeout
202
- if (workflowTimeoutId) {
203
- clearTimeout(workflowTimeoutId);
204
- }
205
-
206
- const duration = Date.now() - startTime;
207
- const runResult: RunResult = {
208
- status: 'succeeded',
209
- results: context.results,
210
- duration,
211
- };
212
-
213
- // Execute afterRun hook
214
- if (definition.hooks?.afterRun) {
215
- await definition.hooks.afterRun(context, runResult);
216
- }
217
-
218
- // Update run status
219
- await storage.updateRun(runId, {
220
- status: 'succeeded',
221
- context: context.results,
222
- finishedAt: new Date(),
223
- });
224
-
225
- emitEvent(events, logger, {
226
- runId,
227
- kind: definition.kind,
228
- eventType: 'run.completed',
229
- timestamp: new Date(),
230
- payload: { results: context.results, duration },
231
- });
232
-
233
- return runResult;
234
-
235
- } catch (error) {
236
- // Clear workflow timeout
237
- if (workflowTimeoutId) {
238
- clearTimeout(workflowTimeoutId);
239
- }
240
-
241
- const duration = Date.now() - startTime;
242
-
243
- // Check if this was a workflow timeout (abort triggered by our timeout handler)
244
- const actualError = workflowTimeoutError ?? error;
245
- const workflowError = WorkflowEngineError.fromError(actualError);
246
-
247
- // Determine final status
248
- const isTimeout = actualError instanceof WorkflowTimeoutError;
249
- const isCanceled = !isTimeout && actualError instanceof WorkflowCanceledError;
250
- const status = isCanceled ? 'canceled' : 'failed';
251
- const eventType = isTimeout ? 'run.timeout' : (isCanceled ? 'run.canceled' : 'run.failed');
252
-
253
- const runResult: RunResult = {
254
- status,
255
- results: context.results,
256
- error: workflowError,
257
- duration,
258
- };
259
-
260
- // Execute afterRun hook (even on failure)
261
- if (definition.hooks?.afterRun) {
262
- try {
263
- await definition.hooks.afterRun(context, runResult);
264
- } catch (hookError) {
265
- logger.error('afterRun hook failed for run ' + runId + ':', hookError);
266
- }
267
- }
268
-
269
- // Update run status
270
- await storage.updateRun(runId, {
271
- status,
272
- context: context.results,
273
- error: workflowError,
274
- finishedAt: new Date(),
275
- });
276
-
277
- emitEvent(events, logger, {
278
- runId,
279
- kind: definition.kind,
280
- eventType,
281
- timestamp: new Date(),
282
- payload: { error: workflowError.message, duration },
283
- });
284
-
285
- return runResult;
286
- }
287
- }
288
-
289
- /**
290
- * Options for executing a single step.
291
- */
292
- interface StepExecutionOptions<TInput = Record<string, unknown>> {
293
- definition: WorkflowDefinition<TInput>;
294
- storage: StorageAdapter;
295
- events: EventTransport;
296
- logger: Logger;
297
- abortController: AbortController;
298
- }
299
-
300
- /**
301
- * Execute a single workflow step with error handling and retries.
302
- */
303
- async function executeStep<TInput>(
304
- step: WorkflowStep<TInput>,
305
- context: WorkflowContext<TInput>,
306
- options: StepExecutionOptions<TInput>
307
- ): Promise<unknown> {
308
- const { definition, storage, events, logger, abortController } = options;
309
-
310
- // Determine error strategy
311
- const onError = step.onError ?? definition.defaultOnError ?? 'fail';
312
- const maxRetries = step.maxRetries ?? 3;
313
- const retryDelay = step.retryDelay ?? 1000;
314
- const retryBackoff = step.retryBackoff ?? 2;
315
-
316
- let attempt = 0;
317
- let lastError: Error | undefined;
318
-
319
- while (true) {
320
- attempt++;
321
-
322
- // Check for cancellation
323
- if (abortController.signal.aborted) {
324
- throw new WorkflowCanceledError(context.runId);
325
- }
326
-
327
- // Create step record
328
- const stepRecord = await storage.createStep({
329
- runId: context.runId,
330
- stepKey: step.key,
331
- stepName: step.name,
332
- status: 'running',
333
- attempt,
334
- startedAt: new Date(),
335
- });
336
-
337
- // Set the current step's ID in context so handlers can track granular operations (e.g., token usage)
338
- context.stepId = stepRecord.id;
339
-
340
- // Emit step started event
341
- emitEvent(events, logger, {
342
- runId: context.runId,
343
- kind: context.kind,
344
- eventType: 'step.started',
345
- stepKey: step.key,
346
- timestamp: new Date(),
347
- payload: { attempt },
348
- });
349
-
350
- // Execute beforeStep hook
351
- if (definition.hooks?.beforeStep) {
352
- await definition.hooks.beforeStep(context, step);
353
- }
354
-
355
- try {
356
- // Execute with optional timeout
357
- // Always race against abort signal to support workflow-level timeouts
358
- let result: unknown;
359
-
360
- if (step.timeout) {
361
- result = await executeWithTimeout(
362
- () => step.handler(context),
363
- step.timeout,
364
- abortController.signal,
365
- step.key
366
- );
367
- } else {
368
- // Race step handler against abort signal for workflow-level timeout support
369
- result = await raceWithAbort(
370
- step.handler(context),
371
- abortController.signal
372
- );
373
- }
374
-
375
- // Step succeeded
376
- await storage.updateStep(stepRecord.id, {
377
- status: 'succeeded',
378
- result,
379
- finishedAt: new Date(),
380
- });
381
-
382
- // Execute afterStep hook
383
- if (definition.hooks?.afterStep) {
384
- await definition.hooks.afterStep(context, step, result);
385
- }
386
-
387
- emitEvent(events, logger, {
388
- runId: context.runId,
389
- kind: context.kind,
390
- eventType: 'step.completed',
391
- stepKey: step.key,
392
- timestamp: new Date(),
393
- payload: { result },
394
- });
395
-
396
- return result;
397
-
398
- } catch (error) {
399
- lastError = error instanceof Error ? error : new Error(String(error));
400
-
401
- // Check if this is a cancellation (abort signal fired)
402
- const isCanceled = error instanceof WorkflowCanceledError || abortController.signal.aborted;
403
-
404
- // Update step record with error
405
- await storage.updateStep(stepRecord.id, {
406
- status: isCanceled ? 'canceled' : 'failed',
407
- error: WorkflowEngineError.fromError(error),
408
- finishedAt: new Date(),
409
- });
410
-
411
- // If canceled, re-throw immediately without retry/skip logic
412
- if (isCanceled) {
413
- emitEvent(events, logger, {
414
- runId: context.runId,
415
- kind: context.kind,
416
- eventType: 'step.failed',
417
- stepKey: step.key,
418
- timestamp: new Date(),
419
- payload: { error: lastError.message, attempt },
420
- });
421
- throw error;
422
- }
423
-
424
- // Execute onStepError hook
425
- if (definition.hooks?.onStepError) {
426
- try {
427
- await definition.hooks.onStepError(context, step, lastError);
428
- } catch (hookError) {
429
- logger.error('onStepError hook failed for run ' + context.runId + ':', hookError);
430
- }
431
- }
432
-
433
- emitEvent(events, logger, {
434
- runId: context.runId,
435
- kind: context.kind,
436
- eventType: 'step.failed',
437
- stepKey: step.key,
438
- timestamp: new Date(),
439
- payload: { error: lastError.message, attempt },
440
- });
441
-
442
- // Handle error based on strategy
443
- if (onError === 'skip') {
444
- logger.warn(`Step "${step.key}" failed, skipping (strategy: skip)`);
445
- emitEvent(events, logger, {
446
- runId: context.runId,
447
- kind: context.kind,
448
- eventType: 'step.skipped',
449
- stepKey: step.key,
450
- timestamp: new Date(),
451
- payload: { reason: 'error', error: lastError.message },
452
- });
453
- return undefined;
454
- }
455
-
456
- if (onError === 'retry' && attempt <= maxRetries) {
457
- const delay = calculateRetryDelay(attempt, retryDelay, retryBackoff);
458
- logger.warn(`Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
459
-
460
- emitEvent(events, logger, {
461
- runId: context.runId,
462
- kind: context.kind,
463
- eventType: 'step.retry',
464
- stepKey: step.key,
465
- timestamp: new Date(),
466
- payload: { attempt, maxRetries, delay, error: lastError.message },
467
- });
468
-
469
- try {
470
- await sleep(delay, abortController.signal);
471
- } catch (sleepError) {
472
- if (sleepError instanceof WorkflowCanceledError) {
473
- await storage.updateStep(stepRecord.id, {
474
- status: 'canceled',
475
- finishedAt: new Date(),
476
- });
477
- }
478
- throw sleepError;
479
- }
480
- continue;
481
- }
482
-
483
- // Strategy is 'fail' or retries exhausted
484
- throw new StepError(step.key, lastError.message, attempt, lastError);
485
- }
486
- }
487
- }
488
-
489
- /**
490
- * Execute a function with a timeout.
491
- *
492
- * Uses Promise.race to resolve as soon as either the function completes, the
493
- * timeout fires, or the abort signal triggers. The `finally` block is critical:
494
- * without it, the losing promise's timer/listener would remain active, leaking
495
- * memory — especially problematic in long-running engines processing many steps.
496
- */
497
- async function executeWithTimeout<T>(
498
- fn: () => Promise<T>,
499
- timeoutMs: number,
500
- signal: AbortSignal,
501
- stepKey: string
502
- ): Promise<T> {
503
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
504
- let onAbort: (() => void) | undefined;
505
-
506
- try {
507
- return await Promise.race([
508
- fn(),
509
- new Promise<never>((_, reject) => {
510
- timeoutId = setTimeout(() => {
511
- reject(new StepTimeoutError(stepKey, timeoutMs));
512
- }, timeoutMs);
513
-
514
- onAbort = () => {
515
- clearTimeout(timeoutId);
516
- reject(new WorkflowCanceledError('run'));
517
- };
518
- signal.addEventListener('abort', onAbort, { once: true });
519
- }),
520
- ]);
521
- } finally {
522
- if (timeoutId) clearTimeout(timeoutId);
523
- if (onAbort) signal.removeEventListener('abort', onAbort);
524
- }
525
- }
526
-
527
- /**
528
- * Race a promise against an abort signal.
529
- *
530
- * Steps without their own timeout still need to respect workflow-level timeouts
531
- * and cancellation. This wraps the step handler in a Promise.race so that an
532
- * abort signal (from workflow timeout or user cancellation) can interrupt it.
533
- *
534
- * The `finally` cleanup removes the abort listener to prevent accumulating
535
- * listeners on the signal across many steps — without it, each step would
536
- * leave a dangling listener even after completing successfully.
537
- */
538
- async function raceWithAbort<T>(
539
- promise: Promise<T>,
540
- signal: AbortSignal
541
- ): Promise<T> {
542
- if (signal.aborted) {
543
- throw new WorkflowCanceledError('run');
544
- }
545
-
546
- let onAbort: (() => void) | undefined;
547
-
548
- try {
549
- return await Promise.race([
550
- promise,
551
- new Promise<never>((_, reject) => {
552
- onAbort = () => reject(new WorkflowCanceledError('run'));
553
- signal.addEventListener('abort', onAbort, { once: true });
554
- }),
555
- ]);
556
- } finally {
557
- if (onAbort) signal.removeEventListener('abort', onAbort);
558
- }
559
- }
560
-
561
- /**
562
- * Helper to emit an event.
563
- */
564
- function emitEvent(events: EventTransport, logger: Logger, event: WorkflowEvent): void {
565
- try {
566
- events.emit(event);
567
- } catch (error) {
568
- // Log but don't fail the workflow for event emission errors
569
- logger.error('Failed to emit event:', error);
570
- }
571
- }