@multiplier-labs/stepflow 0.3.4 → 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 (99) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-U3PCVK6O.js → chunk-D5HW7VEG.js} +4 -2
  3. package/dist/chunk-D5HW7VEG.js.map +1 -0
  4. package/dist/{chunk-5XJBAPXF.js → chunk-JUPZEBR3.js} +3 -1
  5. package/dist/{chunk-5XJBAPXF.js.map → chunk-JUPZEBR3.js.map} +1 -1
  6. package/dist/events/index.d.ts +2 -0
  7. package/dist/events/index.js +1 -1
  8. package/dist/index.js +2 -2
  9. package/dist/storage/index.js +1 -1
  10. package/package.json +23 -12
  11. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  12. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  13. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  14. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  15. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  16. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  17. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  18. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  19. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  20. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  21. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  22. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  23. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  24. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  25. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  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 -42
  30. package/.github/workflows/security.yml +0 -29
  31. package/CHANGELOG.md +0 -34
  32. package/SECURITY.md +0 -40
  33. package/dist/chunk-U3PCVK6O.js.map +0 -1
  34. package/docs/github-packages-setup.md +0 -97
  35. package/docs/stepflow-api-reference.md +0 -1663
  36. package/src/core/engine/concurrency.ts +0 -229
  37. package/src/core/engine/lifecycle.ts +0 -176
  38. package/src/core/engine.test.ts +0 -1435
  39. package/src/core/engine.ts +0 -598
  40. package/src/core/index.ts +0 -7
  41. package/src/core/orchestrator/persistence.ts +0 -165
  42. package/src/core/orchestrator/plan-resolution.ts +0 -140
  43. package/src/core/orchestrator/state-transitions.ts +0 -109
  44. package/src/core/orchestrator/step-runner.ts +0 -427
  45. package/src/core/orchestrator.ts +0 -252
  46. package/src/core/types.ts +0 -220
  47. package/src/events/index.ts +0 -8
  48. package/src/events/memory.test.ts +0 -107
  49. package/src/events/memory.ts +0 -92
  50. package/src/events/socketio.test.ts +0 -378
  51. package/src/events/socketio.ts +0 -262
  52. package/src/events/types.ts +0 -118
  53. package/src/events/webhook.test.ts +0 -949
  54. package/src/events/webhook.ts +0 -807
  55. package/src/index.ts +0 -270
  56. package/src/planning/evaluate-condition.test.ts +0 -153
  57. package/src/planning/evaluate-condition.ts +0 -105
  58. package/src/planning/index.ts +0 -55
  59. package/src/planning/planner.ts +0 -494
  60. package/src/planning/planning.test.ts +0 -727
  61. package/src/planning/registry.test.ts +0 -398
  62. package/src/planning/registry.ts +0 -283
  63. package/src/planning/types.ts +0 -566
  64. package/src/scheduler/cron.test.ts +0 -479
  65. package/src/scheduler/cron.ts +0 -438
  66. package/src/scheduler/index.ts +0 -8
  67. package/src/scheduler/postgres-persistence.ts +0 -530
  68. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  69. package/src/scheduler/sqlite-persistence.ts +0 -286
  70. package/src/scheduler/types.ts +0 -73
  71. package/src/storage/index.ts +0 -27
  72. package/src/storage/memory.test.ts +0 -349
  73. package/src/storage/memory.ts +0 -227
  74. package/src/storage/postgres-core.ts +0 -550
  75. package/src/storage/postgres-migration.test.ts +0 -69
  76. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  77. package/src/storage/postgres.ts +0 -835
  78. package/src/storage/sqlite-core.ts +0 -151
  79. package/src/storage/sqlite.test.ts +0 -642
  80. package/src/storage/sqlite.ts +0 -717
  81. package/src/storage/types.ts +0 -343
  82. package/src/utils/errors.test.ts +0 -150
  83. package/src/utils/errors.ts +0 -161
  84. package/src/utils/id.ts +0 -21
  85. package/src/utils/index.ts +0 -10
  86. package/src/utils/logger.test.ts +0 -121
  87. package/src/utils/logger.ts +0 -96
  88. package/src/utils/pinned-fetch.test.ts +0 -84
  89. package/src/utils/pinned-fetch.ts +0 -118
  90. package/src/utils/postgres-deps.ts +0 -79
  91. package/src/utils/retry.test.ts +0 -177
  92. package/src/utils/retry.ts +0 -121
  93. package/src/utils/safe-json.test.ts +0 -166
  94. package/src/utils/safe-json.ts +0 -107
  95. package/src/utils/webhook-signature.test.ts +0 -111
  96. package/src/utils/webhook-signature.ts +0 -74
  97. package/tsconfig.json +0 -21
  98. package/tsup.config.ts +0 -14
  99. package/vitest.config.ts +0 -9
