@multiplier-labs/stepflow 0.3.1 → 0.3.4

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 (112) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  4. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  5. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  6. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  7. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  8. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  9. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  10. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  11. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  12. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  13. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  14. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  15. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  16. package/.github/CODEOWNERS +15 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/workflows/ci.yml +38 -0
  19. package/.github/workflows/publish.yml +17 -5
  20. package/.github/workflows/security.yml +29 -0
  21. package/CHANGELOG.md +34 -0
  22. package/README.md +24 -0
  23. package/SECURITY.md +40 -0
  24. package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
  25. package/dist/chunk-5XJBAPXF.js.map +1 -0
  26. package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
  27. package/dist/chunk-FMPJBK2N.js.map +1 -0
  28. package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
  29. package/dist/chunk-JPXPB7U5.js.map +1 -0
  30. package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
  31. package/dist/chunk-U3PCVK6O.js.map +1 -0
  32. package/dist/events/index.d.ts +60 -7
  33. package/dist/events/index.js +1 -1
  34. package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
  35. package/dist/index.d.ts +15 -23
  36. package/dist/index.js +784 -547
  37. package/dist/index.js.map +1 -1
  38. package/dist/scheduler/index.d.ts +4 -4
  39. package/dist/scheduler/index.js +2 -2
  40. package/dist/storage/index.d.ts +322 -56
  41. package/dist/storage/index.js +4 -2
  42. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  43. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  44. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  45. package/docs/stepflow-api-reference.md +5 -4
  46. package/package.json +12 -8
  47. package/src/core/engine/concurrency.ts +229 -0
  48. package/src/core/engine/lifecycle.ts +176 -0
  49. package/src/core/engine.test.ts +536 -522
  50. package/src/core/engine.ts +116 -235
  51. package/src/core/index.ts +3 -3
  52. package/src/core/orchestrator/persistence.ts +165 -0
  53. package/src/core/orchestrator/plan-resolution.ts +140 -0
  54. package/src/core/orchestrator/state-transitions.ts +109 -0
  55. package/src/core/orchestrator/step-runner.ts +427 -0
  56. package/src/core/orchestrator.ts +57 -395
  57. package/src/core/types.ts +7 -29
  58. package/src/events/index.ts +4 -4
  59. package/src/events/memory.test.ts +39 -41
  60. package/src/events/memory.ts +7 -10
  61. package/src/events/socketio.test.ts +112 -162
  62. package/src/events/socketio.ts +20 -27
  63. package/src/events/types.ts +16 -19
  64. package/src/events/webhook.test.ts +489 -235
  65. package/src/events/webhook.ts +355 -139
  66. package/src/index.ts +19 -19
  67. package/src/planning/evaluate-condition.test.ts +153 -0
  68. package/src/planning/evaluate-condition.ts +105 -0
  69. package/src/planning/index.ts +6 -3
  70. package/src/planning/planner.ts +45 -152
  71. package/src/planning/planning.test.ts +303 -366
  72. package/src/planning/registry.test.ts +204 -251
  73. package/src/planning/registry.ts +20 -106
  74. package/src/planning/types.ts +23 -27
  75. package/src/scheduler/cron.test.ts +149 -150
  76. package/src/scheduler/cron.ts +58 -69
  77. package/src/scheduler/index.ts +4 -14
  78. package/src/scheduler/postgres-persistence.ts +89 -105
  79. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  80. package/src/scheduler/sqlite-persistence.ts +40 -22
  81. package/src/scheduler/types.ts +4 -9
  82. package/src/storage/index.ts +5 -5
  83. package/src/storage/memory.test.ts +133 -183
  84. package/src/storage/memory.ts +21 -42
  85. package/src/storage/postgres-core.ts +550 -0
  86. package/src/storage/postgres-migration.test.ts +69 -0
  87. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  88. package/src/storage/postgres.ts +222 -937
  89. package/src/storage/sqlite-core.ts +151 -0
  90. package/src/storage/sqlite.test.ts +330 -281
  91. package/src/storage/sqlite.ts +225 -170
  92. package/src/storage/types.ts +33 -58
  93. package/src/utils/errors.test.ts +78 -80
  94. package/src/utils/errors.ts +27 -57
  95. package/src/utils/id.ts +3 -3
  96. package/src/utils/index.ts +6 -4
  97. package/src/utils/logger.test.ts +52 -58
  98. package/src/utils/logger.ts +4 -4
  99. package/src/utils/pinned-fetch.test.ts +84 -0
  100. package/src/utils/pinned-fetch.ts +118 -0
  101. package/src/utils/postgres-deps.ts +20 -4
  102. package/src/utils/retry.test.ts +47 -67
  103. package/src/utils/retry.ts +9 -9
  104. package/src/utils/safe-json.test.ts +166 -0
  105. package/src/utils/safe-json.ts +107 -0
  106. package/src/utils/webhook-signature.test.ts +111 -0
  107. package/src/utils/webhook-signature.ts +74 -0
  108. package/dist/chunk-FQCT3I2A.js.map +0 -1
  109. package/dist/chunk-GXY66V7Q.js.map +0 -1
  110. package/dist/chunk-U5QGN3RT.js.map +0 -1
  111. package/dist/chunk-XCF4PLN7.js.map +0 -1
  112. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -3,10 +3,9 @@
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 { generateId } from '../utils/id';
7
+ import { evaluateCondition, getNestedValue } from './evaluate-condition';
8
8
  import type {
9
- ConditionOperator,
10
9
  Plan,
11
10
  PlannedStep,
12
11
  Planner,
@@ -20,102 +19,7 @@ import type {
20
19
  ResourceEstimate,
21
20
  StepHandlerRegistry,
22
21
  WorkflowKind,
23
- } from "./types";
24
-
25
- // ============================================================================
26
- // Condition Evaluation
27
- // ============================================================================
28
-
29
- /**
30
- * Get a nested value from an object using dot notation.
31
- */
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) {
35
- return (current as Record<string, unknown>)[key];
36
- }
37
- return undefined;
38
- }, obj);
39
- }
40
-
41
- /**
42
- * Evaluate a single condition against a value.
43
- */
44
- function evaluateCondition(
45
- operator: ConditionOperator,
46
- fieldValue: unknown,
47
- conditionValue: unknown,
48
- ): boolean {
49
- switch (operator) {
50
- case "eq":
51
- return fieldValue === conditionValue;
52
-
53
- case "neq":
54
- return fieldValue !== conditionValue;
55
-
56
- case "gt":
57
- return typeof fieldValue === "number" &&
58
- typeof conditionValue === "number"
59
- ? fieldValue > conditionValue
60
- : false;
61
-
62
- case "gte":
63
- return typeof fieldValue === "number" &&
64
- typeof conditionValue === "number"
65
- ? fieldValue >= conditionValue
66
- : false;
67
-
68
- case "lt":
69
- return typeof fieldValue === "number" &&
70
- typeof conditionValue === "number"
71
- ? fieldValue < conditionValue
72
- : false;
73
-
74
- case "lte":
75
- return typeof fieldValue === "number" &&
76
- typeof conditionValue === "number"
77
- ? fieldValue <= conditionValue
78
- : false;
79
-
80
- case "contains":
81
- if (
82
- typeof fieldValue === "string" &&
83
- typeof conditionValue === "string"
84
- ) {
85
- return fieldValue.includes(conditionValue);
86
- }
87
- if (Array.isArray(fieldValue)) {
88
- return fieldValue.includes(conditionValue);
89
- }
90
- return false;
91
-
92
- case "matches":
93
- if (
94
- typeof fieldValue === "string" &&
95
- typeof conditionValue === "string"
96
- ) {
97
- try {
98
- return new RE2(conditionValue).test(fieldValue);
99
- } catch {
100
- return false;
101
- }
102
- }
103
- return false;
104
-
105
- case "exists":
106
- return (
107
- fieldValue !== undefined && fieldValue !== null && fieldValue !== ""
108
- );
109
-
110
- case "notExists":
111
- return (
112
- fieldValue === undefined || fieldValue === null || fieldValue === ""
113
- );
114
-
115
- default:
116
- return false;
117
- }
118
- }
22
+ } from './types';
119
23
 
