@multiplier-labs/stepflow 0.3.1

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 (76) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/README.md +408 -0
  3. package/dist/chunk-FQCT3I2A.js +1512 -0
  4. package/dist/chunk-FQCT3I2A.js.map +1 -0
  5. package/dist/chunk-GXY66V7Q.js +111 -0
  6. package/dist/chunk-GXY66V7Q.js.map +1 -0
  7. package/dist/chunk-U5QGN3RT.js +715 -0
  8. package/dist/chunk-U5QGN3RT.js.map +1 -0
  9. package/dist/chunk-XCF4PLN7.js +575 -0
  10. package/dist/chunk-XCF4PLN7.js.map +1 -0
  11. package/dist/events/index.d.ts +346 -0
  12. package/dist/events/index.js +11 -0
  13. package/dist/events/index.js.map +1 -0
  14. package/dist/index-CPfGsmrp.d.ts +567 -0
  15. package/dist/index.d.ts +748 -0
  16. package/dist/index.js +1707 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/scheduler/index.d.ts +6 -0
  19. package/dist/scheduler/index.js +12 -0
  20. package/dist/scheduler/index.js.map +1 -0
  21. package/dist/storage/index.d.ts +402 -0
  22. package/dist/storage/index.js +13 -0
  23. package/dist/storage/index.js.map +1 -0
  24. package/dist/types-BVShq7zT.d.ts +150 -0
  25. package/dist/types-BlwFEGzj.d.ts +276 -0
  26. package/dist/types-DzkEqnQJ.d.ts +75 -0
  27. package/docs/github-packages-setup.md +97 -0
  28. package/docs/stepflow-api-reference.md +1662 -0
  29. package/package.json +85 -0
  30. package/src/core/engine.test.ts +1421 -0
  31. package/src/core/engine.ts +717 -0
  32. package/src/core/index.ts +7 -0
  33. package/src/core/orchestrator.ts +590 -0
  34. package/src/core/types.ts +242 -0
  35. package/src/events/index.ts +8 -0
  36. package/src/events/memory.test.ts +109 -0
  37. package/src/events/memory.ts +95 -0
  38. package/src/events/socketio.test.ts +428 -0
  39. package/src/events/socketio.ts +269 -0
  40. package/src/events/types.ts +121 -0
  41. package/src/events/webhook.test.ts +695 -0
  42. package/src/events/webhook.ts +591 -0
  43. package/src/index.ts +270 -0
  44. package/src/planning/index.ts +52 -0
  45. package/src/planning/planner.ts +601 -0
  46. package/src/planning/planning.test.ts +790 -0
  47. package/src/planning/registry.test.ts +445 -0
  48. package/src/planning/registry.ts +369 -0
  49. package/src/planning/types.ts +570 -0
  50. package/src/scheduler/cron.test.ts +480 -0
  51. package/src/scheduler/cron.ts +449 -0
  52. package/src/scheduler/index.ts +18 -0
  53. package/src/scheduler/postgres-persistence.test.ts +906 -0
  54. package/src/scheduler/postgres-persistence.ts +546 -0
  55. package/src/scheduler/sqlite-persistence.test.ts +235 -0
  56. package/src/scheduler/sqlite-persistence.ts +268 -0
  57. package/src/scheduler/types.ts +78 -0
  58. package/src/storage/index.ts +27 -0
  59. package/src/storage/memory.test.ts +399 -0
  60. package/src/storage/memory.ts +248 -0
  61. package/src/storage/postgres.ts +1550 -0
  62. package/src/storage/sqlite.test.ts +593 -0
  63. package/src/storage/sqlite.ts +662 -0
  64. package/src/storage/types.ts +368 -0
  65. package/src/utils/errors.test.ts +152 -0
  66. package/src/utils/errors.ts +191 -0
  67. package/src/utils/id.ts +21 -0
  68. package/src/utils/index.ts +8 -0
  69. package/src/utils/logger.test.ts +127 -0
  70. package/src/utils/logger.ts +96 -0
  71. package/src/utils/postgres-deps.ts +63 -0
  72. package/src/utils/retry.test.ts +197 -0
  73. package/src/utils/retry.ts +121 -0
  74. package/tsconfig.json +21 -0
  75. package/tsup.config.ts +14 -0
  76. package/vitest.config.ts +9 -0
