@multiplier-labs/stepflow 0.3.1 → 0.3.3

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/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  2. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  3. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  4. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  5. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  6. package/.github/CODEOWNERS +15 -0
  7. package/.github/dependabot.yml +23 -0
  8. package/.github/workflows/ci.yml +38 -0
  9. package/.github/workflows/publish.yml +11 -5
  10. package/.github/workflows/security.yml +29 -0
  11. package/CHANGELOG.md +34 -0
  12. package/README.md +24 -0
  13. package/SECURITY.md +40 -0
  14. package/dist/{chunk-FQCT3I2A.js → chunk-4VOKMGP6.js} +23 -42
  15. package/dist/chunk-4VOKMGP6.js.map +1 -0
  16. package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
  17. package/dist/chunk-GYPNJBDP.js.map +1 -0
  18. package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
  19. package/dist/chunk-KHTUWLVZ.js.map +1 -0
  20. package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
  21. package/dist/chunk-ML35PIHX.js.map +1 -0
  22. package/dist/events/index.d.ts +4 -4
  23. package/dist/events/index.js +1 -1
  24. package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
  25. package/dist/index.d.ts +14 -12
  26. package/dist/index.js +40 -82
  27. package/dist/index.js.map +1 -1
  28. package/dist/scheduler/index.d.ts +4 -4
  29. package/dist/scheduler/index.js +2 -2
  30. package/dist/storage/index.d.ts +13 -13
  31. package/dist/storage/index.js +2 -2
  32. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  33. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  34. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  35. package/docs/stepflow-api-reference.md +2 -3
  36. package/package.json +7 -6
  37. package/src/core/engine.test.ts +466 -522
  38. package/src/core/engine.ts +42 -67
  39. package/src/core/index.ts +3 -3
  40. package/src/core/orchestrator.ts +64 -83
  41. package/src/core/types.ts +7 -29
  42. package/src/events/index.ts +4 -4
  43. package/src/events/memory.test.ts +39 -41
  44. package/src/events/memory.ts +7 -10
  45. package/src/events/socketio.test.ts +112 -162
  46. package/src/events/socketio.ts +20 -27
  47. package/src/events/types.ts +16 -19
  48. package/src/events/webhook.test.ts +197 -235
  49. package/src/events/webhook.ts +44 -71
  50. package/src/index.ts +19 -19
  51. package/src/planning/index.ts +6 -3
  52. package/src/planning/planner.ts +66 -91
  53. package/src/planning/planning.test.ts +303 -366
  54. package/src/planning/registry.test.ts +204 -251
  55. package/src/planning/registry.ts +39 -43
  56. package/src/planning/types.ts +23 -27
  57. package/src/scheduler/cron.test.ts +141 -150
  58. package/src/scheduler/cron.ts +32 -67
  59. package/src/scheduler/index.ts +4 -14
  60. package/src/scheduler/postgres-persistence.ts +71 -99
  61. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  62. package/src/scheduler/sqlite-persistence.ts +12 -19
  63. package/src/scheduler/types.ts +4 -9
  64. package/src/storage/index.ts +5 -5
  65. package/src/storage/memory.test.ts +133 -183
  66. package/src/storage/memory.ts +21 -42
  67. package/src/storage/postgres.ts +197 -393
  68. package/src/storage/sqlite.test.ts +154 -281
  69. package/src/storage/sqlite.ts +50 -85
  70. package/src/storage/types.ts +33 -58
  71. package/src/utils/errors.test.ts +78 -80
  72. package/src/utils/errors.ts +27 -57
  73. package/src/utils/id.ts +3 -3
  74. package/src/utils/index.ts +4 -4
  75. package/src/utils/logger.test.ts +52 -58
  76. package/src/utils/logger.ts +4 -4
  77. package/src/utils/postgres-deps.ts +4 -4
  78. package/src/utils/retry.test.ts +47 -67
  79. package/src/utils/retry.ts +9 -9
  80. package/dist/chunk-FQCT3I2A.js.map +0 -1
  81. package/dist/chunk-GXY66V7Q.js.map +0 -1
  82. package/dist/chunk-U5QGN3RT.js.map +0 -1
  83. package/dist/chunk-XCF4PLN7.js.map +0 -1
  84. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -3,8 +3,8 @@
3
3
  * Selects recipes and generates plans based on conditions and input analysis.