@@ -1,494 +0,0 @@
1
- /**
2
- * Rule-based planner implementation.
3
- * Selects recipes and generates plans based on conditions and input analysis.
4
- */
5
-
6
- import { generateId } from '../utils/id';
7
- import { evaluateCondition, getNestedValue } from './evaluate-condition';
8
- import type {
9
- Plan,
10
- PlannedStep,
11
- Planner,
12
- PlanModification,
13
- PlanningContext,
14
- PlanValidationResult,
15
- Recipe,
16
- RecipeCondition,
17
- RecipeRegistry,
18
- RecipeSelectionResult,
19
- ResourceEstimate,
20
- StepHandlerRegistry,
21
- WorkflowKind,
22
- } from './types';
23
-
24
- /**
25
- * Score a recipe's conditions against input.
26
- * Returns a score from 0-100 based on how well conditions match.
27
- */
28
- function scoreConditions(
29
- conditions: RecipeCondition[] | undefined,
30
- input: Record<string, unknown>
31
- ): number {
32
- // No conditions = default match with low score
33
- if (!conditions || conditions.length === 0) {
34
- return 10;
35
- }
36
-
37
- let matched = 0;
38
- for (const condition of conditions) {
39
- const fieldValue = getNestedValue(input, condition.field);
40
- if (evaluateCondition(condition.operator, fieldValue, condition.value)) {
41
- matched++;
42
- }
43
- }
44
-
45
- // All conditions must match for any positive score
46
- if (matched < conditions.length) {
47
- return 0;
48
- }
49
-
50
- // Base score 50 ensures condition-matched recipes beat unconditional defaults (score 10).
51
- // Each additional condition adds 10 points, capped at 100 so forced recipes (100) still win.
52
- return Math.min(100, 50 + conditions.length * 10);
53
- }
54
-
55
- // ============================================================================
56
- // Rule-Based Planner Implementation
57
- // ============================================================================
58
-
59
- /**
60
- * Configuration for the RuleBasedPlanner.
61
- */
62
- export interface RuleBasedPlannerConfig {
63
- /** Recipe registry to use */
64
- recipeRegistry: RecipeRegistry;
65
-
66
- /** Step handler registry for validation */
67
- handlerRegistry?: StepHandlerRegistry;
68
-
69
- /** Whether to validate handler references during planning */
70
- validateHandlers?: boolean;
71
-
72
- /** Resource estimation defaults (used by estimateResources) */
73
- resourceEstimates?: {
74
- /** Estimated API calls per step (default: 1) */
75
- apiCallsPerStep?: number;
76
- /** Estimated tokens per step (default: 500) */
77
- tokensPerStep?: number;
78
- /** Estimated duration per step in ms (default: 2000) */
79
- durationPerStep?: number;
80
- /** Estimated API calls per child workflow (default: 5) */
81
- apiCallsPerChild?: number;
82
- };
83
- }
84
-
85
- /**
86
- * Rule-based planner that selects recipes based on condition matching.
87
- */
88
- export class RuleBasedPlanner implements Planner {
89
- private recipeRegistry: RecipeRegistry;
90
- private handlerRegistry?: StepHandlerRegistry;
91
- private validateHandlers: boolean;
92
- private resourceEstimates: Required<NonNullable<RuleBasedPlannerConfig['resourceEstimates']>>;
93
-
94
- constructor(config: RuleBasedPlannerConfig) {
95
- this.recipeRegistry = config.recipeRegistry;
96
- this.handlerRegistry = config.handlerRegistry;
97
- this.validateHandlers = config.validateHandlers ?? false;
98
- this.resourceEstimates = {
99
- apiCallsPerStep: config.resourceEstimates?.apiCallsPerStep ?? 1,
100
- tokensPerStep: config.resourceEstimates?.tokensPerStep ?? 500,
101
- durationPerStep: config.resourceEstimates?.durationPerStep ?? 2000,
102
- apiCallsPerChild: config.resourceEstimates?.apiCallsPerChild ?? 5,
103
- };
104
- }
105
-
106
- /**
107
- * Select the best recipe for a workflow kind and input.
108
- */
109
- async selectRecipe(
110
- workflowKind: WorkflowKind,
111
- input: Record<string, unknown>,
112
- context?: PlanningContext
113
- ): Promise<RecipeSelectionResult> {
114
- // Check for forced recipe in hints
115
- if (context?.hints?.forceRecipeId) {
116
- const forced = this.recipeRegistry.get(context.hints.forceRecipeId);
117
- if (forced) {
118
- return {
119
- recipe: forced,
120
- score: 100,
121
- reason: `Forced recipe: ${context.hints.forceRecipeId}`,
122
- };
123
- }
124
- }
125
-
126
- // Check for preferred variant in hints
127
- if (context?.hints?.preferredVariant) {
128
- const preferred = this.recipeRegistry.getVariant(
129
- workflowKind,
130
- context.hints.preferredVariant
131
- );
132
- if (preferred) {
133
- return {
134
- recipe: preferred,
135
- score: 90,
136
- reason: `Preferred variant: ${context.hints.preferredVariant}`,
137
- };
138
- }
139
- }
140
-
141
- // Get all recipes for this workflow kind
142
- const recipes = this.recipeRegistry.getByKind(workflowKind);
143
-
144
- if (recipes.length === 0) {
145
- throw new Error(`No recipes found for workflow kind: ${workflowKind}`);
146
- }
147
-
148
- // Score each recipe
149
- const scored = recipes.map(recipe => ({
150
- recipe,
151
- conditionScore: scoreConditions(recipe.conditions, input),
152
- priorityScore: recipe.priority ?? 0,
153
- }));
154
-
155
- // Sort by condition score (desc), then priority (desc)
156
- scored.sort((a, b) => {
157
- if (b.conditionScore !== a.conditionScore) {
158
- return b.conditionScore - a.conditionScore;
159
- }
160
- return b.priorityScore - a.priorityScore;
161
- });
162
-
163
- // Find best matching recipe
164
- const best = scored.find(s => s.conditionScore > 0);
165
-
166
- if (best) {
167
- const reason = this.buildSelectionReason(best.recipe, input);
168
- return {
169
- recipe: best.recipe,
170
- score: best.conditionScore,
171
- reason,
172
- };
173
- }
174
-
175
- // Fall back to default recipe
176
- const defaultRecipe = this.recipeRegistry.getDefault(workflowKind);
177
- if (defaultRecipe) {
178
- return {
179
- recipe: defaultRecipe,
180
- score: 10,
181
- reason: `Fallback to default recipe: ${defaultRecipe.id}`,
182
- };
183
- }
184
-
185
- // Last resort: use first recipe
186
- return {
187
- recipe: recipes[0],
188
- score: 5,
189
- reason: `No matching recipe, using first available: ${recipes[0].id}`,
190
- };
191
- }
192
-
193
- /**
194
- * Generate a plan from a recipe and input.
195
- */
196
- async generatePlan(
197
- recipe: Recipe,
198
- input: Record<string, unknown>,
199
- context?: PlanningContext
200
- ): Promise<Plan> {
201
- const modifications: PlanModification[] = [];
202
-
203
- // Start with recipe steps
204
- let steps = recipe.steps.map(step => this.recipeStepToPlannedStep(step));
205
-
206
- // Apply hint-based modifications
207
- if (context?.hints) {
208
- const { skipSteps, includeSteps, additionalConfig } = context.hints;
209
-
210
- // Mark steps to skip
211
- if (skipSteps && skipSteps.length > 0) {
212
- for (const stepKey of skipSteps) {
213
- const stepIndex = steps.findIndex(s => s.key === stepKey);
214
- if (stepIndex >= 0) {
215
- steps = steps.filter(s => s.key !== stepKey);
216
- modifications.push({
217
- type: 'remove_step',
218
- stepKey,
219
- value: null,
220
- reason: `Skipped via planning hints`,
221
- });
222
- }
223
- }
224
- }
225
-
226
- // Apply additional config to all steps
227
- if (additionalConfig) {
228
- for (const step of steps) {
229
- step.config = { ...step.config, ...additionalConfig };
230
- }
231
- modifications.push({
232
- type: 'modify_step',
233
- value: additionalConfig,
234
- reason: 'Applied additional config from planning hints',
235
- });
236
- }
237
- }
238
-
239
- // Apply constraint-based modifications
240
- if (context?.constraints) {
241
- const constraintMods = this.applyConstraints(steps, context.constraints);
242
- modifications.push(...constraintMods.modifications);
243
- steps = constraintMods.steps;
244
- }
245
-
246
- // Build the plan
247
- const plan: Plan = {
248
- id: generateId(),
249
- recipeId: recipe.id,
250
- variant: recipe.variant,
251
- modifications,
252
- steps,
253
- defaults: recipe.defaults ?? {},
254
- reasoning: this.buildPlanReasoning(recipe, modifications, context),
255
- createdAt: new Date(),
256
- };
257
-
258
- // Estimate resources
259
- plan.resourceEstimate = this.estimateResources(plan);
260
-
261
- return plan;
262
- }
263
-
264
- /**
265
- * Combined operation: select recipe and generate plan.
266
- */
267
- async plan(
268
- workflowKind: WorkflowKind,
269
- input: Record<string, unknown>,
270
- context?: PlanningContext
271
- ): Promise<Plan> {
272
- const selection = await this.selectRecipe(workflowKind, input, context);
273
- const plan = await this.generatePlan(selection.recipe, input, context);
274
-
275
- // Add selection reasoning to plan
276
- plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ''}`.trim();
277
-
278
- return plan;
279
- }
280
-
281
- /**
282
- * Validate a plan before execution.
283
- */
284
- validatePlan(plan: Plan): PlanValidationResult {
285
- const errors: string[] = [];
286
- const warnings: string[] = [];
287
-
288
- // Check plan has steps
289
- if (!plan.steps || plan.steps.length === 0) {
290
- errors.push('Plan has no steps');
291
- }
292
-
293
- // Check for duplicate step keys
294
- const stepKeys = new Set<string>();
295
- for (const step of plan.steps) {
296
- if (stepKeys.has(step.key)) {
297
- errors.push(`Duplicate step key: ${step.key}`);
298
- }
299
- stepKeys.add(step.key);
300
- }
301
-
302
- // Validate each step
303
- for (const step of plan.steps) {
304
- if (!step.key) {
305
- errors.push('Step missing key');
306
- }
307
- if (!step.name) {
308
- warnings.push(`Step '${step.key}' missing name`);
309
- }
310
- if (!step.handlerRef) {
311
- errors.push(`Step '${step.key}' missing handlerRef`);
312
- }
313
-
314
- // Validate handler exists (if registry available)
315
- if (this.validateHandlers && this.handlerRegistry) {
316
- if (!this.handlerRegistry.has(step.handlerRef)) {
317
- errors.push(`Step '${step.key}' references unknown handler: ${step.handlerRef}`);
318
- }
319
- }
320
- }
321
-
322
- // Check child workflows
323
- if (plan.childWorkflows) {
324
- for (const child of plan.childWorkflows) {
325
- if (!child.kind) {
326
- errors.push('Child workflow missing kind');
327
- }
328
- }
329
- }
330
-
331
- return {
332
- valid: errors.length === 0,
333
- errors,
334
- warnings,
335
- };
336
- }
337
-
338
- /**
339
- * Estimate resources required for a plan.
340
- */
341
- estimateResources(plan: Plan): ResourceEstimate {
342
- const { apiCallsPerStep, tokensPerStep, durationPerStep, apiCallsPerChild } = this.resourceEstimates;
343
-
344
- const stepCount = plan.steps.length;
345
- const childCount = plan.childWorkflows?.length ?? 0;
346
-
347
- return {
348
- apiCalls: stepCount * apiCallsPerStep + childCount * apiCallsPerChild,
349
- tokens: stepCount * tokensPerStep,
350
- duration: stepCount * durationPerStep,
351
- };
352
- }
353
-
354
- // ============================================================================
355
- // Private Helpers
356
- // ============================================================================
357
-
358
- /**
359
- * Convert a recipe step to a planned step.
360
- */
361
- private recipeStepToPlannedStep(step: Recipe['steps'][0]): PlannedStep {
362
- return {
363
- key: step.key,
364
- name: step.name,
365
- handlerRef: step.handlerRef,
366
- config: step.config ?? {},
367
- onError: step.onError,
368
- maxRetries: step.maxRetries,
369
- retryDelay: step.retryDelay,
370
- retryBackoff: step.retryBackoff,
371
- timeout: step.timeout,
372
- skipCondition: step.skipCondition,
373
- };
374
- }
375
-
376
- /**
377
- * Apply constraints to steps and generate modifications.
378
- */
379
- private applyConstraints(
380
- steps: PlannedStep[],
381
- constraints: NonNullable<PlanningContext['constraints']>
382
- ): { steps: PlannedStep[]; modifications: PlanModification[] } {
383
- const modifications: PlanModification[] = [];
384
- const perStepTimeout = constraints.maxDuration
385
- ? Math.floor(constraints.maxDuration / steps.length)
386
- : undefined;
387
-
388
- // Apply all constraint mutations in a single pass
389
- const modifiedSteps = steps.map(step => {
390
- const modified = { ...step };
391
-
392
- if (constraints.priority === 'speed') {
393
- modified.timeout = modified.timeout ? Math.min(modified.timeout, 30000) : 30000;
394
- modified.maxRetries = Math.min(modified.maxRetries ?? 3, 1);
395
- }
396
-
397
- if (constraints.priority === 'cost') {
398
- modified.maxRetries = 0;
399
- }
400
-
401
- if (perStepTimeout !== undefined) {
402
- modified.timeout = modified.timeout
403
- ? Math.min(modified.timeout, perStepTimeout)
404
- : perStepTimeout;
405
- }
406
-
407
- return modified;
408
- });
409
-
410
- // Record what was applied
411
- if (constraints.priority === 'speed') {
412
- modifications.push({
413
- type: 'set_default',
414
- value: { priority: 'speed' },
415
- reason: 'Optimized for speed: reduced timeouts and retries',
416
- });
417
- }
418
- if (constraints.priority === 'cost') {
419
- modifications.push({
420
- type: 'set_default',
421
- value: { priority: 'cost' },
422
- reason: 'Optimized for cost: disabled retries',
423
- });
424
- }
425
- if (constraints.maxDuration) {
426
- modifications.push({
427
- type: 'set_default',
428
- value: { maxDuration: constraints.maxDuration },
429
- reason: `Applied duration constraint: ${constraints.maxDuration}ms total`,
430
- });
431
- }
432
-
433
- return { steps: modifiedSteps, modifications };
434
- }
435
-
436
- /**
437
- * Build a human-readable selection reason.
438
- */
439
- private buildSelectionReason(
440
- recipe: Recipe,
441
- input: Record<string, unknown>
442
- ): string {
443
- const parts: string[] = [`Selected recipe: ${recipe.id}`];
444
-
445
- if (recipe.conditions && recipe.conditions.length > 0) {
446
- const conditionDescriptions = recipe.conditions.map(c => {
447
- const value = getNestedValue(input, c.field);
448
- return `${c.field} ${c.operator} ${JSON.stringify(c.value)} (actual: ${JSON.stringify(value)})`;
449
- });
450
- parts.push(`Matched conditions: ${conditionDescriptions.join(', ')}`);
451
- }
452
-
453
- if (recipe.priority !== undefined && recipe.priority > 0) {
454
- parts.push(`Priority: ${recipe.priority}`);
455
- }
456
-
457
- return parts.join('. ');
458
- }
459
-
460
- /**
461
- * Build reasoning text for a plan.
462
- */
463
- private buildPlanReasoning(
464
- recipe: Recipe,
465
- modifications: PlanModification[],
466
- context?: PlanningContext
467
- ): string {
468
- const parts: string[] = [];
469
-
470
- parts.push(`Using recipe '${recipe.name}' (${recipe.variant} variant)`);
471
-
472
- if (modifications.length > 0) {
473
- parts.push(`Applied ${modifications.length} modification(s)`);
474
- }
475
-
476
- if (context?.constraints) {
477
- const constraintList: string[] = [];
478
- if (context.constraints.priority) {
479
- constraintList.push(`priority=${context.constraints.priority}`);
480
- }
481
- if (context.constraints.maxDuration) {
482
- constraintList.push(`maxDuration=${context.constraints.maxDuration}ms`);
483
- }
484
- if (context.constraints.maxApiCalls) {
485
- constraintList.push(`maxApiCalls=${context.constraints.maxApiCalls}`);
486
- }
487
- if (constraintList.length > 0) {
488
- parts.push(`Constraints: ${constraintList.join(', ')}`);
489
- }
490
- }
491
-
492
- return parts.join('. ') + '.';
493
- }
494
- }