package/dist/index.js ADDED
@@ -0,0 +1,1707 @@
1
+ import {
2
+ MemoryStorageAdapter,
3
+ PostgresStorageAdapter,
4
+ SQLiteStorageAdapter
5
+ } from "./chunk-FQCT3I2A.js";
6
+ import {
7
+ MemoryEventTransport,
8
+ SocketIOEventTransport,
9
+ WebhookEventTransport
10
+ } from "./chunk-XCF4PLN7.js";
11
+ import {
12
+ CronScheduler,
13
+ PostgresSchedulePersistence,
14
+ SQLiteSchedulePersistence
15
+ } from "./chunk-U5QGN3RT.js";
16
+ import {
17
+ ConsoleLogger,
18
+ SilentLogger,
19
+ createScopedLogger,
20
+ generateId,
21
+ sanitizeErrorForStorage
22
+ } from "./chunk-GXY66V7Q.js";
23
+
24
+ // src/utils/errors.ts
25
+ var WorkflowEngineError = class _WorkflowEngineError extends Error {
26
+ code;
27
+ details;
28
+ constructor(code, message, details) {
29
+ super(message);
30
+ this.name = "WorkflowEngineError";
31
+ this.code = code;
32
+ this.details = details;
33
+ }
34
+ /**
35
+ * Convert to a WorkflowError record for storage.
36
+ */
37
+ toRecord() {
38
+ return {
39
+ code: this.code,
40
+ message: this.message,
41
+ stack: this.stack,
42
+ details: this.details
43
+ };
44
+ }
45
+ /**
46
+ * Create a WorkflowError record from any error.
47
+ */
48
+ static fromError(error, defaultCode = "UNKNOWN_ERROR") {
49
+ if (error instanceof _WorkflowEngineError) {
50
+ return error.toRecord();
51
+ }
52
+ if (error instanceof Error) {
53
+ return {
54
+ code: defaultCode,
55
+ message: error.message,
56
+ stack: error.stack
57
+ };
58
+ }
59
+ return {
60
+ code: defaultCode,
61
+ message: String(error)
62
+ };
63
+ }
64
+ };
65
+ var WorkflowNotFoundError = class extends WorkflowEngineError {
66
+ constructor(kind) {
67
+ super("WORKFLOW_NOT_FOUND", `Workflow "${kind}" is not registered`, {
68
+ kind
69
+ });
70
+ this.name = "WorkflowNotFoundError";
71
+ }
72
+ };
73
+ var WorkflowAlreadyRegisteredError = class extends WorkflowEngineError {
74
+ constructor(kind) {
75
+ super(
76
+ "WORKFLOW_ALREADY_REGISTERED",
77
+ `Workflow "${kind}" is already registered`,
78
+ { kind }
79
+ );
80
+ this.name = "WorkflowAlreadyRegisteredError";
81
+ }
82
+ };
83
+ var RunNotFoundError = class extends WorkflowEngineError {
84
+ constructor(runId) {
85
+ super("RUN_NOT_FOUND", `Run "${runId}" not found`, { runId });
86
+ this.name = "RunNotFoundError";
87
+ }
88
+ };
89
+ var StepError = class extends WorkflowEngineError {
90
+ stepKey;
91
+ attempt;
92
+ cause;
93
+ constructor(stepKey, message, attempt, cause) {
94
+ super("STEP_ERROR", message, { stepKey, attempt });
95
+ this.name = "StepError";
96
+ this.stepKey = stepKey;
97
+ this.attempt = attempt;
98
+ this.cause = cause;
99
+ }
100
+ };
101
+ var StepTimeoutError = class extends WorkflowEngineError {
102
+ stepKey;
103
+ timeoutMs;
104
+ constructor(stepKey, timeoutMs) {
105
+ super("STEP_TIMEOUT", `Step "${stepKey}" timed out after ${timeoutMs}ms`, {
106
+ stepKey,
107
+ timeoutMs
108
+ });
109
+ this.name = "StepTimeoutError";
110
+ this.stepKey = stepKey;
111
+ this.timeoutMs = timeoutMs;
112
+ }
113
+ };
114
+ var WorkflowCanceledError = class extends WorkflowEngineError {
115
+ constructor(runId) {
116
+ super("WORKFLOW_CANCELED", `Workflow run "${runId}" was canceled`, {
117
+ runId
118
+ });
119
+ this.name = "WorkflowCanceledError";
120
+ }
121
+ };
122
+ var WaitForRunTimeoutError = class extends WorkflowEngineError {
123
+ runId;
124
+ timeoutMs;
125
+ constructor(runId, timeoutMs) {
126
+ super(
127
+ "WAIT_FOR_RUN_TIMEOUT",
128
+ `Timeout waiting for run ${runId} after ${timeoutMs}ms`,
129
+ {
130
+ runId,
131
+ timeoutMs
132
+ }
133
+ );
134
+ this.name = "WaitForRunTimeoutError";
135
+ this.runId = runId;
136
+ this.timeoutMs = timeoutMs;
137
+ }
138
+ };
139
+ var WorkflowTimeoutError = class extends WorkflowEngineError {
140
+ timeoutMs;
141
+ constructor(runId, timeoutMs) {
142
+ super(
143
+ "WORKFLOW_TIMEOUT",
144
+ `Workflow run "${runId}" timed out after ${timeoutMs}ms`,
145
+ {
146
+ runId,
147
+ timeoutMs
148
+ }
149
+ );
150
+ this.name = "WorkflowTimeoutError";
151
+ this.timeoutMs = timeoutMs;
152
+ }
153
+ };
154
+
155
+ // src/utils/retry.ts
156
+ var DEFAULT_RETRY_OPTIONS = {
157
+ maxRetries: 3,
158
+ delay: 1e3,
159
+ backoff: 2
160
+ };
161
+ function sleep(ms, signal) {
162
+ return new Promise((resolve, reject) => {
163
+ if (signal?.aborted) {
164
+ reject(new WorkflowCanceledError("run"));
165
+ return;
166
+ }
167
+ const cleanup = () => {
168
+ clearTimeout(timeoutId);
169
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
170
+ };
171
+ let onAbort;
172
+ const timeoutId = setTimeout(() => {
173
+ cleanup();
174
+ resolve();
175
+ }, ms);
176
+ if (signal) {
177
+ onAbort = () => {
178
+ cleanup();
179
+ reject(new WorkflowCanceledError("run"));
180
+ };
181
+ signal.addEventListener("abort", onAbort, { once: true });
182
+ }
183
+ });
184
+ }
185
+ async function withRetry(fn, options = {}) {
186
+ const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
187
+ let lastError;
188
+ let currentDelay = opts.delay;
189
+ for (let attempt = 1; attempt <= opts.maxRetries + 1; attempt++) {
190
+ if (opts.signal?.aborted) {
191
+ throw new Error("Aborted");
192
+ }
193
+ try {
194
+ return await fn();
195
+ } catch (error) {
196
+ lastError = error instanceof Error ? error : new Error(String(error));
197
+ if (attempt > opts.maxRetries) {
198
+ throw lastError;
199
+ }
200
+ opts.onRetry?.(attempt, lastError, currentDelay);
201
+ await sleep(currentDelay, opts.signal);
202
+ currentDelay = Math.round(currentDelay * opts.backoff);
203
+ }
204
+ }
205
+ throw lastError ?? new Error("Retry failed");
206
+ }
207
+ function calculateRetryDelay(attempt, baseDelay, backoff) {
208
+ return Math.round(baseDelay * Math.pow(backoff, attempt - 1));
209
+ }
210
+
211
+ // src/core/orchestrator.ts
212
+ async function executeWorkflow(options) {
213
+ const {
214
+ runId,
215
+ definition,
216
+ input,
217
+ metadata = {},
218
+ storage,
219
+ events,
220
+ logger,
221
+ abortController,
222
+ spawnChild,
223
+ checkpoint
224
+ } = options;
225
+ const startTime = Date.now();
226
+ const isResume = !!checkpoint;
227
+ await storage.updateRun(runId, {
228
+ status: "running",
229
+ ...isResume ? {} : { startedAt: /* @__PURE__ */ new Date() }
230
+ });
231
+ emitEvent(events, logger, {
232
+ runId,
233
+ kind: definition.kind,
234
+ eventType: isResume ? "run.resumed" : "run.started",
235
+ timestamp: /* @__PURE__ */ new Date(),
236
+ payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : void 0
237
+ });
238
+ const context = {
239
+ runId,
240
+ stepId: "",
241
+ // Will be set for each step before handler is called
242
+ kind: definition.kind,
243
+ input,
244
+ results: checkpoint?.results ? { ...checkpoint.results } : {},
245
+ metadata,
246
+ logger: createScopedLogger(logger, runId),
247
+ signal: abortController.signal,
248
+ spawnChild,
249
+ emit: (eventType, payload) => {
250
+ emitEvent(events, logger, {
251
+ runId,
252
+ kind: definition.kind,
253
+ eventType,
254
+ timestamp: /* @__PURE__ */ new Date(),
255
+ payload
256
+ });
257
+ }
258
+ };
259
+ let workflowTimeoutId;
260
+ let workflowTimeoutError;
261
+ if (definition.timeout) {
262
+ workflowTimeoutId = setTimeout(() => {
263
+ workflowTimeoutError = new WorkflowTimeoutError(
264
+ runId,
265
+ definition.timeout
266
+ );
267
+ abortController.abort();
268
+ }, definition.timeout);
269
+ }
270
+ try {
271
+ if (definition.hooks?.beforeRun) {
272
+ await definition.hooks.beforeRun(context);
273
+ }
274
+ for (const step of definition.steps) {
275
+ if (abortController.signal.aborted) {
276
+ throw new WorkflowCanceledError(runId);
277
+ }
278
+ context.currentStep = step.key;
279
+ if (checkpoint?.completedStepKeys.has(step.key)) {
280
+ logger.info(
281
+ `Skipping step "${step.key}" (already completed in previous run)`
282
+ );
283
+ emitEvent(events, logger, {
284
+ runId,
285
+ kind: definition.kind,
286
+ eventType: "step.skipped",
287
+ stepKey: step.key,
288
+ timestamp: /* @__PURE__ */ new Date(),
289
+ payload: { reason: "checkpoint" }
290
+ });
291
+ continue;
292
+ }
293
+ if (step.skipIf) {
294
+ const shouldSkip = await step.skipIf(context);
295
+ if (shouldSkip) {
296
+ logger.info(`Skipping step "${step.key}" (condition met)`);
297
+ emitEvent(events, logger, {
298
+ runId,
299
+ kind: definition.kind,
300
+ eventType: "step.skipped",
301
+ stepKey: step.key,
302
+ timestamp: /* @__PURE__ */ new Date()
303
+ });
304
+ continue;
305
+ }
306
+ }
307
+ const result = await executeStep(step, context, {
308
+ definition,
309
+ storage,
310
+ events,
311
+ logger,
312
+ abortController
313
+ });
314
+ context.results[step.key] = result;
315
+ await storage.updateRun(runId, {
316
+ context: { ...context.results }
317
+ });
318
+ }
319
+ if (workflowTimeoutId) {
320
+ clearTimeout(workflowTimeoutId);
321
+ }
322
+ const duration = Date.now() - startTime;
323
+ const runResult = {
324
+ status: "succeeded",
325
+ results: context.results,
326
+ duration
327
+ };
328
+ if (definition.hooks?.afterRun) {
329
+ await definition.hooks.afterRun(context, runResult);
330
+ }
331
+ await storage.updateRun(runId, {
332
+ status: "succeeded",
333
+ context: context.results,
334
+ finishedAt: /* @__PURE__ */ new Date()
335
+ });
336
+ emitEvent(events, logger, {
337
+ runId,
338
+ kind: definition.kind,
339
+ eventType: "run.completed",
340
+ timestamp: /* @__PURE__ */ new Date(),
341
+ payload: { results: context.results, duration }
342
+ });
343
+ return runResult;
344
+ } catch (error) {
345
+ if (workflowTimeoutId) {
346
+ clearTimeout(workflowTimeoutId);
347
+ }
348
+ const duration = Date.now() - startTime;
349
+ const actualError = workflowTimeoutError ?? error;
350
+ const workflowError = WorkflowEngineError.fromError(actualError);
351
+ const isTimeout = actualError instanceof WorkflowTimeoutError;
352
+ const isCanceled = !isTimeout && actualError instanceof WorkflowCanceledError;
353
+ const status = isCanceled ? "canceled" : "failed";
354
+ const eventType = isTimeout ? "run.timeout" : isCanceled ? "run.canceled" : "run.failed";
355
+ const runResult = {
356
+ status,
357
+ results: context.results,
358
+ error: workflowError,
359
+ duration
360
+ };
361
+ if (definition.hooks?.afterRun) {
362
+ try {
363
+ await definition.hooks.afterRun(context, runResult);
364
+ } catch (hookError) {
365
+ logger.error("afterRun hook failed:", hookError);
366
+ }
367
+ }
368
+ await storage.updateRun(runId, {
369
+ status,
370
+ context: context.results,
371
+ error: workflowError,
372
+ finishedAt: /* @__PURE__ */ new Date()
373
+ });
374
+ emitEvent(events, logger, {
375
+ runId,
376
+ kind: definition.kind,
377
+ eventType,
378
+ timestamp: /* @__PURE__ */ new Date(),
379
+ payload: { error: workflowError.message, duration }
380
+ });
381
+ return runResult;
382
+ }
383
+ }
384
+ async function executeStep(step, context, options) {
385
+ const { definition, storage, events, logger, abortController } = options;
386
+ const onError = step.onError ?? definition.defaultOnError ?? "fail";
387
+ const maxRetries = step.maxRetries ?? 3;
388
+ const retryDelay = step.retryDelay ?? 1e3;
389
+ const retryBackoff = step.retryBackoff ?? 2;
390
+ let attempt = 0;
391
+ let lastError;
392
+ while (true) {
393
+ attempt++;
394
+ if (abortController.signal.aborted) {
395
+ throw new WorkflowCanceledError(context.runId);
396
+ }
397
+ const stepRecord = await storage.createStep({
398
+ runId: context.runId,
399
+ stepKey: step.key,
400
+ stepName: step.name,
401
+ status: "running",
402
+ attempt,
403
+ startedAt: /* @__PURE__ */ new Date()
404
+ });
405
+ context.stepId = stepRecord.id;
406
+ emitEvent(events, logger, {
407
+ runId: context.runId,
408
+ kind: context.kind,
409
+ eventType: "step.started",
410
+ stepKey: step.key,
411
+ timestamp: /* @__PURE__ */ new Date(),
412
+ payload: { attempt }
413
+ });
414
+ if (definition.hooks?.beforeStep) {
415
+ await definition.hooks.beforeStep(context, step);
416
+ }
417
+ try {
418
+ let result;
419
+ if (step.timeout) {
420
+ result = await executeWithTimeout(
421
+ () => step.handler(context),
422
+ step.timeout,
423
+ abortController.signal,
424
+ step.key
425
+ );
426
+ } else {
427
+ result = await raceWithAbort(
428
+ step.handler(context),
429
+ abortController.signal
430
+ );
431
+ }
432
+ await storage.updateStep(stepRecord.id, {
433
+ status: "succeeded",
434
+ result,
435
+ finishedAt: /* @__PURE__ */ new Date()
436
+ });
437
+ if (definition.hooks?.afterStep) {
438
+ await definition.hooks.afterStep(context, step, result);
439
+ }
440
+ emitEvent(events, logger, {
441
+ runId: context.runId,
442
+ kind: context.kind,
443
+ eventType: "step.completed",
444
+ stepKey: step.key,
445
+ timestamp: /* @__PURE__ */ new Date(),
446
+ payload: { result }
447
+ });
448
+ return result;
449
+ } catch (error) {
450
+ lastError = error instanceof Error ? error : new Error(String(error));
451
+ const isCanceled = error instanceof WorkflowCanceledError || abortController.signal.aborted;
452
+ await storage.updateStep(stepRecord.id, {
453
+ status: isCanceled ? "canceled" : "failed",
454
+ error: WorkflowEngineError.fromError(error),
455
+ finishedAt: /* @__PURE__ */ new Date()
456
+ });
457
+ if (isCanceled) {
458
+ emitEvent(events, logger, {
459
+ runId: context.runId,
460
+ kind: context.kind,
461
+ eventType: "step.failed",
462
+ stepKey: step.key,
463
+ timestamp: /* @__PURE__ */ new Date(),
464
+ payload: { error: lastError.message, attempt }
465
+ });
466
+ throw error;
467
+ }
468
+ if (definition.hooks?.onStepError) {
469
+ try {
470
+ await definition.hooks.onStepError(context, step, lastError);
471
+ } catch (hookError) {
472
+ logger.error("onStepError hook failed:", hookError);
473
+ }
474
+ }
475
+ emitEvent(events, logger, {
476
+ runId: context.runId,
477
+ kind: context.kind,
478
+ eventType: "step.failed",
479
+ stepKey: step.key,
480
+ timestamp: /* @__PURE__ */ new Date(),
481
+ payload: { error: lastError.message, attempt }
482
+ });
483
+ if (onError === "skip") {
484
+ logger.warn(`Step "${step.key}" failed, skipping (strategy: skip)`);
485
+ emitEvent(events, logger, {
486
+ runId: context.runId,
487
+ kind: context.kind,
488
+ eventType: "step.skipped",
489
+ stepKey: step.key,
490
+ timestamp: /* @__PURE__ */ new Date(),
491
+ payload: { reason: "error", error: lastError.message }
492
+ });
493
+ return void 0;
494
+ }
495
+ if (onError === "retry" && attempt <= maxRetries) {
496
+ const delay = calculateRetryDelay(attempt, retryDelay, retryBackoff);
497
+ logger.warn(
498
+ `Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`
499
+ );
500
+ emitEvent(events, logger, {
501
+ runId: context.runId,
502
+ kind: context.kind,
503
+ eventType: "step.retry",
504
+ stepKey: step.key,
505
+ timestamp: /* @__PURE__ */ new Date(),
506
+ payload: { attempt, maxRetries, delay, error: lastError.message }
507
+ });
508
+ try {
509
+ await sleep(delay, abortController.signal);
510
+ } catch (sleepError) {
511
+ if (sleepError instanceof WorkflowCanceledError) {
512
+ await storage.updateStep(stepRecord.id, {
513
+ status: "canceled",
514
+ finishedAt: /* @__PURE__ */ new Date()
515
+ });
516
+ }
517
+ throw sleepError;
518
+ }
519
+ continue;
520
+ }
521
+ throw new StepError(step.key, lastError.message, attempt, lastError);
522
+ }
523
+ }
524
+ }
525
+ async function executeWithTimeout(fn, timeoutMs, signal, stepKey) {
526
+ let timeoutId;
527
+ let onAbort;
528
+ try {
529
+ return await Promise.race([
530
+ fn(),
531
+ new Promise((_, reject) => {
532
+ timeoutId = setTimeout(() => {
533
+ reject(new StepTimeoutError(stepKey, timeoutMs));
534
+ }, timeoutMs);
535
+ onAbort = () => {
536
+ clearTimeout(timeoutId);
537
+ reject(new WorkflowCanceledError("run"));
538
+ };
539
+ signal.addEventListener("abort", onAbort, { once: true });
540
+ })
541
+ ]);
542
+ } finally {
543
+ if (timeoutId) clearTimeout(timeoutId);
544
+ if (onAbort) signal.removeEventListener("abort", onAbort);
545
+ }
546
+ }
547
+ async function raceWithAbort(promise, signal) {
548
+ if (signal.aborted) {
549
+ throw new WorkflowCanceledError("run");
550
+ }
551
+ let onAbort;
552
+ try {
553
+ return await Promise.race([
554
+ promise,
555
+ new Promise((_, reject) => {
556
+ onAbort = () => reject(new WorkflowCanceledError("run"));
557
+ signal.addEventListener("abort", onAbort, { once: true });
558
+ })
559
+ ]);
560
+ } finally {
561
+ if (onAbort) signal.removeEventListener("abort", onAbort);
562
+ }
563
+ }
564
+ function emitEvent(events, logger, event) {
565
+ try {
566
+ events.emit(event);
567
+ } catch (error) {
568
+ logger.error("Failed to emit event:", error);
569
+ }
570
+ }
571
+
572
+ // src/core/engine.ts
573
+ var WorkflowEngine = class _WorkflowEngine {
574
+ registry = /* @__PURE__ */ new Map();
575
+ storage;
576
+ events;
577
+ logger;
578
+ activeRuns = /* @__PURE__ */ new Map();
579
+ settings;
580
+ runQueue = [];
581
+ constructor(config = {}) {
582
+ this.storage = config.storage ?? new MemoryStorageAdapter();
583
+ this.events = config.events ?? new MemoryEventTransport();
584
+ this.logger = config.logger ?? new ConsoleLogger();
585
+ this.settings = config.settings ?? {};
586
+ }
587
+ /**
588
+ * Initialize the engine and its storage/event adapters.
589
+ * Call this before starting runs if your storage adapter requires initialization
590
+ * (e.g., PostgresStorageAdapter).
591
+ */
592
+ async initialize() {
593
+ if (this.storage.initialize) {
594
+ await this.storage.initialize();
595
+ }
596
+ }
597
+ /**
598
+ * Get the current number of active runs.
599
+ */
600
+ getActiveRunCount() {
601
+ return this.activeRuns.size;
602
+ }
603
+ /**
604
+ * Get the number of queued runs waiting for capacity.
605
+ */
606
+ getQueuedRunCount() {
607
+ return this.runQueue.length;
608
+ }
609
+ /**
610
+ * Check if capacity is available for a new run.
611
+ */
612
+ hasCapacity() {
613
+ const maxConcurrency = this.settings.maxConcurrency;
614
+ if (maxConcurrency === void 0 || maxConcurrency <= 0) {
615
+ return true;
616
+ }
617
+ return this.activeRuns.size < maxConcurrency;
618
+ }
619
+ // ============================================================================
620
+ // Workflow Registration
621
+ // ============================================================================
622
+ /**
623
+ * Register a workflow definition.
624
+ * Must be called before runs of this type can be started.
625
+ *
626
+ * @param definition - The workflow definition
627
+ * @throws WorkflowAlreadyRegisteredError if already registered
628
+ */
629
+ registerWorkflow(definition) {
630
+ if (this.registry.has(definition.kind)) {
631
+ throw new WorkflowAlreadyRegisteredError(definition.kind);
632
+ }
633
+ this.registry.set(definition.kind, definition);
634
+ this.logger.debug(`Registered workflow: ${definition.kind}`);
635
+ }
636
+ /**
637
+ * Unregister a workflow definition.
638
+ *
639
+ * @param kind - The workflow kind to unregister
640
+ * @returns true if the workflow was unregistered, false if not found
641
+ */
642
+ unregisterWorkflow(kind) {
643
+ const deleted = this.registry.delete(kind);
644
+ if (deleted) {
645
+ this.logger.debug(`Unregistered workflow: ${kind}`);
646
+ }
647
+ return deleted;
648
+ }
649
+ /**
650
+ * Get a registered workflow definition.
651
+ *
652
+ * @param kind - The workflow kind
653
+ * @returns The workflow definition or undefined
654
+ */
655
+ getWorkflow(kind) {
656
+ return this.registry.get(kind);
657
+ }
658
+ /**
659
+ * Get all registered workflow kinds.
660
+ */
661
+ getRegisteredWorkflows() {
662
+ return Array.from(this.registry.keys());
663
+ }
664
+ // ============================================================================
665
+ // Run Management
666
+ // ============================================================================
667
+ /**
668
+ * Start a new workflow run (non-blocking).
669
+ * The run executes asynchronously and this method returns immediately.
670
+ * If maxConcurrency is set and reached, the run is queued.
671
+ *
672
+ * @param options - Run options including kind and input
673
+ * @returns The generated run ID
674
+ * @throws WorkflowNotFoundError if the workflow kind is not registered
675
+ */
676
+ async startRun(options) {
677
+ const definition = this.registry.get(options.kind);
678
+ if (!definition) {
679
+ throw new WorkflowNotFoundError(options.kind);
680
+ }
681
+ const run = await this.storage.createRun({
682
+ kind: options.kind,
683
+ status: "queued",
684
+ parentRunId: options.parentRunId,
685
+ input: options.input ?? {},
686
+ metadata: options.metadata ?? {},
687
+ context: {}
688
+ });
689
+ const runId = run.id;
690
+ this.events.emit({
691
+ runId,
692
+ kind: options.kind,
693
+ eventType: "run.created",
694
+ timestamp: /* @__PURE__ */ new Date(),
695
+ payload: {
696
+ input: options.input,
697
+ metadata: options.metadata,
698
+ priority: options.priority ?? 0
699
+ }
700
+ });
701
+ if (this.hasCapacity()) {
702
+ this.executeRun(
703
+ runId,
704
+ definition,
705
+ options.input ?? {},
706
+ options.metadata,
707
+ options.delay
708
+ );
709
+ } else {
710
+ this.queueRun({
711
+ runId,
712
+ definition,
713
+ input: options.input ?? {},
714
+ metadata: options.metadata,
715
+ priority: options.priority ?? 0,
716
+ queuedAt: /* @__PURE__ */ new Date()
717
+ });
718
+ this.logger.debug(
719
+ `Run ${runId} queued (${this.runQueue.length} in queue)`
720
+ );
721
+ this.events.emit({
722
+ runId,
723
+ kind: options.kind,
724
+ eventType: "run.queued",
725
+ timestamp: /* @__PURE__ */ new Date(),
726
+ payload: { queuePosition: this.runQueue.length }
727
+ });
728
+ }
729
+ return runId;
730
+ }
731
+ /**
732
+ * Queue a run in priority order.
733
+ */
734
+ queueRun(queuedRun) {
735
+ let inserted = false;
736
+ for (let i = 0; i < this.runQueue.length; i++) {
737
+ if (queuedRun.priority > this.runQueue[i].priority) {
738
+ this.runQueue.splice(i, 0, queuedRun);
739
+ inserted = true;
740
+ break;
741
+ }
742
+ }
743
+ if (!inserted) {
744
+ this.runQueue.push(queuedRun);
745
+ }
746
+ }
747
+ /**
748
+ * Execute a run (internal method).
749
+ */
750
+ executeRun(runId, definition, input, metadata, delay) {
751
+ this.launchRun(runId, definition, input, metadata, delay);
752
+ }
753
+ /**
754
+ * Launch a workflow run asynchronously.
755
+ * Shared by both executeRun (new runs) and resumeRun (checkpoint recovery).
756
+ */
757
+ launchRun(runId, definition, input, metadata, delay, checkpoint) {
758
+ const abortController = new AbortController();
759
+ this.activeRuns.set(runId, abortController);
760
+ const execute = async () => {
761
+ try {
762
+ await executeWorkflow({
763
+ runId,
764
+ definition,
765
+ input,
766
+ metadata,
767
+ storage: this.storage,
768
+ events: this.events,
769
+ logger: this.logger,
770
+ abortController,
771
+ spawnChild: (childOptions) => this.spawnChild(runId, childOptions),
772
+ checkpoint
773
+ });
774
+ } finally {
775
+ this.activeRuns.delete(runId);
776
+ this.processQueue();
777
+ }
778
+ };
779
+ if (delay) {
780
+ setTimeout(execute, delay);
781
+ } else {
782
+ setImmediate(execute);
783
+ }
784
+ }
785
+ /**
786
+ * Process the queue and start runs if capacity is available.
787
+ */
788
+ processQueue() {
789
+ while (this.hasCapacity() && this.runQueue.length > 0) {
790
+ const next = this.runQueue.shift();
791
+ this.logger.debug(`Starting queued run ${next.runId}`);
792
+ this.events.emit({
793
+ runId: next.runId,
794
+ kind: next.definition.kind,
795
+ eventType: "run.dequeued",
796
+ timestamp: /* @__PURE__ */ new Date()
797
+ });
798
+ this.executeRun(next.runId, next.definition, next.input, next.metadata);
799
+ }
800
+ }
801
+ /**
802
+ * Start a child workflow from within a parent workflow.
803
+ * Called internally by the context.spawnChild helper.
804
+ */
805
+ async spawnChild(parentRunId, options) {
806
+ return this.startRun({
807
+ kind: options.kind,
808
+ input: options.input,
809
+ metadata: options.metadata,
810
+ parentRunId
811
+ });
812
+ }
813
+ /**
814
+ * Cancel a running workflow.
815
+ * Signals the workflow to stop at the next cancellation point.
816
+ *
817
+ * @param runId - The run ID to cancel
818
+ * @throws {RunNotFoundError} If no run with the given ID exists
819
+ */
820
+ async cancelRun(runId) {
821
+ const run = await this.storage.getRun(runId);
822
+ if (!run) {
823
+ throw new RunNotFoundError(runId);
824
+ }
825
+ if (["succeeded", "failed", "canceled", "timeout"].includes(run.status)) {
826
+ return;
827
+ }
828
+ const queueIndex = this.runQueue.findIndex((q) => q.runId === runId);
829
+ if (queueIndex !== -1) {
830
+ this.runQueue.splice(queueIndex, 1);
831
+ }
832
+ const controller = this.activeRuns.get(runId);
833
+ if (controller) {
834
+ controller.abort();
835
+ }
836
+ await this.storage.updateRun(runId, {
837
+ status: "canceled",
838
+ finishedAt: /* @__PURE__ */ new Date()
839
+ });
840
+ this.events.emit({
841
+ runId,
842
+ kind: run.kind,
843
+ eventType: "run.canceled",
844
+ timestamp: /* @__PURE__ */ new Date()
845
+ });
846
+ }
847
+ /**
848
+ * Get the current status of a run.
849
+ *
850
+ * @param runId - The run ID to look up
851
+ * @returns The run record or null if not found
852
+ */
853
+ async getRunStatus(runId) {
854
+ return this.storage.getRun(runId);
855
+ }
856
+ static TERMINAL_STATUSES = [
857
+ "succeeded",
858
+ "failed",
859
+ "canceled",
860
+ "timeout"
861
+ ];
862
+ static TERMINAL_EVENT_TYPES = [
863
+ "run.completed",
864
+ "run.failed",
865
+ "run.canceled",
866
+ "run.timeout"
867
+ ];
868
+ /**
869
+ * Wait for a run to complete.
870
+ * Subscribes to run events and resolves when a terminal event fires.
871
+ * Falls back to an initial storage read to avoid race conditions.
872
+ *
873
+ * @param runId - The run ID to wait for
874
+ * @param options - Wait options
875
+ * @returns The final run record
876
+ */
877
+ async waitForRun(runId, options = {}) {
878
+ const timeout = options.timeout ?? 6e4;
879
+ const existingRun = await this.storage.getRun(runId);
880
+ if (!existingRun) {
881
+ throw new RunNotFoundError(runId);
882
+ }
883
+ if (_WorkflowEngine.TERMINAL_STATUSES.includes(existingRun.status)) {
884
+ return existingRun;
885
+ }
886
+ return new Promise((resolve, reject) => {
887
+ let timeoutId;
888
+ let unsubscribe;
889
+ const cleanup = () => {
890
+ if (timeoutId) clearTimeout(timeoutId);
891
+ if (unsubscribe) unsubscribe();
892
+ };
893
+ timeoutId = setTimeout(() => {
894
+ cleanup();
895
+ reject(new WaitForRunTimeoutError(runId, timeout));
896
+ }, timeout);
897
+ unsubscribe = this.events.subscribe(runId, async (event) => {
898
+ if (_WorkflowEngine.TERMINAL_EVENT_TYPES.includes(event.eventType)) {
899
+ cleanup();
900
+ try {
901
+ const run = await this.storage.getRun(runId);
902
+ if (!run) {
903
+ reject(new RunNotFoundError(runId));
904
+ } else {
905
+ resolve(run);
906
+ }
907
+ } catch (err) {
908
+ reject(err);
909
+ }
910
+ }
911
+ });
912
+ });
913
+ }
914
+ // ============================================================================
915
+ // Resume Support
916
+ // ============================================================================
917
+ /**
918
+ * Resume an interrupted workflow run from its checkpoint.
919
+ * The run must be in 'queued' or 'running' status.
920
+ *
921
+ * @param runId - The run ID to resume
922
+ * @returns The run ID (same as input)
923
+ * @throws {RunNotFoundError} If no run with the given ID exists
924
+ * @throws {Error} If the run status is not 'queued' or 'running'
925
+ * @throws {WorkflowNotFoundError} If the workflow kind is not registered
926
+ */
927
+ async resumeRun(runId) {
928
+ const run = await this.storage.getRun(runId);
929
+ if (!run) {
930
+ throw new RunNotFoundError(runId);
931
+ }
932
+ if (!["queued", "running"].includes(run.status)) {
933
+ throw new Error(`Cannot resume run ${runId}: status is "${run.status}"`);
934
+ }
935
+ const definition = this.registry.get(run.kind);
936
+ if (!definition) {
937
+ throw new WorkflowNotFoundError(run.kind);
938
+ }
939
+ if (this.activeRuns.has(runId)) {
940
+ this.logger.warn(`Run ${runId} is already active, skipping resume`);
941
+ return runId;
942
+ }
943
+ this.logger.info(`Resuming run ${runId} from checkpoint`);
944
+ const completedStepKeys = new Set(Object.keys(run.context));
945
+ this.launchRun(runId, definition, run.input, run.metadata, void 0, {
946
+ completedStepKeys,
947
+ results: run.context
948
+ });
949
+ return runId;
950
+ }
951
+ /**
952
+ * Get all runs that were interrupted and can be resumed.
953
+ * Returns runs with status 'queued' or 'running'.
954
+ */
955
+ async getResumableRuns() {
956
+ const result = await this.storage.listRuns({
957
+ status: ["queued", "running"]
958
+ });
959
+ return result.items;
960
+ }
961
+ /**
962
+ * Resume all interrupted runs.
963
+ * Useful for recovering after a server restart.
964
+ *
965
+ * @returns Array of resumed run IDs
966
+ */
967
+ async resumeAllInterrupted() {
968
+ const resumableRuns = await this.getResumableRuns();
969
+ const resumedIds = [];
970
+ for (const run of resumableRuns) {
971
+ if (this.registry.has(run.kind)) {
972
+ try {
973
+ await this.resumeRun(run.id);
974
+ resumedIds.push(run.id);
975
+ } catch (error) {
976
+ this.logger.error(`Failed to resume run ${run.id}:`, error);
977
+ }
978
+ } else {
979
+ this.logger.warn(
980
+ `Cannot resume run ${run.id}: workflow "${run.kind}" not registered`
981
+ );
982
+ }
983
+ }
984
+ return resumedIds;
985
+ }
986
+ // ============================================================================
987
+ // Event Subscription
988
+ // ============================================================================
989
+ /**
990
+ * Subscribe to events for a specific run.
991
+ *
992
+ * @param runId - The run ID to subscribe to
993
+ * @param callback - Event handler
994
+ * @returns Unsubscribe function
995
+ */
996
+ subscribeToRun(runId, callback) {
997
+ return this.events.subscribe(runId, callback);
998
+ }
999
+ /**
1000
+ * Subscribe to all workflow events.
1001
+ *
1002
+ * @param callback - Event handler
1003
+ * @returns Unsubscribe function
1004
+ */
1005
+ subscribeToAll(callback) {
1006
+ return this.events.subscribeAll(callback);
1007
+ }
1008
+ // ============================================================================
1009
+ // Storage Access
1010
+ // ============================================================================
1011
+ /**
1012
+ * Get the storage adapter.
1013
+ * Useful for querying runs and steps directly.
1014
+ */
1015
+ getStorage() {
1016
+ return this.storage;
1017
+ }
1018
+ /**
1019
+ * Get the event transport.
1020
+ * Useful for custom event handling.
1021
+ */
1022
+ getEvents() {
1023
+ return this.events;
1024
+ }
1025
+ // ============================================================================
1026
+ // Lifecycle
1027
+ // ============================================================================
1028
+ /**
1029
+ * Shutdown the engine gracefully.
1030
+ * Cancels all active runs and closes resources.
1031
+ */
1032
+ async shutdown() {
1033
+ this.logger.info("Shutting down workflow engine...");
1034
+ for (const [runId, controller] of this.activeRuns) {
1035
+ this.logger.debug(`Canceling active run: ${runId}`);
1036
+ controller.abort();
1037
+ }
1038
+ if (this.events.close) {
1039
+ this.events.close();
1040
+ }
1041
+ if (this.storage.close) {
1042
+ await this.storage.close();
1043
+ }
1044
+ this.activeRuns.clear();
1045
+ this.logger.info("Workflow engine shutdown complete");
1046
+ }
1047
+ };
1048
+
1049
+ // src/planning/registry.ts
1050
+ import RE2 from "re2";
1051
+ var MemoryStepHandlerRegistry = class {
1052
+ handlers = /* @__PURE__ */ new Map();
1053
+ tagIndex = /* @__PURE__ */ new Map();
1054
+ /** Register a step handler. Throws if a handler with the same ID is already registered. */
1055
+ register(handler) {
1056
+ if (this.handlers.has(handler.id)) {
1057
+ throw new Error(`Step handler '${handler.id}' is already registered`);
1058
+ }
1059
+ this.handlers.set(handler.id, handler);
1060
+ if (handler.tags) {
1061
+ for (const tag of handler.tags) {
1062
+ if (!this.tagIndex.has(tag)) {
1063
+ this.tagIndex.set(tag, /* @__PURE__ */ new Set());
1064
+ }
1065
+ this.tagIndex.get(tag).add(handler.id);
1066
+ }
1067
+ }
1068
+ }
1069
+ /** Get a handler by its unique ID, or undefined if not registered. */
1070
+ get(id) {
1071
+ return this.handlers.get(id);
1072
+ }
1073
+ /** Check whether a handler with the given ID is registered. */
1074
+ has(id) {
1075
+ return this.handlers.has(id);
1076
+ }
1077
+ /** List all registered step handlers. */
1078
+ list() {
1079
+ return Array.from(this.handlers.values());
1080
+ }
1081
+ /** List all handlers tagged with the given tag. */
1082
+ listByTag(tag) {
1083
+ const ids = this.tagIndex.get(tag);
1084
+ if (!ids) return [];
1085
+ return Array.from(ids).map((id) => this.handlers.get(id)).filter((h) => h !== void 0);
1086
+ }
1087
+ /**
1088
+ * Resolve a handler reference to a WorkflowStep handler function.
1089
+ * Returns undefined if the handler is not found.
1090
+ */
1091
+ resolve(handlerRef) {
1092
+ const registered = this.handlers.get(handlerRef);
1093
+ return registered?.handler;
1094
+ }
1095
+ /**
1096
+ * Clear all registered handlers (useful for testing).
1097
+ */
1098
+ clear() {
1099
+ this.handlers.clear();
1100
+ this.tagIndex.clear();
1101
+ }
1102
+ };
1103
+ var MemoryRecipeRegistry = class {
1104
+ recipes = /* @__PURE__ */ new Map();
1105
+ kindIndex = /* @__PURE__ */ new Map();
1106
+ variantIndex = /* @__PURE__ */ new Map();
1107
+ // "kind:variant" -> recipeId
1108
+ tagIndex = /* @__PURE__ */ new Map();
1109
+ /** Register a single recipe. Throws if a recipe with the same ID or kind:variant pair is already registered. */
1110
+ register(recipe) {
1111
+ if (this.recipes.has(recipe.id)) {
1112
+ throw new Error(`Recipe '${recipe.id}' is already registered`);
1113
+ }
1114
+ this.recipes.set(recipe.id, recipe);
1115
+ if (!this.kindIndex.has(recipe.workflowKind)) {
1116
+ this.kindIndex.set(recipe.workflowKind, /* @__PURE__ */ new Set());
1117
+ }
1118
+ this.kindIndex.get(recipe.workflowKind).add(recipe.id);
1119
+ const variantKey = `${recipe.workflowKind}:${recipe.variant}`;
1120
+ if (this.variantIndex.has(variantKey)) {
1121
+ throw new Error(
1122
+ `Recipe variant '${recipe.variant}' for '${recipe.workflowKind}' is already registered`
1123
+ );
1124
+ }
1125
+ this.variantIndex.set(variantKey, recipe.id);
1126
+ if (recipe.tags) {
1127
+ for (const tag of recipe.tags) {
1128
+ if (!this.tagIndex.has(tag)) {
1129
+ this.tagIndex.set(tag, /* @__PURE__ */ new Set());
1130
+ }
1131
+ this.tagIndex.get(tag).add(recipe.id);
1132
+ }
1133
+ }
1134
+ }
1135
+ /** Register multiple recipes at once. */
1136
+ registerAll(recipes) {
1137
+ for (const recipe of recipes) {
1138
+ this.register(recipe);
1139
+ }
1140
+ }
1141
+ /** Get a recipe by its unique ID, or undefined if not registered. */
1142
+ get(recipeId) {
1143
+ return this.recipes.get(recipeId);
1144
+ }
1145
+ /** Check whether a recipe with the given ID is registered. */
1146
+ has(recipeId) {
1147
+ return this.recipes.has(recipeId);
1148
+ }
1149
+ /** Get all recipes registered for a given workflow kind. */
1150
+ getByKind(workflowKind) {
1151
+ const ids = this.kindIndex.get(workflowKind);
1152
+ if (!ids) return [];
1153
+ return Array.from(ids).map((id) => this.recipes.get(id)).filter((r) => r !== void 0);
1154
+ }
1155
+ /** Get the recipe for a specific workflow kind and variant combination. */
1156
+ getVariant(workflowKind, variant) {
1157
+ const variantKey = `${workflowKind}:${variant}`;
1158
+ const recipeId = this.variantIndex.get(variantKey);
1159
+ if (!recipeId) return void 0;
1160
+ return this.recipes.get(recipeId);
1161
+ }
1162
+ /**
1163
+ * Get the default recipe for a workflow kind.
1164
+ * Returns the 'default' variant if one exists, otherwise falls back to the
1165
+ * recipe with the lowest numeric priority value (lower number = higher precedence).
1166
+ */
1167
+ getDefault(workflowKind) {
1168
+ const defaultRecipe = this.getVariant(workflowKind, "default");
1169
+ if (defaultRecipe) return defaultRecipe;
1170
+ const recipes = this.getByKind(workflowKind);
1171
+ if (recipes.length === 0) return void 0;
1172
+ return recipes.reduce((lowest, current) => {
1173
+ const currentPriority = current.priority ?? 0;
1174
+ const lowestPriority = lowest.priority ?? 0;
1175
+ return currentPriority < lowestPriority ? current : lowest;
1176
+ });
1177
+ }
1178
+ /** List all variant names registered for a workflow kind. */
1179
+ listVariants(workflowKind) {
1180
+ const recipes = this.getByKind(workflowKind);
1181
+ return recipes.map((r) => r.variant);
1182
+ }
1183
+ /** Query recipes with optional filters for kind, variant, tags, and input conditions. */
1184
+ query(options) {
1185
+ let results = this.list();
1186
+ if (options.workflowKind) {
1187
+ results = results.filter((r) => r.workflowKind === options.workflowKind);
1188
+ }
1189
+ if (options.variant) {
1190
+ results = results.filter((r) => r.variant === options.variant);
1191
+ }
1192
+ if (options.tags && options.tags.length > 0) {
1193
+ results = results.filter(
1194
+ (r) => options.tags.some((tag) => r.tags?.includes(tag))
1195
+ );
1196
+ }
1197
+ if (options.matchConditions) {
1198
+ results = results.filter((r) => {
1199
+ if (!r.conditions || r.conditions.length === 0) return true;
1200
+ return this.evaluateConditions(r.conditions, options.matchConditions);
1201
+ });
1202
+ }
1203
+ return results;
1204
+ }
1205
+ /** List all registered recipes. */
1206
+ list() {
1207
+ return Array.from(this.recipes.values());
1208
+ }
1209
+ /**
1210
+ * Clear all registered recipes (useful for testing).
1211
+ */
1212
+ clear() {
1213
+ this.recipes.clear();
1214
+ this.kindIndex.clear();
1215
+ this.variantIndex.clear();
1216
+ this.tagIndex.clear();
1217
+ }
1218
+ /**
1219
+ * Evaluate recipe conditions against an input.
1220
+ * Returns true if all conditions match.
1221
+ *
1222
+ * Note: This intentionally mirrors the condition evaluation in planner.ts
1223
+ * to keep registry queries independent of the planner implementation.
1224
+ */
1225
+ evaluateConditions(conditions, input) {
1226
+ if (!conditions || conditions.length === 0) return true;
1227
+ return conditions.every((condition) => {
1228
+ const fieldValue = this.getNestedValue(input, condition.field);
1229
+ return this.evaluateCondition(condition, fieldValue);
1230
+ });
1231
+ }
1232
+ /**
1233
+ * Get a nested value from an object using dot notation.
1234
+ */
1235
+ getNestedValue(obj, path) {
1236
+ return path.split(".").reduce((current, key) => {
1237
+ if (current && typeof current === "object" && key in current) {
1238
+ return current[key];
1239
+ }
1240
+ return void 0;
1241
+ }, obj);
1242
+ }
1243
+ /**
1244
+ * Evaluate a single condition.
1245
+ */
1246
+ evaluateCondition(condition, fieldValue) {
1247
+ const { operator, value } = condition;
1248
+ switch (operator) {
1249
+ case "eq":
1250
+ return fieldValue === value;
1251
+ case "neq":
1252
+ return fieldValue !== value;
1253
+ case "gt":
1254
+ return typeof fieldValue === "number" && typeof value === "number" ? fieldValue > value : false;
1255
+ case "gte":
1256
+ return typeof fieldValue === "number" && typeof value === "number" ? fieldValue >= value : false;
1257
+ case "lt":
1258
+ return typeof fieldValue === "number" && typeof value === "number" ? fieldValue < value : false;
1259
+ case "lte":
1260
+ return typeof fieldValue === "number" && typeof value === "number" ? fieldValue <= value : false;
1261
+ case "contains":
1262
+ if (typeof fieldValue === "string" && typeof value === "string") {
1263
+ return fieldValue.includes(value);
1264
+ }
1265
+ if (Array.isArray(fieldValue)) {
1266
+ return fieldValue.includes(value);
1267
+ }
1268
+ return false;
1269
+ case "matches":
1270
+ if (typeof fieldValue === "string" && typeof value === "string") {
1271
+ try {
1272
+ return new RE2(value).test(fieldValue);
1273
+ } catch {
1274
+ return false;
1275
+ }
1276
+ }
1277
+ return false;
1278
+ case "exists":
1279
+ return fieldValue !== void 0 && fieldValue !== null && fieldValue !== "";
1280
+ case "notExists":
1281
+ return fieldValue === void 0 || fieldValue === null || fieldValue === "";
1282
+ default:
1283
+ return false;
1284
+ }
1285
+ }
1286
+ };
1287
+ function createRegistry() {
1288
+ return {
1289
+ recipes: new MemoryRecipeRegistry(),
1290
+ handlers: new MemoryStepHandlerRegistry()
1291
+ };
1292
+ }
1293
+
1294
+ // src/planning/planner.ts
1295
+ import RE22 from "re2";
1296
+ function getNestedValue(obj, path) {
1297
+ return path.split(".").reduce((current, key) => {
1298
+ if (current && typeof current === "object" && key in current) {
1299
+ return current[key];
1300
+ }
1301
+ return void 0;
1302
+ }, obj);
1303
+ }
1304
+ function evaluateCondition(operator, fieldValue, conditionValue) {
1305
+ switch (operator) {
1306
+ case "eq":
1307
+ return fieldValue === conditionValue;
1308
+ case "neq":
1309
+ return fieldValue !== conditionValue;
1310
+ case "gt":
1311
+ return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue > conditionValue : false;
1312
+ case "gte":
1313
+ return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue >= conditionValue : false;
1314
+ case "lt":
1315
+ return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue < conditionValue : false;
1316
+ case "lte":
1317
+ return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue <= conditionValue : false;
1318
+ case "contains":
1319
+ if (typeof fieldValue === "string" && typeof conditionValue === "string") {
1320
+ return fieldValue.includes(conditionValue);
1321
+ }
1322
+ if (Array.isArray(fieldValue)) {
1323
+ return fieldValue.includes(conditionValue);
1324
+ }
1325
+ return false;
1326
+ case "matches":
1327
+ if (typeof fieldValue === "string" && typeof conditionValue === "string") {
1328
+ try {
1329
+ return new RE22(conditionValue).test(fieldValue);
1330
+ } catch {
1331
+ return false;
1332
+ }
1333
+ }
1334
+ return false;
1335
+ case "exists":
1336
+ return fieldValue !== void 0 && fieldValue !== null && fieldValue !== "";
1337
+ case "notExists":
1338
+ return fieldValue === void 0 || fieldValue === null || fieldValue === "";
1339
+ default:
1340
+ return false;
1341
+ }
1342
+ }
1343
+ function scoreConditions(conditions, input) {
1344
+ if (!conditions || conditions.length === 0) {
1345
+ return 10;
1346
+ }
1347
+ let matched = 0;
1348
+ for (const condition of conditions) {
1349
+ const fieldValue = getNestedValue(input, condition.field);
1350
+ if (evaluateCondition(condition.operator, fieldValue, condition.value)) {
1351
+ matched++;
1352
+ }
1353
+ }
1354
+ if (matched < conditions.length) {
1355
+ return 0;
1356
+ }
1357
+ return Math.min(100, 50 + conditions.length * 10);
1358
+ }
1359
+ var RuleBasedPlanner = class {
1360
+ recipeRegistry;
1361
+ handlerRegistry;
1362
+ validateHandlers;
1363
+ resourceEstimates;
1364
+ constructor(config) {
1365
+ this.recipeRegistry = config.recipeRegistry;
1366
+ this.handlerRegistry = config.handlerRegistry;
1367
+ this.validateHandlers = config.validateHandlers ?? false;
1368
+ this.resourceEstimates = {
1369
+ apiCallsPerStep: config.resourceEstimates?.apiCallsPerStep ?? 1,
1370
+ tokensPerStep: config.resourceEstimates?.tokensPerStep ?? 500,
1371
+ durationPerStep: config.resourceEstimates?.durationPerStep ?? 2e3,
1372
+ apiCallsPerChild: config.resourceEstimates?.apiCallsPerChild ?? 5
1373
+ };
1374
+ }
1375
+ /**
1376
+ * Select the best recipe for a workflow kind and input.
1377
+ */
1378
+ async selectRecipe(workflowKind, input, context) {
1379
+ if (context?.hints?.forceRecipeId) {
1380
+ const forced = this.recipeRegistry.get(context.hints.forceRecipeId);
1381
+ if (forced) {
1382
+ return {
1383
+ recipe: forced,
1384
+ score: 100,
1385
+ reason: `Forced recipe: ${context.hints.forceRecipeId}`
1386
+ };
1387
+ }
1388
+ }
1389
+ if (context?.hints?.preferredVariant) {
1390
+ const preferred = this.recipeRegistry.getVariant(
1391
+ workflowKind,
1392
+ context.hints.preferredVariant
1393
+ );
1394
+ if (preferred) {
1395
+ return {
1396
+ recipe: preferred,
1397
+ score: 90,
1398
+ reason: `Preferred variant: ${context.hints.preferredVariant}`
1399
+ };
1400
+ }
1401
+ }
1402
+ const recipes = this.recipeRegistry.getByKind(workflowKind);
1403
+ if (recipes.length === 0) {
1404
+ throw new Error(`No recipes found for workflow kind: ${workflowKind}`);
1405
+ }
1406
+ const scored = recipes.map((recipe) => ({
1407
+ recipe,
1408
+ conditionScore: scoreConditions(recipe.conditions, input),
1409
+ priorityScore: recipe.priority ?? 0
1410
+ }));
1411
+ scored.sort((a, b) => {
1412
+ if (b.conditionScore !== a.conditionScore) {
1413
+ return b.conditionScore - a.conditionScore;
1414
+ }
1415
+ return b.priorityScore - a.priorityScore;
1416
+ });
1417
+ const best = scored.find((s) => s.conditionScore > 0);
1418
+ if (best) {
1419
+ const reason = this.buildSelectionReason(best.recipe, input);
1420
+ return {
1421
+ recipe: best.recipe,
1422
+ score: best.conditionScore,
1423
+ reason
1424
+ };
1425
+ }
1426
+ const defaultRecipe = this.recipeRegistry.getDefault(workflowKind);
1427
+ if (defaultRecipe) {
1428
+ return {
1429
+ recipe: defaultRecipe,
1430
+ score: 10,
1431
+ reason: `Fallback to default recipe: ${defaultRecipe.id}`
1432
+ };
1433
+ }
1434
+ return {
1435
+ recipe: recipes[0],
1436
+ score: 5,
1437
+ reason: `No matching recipe, using first available: ${recipes[0].id}`
1438
+ };
1439
+ }
1440
+ /**
1441
+ * Generate a plan from a recipe and input.
1442
+ */
1443
+ async generatePlan(recipe, input, context) {
1444
+ const modifications = [];
1445
+ let steps = recipe.steps.map((step) => this.recipeStepToPlannedStep(step));
1446
+ if (context?.hints) {
1447
+ const { skipSteps, includeSteps, additionalConfig } = context.hints;
1448
+ if (skipSteps && skipSteps.length > 0) {
1449
+ for (const stepKey of skipSteps) {
1450
+ const stepIndex = steps.findIndex((s) => s.key === stepKey);
1451
+ if (stepIndex >= 0) {
1452
+ steps = steps.filter((s) => s.key !== stepKey);
1453
+ modifications.push({
1454
+ type: "remove_step",
1455
+ stepKey,
1456
+ value: null,
1457
+ reason: `Skipped via planning hints`
1458
+ });
1459
+ }
1460
+ }
1461
+ }
1462
+ if (additionalConfig) {
1463
+ for (const step of steps) {
1464
+ step.config = { ...step.config, ...additionalConfig };
1465
+ }
1466
+ modifications.push({
1467
+ type: "modify_step",
1468
+ value: additionalConfig,
1469
+ reason: "Applied additional config from planning hints"
1470
+ });
1471
+ }
1472
+ }
1473
+ if (context?.constraints) {
1474
+ const constraintMods = this.applyConstraints(steps, context.constraints);
1475
+ modifications.push(...constraintMods.modifications);
1476
+ steps = constraintMods.steps;
1477
+ }
1478
+ const plan = {
1479
+ id: generateId(),
1480
+ recipeId: recipe.id,
1481
+ variant: recipe.variant,
1482
+ modifications,
1483
+ steps,
1484
+ defaults: recipe.defaults ?? {},
1485
+ reasoning: this.buildPlanReasoning(recipe, modifications, context),
1486
+ createdAt: /* @__PURE__ */ new Date()
1487
+ };
1488
+ plan.resourceEstimate = this.estimateResources(plan);
1489
+ return plan;
1490
+ }
1491
+ /**
1492
+ * Combined operation: select recipe and generate plan.
1493
+ */
1494
+ async plan(workflowKind, input, context) {
1495
+ const selection = await this.selectRecipe(workflowKind, input, context);
1496
+ const plan = await this.generatePlan(selection.recipe, input, context);
1497
+ plan.reasoning = `${selection.reason}. ${plan.reasoning ?? ""}`.trim();
1498
+ return plan;
1499
+ }
1500
+ /**
1501
+ * Validate a plan before execution.
1502
+ */
1503
+ validatePlan(plan) {
1504
+ const errors = [];
1505
+ const warnings = [];
1506
+ if (!plan.steps || plan.steps.length === 0) {
1507
+ errors.push("Plan has no steps");
1508
+ }
1509
+ const stepKeys = /* @__PURE__ */ new Set();
1510
+ for (const step of plan.steps) {
1511
+ if (stepKeys.has(step.key)) {
1512
+ errors.push(`Duplicate step key: ${step.key}`);
1513
+ }
1514
+ stepKeys.add(step.key);
1515
+ }
1516
+ for (const step of plan.steps) {
1517
+ if (!step.key) {
1518
+ errors.push("Step missing key");
1519
+ }
1520
+ if (!step.name) {
1521
+ warnings.push(`Step '${step.key}' missing name`);
1522
+ }
1523
+ if (!step.handlerRef) {
1524
+ errors.push(`Step '${step.key}' missing handlerRef`);
1525
+ }
1526
+ if (this.validateHandlers && this.handlerRegistry) {
1527
+ if (!this.handlerRegistry.has(step.handlerRef)) {
1528
+ errors.push(
1529
+ `Step '${step.key}' references unknown handler: ${step.handlerRef}`
1530
+ );
1531
+ }
1532
+ }
1533
+ }
1534
+ if (plan.childWorkflows) {
1535
+ for (const child of plan.childWorkflows) {
1536
+ if (!child.kind) {
1537
+ errors.push("Child workflow missing kind");
1538
+ }
1539
+ }
1540
+ }
1541
+ return {
1542
+ valid: errors.length === 0,
1543
+ errors,
1544
+ warnings
1545
+ };
1546
+ }
1547
+ /**
1548
+ * Estimate resources required for a plan.
1549
+ */
1550
+ estimateResources(plan) {
1551
+ const {
1552
+ apiCallsPerStep,
1553
+ tokensPerStep,
1554
+ durationPerStep,
1555
+ apiCallsPerChild
1556
+ } = this.resourceEstimates;
1557
+ const stepCount = plan.steps.length;
1558
+ const childCount = plan.childWorkflows?.length ?? 0;
1559
+ return {
1560
+ apiCalls: stepCount * apiCallsPerStep + childCount * apiCallsPerChild,
1561
+ tokens: stepCount * tokensPerStep,
1562
+ duration: stepCount * durationPerStep
1563
+ };
1564
+ }
1565
+ // ============================================================================
1566
+ // Private Helpers
1567
+ // ============================================================================
1568
+ /**
1569
+ * Convert a recipe step to a planned step.
1570
+ */
1571
+ recipeStepToPlannedStep(step) {
1572
+ return {
1573
+ key: step.key,
1574
+ name: step.name,
1575
+ handlerRef: step.handlerRef,
1576
+ config: step.config ?? {},
1577
+ onError: step.onError,
1578
+ maxRetries: step.maxRetries,
1579
+ retryDelay: step.retryDelay,
1580
+ retryBackoff: step.retryBackoff,
1581
+ timeout: step.timeout,
1582
+ skipCondition: step.skipCondition
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Apply constraints to steps and generate modifications.
1587
+ */
1588
+ applyConstraints(steps, constraints) {
1589
+ const modifications = [];
1590
+ const perStepTimeout = constraints.maxDuration ? Math.floor(constraints.maxDuration / steps.length) : void 0;
1591
+ const modifiedSteps = steps.map((step) => {
1592
+ const modified = { ...step };
1593
+ if (constraints.priority === "speed") {
1594
+ modified.timeout = modified.timeout ? Math.min(modified.timeout, 3e4) : 3e4;
1595
+ modified.maxRetries = Math.min(modified.maxRetries ?? 3, 1);
1596
+ }
1597
+ if (constraints.priority === "cost") {
1598
+ modified.maxRetries = 0;
1599
+ }
1600
+ if (perStepTimeout !== void 0) {
1601
+ modified.timeout = modified.timeout ? Math.min(modified.timeout, perStepTimeout) : perStepTimeout;
1602
+ }
1603
+ return modified;
1604
+ });
1605
+ if (constraints.priority === "speed") {
1606
+ modifications.push({
1607
+ type: "set_default",
1608
+ value: { priority: "speed" },
1609
+ reason: "Optimized for speed: reduced timeouts and retries"
1610
+ });
1611
+ }
1612
+ if (constraints.priority === "cost") {
1613
+ modifications.push({
1614
+ type: "set_default",
1615
+ value: { priority: "cost" },
1616
+ reason: "Optimized for cost: disabled retries"
1617
+ });
1618
+ }
1619
+ if (constraints.maxDuration) {
1620
+ modifications.push({
1621
+ type: "set_default",
1622
+ value: { maxDuration: constraints.maxDuration },
1623
+ reason: `Applied duration constraint: ${constraints.maxDuration}ms total`
1624
+ });
1625
+ }
1626
+ return { steps: modifiedSteps, modifications };
1627
+ }
1628
+ /**
1629
+ * Build a human-readable selection reason.
1630
+ */
1631
+ buildSelectionReason(recipe, input) {
1632
+ const parts = [`Selected recipe: ${recipe.id}`];
1633
+ if (recipe.conditions && recipe.conditions.length > 0) {
1634
+ const conditionDescriptions = recipe.conditions.map((c) => {
1635
+ const value = getNestedValue(input, c.field);
1636
+ return `${c.field} ${c.operator} ${JSON.stringify(c.value)} (actual: ${JSON.stringify(value)})`;
1637
+ });
1638
+ parts.push(`Matched conditions: ${conditionDescriptions.join(", ")}`);
1639
+ }
1640
+ if (recipe.priority !== void 0 && recipe.priority > 0) {
1641
+ parts.push(`Priority: ${recipe.priority}`);
1642
+ }
1643
+ return parts.join(". ");
1644
+ }
1645
+ /**
1646
+ * Build reasoning text for a plan.
1647
+ */
1648
+ buildPlanReasoning(recipe, modifications, context) {
1649
+ const parts = [];
1650
+ parts.push(`Using recipe '${recipe.name}' (${recipe.variant} variant)`);
1651
+ if (modifications.length > 0) {
1652
+ parts.push(`Applied ${modifications.length} modification(s)`);
1653
+ }
1654
+ if (context?.constraints) {
1655
+ const constraintList = [];
1656
+ if (context.constraints.priority) {
1657
+ constraintList.push(`priority=${context.constraints.priority}`);
1658
+ }
1659
+ if (context.constraints.maxDuration) {
1660
+ constraintList.push(`maxDuration=${context.constraints.maxDuration}ms`);
1661
+ }
1662
+ if (context.constraints.maxApiCalls) {
1663
+ constraintList.push(`maxApiCalls=${context.constraints.maxApiCalls}`);
1664
+ }
1665
+ if (constraintList.length > 0) {
1666
+ parts.push(`Constraints: ${constraintList.join(", ")}`);
1667
+ }
1668
+ }
1669
+ return parts.join(". ") + ".";
1670
+ }
1671
+ };
1672
+ export {
1673
+ ConsoleLogger,
1674
+ CronScheduler,
1675
+ DEFAULT_RETRY_OPTIONS,
1676
+ MemoryEventTransport,
1677
+ MemoryRecipeRegistry,
1678
+ MemoryStepHandlerRegistry,
1679
+ MemoryStorageAdapter,
1680
+ PostgresSchedulePersistence,
1681
+ PostgresStorageAdapter as PostgresStorage,
1682
+ PostgresStorageAdapter,
1683
+ RuleBasedPlanner,
1684
+ RunNotFoundError,
1685
+ SQLiteSchedulePersistence,
1686
+ SQLiteStorageAdapter,
1687
+ SilentLogger,
1688
+ SocketIOEventTransport,
1689
+ StepError,
1690
+ StepTimeoutError,
1691
+ WaitForRunTimeoutError,
1692
+ WebhookEventTransport,
1693
+ WorkflowAlreadyRegisteredError,
1694
+ WorkflowCanceledError,
1695
+ WorkflowEngine,
1696
+ WorkflowEngineError,
1697
+ WorkflowNotFoundError,
1698
+ WorkflowTimeoutError,
1699
+ calculateRetryDelay,
1700
+ createRegistry,
1701
+ createScopedLogger,
1702
+ generateId,
1703
+ sanitizeErrorForStorage,
1704
+ sleep,
1705
+ withRetry
1706
+ };
1707
+ //# sourceMappingURL=index.js.map