@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,576 +0,0 @@
1
- /**
2
- * Rule-based planner implementation.
3
- * Selects recipes and generates plans based on conditions and input analysis.
4
- */
5
-
6
- import RE2 from 're2';
7
- import { generateId } from '../utils/id';
8
- import type {
9
- ConditionOperator,
10
- Plan,
11
- PlannedStep,
12
- Planner,
13
- PlanModification,
14
- PlanningContext,
15
- PlanValidationResult,
16
- Recipe,
17
- RecipeCondition,
18
- RecipeRegistry,
19
- RecipeSelectionResult,
20
- ResourceEstimate,
21
- StepHandlerRegistry,
22
- 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' && typeof conditionValue === 'number'
58
- ? fieldValue > conditionValue
59
- : false;
60
-
61
- case 'gte':
62
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
63
- ? fieldValue >= conditionValue
64
- : false;
65
-
66
- case 'lt':
67
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
68
- ? fieldValue < conditionValue
69
- : false;
70
-
71
- case 'lte':
72
- return typeof fieldValue === 'number' && typeof conditionValue === 'number'
73
- ? fieldValue <= conditionValue
74
- : false;
75
-
76
- case 'contains':
77
- if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
78
- return fieldValue.includes(conditionValue);
79
- }
80
- if (Array.isArray(fieldValue)) {
81
- return fieldValue.includes(conditionValue);
82
- }
83
- return false;
84
-
85
- case 'matches':
86
- if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {
87
- try {
88
- return new RE2(conditionValue).test(fieldValue);
89
- } catch {
90
- return false;
91
- }
92
- }
93
- return false;
94
-
95
- case 'exists':
96
- return fieldValue !== undefined && fieldValue !== null && fieldValue !== '';
97
-
98
- case 'notExists':
99
- return fieldValue === undefined || fieldValue === null || fieldValue === '';
100
-
101
- default:
102
- return false;
103
- }
104
- }
105
-
106
- /**
107
- * Score a recipe's conditions against input.
108
- * Returns a score from 0-100 based on how well conditions match.
109
- */
110
- function scoreConditions(
111
- conditions: RecipeCondition[] | undefined,
112
- input: Record<string, unknown>
113
- ): number {
114
- // No conditions = default match with low score
115
- if (!conditions || conditions.length === 0) {
116
- return 10;
117
- }
118
-
119
- let matched = 0;
120
- for (const condition of conditions) {
121
- const fieldValue = getNestedValue(input, condition.field);
122
- if (evaluateCondition(condition.operator, fieldValue, condition.value)) {
123
- matched++;
124
- }
125
- }
126
-
127
- // All conditions must match for any positive score
128
- if (matched < conditions.length) {
129
- return 0;
130
- }
131
-
132
- // Base score 50 ensures condition-matched recipes beat unconditional defaults (score 10).
133
- // Each additional condition adds 10 points, capped at 100 so forced recipes (100) still win.
134
- return Math.min(100, 50 + conditions.length * 10);
135
- }
136
-
137
- // ============================================================================
138
- // Rule-Based Planner Implementation
139
- // ============================================================================
140
-
141
- /**
142
- * Configuration for the RuleBasedPlanner.
143
- */
144
- export interface RuleBasedPlannerConfig {
145
- /** Recipe registry to use */
146
- recipeRegistry: RecipeRegistry;
147
-
148
- /** Step handler registry for validation */
149
- handlerRegistry?: StepHandlerRegistry;
150
-
151
- /** Whether to validate handler references during planning */
152
- validateHandlers?: boolean;
153
-
154
- /** Resource estimation defaults (used by estimateResources) */
155
- resourceEstimates?: {
156
- /** Estimated API calls per step (default: 1) */
157
- apiCallsPerStep?: number;
158
- /** Estimated tokens per step (default: 500) */
159
- tokensPerStep?: number;
160
- /** Estimated duration per step in ms (default: 2000) */
161
- durationPerStep?: number;
162
- /** Estimated API calls per child workflow (default: 5) */
163
- apiCallsPerChild?: number;
164
- };
165
- }
166
-
167
- /**
168
- * Rule-based planner that selects recipes based on condition matching.
169
- */
170
- export class RuleBasedPlanner implements Planner {
171
- private recipeRegistry: RecipeRegistry;
172
- private handlerRegistry?: StepHandlerRegistry;
173
- private validateHandlers: boolean;
174
- private resourceEstimates: Required<NonNullable<RuleBasedPlannerConfig['resourceEstimates']>>;
175
-
176
- constructor(config: RuleBasedPlannerConfig) {
177
- this.recipeRegistry = config.recipeRegistry;
178
- this.handlerRegistry = config.handlerRegistry;
179
- this.validateHandlers = config.validateHandlers ?? false;
180
- this.resourceEstimates = {
181
- apiCallsPerStep: config.resourceEstimates?.apiCallsPerStep ?? 1,
182
- tokensPerStep: config.resourceEstimates?.tokensPerStep ?? 500,
183
- durationPerStep: config.resourceEstimates?.durationPerStep ?? 2000,
184
- apiCallsPerChild: config.resourceEstimates?.apiCallsPerChild ?? 5,
185
- };
186
- }
187
-
188
- /**
189
- * Select the best recipe for a workflow kind and input.
190
- */
191
- async selectRecipe(
192
- workflowKind: WorkflowKind,
193
- input: Record<string, unknown>,
194
- context?: PlanningContext
195
- ): Promise<RecipeSelectionResult> {
196
- // Check for forced recipe in hints
197
- if (context?.hints?.forceRecipeId) {
198
- const forced = this.recipeRegistry.get(context.hints.forceRecipeId);
199
- if (forced) {
200
- return {
201
- recipe: forced,
202
- score: 100,
203
- reason: `Forced recipe: ${context.hints.forceRecipeId}`,
204
- };
205
- }
206
- }
207
-
208
- // Check for preferred variant in hints
209
- if (context?.hints?.preferredVariant) {
210
- const preferred = this.recipeRegistry.getVariant(
211
- workflowKind,
212
- context.hints.preferredVariant
213
- );
214
- if (preferred) {
215
- return {
216
- recipe: preferred,
217
- score: 90,
218
- reason: `Preferred variant: ${context.hints.preferredVariant}`,
219
- };
220
- }
221
- }
222
-
223
- // Get all recipes for this workflow kind
224
- const recipes = this.recipeRegistry.getByKind(workflowKind);
225
-
226
- if (recipes.length === 0) {
227
- throw new Error(`No recipes found for workflow kind: ${workflowKind}`);
228
- }
229
-
230
- // Score each recipe
231
- const scored = recipes.map(recipe => ({
232
- recipe,
233
- conditionScore: scoreConditions(recipe.conditions, input),
234
- priorityScore: recipe.priority ?? 0,
235
- }));
236
-
237
- // Sort by condition score (desc), then priority (desc)
238
- scored.sort((a, b) => {
239
- if (b.conditionScore !== a.conditionScore) {
240
- return b.conditionScore - a.conditionScore;
241
- }
242
- return b.priorityScore - a.priorityScore;
243
- });
244
-
245
- // Find best matching recipe
246
- const best = scored.find(s => s.conditionScore > 0);
247
-
248
- if (best) {
249
- const reason = this.buildSelectionReason(best.recipe, input);
250
- return {
251
- recipe: best.recipe,
252
- score: best.conditionScore,
253
- reason,
254
- };
255
- }
256
-
257
- // Fall back to default recipe
258
- const defaultRecipe = this.recipeRegistry.getDefault(workflowKind);
259
- if (defaultRecipe) {
260
- return {
261
- recipe: defaultRecipe,
262
- score: 10,
263
- reason: `Fallback to default recipe: ${defaultRecipe.id}`,
264
- };
265
- }
266
-
267
- // Last resort: use first recipe
268
- return {
269
- recipe: recipes[0],
270
- score: 5,
271
- reason: `No matching recipe, using first available: ${recipes[0].id}`,
272
- };
273
- }
274
-
275
- /**
276
- * Generate a plan from a recipe and input.
277
- */
278
- async generatePlan(
279
- recipe: Recipe,
280
- input: Record<string, unknown>,
281
- context?: PlanningContext
282
- ): Promise<Plan> {
283
- const modifications: PlanModification[] = [];
284
-
285
- // Start with recipe steps
286
- let steps = recipe.steps.map(step => this.recipeStepToPlannedStep(step));
287
-
288
- // Apply hint-based modifications
289
- if (context?.hints) {
290
- const { skipSteps, includeSteps, additionalConfig } = context.hints;
291
-
292
- // Mark steps to skip
293
- if (skipSteps && skipSteps.length > 0) {
294
- for (const stepKey of skipSteps) {
295
- const stepIndex = steps.findIndex(s => s.key === stepKey);
296
- if (stepIndex >= 0) {
297
- steps = steps.filter(s => s.key !== stepKey);
298
- modifications.push({
299
- type: 'remove_step',
300
- stepKey,
301
- value: null,
302
- reason: `Skipped via planning hints`,
303
- });
304
- }
305
- }
306
- }
307
-
308
- // Apply additional config to all steps
309
- if (additionalConfig) {
310
- for (const step of steps) {
311
- step.config = { ...step.config, ...additionalConfig };
312
- }
313
- modifications.push({
314
- type: 'modify_step',
315
- value: additionalConfig,
316
- reason: 'Applied additional config from planning hints',
317
- });
318
- }
319
- }
320
-
321
- // Apply constraint-based modifications
322
- if (context?.constraints) {
323
- const constraintMods = this.applyConstraints(steps, context.constraints);
324
- modifications.push(...constraintMods.modifications);
325
- steps = constraintMods.steps;
326
- }
327
-
328
- // Build the plan
329
- const plan: Plan = {
330
- id: generateId(),
331
- recipeId: recipe.id,
332
- variant: recipe.variant,
333
- modifications,
334
- steps,
335
- defaults: recipe.defaults ?? {},
336
- reasoning: this.buildPlanReasoning(recipe, modifications, context),
337
- createdAt: new Date(),
338
- };
339
-
340
- // Estimate resources
341
- plan.resourceEstimate = this.estimateResources(plan);
342
-
343
- return plan;
344
- }
345
-
346
- /**
347
- * Combined operation: select recipe and generate plan.
348
- */
349
- async plan(
350
- workflowKind: WorkflowKind,
351
- input: Record<string, unknown>,
352
- context?: PlanningContext
353
- ): Promise<Plan> {
354
- const selection = await this.selectRecipe(workflowKind, input, context);
355
- const plan = await this.generatePlan(selection.recipe, input, context);
356
-
357
- // Add selection reasoning to plan
358
- plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ''}`.trim();
359
-
360
- return plan;
361
- }
362
-
363
- /**
364
- * Validate a plan before execution.
365
- */
366
- validatePlan(plan: Plan): PlanValidationResult {
367
- const errors: string[] = [];
368
- const warnings: string[] = [];
369
-
370
- // Check plan has steps
371
- if (!plan.steps || plan.steps.length === 0) {
372
- errors.push('Plan has no steps');
373
- }
374
-
375
- // Check for duplicate step keys
376
- const stepKeys = new Set<string>();
377
- for (const step of plan.steps) {
378
- if (stepKeys.has(step.key)) {
379
- errors.push(`Duplicate step key: ${step.key}`);
380
- }
381
- stepKeys.add(step.key);
382
- }
383
-
384
- // Validate each step
385
- for (const step of plan.steps) {
386
- if (!step.key) {
387
- errors.push('Step missing key');
388
- }
389
- if (!step.name) {
390
- warnings.push(`Step '${step.key}' missing name`);
391
- }
392
- if (!step.handlerRef) {
393
- errors.push(`Step '${step.key}' missing handlerRef`);
394
- }
395
-
396
- // Validate handler exists (if registry available)
397
- if (this.validateHandlers && this.handlerRegistry) {
398
- if (!this.handlerRegistry.has(step.handlerRef)) {
399
- errors.push(`Step '${step.key}' references unknown handler: ${step.handlerRef}`);
400
- }
401
- }
402
- }
403
-
404
- // Check child workflows
405
- if (plan.childWorkflows) {
406
- for (const child of plan.childWorkflows) {
407
- if (!child.kind) {
408
- errors.push('Child workflow missing kind');
409
- }
410
- }
411
- }
412
-
413
- return {
414
- valid: errors.length === 0,
415
- errors,
416
- warnings,
417
- };
418
- }
419
-
420
- /**
421
- * Estimate resources required for a plan.
422
- */
423
- estimateResources(plan: Plan): ResourceEstimate {
424
- const { apiCallsPerStep, tokensPerStep, durationPerStep, apiCallsPerChild } = this.resourceEstimates;
425
-
426
- const stepCount = plan.steps.length;
427
- const childCount = plan.childWorkflows?.length ?? 0;
428
-
429
- return {
430
- apiCalls: stepCount * apiCallsPerStep + childCount * apiCallsPerChild,
431
- tokens: stepCount * tokensPerStep,
432
- duration: stepCount * durationPerStep,
433
- };
434
- }
435
-
436
- // ============================================================================
437
- // Private Helpers
438
- // ============================================================================
439
-
440
- /**
441
- * Convert a recipe step to a planned step.
442
- */
443
- private recipeStepToPlannedStep(step: Recipe['steps'][0]): PlannedStep {
444
- return {
445
- key: step.key,
446
- name: step.name,
447
- handlerRef: step.handlerRef,
448
- config: step.config ?? {},
449
- onError: step.onError,
450
- maxRetries: step.maxRetries,
451
- retryDelay: step.retryDelay,
452
- retryBackoff: step.retryBackoff,
453
- timeout: step.timeout,
454
- skipCondition: step.skipCondition,
455
- };
456
- }
457
-
458
- /**
459
- * Apply constraints to steps and generate modifications.
460
- */
461
- private applyConstraints(
462
- steps: PlannedStep[],
463
- constraints: NonNullable<PlanningContext['constraints']>
464
- ): { steps: PlannedStep[]; modifications: PlanModification[] } {
465
- const modifications: PlanModification[] = [];
466
- const perStepTimeout = constraints.maxDuration
467
- ? Math.floor(constraints.maxDuration / steps.length)
468
- : undefined;
469
-
470
- // Apply all constraint mutations in a single pass
471
- const modifiedSteps = steps.map(step => {
472
- const modified = { ...step };
473
-
474
- if (constraints.priority === 'speed') {
475
- modified.timeout = modified.timeout ? Math.min(modified.timeout, 30000) : 30000;
476
- modified.maxRetries = Math.min(modified.maxRetries ?? 3, 1);
477
- }
478
-
479
- if (constraints.priority === 'cost') {
480
- modified.maxRetries = 0;
481
- }
482
-
483
- if (perStepTimeout !== undefined) {
484
- modified.timeout = modified.timeout
485
- ? Math.min(modified.timeout, perStepTimeout)
486
- : perStepTimeout;
487
- }
488
-
489
- return modified;
490
- });
491
-
492
- // Record what was applied
493
- if (constraints.priority === 'speed') {
494
- modifications.push({
495
- type: 'set_default',
496
- value: { priority: 'speed' },
497
- reason: 'Optimized for speed: reduced timeouts and retries',
498
- });
499
- }
500
- if (constraints.priority === 'cost') {
501
- modifications.push({
502
- type: 'set_default',
503
- value: { priority: 'cost' },
504
- reason: 'Optimized for cost: disabled retries',
505
- });
506
- }
507
- if (constraints.maxDuration) {
508
- modifications.push({
509
- type: 'set_default',
510
- value: { maxDuration: constraints.maxDuration },
511
- reason: `Applied duration constraint: ${constraints.maxDuration}ms total`,
512
- });
513
- }
514
-
515
- return { steps: modifiedSteps, modifications };
516
- }
517
-
518
- /**
519
- * Build a human-readable selection reason.
520
- */
521
- private buildSelectionReason(
522
- recipe: Recipe,
523
- input: Record<string, unknown>
524
- ): string {
525
- const parts: string[] = [`Selected recipe: ${recipe.id}`];
526
-
527
- if (recipe.conditions && recipe.conditions.length > 0) {
528
- const conditionDescriptions = recipe.conditions.map(c => {
529
- const value = getNestedValue(input, c.field);
530
- return `${c.field} ${c.operator} ${JSON.stringify(c.value)} (actual: ${JSON.stringify(value)})`;
531
- });
532
- parts.push(`Matched conditions: ${conditionDescriptions.join(', ')}`);
533
- }
534
-
535
- if (recipe.priority !== undefined && recipe.priority > 0) {
536
- parts.push(`Priority: ${recipe.priority}`);
537
- }
538
-
539
- return parts.join('. ');
540
- }
541
-
542
- /**
543
- * Build reasoning text for a plan.
544
- */
545
- private buildPlanReasoning(
546
- recipe: Recipe,
547
- modifications: PlanModification[],
548
- context?: PlanningContext
549
- ): string {
550
- const parts: string[] = [];
551
-
552
- parts.push(`Using recipe '${recipe.name}' (${recipe.variant} variant)`);
553
-
554
- if (modifications.length > 0) {
555
- parts.push(`Applied ${modifications.length} modification(s)`);
556
- }
557
-
558
- if (context?.constraints) {
559
- const constraintList: string[] = [];
560
- if (context.constraints.priority) {
561
- constraintList.push(`priority=${context.constraints.priority}`);
562
- }
563
- if (context.constraints.maxDuration) {
564
- constraintList.push(`maxDuration=${context.constraints.maxDuration}ms`);
565
- }
566
- if (context.constraints.maxApiCalls) {
567
- constraintList.push(`maxApiCalls=${context.constraints.maxApiCalls}`);
568
- }
569
- if (constraintList.length > 0) {
570
- parts.push(`Constraints: ${constraintList.join(', ')}`);
571
- }
572
- }
573
-
574
- return parts.join('. ') + '.';
575
- }
576
- }