@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.
- package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
package/src/planning/registry.ts
CHANGED
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
* Provides storage and retrieval of recipes and handlers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import type { WorkflowStep } from '../core/types';
|
|
7
|
+
import { evaluateCondition, getNestedValue } from './evaluate-condition';
|
|
8
8
|
import type {
|
|
9
9
|
Recipe,
|
|
10
|
-
RecipeCondition,
|
|
11
10
|
RecipeRegistry,
|
|
12
11
|
RecipeQueryOptions,
|
|
13
12
|
RegisteredStepHandler,
|
|
14
13
|
StepHandlerRegistry,
|
|
15
14
|
WorkflowKind,
|
|
16
|
-
} from
|
|
15
|
+
} from './types';
|
|
17
16
|
|
|
18
17
|
// ============================================================================
|
|
19
18
|
// Step Handler Registry Implementation
|
|
@@ -28,7 +27,7 @@ export class MemoryStepHandlerRegistry implements StepHandlerRegistry {
|
|
|
28
27
|
|
|
29
28
|
/** Register a step handler. Throws if a handler with the same ID is already registered. */
|
|
30
29
|
register<TInput = Record<string, unknown>>(
|
|
31
|
-
handler: RegisteredStepHandler<TInput
|
|
30
|
+
handler: RegisteredStepHandler<TInput>
|
|
32
31
|
): void {
|
|
33
32
|
if (this.handlers.has(handler.id)) {
|
|
34
33
|
throw new Error(`Step handler '${handler.id}' is already registered`);
|
|
@@ -68,7 +67,7 @@ export class MemoryStepHandlerRegistry implements StepHandlerRegistry {
|
|
|
68
67
|
if (!ids) return [];
|
|
69
68
|
|
|
70
69
|
return Array.from(ids)
|
|
71
|
-
.map(
|
|
70
|
+
.map(id => this.handlers.get(id))
|
|
72
71
|
.filter((h): h is RegisteredStepHandler => h !== undefined);
|
|
73
72
|
}
|
|
74
73
|
|
|
@@ -76,7 +75,7 @@ export class MemoryStepHandlerRegistry implements StepHandlerRegistry {
|
|
|
76
75
|
* Resolve a handler reference to a WorkflowStep handler function.
|
|
77
76
|
* Returns undefined if the handler is not found.
|
|
78
77
|
*/
|
|
79
|
-
resolve(handlerRef: string): WorkflowStep[
|
|
78
|
+
resolve(handlerRef: string): WorkflowStep['handler'] | undefined {
|
|
80
79
|
const registered = this.handlers.get(handlerRef);
|
|
81
80
|
return registered?.handler;
|
|
82
81
|
}
|
|
@@ -121,7 +120,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
|
|
|
121
120
|
const variantKey = `${recipe.workflowKind}:${recipe.variant}`;
|
|
122
121
|
if (this.variantIndex.has(variantKey)) {
|
|
123
122
|
throw new Error(
|
|
124
|
-
`Recipe variant '${recipe.variant}' for '${recipe.workflowKind}' is already registered
|
|
123
|
+
`Recipe variant '${recipe.variant}' for '${recipe.workflowKind}' is already registered`
|
|
125
124
|
);
|
|
126
125
|
}
|
|
127
126
|
this.variantIndex.set(variantKey, recipe.id);
|
|
@@ -160,7 +159,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
|
|
|
160
159
|
if (!ids) return [];
|
|
161
160
|
|
|
162
161
|
return Array.from(ids)
|
|
163
|
-
.map(
|
|
162
|
+
.map(id => this.recipes.get(id))
|
|
164
163
|
.filter((r): r is Recipe => r !== undefined);
|
|
165
164
|
}
|
|
166
165
|
|
|
@@ -179,7 +178,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
|
|
|
179
178
|
*/
|
|
180
179
|
getDefault(workflowKind: WorkflowKind): Recipe | undefined {
|
|
181
180
|
// First try to find a 'default' variant
|
|
182
|
-
const defaultRecipe = this.getVariant(workflowKind,
|
|
181
|
+
const defaultRecipe = this.getVariant(workflowKind, 'default');
|
|
183
182
|
if (defaultRecipe) return defaultRecipe;
|
|
184
183
|
|
|
185
184
|
// Fall back to the recipe with the lowest numeric priority (highest precedence).
|
|
@@ -198,7 +197,7 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
|
|
|
198
197
|
/** List all variant names registered for a workflow kind. */
|
|
199
198
|
listVariants(workflowKind: WorkflowKind): string[] {
|
|
200
199
|
const recipes = this.getByKind(workflowKind);
|
|
201
|
-
return recipes.map(
|
|
200
|
+
return recipes.map(r => r.variant);
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
/** Query recipes with optional filters for kind, variant, tags, and input conditions. */
|
|
@@ -206,21 +205,21 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
|
|
|
206
205
|
let results = this.list();
|
|
207
206
|
|
|
208
207
|
if (options.workflowKind) {
|
|
209
|
-
results = results.filter(
|
|
208
|
+
results = results.filter(r => r.workflowKind === options.workflowKind);
|
|
210
209
|
}
|
|
211
210
|
|
|
212
211
|
if (options.variant) {
|
|
213
|
-
results = results.filter(
|
|
212
|
+
results = results.filter(r => r.variant === options.variant);
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
if (options.tags && options.tags.length > 0) {
|
|
217
|
-
results = results.filter(
|
|
218
|
-
options.tags!.some(
|
|
216
|
+
results = results.filter(r =>
|
|
217
|
+
options.tags!.some(tag => r.tags?.includes(tag))
|
|
219
218
|
);
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
if (options.matchConditions) {
|
|
223
|
-
results = results.filter(
|
|
222
|
+
results = results.filter(r => {
|
|
224
223
|
if (!r.conditions || r.conditions.length === 0) return true;
|
|
225
224
|
return this.evaluateConditions(r.conditions, options.matchConditions!);
|
|
226
225
|
});
|
|
@@ -247,103 +246,18 @@ export class MemoryRecipeRegistry implements RecipeRegistry {
|
|
|
247
246
|
/**
|
|
248
247
|
* Evaluate recipe conditions against an input.
|
|
249
248
|
* Returns true if all conditions match.
|
|
250
|
-
*
|
|
251
|
-
* Note: This intentionally mirrors the condition evaluation in planner.ts
|
|
252
|
-
* to keep registry queries independent of the planner implementation.
|
|
253
249
|
*/
|
|
254
250
|
private evaluateConditions(
|
|
255
|
-
conditions: Recipe[
|
|
256
|
-
input: Record<string, unknown
|
|
251
|
+
conditions: Recipe['conditions'],
|
|
252
|
+
input: Record<string, unknown>
|
|
257
253
|
): boolean {
|
|
258
254
|
if (!conditions || conditions.length === 0) return true;
|
|
259
255
|
|
|
260
|
-
return conditions.every(
|
|
261
|
-
const fieldValue =
|
|
262
|
-
return
|
|
256
|
+
return conditions.every(condition => {
|
|
257
|
+
const fieldValue = getNestedValue(input, condition.field);
|
|
258
|
+
return evaluateCondition(condition.operator, fieldValue, condition.value);
|
|
263
259
|
});
|
|
264
260
|
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Get a nested value from an object using dot notation.
|
|
268
|
-
*/
|
|
269
|
-
private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
270
|
-
return path.split(".").reduce<unknown>((current, key) => {
|
|
271
|
-
if (current && typeof current === "object" && key in current) {
|
|
272
|
-
return (current as Record<string, unknown>)[key];
|
|
273
|
-
}
|
|
274
|
-
return undefined;
|
|
275
|
-
}, obj);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Evaluate a single condition.
|
|
280
|
-
*/
|
|
281
|
-
private evaluateCondition(
|
|
282
|
-
condition: RecipeCondition,
|
|
283
|
-
fieldValue: unknown,
|
|
284
|
-
): boolean {
|
|
285
|
-
const { operator, value } = condition;
|
|
286
|
-
|
|
287
|
-
switch (operator) {
|
|
288
|
-
case "eq":
|
|
289
|
-
return fieldValue === value;
|
|
290
|
-
|
|
291
|
-
case "neq":
|
|
292
|
-
return fieldValue !== value;
|
|
293
|
-
|
|
294
|
-
case "gt":
|
|
295
|
-
return typeof fieldValue === "number" && typeof value === "number"
|
|
296
|
-
? fieldValue > value
|
|
297
|
-
: false;
|
|
298
|
-
|
|
299
|
-
case "gte":
|
|
300
|
-
return typeof fieldValue === "number" && typeof value === "number"
|
|
301
|
-
? fieldValue >= value
|
|
302
|
-
: false;
|
|
303
|
-
|
|
304
|
-
case "lt":
|
|
305
|
-
return typeof fieldValue === "number" && typeof value === "number"
|
|
306
|
-
? fieldValue < value
|
|
307
|
-
: false;
|
|
308
|
-
|
|
309
|
-
case "lte":
|
|
310
|
-
return typeof fieldValue === "number" && typeof value === "number"
|
|
311
|
-
? fieldValue <= value
|
|
312
|
-
: false;
|
|
313
|
-
|
|
314
|
-
case "contains":
|
|
315
|
-
if (typeof fieldValue === "string" && typeof value === "string") {
|
|
316
|
-
return fieldValue.includes(value);
|
|
317
|
-
}
|
|
318
|
-
if (Array.isArray(fieldValue)) {
|
|
319
|
-
return fieldValue.includes(value);
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
|
|
323
|
-
case "matches":
|
|
324
|
-
if (typeof fieldValue === "string" && typeof value === "string") {
|
|
325
|
-
try {
|
|
326
|
-
return new RE2(value).test(fieldValue);
|
|
327
|
-
} catch {
|
|
328
|
-
return false;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return false;
|
|
332
|
-
|
|
333
|
-
case "exists":
|
|
334
|
-
return (
|
|
335
|
-
fieldValue !== undefined && fieldValue !== null && fieldValue !== ""
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
case "notExists":
|
|
339
|
-
return (
|
|
340
|
-
fieldValue === undefined || fieldValue === null || fieldValue === ""
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
default:
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
261
|
}
|
|
348
262
|
|
|
349
263
|
// ============================================================================
|
package/src/planning/types.ts
CHANGED
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
* Enables dynamic workflow orchestration through recipes and plans.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
WorkflowKind,
|
|
8
|
-
StepErrorStrategy,
|
|
9
|
-
WorkflowStep,
|
|
10
|
-
} from "../core/types";
|
|
6
|
+
import type { WorkflowKind, StepErrorStrategy, WorkflowStep } from '../core/types';
|
|
11
7
|
|
|
12
8
|
// Re-export core types used in planning
|
|
13
|
-
export type { WorkflowKind, StepErrorStrategy } from
|
|
9
|
+
export type { WorkflowKind, StepErrorStrategy } from '../core/types';
|
|
14
10
|
|
|
15
11
|
// ============================================================================
|
|
16
12
|
// Recipe Types
|
|
@@ -20,16 +16,16 @@ export type { WorkflowKind, StepErrorStrategy } from "../core/types";
|
|
|
20
16
|
* Comparison operators for recipe conditions.
|
|
21
17
|
*/
|
|
22
18
|
export type ConditionOperator =
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
19
|
+
| 'eq' // equals
|
|
20
|
+
| 'neq' // not equals
|
|
21
|
+
| 'gt' // greater than
|
|
22
|
+
| 'gte' // greater than or equal
|
|
23
|
+
| 'lt' // less than
|
|
24
|
+
| 'lte' // less than or equal
|
|
25
|
+
| 'contains' // string/array contains
|
|
26
|
+
| 'matches' // regex match
|
|
27
|
+
| 'exists' // field exists and is truthy
|
|
28
|
+
| 'notExists'; // field does not exist or is falsy
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
31
|
* A condition that determines when a recipe should be selected.
|
|
@@ -169,11 +165,11 @@ export interface Recipe {
|
|
|
169
165
|
* Types of modifications that can be made to a plan.
|
|
170
166
|
*/
|
|
171
167
|
export type PlanModificationType =
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
168
|
+
| 'add_step' // Add a new step
|
|
169
|
+
| 'remove_step' // Remove an existing step
|
|
170
|
+
| 'modify_step' // Modify step configuration
|
|
171
|
+
| 'reorder_steps' // Change step order
|
|
172
|
+
| 'set_default'; // Override a default parameter
|
|
177
173
|
|
|
178
174
|
/**
|
|
179
175
|
* A modification to apply to a recipe when generating a plan.
|
|
@@ -306,7 +302,7 @@ export interface Plan {
|
|
|
306
302
|
/**
|
|
307
303
|
* Priority modes for planning.
|
|
308
304
|
*/
|
|
309
|
-
export type PlanningPriority =
|
|
305
|
+
export type PlanningPriority = 'speed' | 'quality' | 'cost' | 'balanced';
|
|
310
306
|
|
|
311
307
|
/**
|
|
312
308
|
* Constraints that limit planning and execution.
|
|
@@ -407,7 +403,7 @@ export interface Planner {
|
|
|
407
403
|
selectRecipe(
|
|
408
404
|
workflowKind: WorkflowKind,
|
|
409
405
|
input: Record<string, unknown>,
|
|
410
|
-
context?: PlanningContext
|
|
406
|
+
context?: PlanningContext
|
|
411
407
|
): Promise<RecipeSelectionResult>;
|
|
412
408
|
|
|
413
409
|
/**
|
|
@@ -416,7 +412,7 @@ export interface Planner {
|
|
|
416
412
|
generatePlan(
|
|
417
413
|
recipe: Recipe,
|
|
418
414
|
input: Record<string, unknown>,
|
|
419
|
-
context?: PlanningContext
|
|
415
|
+
context?: PlanningContext
|
|
420
416
|
): Promise<Plan>;
|
|
421
417
|
|
|
422
418
|
/**
|
|
@@ -425,7 +421,7 @@ export interface Planner {
|
|
|
425
421
|
plan(
|
|
426
422
|
workflowKind: WorkflowKind,
|
|
427
423
|
input: Record<string, unknown>,
|
|
428
|
-
context?: PlanningContext
|
|
424
|
+
context?: PlanningContext
|
|
429
425
|
): Promise<Plan>;
|
|
430
426
|
|
|
431
427
|
/**
|
|
@@ -454,7 +450,7 @@ export interface RegisteredStepHandler<TInput = Record<string, unknown>> {
|
|
|
454
450
|
description?: string;
|
|
455
451
|
|
|
456
452
|
/** The handler function */
|
|
457
|
-
handler: WorkflowStep<TInput>[
|
|
453
|
+
handler: WorkflowStep<TInput>['handler'];
|
|
458
454
|
|
|
459
455
|
/** Tags for categorization */
|
|
460
456
|
tags?: string[];
|
|
@@ -469,7 +465,7 @@ export interface StepHandlerRegistry {
|
|
|
469
465
|
* Register a step handler.
|
|
470
466
|
*/
|
|
471
467
|
register<TInput = Record<string, unknown>>(
|
|
472
|
-
handler: RegisteredStepHandler<TInput
|
|
468
|
+
handler: RegisteredStepHandler<TInput>
|
|
473
469
|
): void;
|
|
474
470
|
|
|
475
471
|
/**
|