@multiplier-labs/stepflow 0.3.4 → 0.3.8

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 (109) hide show
  1. package/README.md +9 -0
  2. package/dist/{chunk-5XJBAPXF.js → chunk-4IDPWWGS.js} +15 -4
  3. package/dist/chunk-4IDPWWGS.js.map +1 -0
  4. package/dist/{chunk-U3PCVK6O.js → chunk-T6GAZBTQ.js} +70 -38
  5. package/dist/chunk-T6GAZBTQ.js.map +1 -0
  6. package/dist/{chunk-FMPJBK2N.js → chunk-TVSADUYU.js} +3 -5
  7. package/dist/{chunk-FMPJBK2N.js.map → chunk-TVSADUYU.js.map} +1 -1
  8. package/dist/events/index.d.ts +43 -3
  9. package/dist/events/index.js +7 -3
  10. package/dist/{index-Cd0IZ40L.d.ts → index-CdXDv30T.d.ts} +10 -2
  11. package/dist/index.d.ts +5 -4
  12. package/dist/index.js +159 -46
  13. package/dist/index.js.map +1 -1
  14. package/dist/scheduler/index.d.ts +2 -2
  15. package/dist/scheduler/index.js +1 -1
  16. package/dist/storage/index.d.ts +6 -4
  17. package/dist/storage/index.js +1 -1
  18. package/dist/{types-DSEjiLMj.d.ts → types-BfWe5yP6.d.ts} +2 -0
  19. package/package.json +26 -14
  20. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +0 -146
  21. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +0 -74
  22. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +0 -179
  23. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +0 -161
  24. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +0 -34
  25. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +0 -152
  26. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +0 -152
  27. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +0 -34
  28. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +0 -94
  29. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +0 -93
  30. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +0 -10
  31. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +0 -138
  32. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +0 -121
  33. package/.codekin/reports/security/2026-04-03_security-audit.md +0 -208
  34. package/.codekin/reports/security/2026-04-11_security-audit.md +0 -297
  35. package/.github/CODEOWNERS +0 -15
  36. package/.github/dependabot.yml +0 -23
  37. package/.github/workflows/ci.yml +0 -38
  38. package/.github/workflows/publish.yml +0 -42
  39. package/.github/workflows/security.yml +0 -29
  40. package/CHANGELOG.md +0 -34
  41. package/SECURITY.md +0 -40
  42. package/dist/chunk-5XJBAPXF.js.map +0 -1
  43. package/dist/chunk-U3PCVK6O.js.map +0 -1
  44. package/docs/github-packages-setup.md +0 -97
  45. package/docs/stepflow-api-reference.md +0 -1663
  46. package/src/core/engine/concurrency.ts +0 -229
  47. package/src/core/engine/lifecycle.ts +0 -176
  48. package/src/core/engine.test.ts +0 -1435
  49. package/src/core/engine.ts +0 -598
  50. package/src/core/index.ts +0 -7
  51. package/src/core/orchestrator/persistence.ts +0 -165
  52. package/src/core/orchestrator/plan-resolution.ts +0 -140
  53. package/src/core/orchestrator/state-transitions.ts +0 -109
  54. package/src/core/orchestrator/step-runner.ts +0 -427
  55. package/src/core/orchestrator.ts +0 -252
  56. package/src/core/types.ts +0 -220
  57. package/src/events/index.ts +0 -8
  58. package/src/events/memory.test.ts +0 -107
  59. package/src/events/memory.ts +0 -92
  60. package/src/events/socketio.test.ts +0 -378
  61. package/src/events/socketio.ts +0 -262
  62. package/src/events/types.ts +0 -118
  63. package/src/events/webhook.test.ts +0 -949
  64. package/src/events/webhook.ts +0 -807
  65. package/src/index.ts +0 -270
  66. package/src/planning/evaluate-condition.test.ts +0 -153
  67. package/src/planning/evaluate-condition.ts +0 -105
  68. package/src/planning/index.ts +0 -55
  69. package/src/planning/planner.ts +0 -494
  70. package/src/planning/planning.test.ts +0 -727
  71. package/src/planning/registry.test.ts +0 -398
  72. package/src/planning/registry.ts +0 -283
  73. package/src/planning/types.ts +0 -566
  74. package/src/scheduler/cron.test.ts +0 -479
  75. package/src/scheduler/cron.ts +0 -438
  76. package/src/scheduler/index.ts +0 -8
  77. package/src/scheduler/postgres-persistence.ts +0 -530
  78. package/src/scheduler/sqlite-persistence.test.ts +0 -235
  79. package/src/scheduler/sqlite-persistence.ts +0 -286
  80. package/src/scheduler/types.ts +0 -73
  81. package/src/storage/index.ts +0 -27
  82. package/src/storage/memory.test.ts +0 -349
  83. package/src/storage/memory.ts +0 -227
  84. package/src/storage/postgres-core.ts +0 -550
  85. package/src/storage/postgres-migration.test.ts +0 -69
  86. package/src/storage/postgres-transaction-pagination.test.ts +0 -107
  87. package/src/storage/postgres.ts +0 -835
  88. package/src/storage/sqlite-core.ts +0 -151
  89. package/src/storage/sqlite.test.ts +0 -642
  90. package/src/storage/sqlite.ts +0 -717
  91. package/src/storage/types.ts +0 -343
  92. package/src/utils/errors.test.ts +0 -150
  93. package/src/utils/errors.ts +0 -161
  94. package/src/utils/id.ts +0 -21
  95. package/src/utils/index.ts +0 -10
  96. package/src/utils/logger.test.ts +0 -121
  97. package/src/utils/logger.ts +0 -96
  98. package/src/utils/pinned-fetch.test.ts +0 -84
  99. package/src/utils/pinned-fetch.ts +0 -118
  100. package/src/utils/postgres-deps.ts +0 -79
  101. package/src/utils/retry.test.ts +0 -177
  102. package/src/utils/retry.ts +0 -121
  103. package/src/utils/safe-json.test.ts +0 -166
  104. package/src/utils/safe-json.ts +0 -107
  105. package/src/utils/webhook-signature.test.ts +0 -111
  106. package/src/utils/webhook-signature.ts +0 -74
  107. package/tsconfig.json +0 -21
  108. package/tsup.config.ts +0 -14
  109. 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
- }