120
24
  /**
121
25
  * Score a recipe's conditions against input.
@@ -123,7 +27,7 @@ function evaluateCondition(
123
27
  */
124
28
  function scoreConditions(
125
29
  conditions: RecipeCondition[] | undefined,
126
- input: Record<string, unknown>,
30
+ input: Record<string, unknown>
127
31
  ): number {
128
32
  // No conditions = default match with low score
129
33
  if (!conditions || conditions.length === 0) {
@@ -185,9 +89,7 @@ export class RuleBasedPlanner implements Planner {
185
89
  private recipeRegistry: RecipeRegistry;
186
90
  private handlerRegistry?: StepHandlerRegistry;
187
91
  private validateHandlers: boolean;
188
- private resourceEstimates: Required<
189
- NonNullable<RuleBasedPlannerConfig["resourceEstimates"]>
190
- >;
92
+ private resourceEstimates: Required<NonNullable<RuleBasedPlannerConfig['resourceEstimates']>>;
191
93
 
192
94
  constructor(config: RuleBasedPlannerConfig) {
193
95
  this.recipeRegistry = config.recipeRegistry;
@@ -207,7 +109,7 @@ export class RuleBasedPlanner implements Planner {
207
109
  async selectRecipe(
208
110
  workflowKind: WorkflowKind,
209
111
  input: Record<string, unknown>,
210
- context?: PlanningContext,
112
+ context?: PlanningContext
211
113
  ): Promise<RecipeSelectionResult> {
212
114
  // Check for forced recipe in hints
213
115
  if (context?.hints?.forceRecipeId) {
@@ -225,7 +127,7 @@ export class RuleBasedPlanner implements Planner {
225
127
  if (context?.hints?.preferredVariant) {
226
128
  const preferred = this.recipeRegistry.getVariant(
227
129
  workflowKind,
228
- context.hints.preferredVariant,
130
+ context.hints.preferredVariant
229
131
  );
230
132
  if (preferred) {
231
133
  return {
@@ -244,7 +146,7 @@ export class RuleBasedPlanner implements Planner {
244
146
  }
245
147
 
246
148
  // Score each recipe
247
- const scored = recipes.map((recipe) => ({
149
+ const scored = recipes.map(recipe => ({
248
150
  recipe,
249
151
  conditionScore: scoreConditions(recipe.conditions, input),
250
152
  priorityScore: recipe.priority ?? 0,
@@ -259,7 +161,7 @@ export class RuleBasedPlanner implements Planner {
259
161
  });
260
162
 
261
163
  // Find best matching recipe
262
- const best = scored.find((s) => s.conditionScore > 0);
164
+ const best = scored.find(s => s.conditionScore > 0);
263
165
 
264
166
  if (best) {
265
167
  const reason = this.buildSelectionReason(best.recipe, input);
@@ -294,12 +196,12 @@ export class RuleBasedPlanner implements Planner {
294
196
  async generatePlan(
295
197
  recipe: Recipe,
296
198
  input: Record<string, unknown>,
297
- context?: PlanningContext,
199
+ context?: PlanningContext
298
200
  ): Promise<Plan> {
299
201
  const modifications: PlanModification[] = [];
300
202
 
301
203
  // Start with recipe steps
302
- let steps = recipe.steps.map((step) => this.recipeStepToPlannedStep(step));
204
+ let steps = recipe.steps.map(step => this.recipeStepToPlannedStep(step));
303
205
 
304
206
  // Apply hint-based modifications
305
207
  if (context?.hints) {
@@ -308,11 +210,11 @@ export class RuleBasedPlanner implements Planner {
308
210
  // Mark steps to skip
309
211
  if (skipSteps && skipSteps.length > 0) {
310
212
  for (const stepKey of skipSteps) {
311
- const stepIndex = steps.findIndex((s) => s.key === stepKey);
213
+ const stepIndex = steps.findIndex(s => s.key === stepKey);
312
214
  if (stepIndex >= 0) {
313
- steps = steps.filter((s) => s.key !== stepKey);
215
+ steps = steps.filter(s => s.key !== stepKey);
314
216
  modifications.push({
315
- type: "remove_step",
217
+ type: 'remove_step',
316
218
  stepKey,
317
219
  value: null,
318
220
  reason: `Skipped via planning hints`,
@@ -327,9 +229,9 @@ export class RuleBasedPlanner implements Planner {
327
229
  step.config = { ...step.config, ...additionalConfig };
328
230
  }
329
231
  modifications.push({
330
- type: "modify_step",
232
+ type: 'modify_step',
331
233
  value: additionalConfig,
332
- reason: "Applied additional config from planning hints",
234
+ reason: 'Applied additional config from planning hints',
333
235
  });
334
236
  }
335
237
  }
@@ -365,13 +267,13 @@ export class RuleBasedPlanner implements Planner {
365
267
  async plan(
366
268
  workflowKind: WorkflowKind,
367
269
  input: Record<string, unknown>,
368
- context?: PlanningContext,
270
+ context?: PlanningContext
369
271
  ): Promise<Plan> {
370
272
  const selection = await this.selectRecipe(workflowKind, input, context);
371
273
  const plan = await this.generatePlan(selection.recipe, input, context);
372
274
 
373
275
  // Add selection reasoning to plan
374
- plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ""}`.trim();
276
+ plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ''}`.trim();
375
277
 
376
278
  return plan;
377
279
  }
@@ -385,7 +287,7 @@ export class RuleBasedPlanner implements Planner {
385
287
 
386
288
  // Check plan has steps
387
289
  if (!plan.steps || plan.steps.length === 0) {
388
- errors.push("Plan has no steps");
290
+ errors.push('Plan has no steps');
389
291
  }
390
292
 
391
293
  // Check for duplicate step keys
@@ -400,7 +302,7 @@ export class RuleBasedPlanner implements Planner {
400
302
  // Validate each step
401
303
  for (const step of plan.steps) {
402
304
  if (!step.key) {
403
- errors.push("Step missing key");
305
+ errors.push('Step missing key');
404
306
  }
405
307
  if (!step.name) {
406
308
  warnings.push(`Step '${step.key}' missing name`);
@@ -412,9 +314,7 @@ export class RuleBasedPlanner implements Planner {
412
314
  // Validate handler exists (if registry available)
413
315
  if (this.validateHandlers && this.handlerRegistry) {
414
316
  if (!this.handlerRegistry.has(step.handlerRef)) {
415
- errors.push(
416
- `Step '${step.key}' references unknown handler: ${step.handlerRef}`,
417
- );
317
+ errors.push(`Step '${step.key}' references unknown handler: ${step.handlerRef}`);
418
318
  }
419
319
  }
420
320
  }
@@ -423,7 +323,7 @@ export class RuleBasedPlanner implements Planner {
423
323
  if (plan.childWorkflows) {
424
324
  for (const child of plan.childWorkflows) {
425
325
  if (!child.kind) {
426
- errors.push("Child workflow missing kind");
326
+ errors.push('Child workflow missing kind');
427
327
  }
428
328
  }
429
329
  }
@@ -439,12 +339,7 @@ export class RuleBasedPlanner implements Planner {
439
339
  * Estimate resources required for a plan.
440
340
  */
441
341
  estimateResources(plan: Plan): ResourceEstimate {
442
- const {
443
- apiCallsPerStep,
444
- tokensPerStep,
445
- durationPerStep,
446
- apiCallsPerChild,
447
- } = this.resourceEstimates;
342
+ const { apiCallsPerStep, tokensPerStep, durationPerStep, apiCallsPerChild } = this.resourceEstimates;
448
343
 
449
344
  const stepCount = plan.steps.length;
450
345
  const childCount = plan.childWorkflows?.length ?? 0;
@@ -463,7 +358,7 @@ export class RuleBasedPlanner implements Planner {
463
358
  /**
464
359
  * Convert a recipe step to a planned step.
465
360
  */
466
- private recipeStepToPlannedStep(step: Recipe["steps"][0]): PlannedStep {
361
+ private recipeStepToPlannedStep(step: Recipe['steps'][0]): PlannedStep {
467
362
  return {
468
363
  key: step.key,
469
364
  name: step.name,
@@ -483,7 +378,7 @@ export class RuleBasedPlanner implements Planner {
483
378
  */
484
379
  private applyConstraints(
485
380
  steps: PlannedStep[],
486
- constraints: NonNullable<PlanningContext["constraints"]>,
381
+ constraints: NonNullable<PlanningContext['constraints']>
487
382
  ): { steps: PlannedStep[]; modifications: PlanModification[] } {
488
383
  const modifications: PlanModification[] = [];
489
384
  const perStepTimeout = constraints.maxDuration
@@ -491,17 +386,15 @@ export class RuleBasedPlanner implements Planner {
491
386
  : undefined;
492
387
 
493
388
  // Apply all constraint mutations in a single pass
494
- const modifiedSteps = steps.map((step) => {
389
+ const modifiedSteps = steps.map(step => {
495
390
  const modified = { ...step };
496
391
 
497
- if (constraints.priority === "speed") {
498
- modified.timeout = modified.timeout
499
- ? Math.min(modified.timeout, 30000)
500
- : 30000;
392
+ if (constraints.priority === 'speed') {
393
+ modified.timeout = modified.timeout ? Math.min(modified.timeout, 30000) : 30000;
501
394
  modified.maxRetries = Math.min(modified.maxRetries ?? 3, 1);
502
395
  }
503
396
 
504
- if (constraints.priority === "cost") {
397
+ if (constraints.priority === 'cost') {
505
398
  modified.maxRetries = 0;
506
399
  }
507
400
 
@@ -515,23 +408,23 @@ export class RuleBasedPlanner implements Planner {
515
408
  });
516
409
 
517
410
  // Record what was applied
518
- if (constraints.priority === "speed") {
411
+ if (constraints.priority === 'speed') {
519
412
  modifications.push({
520
- type: "set_default",
521
- value: { priority: "speed" },
522
- reason: "Optimized for speed: reduced timeouts and retries",
413
+ type: 'set_default',
414
+ value: { priority: 'speed' },
415
+ reason: 'Optimized for speed: reduced timeouts and retries',
523
416
  });
524
417
  }
525
- if (constraints.priority === "cost") {
418
+ if (constraints.priority === 'cost') {
526
419
  modifications.push({
527
- type: "set_default",
528
- value: { priority: "cost" },
529
- reason: "Optimized for cost: disabled retries",
420
+ type: 'set_default',
421
+ value: { priority: 'cost' },
422
+ reason: 'Optimized for cost: disabled retries',
530
423
  });
531
424
  }
532
425
  if (constraints.maxDuration) {
533
426
  modifications.push({
534
- type: "set_default",
427
+ type: 'set_default',
535
428
  value: { maxDuration: constraints.maxDuration },
536
429
  reason: `Applied duration constraint: ${constraints.maxDuration}ms total`,
537
430
  });
@@ -545,23 +438,23 @@ export class RuleBasedPlanner implements Planner {
545
438
  */
546
439
  private buildSelectionReason(
547
440
  recipe: Recipe,
548
- input: Record<string, unknown>,
441
+ input: Record<string, unknown>
549
442
  ): string {
550
443
  const parts: string[] = [`Selected recipe: ${recipe.id}`];
551
444
 
552
445
  if (recipe.conditions && recipe.conditions.length > 0) {
553
- const conditionDescriptions = recipe.conditions.map((c) => {
446
+ const conditionDescriptions = recipe.conditions.map(c => {
554
447
  const value = getNestedValue(input, c.field);
555
448
  return `${c.field} ${c.operator} ${JSON.stringify(c.value)} (actual: ${JSON.stringify(value)})`;
556
449
  });
557
- parts.push(`Matched conditions: ${conditionDescriptions.join(", ")}`);
450
+ parts.push(`Matched conditions: ${conditionDescriptions.join(', ')}`);
558
451
  }
559
452
 
560
453
  if (recipe.priority !== undefined && recipe.priority > 0) {
561
454
  parts.push(`Priority: ${recipe.priority}`);
562
455
  }
563
456
 
564
- return parts.join(". ");
457
+ return parts.join('. ');
565
458
  }
566
459
 
567
460
  /**
@@ -570,7 +463,7 @@ export class RuleBasedPlanner implements Planner {
570
463
  private buildPlanReasoning(
571
464
  recipe: Recipe,
572
465
  modifications: PlanModification[],
573
- context?: PlanningContext,
466
+ context?: PlanningContext
574
467
  ): string {
575
468
  const parts: string[] = [];
576
469
 
@@ -592,10 +485,10 @@ export class RuleBasedPlanner implements Planner {
592
485
  constraintList.push(`maxApiCalls=${context.constraints.maxApiCalls}`);
593
486
  }
594
487
  if (constraintList.length > 0) {
595
- parts.push(`Constraints: ${constraintList.join(", ")}`);
488
+ parts.push(`Constraints: ${constraintList.join(', ')}`);
596
489
  }
597
490
  }
598
491
 
599
- return parts.join(". ") + ".";
492
+ return parts.join('. ') + '.';
600
493
  }
601
494
  }