4
4
  */
5
5
 
6
- import RE2 from "re2";
7
- import { generateId } from "../utils/id";
6
+ import RE2 from 're2';
7
+ import { generateId } from '../utils/id';
8
8
  import type {
9
9
  ConditionOperator,
10
10
  Plan,
@@ -20,7 +20,7 @@ import type {
20
20
  ResourceEstimate,
21
21
  StepHandlerRegistry,
22
22
  WorkflowKind,
23
- } from "./types";
23
+ } from './types';
24
24
 
25
25
  // ============================================================================
26
26
  // Condition Evaluation
@@ -30,8 +30,8 @@ import type {
30
30
  * Get a nested value from an object using dot notation.
31
31
  */
32
32
  function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
33
- return path.split(".").reduce<unknown>((current, key) => {
34
- if (current && typeof current === "object" && key in current) {
33
+ return path.split('.').reduce<unknown>((current, key) => {
34
+ if (current && typeof current === 'object' && key in current) {
35
35
  return (current as Record<string, unknown>)[key];
36
36
  }
37
37
  return undefined;
@@ -44,44 +44,37 @@ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
44
44
  function evaluateCondition(
45
45
  operator: ConditionOperator,
46
46
  fieldValue: unknown,
47
- conditionValue: unknown,
47
+ conditionValue: unknown
48
48
  ): boolean {
49
49
  switch (operator) {
50
- case "eq":
50
+ case 'eq':
51
51
  return fieldValue === conditionValue;
52
52
 
53
- case "neq":
53
+ case 'neq':
54
54
  return fieldValue !== conditionValue;
55
55
 
56
- case "gt":
57
- return typeof fieldValue === "number" &&
58
- typeof conditionValue === "number"
56
+ case 'gt':
57
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
59
58
  ? fieldValue > conditionValue
60
59
  : false;
61
60
 
62
- case "gte":
63
- return typeof fieldValue === "number" &&
64
- typeof conditionValue === "number"
61
+ case 'gte':
62
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
65
63
  ? fieldValue >= conditionValue
66
64
  : false;
67
65
 
68
- case "lt":
69
- return typeof fieldValue === "number" &&
70
- typeof conditionValue === "number"
66
+ case 'lt':
67
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
71
68
  ? fieldValue < conditionValue
72
69
  : false;
73
70
 
74
- case "lte":
75
- return typeof fieldValue === "number" &&
76
- typeof conditionValue === "number"
71
+ case 'lte':
72
+ return typeof fieldValue === 'number' && typeof conditionValue === 'number'
77
73
  ? fieldValue <= conditionValue
78
74
  : false;
79
75
 
80
- case "contains":
81
- if (
82
- typeof fieldValue === "string" &&
83
- typeof conditionValue === "string"
84
- ) {
76
+ case 'contains':
77
+ if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
85
78
  return fieldValue.includes(conditionValue);
86
79
  }
87
80
  if (Array.isArray(fieldValue)) {
@@ -89,11 +82,8 @@ function evaluateCondition(
89
82
  }
90
83
  return false;
91
84
 
92
- case "matches":
93
- if (
94
- typeof fieldValue === "string" &&
95
- typeof conditionValue === "string"
96
- ) {
85
+ case 'matches':
86
+ if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
97
87
  try {
98
88
  return new RE2(conditionValue).test(fieldValue);
99
89
  } catch {
@@ -102,15 +92,11 @@ function evaluateCondition(
102
92
  }
103
93
  return false;
104
94
 
105
- case "exists":
106
- return (
107
- fieldValue !== undefined && fieldValue !== null && fieldValue !== ""
108
- );
95
+ case 'exists':
96
+ return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
109
97
 
110
- case "notExists":
111
- return (
112
- fieldValue === undefined || fieldValue === null || fieldValue === ""
113
- );
98
+ case 'notExists':
99
+ return fieldValue === undefined || fieldValue === null || fieldValue === '';
114
100
 
115
101
  default:
116
102
  return false;
@@ -123,7 +109,7 @@ function evaluateCondition(
123
109
  */
124
110
  function scoreConditions(
125
111
  conditions: RecipeCondition[] | undefined,
126
- input: Record<string, unknown>,
112
+ input: Record<string, unknown>
127
113
  ): number {
128
114
  // No conditions = default match with low score
129
115
  if (!conditions || conditions.length === 0) {
@@ -185,9 +171,7 @@ export class RuleBasedPlanner implements Planner {
185
171
  private recipeRegistry: RecipeRegistry;
186
172
  private handlerRegistry?: StepHandlerRegistry;
187
173
  private validateHandlers: boolean;
188
- private resourceEstimates: Required<
189
- NonNullable<RuleBasedPlannerConfig["resourceEstimates"]>
190
- >;
174
+ private resourceEstimates: Required<NonNullable<RuleBasedPlannerConfig['resourceEstimates']>>;
191
175
 
192
176
  constructor(config: RuleBasedPlannerConfig) {
193
177
  this.recipeRegistry = config.recipeRegistry;
@@ -207,7 +191,7 @@ export class RuleBasedPlanner implements Planner {
207
191
  async selectRecipe(
208
192
  workflowKind: WorkflowKind,
209
193
  input: Record<string, unknown>,
210
- context?: PlanningContext,
194
+ context?: PlanningContext
211
195
  ): Promise<RecipeSelectionResult> {
212
196
  // Check for forced recipe in hints
213
197
  if (context?.hints?.forceRecipeId) {
@@ -225,7 +209,7 @@ export class RuleBasedPlanner implements Planner {
225
209
  if (context?.hints?.preferredVariant) {
226
210
  const preferred = this.recipeRegistry.getVariant(
227
211
  workflowKind,
228
- context.hints.preferredVariant,
212
+ context.hints.preferredVariant
229
213
  );
230
214
  if (preferred) {
231
215
  return {
@@ -244,7 +228,7 @@ export class RuleBasedPlanner implements Planner {
244
228
  }
245
229
 
246
230
  // Score each recipe
247
- const scored = recipes.map((recipe) => ({
231
+ const scored = recipes.map(recipe => ({
248
232
  recipe,
249
233
  conditionScore: scoreConditions(recipe.conditions, input),
250
234
  priorityScore: recipe.priority ?? 0,
@@ -259,7 +243,7 @@ export class RuleBasedPlanner implements Planner {
259
243
  });
260
244
 
261
245
  // Find best matching recipe
262
- const best = scored.find((s) => s.conditionScore > 0);
246
+ const best = scored.find(s => s.conditionScore > 0);
263
247
 
264
248
  if (best) {
265
249
  const reason = this.buildSelectionReason(best.recipe, input);
@@ -294,12 +278,12 @@ export class RuleBasedPlanner implements Planner {
294
278
  async generatePlan(
295
279
  recipe: Recipe,
296
280
  input: Record<string, unknown>,
297
- context?: PlanningContext,
281
+ context?: PlanningContext
298
282
  ): Promise<Plan> {
299
283
  const modifications: PlanModification[] = [];
300
284
 
301
285
  // Start with recipe steps
302
- let steps = recipe.steps.map((step) => this.recipeStepToPlannedStep(step));
286
+ let steps = recipe.steps.map(step => this.recipeStepToPlannedStep(step));
303
287
 
304
288
  // Apply hint-based modifications
305
289
  if (context?.hints) {
@@ -308,11 +292,11 @@ export class RuleBasedPlanner implements Planner {
308
292
  // Mark steps to skip
309
293
  if (skipSteps && skipSteps.length > 0) {
310
294
  for (const stepKey of skipSteps) {
311
- const stepIndex = steps.findIndex((s) => s.key === stepKey);
295
+ const stepIndex = steps.findIndex(s => s.key === stepKey);
312
296
  if (stepIndex >= 0) {
313
- steps = steps.filter((s) => s.key !== stepKey);
297
+ steps = steps.filter(s => s.key !== stepKey);
314
298
  modifications.push({
315
- type: "remove_step",
299
+ type: 'remove_step',
316
300
  stepKey,
317
301
  value: null,
318
302
  reason: `Skipped via planning hints`,
@@ -327,9 +311,9 @@ export class RuleBasedPlanner implements Planner {
327
311
  step.config = { ...step.config, ...additionalConfig };
328
312
  }
329
313
  modifications.push({
330
- type: "modify_step",
314
+ type: 'modify_step',
331
315
  value: additionalConfig,
332
- reason: "Applied additional config from planning hints",
316
+ reason: 'Applied additional config from planning hints',
333
317
  });
334
318
  }
335
319
  }
@@ -365,13 +349,13 @@ export class RuleBasedPlanner implements Planner {
365
349
  async plan(
366
350
  workflowKind: WorkflowKind,
367
351
  input: Record<string, unknown>,
368
- context?: PlanningContext,
352
+ context?: PlanningContext
369
353
  ): Promise<Plan> {
370
354
  const selection = await this.selectRecipe(workflowKind, input, context);
371
355
  const plan = await this.generatePlan(selection.recipe, input, context);
372
356
 
373
357
  // Add selection reasoning to plan
374
- plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ""}`.trim();
358
+ plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ''}`.trim();
375
359
 
376
360
  return plan;
377
361
  }
@@ -385,7 +369,7 @@ export class RuleBasedPlanner implements Planner {
385
369
 
386
370
  // Check plan has steps
387
371
  if (!plan.steps || plan.steps.length === 0) {
388
- errors.push("Plan has no steps");
372
+ errors.push('Plan has no steps');
389
373
  }
390
374
 
391
375
  // Check for duplicate step keys
@@ -400,7 +384,7 @@ export class RuleBasedPlanner implements Planner {
400
384
  // Validate each step
401
385
  for (const step of plan.steps) {
402
386
  if (!step.key) {
403
- errors.push("Step missing key");
387
+ errors.push('Step missing key');
404
388
  }
405
389
  if (!step.name) {
406
390
  warnings.push(`Step '${step.key}' missing name`);
@@ -412,9 +396,7 @@ export class RuleBasedPlanner implements Planner {
412
396
  // Validate handler exists (if registry available)
413
397
  if (this.validateHandlers && this.handlerRegistry) {
414
398
  if (!this.handlerRegistry.has(step.handlerRef)) {
415
- errors.push(
416
- `Step '${step.key}' references unknown handler: ${step.handlerRef}`,
417
- );
399
+ errors.push(`Step '${step.key}' references unknown handler: ${step.handlerRef}`);
418
400
  }
419
401
  }
420
402
  }
@@ -423,7 +405,7 @@ export class RuleBasedPlanner implements Planner {
423
405
  if (plan.childWorkflows) {
424
406
  for (const child of plan.childWorkflows) {
425
407
  if (!child.kind) {
426
- errors.push("Child workflow missing kind");
408
+ errors.push('Child workflow missing kind');
427
409
  }
428
410
  }
429
411
  }
@@ -439,12 +421,7 @@ export class RuleBasedPlanner implements Planner {
439
421
  * Estimate resources required for a plan.
440
422
  */
441
423
  estimateResources(plan: Plan): ResourceEstimate {
442
- const {
443
- apiCallsPerStep,
444
- tokensPerStep,
445
- durationPerStep,
446
- apiCallsPerChild,
447
- } = this.resourceEstimates;
424
+ const { apiCallsPerStep, tokensPerStep, durationPerStep, apiCallsPerChild } = this.resourceEstimates;
448
425
 
449
426
  const stepCount = plan.steps.length;
450
427
  const childCount = plan.childWorkflows?.length ?? 0;
@@ -463,7 +440,7 @@ export class RuleBasedPlanner implements Planner {
463
440
  /**
464
441
  * Convert a recipe step to a planned step.
465
442
  */
466
- private recipeStepToPlannedStep(step: Recipe["steps"][0]): PlannedStep {
443
+ private recipeStepToPlannedStep(step: Recipe['steps'][0]): PlannedStep {
467
444
  return {
468
445
  key: step.key,
469
446
  name: step.name,
@@ -483,7 +460,7 @@ export class RuleBasedPlanner implements Planner {
483
460
  */
484
461
  private applyConstraints(
485
462
  steps: PlannedStep[],
486
- constraints: NonNullable<PlanningContext["constraints"]>,
463
+ constraints: NonNullable<PlanningContext['constraints']>
487
464
  ): { steps: PlannedStep[]; modifications: PlanModification[] } {
488
465
  const modifications: PlanModification[] = [];
489
466
  const perStepTimeout = constraints.maxDuration
@@ -491,17 +468,15 @@ export class RuleBasedPlanner implements Planner {
491
468
  : undefined;
492
469
 
493
470
  // Apply all constraint mutations in a single pass
494
- const modifiedSteps = steps.map((step) => {
471
+ const modifiedSteps = steps.map(step => {
495
472
  const modified = { ...step };
496
473
 
497
- if (constraints.priority === "speed") {
498
- modified.timeout = modified.timeout
499
- ? Math.min(modified.timeout, 30000)
500
- : 30000;
474
+ if (constraints.priority === 'speed') {
475
+ modified.timeout = modified.timeout ? Math.min(modified.timeout, 30000) : 30000;
501
476
  modified.maxRetries = Math.min(modified.maxRetries ?? 3, 1);
502
477
  }
503
478
 
504
- if (constraints.priority === "cost") {
479
+ if (constraints.priority === 'cost') {
505
480
  modified.maxRetries = 0;
506
481
  }
507
482
 
@@ -515,23 +490,23 @@ export class RuleBasedPlanner implements Planner {
515
490
  });
516
491
 
517
492
  // Record what was applied
518
- if (constraints.priority === "speed") {
493
+ if (constraints.priority === 'speed') {
519
494
  modifications.push({
520
- type: "set_default",
521
- value: { priority: "speed" },
522
- reason: "Optimized for speed: reduced timeouts and retries",
495
+ type: 'set_default',
496
+ value: { priority: 'speed' },
497
+ reason: 'Optimized for speed: reduced timeouts and retries',
523
498
  });
524
499
  }
525
- if (constraints.priority === "cost") {
500
+ if (constraints.priority === 'cost') {
526
501
  modifications.push({
527
- type: "set_default",
528
- value: { priority: "cost" },
529
- reason: "Optimized for cost: disabled retries",
502
+ type: 'set_default',
503
+ value: { priority: 'cost' },
504
+ reason: 'Optimized for cost: disabled retries',
530
505
  });
531
506
  }
532
507
  if (constraints.maxDuration) {
533
508
  modifications.push({
534
- type: "set_default",
509
+ type: 'set_default',
535
510
  value: { maxDuration: constraints.maxDuration },
536
511
  reason: `Applied duration constraint: ${constraints.maxDuration}ms total`,
537
512
  });
@@ -545,23 +520,23 @@ export class RuleBasedPlanner implements Planner {
545
520
  */
546
521
  private buildSelectionReason(
547
522
  recipe: Recipe,
548
- input: Record<string, unknown>,
523
+ input: Record<string, unknown>
549
524
  ): string {
550
525
  const parts: string[] = [`Selected recipe: ${recipe.id}`];
551
526
 
552
527
  if (recipe.conditions && recipe.conditions.length > 0) {
553
- const conditionDescriptions = recipe.conditions.map((c) => {
528
+ const conditionDescriptions = recipe.conditions.map(c => {
554
529
  const value = getNestedValue(input, c.field);
555
530
  return `${c.field} ${c.operator} ${JSON.stringify(c.value)} (actual: ${JSON.stringify(value)})`;
556
531
  });
557
- parts.push(`Matched conditions: ${conditionDescriptions.join(", ")}`);
532
+ parts.push(`Matched conditions: ${conditionDescriptions.join(', ')}`);
558
533
  }
559
534
 
560
535
  if (recipe.priority !== undefined && recipe.priority > 0) {
561
536
  parts.push(`Priority: ${recipe.priority}`);
562
537
  }
563
538
 
564
- return parts.join(". ");
539
+ return parts.join('. ');
565
540
  }
566
541
 
567
542
  /**
@@ -570,7 +545,7 @@ export class RuleBasedPlanner implements Planner {
570
545
  private buildPlanReasoning(
571
546
  recipe: Recipe,
572
547
  modifications: PlanModification[],
573
- context?: PlanningContext,
548
+ context?: PlanningContext
574
549
  ): string {
575
550
  const parts: string[] = [];
576
551
 
@@ -592,10 +567,10 @@ export class RuleBasedPlanner implements Planner {
592
567
  constraintList.push(`maxApiCalls=${context.constraints.maxApiCalls}`);
593
568
  }
594
569
  if (constraintList.length > 0) {
595
- parts.push(`Constraints: ${constraintList.join(", ")}`);
570
+ parts.push(`Constraints: ${constraintList.join(', ')}`);
596
571
  }
597
572
  }
598
573
 
599
- return parts.join(". ") + ".";
574
+ return parts.join('. ') + '.';
600
575
  }
601
576
  }