@multiplier-labs/stepflow 0.3.3 → 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-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -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-11_dependency-health.md +93 -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-11_security-audit.md +297 -0
- package/.github/workflows/publish.yml +6 -0
- package/dist/{chunk-4VOKMGP6.js → chunk-5XJBAPXF.js} +517 -522
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-GYPNJBDP.js → chunk-U3PCVK6O.js} +266 -47
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +56 -3
- package/dist/events/index.js +1 -1
- package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
- package/dist/index.d.ts +2 -12
- package/dist/index.js +764 -485
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +313 -47
- package/dist/storage/index.js +4 -2
- package/docs/stepflow-api-reference.md +3 -1
- package/package.json +7 -4
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +70 -0
- package/src/core/engine.ts +91 -185
- 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 +40 -359
- package/src/events/webhook.test.ts +292 -0
- package/src/events/webhook.ts +320 -77
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/planner.ts +1 -83
- package/src/planning/registry.ts +3 -85
- package/src/scheduler/cron.test.ts +9 -1
- package/src/scheduler/cron.ts +26 -2
- package/src/scheduler/postgres-persistence.ts +27 -15
- package/src/scheduler/sqlite-persistence.ts +28 -3
- 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 +171 -690
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +177 -1
- package/src/storage/sqlite.ts +183 -93
- package/src/utils/index.ts +2 -0
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +16 -0
- 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-4VOKMGP6.js.map +0 -1
- package/dist/chunk-GYPNJBDP.js.map +0 -1
- package/dist/chunk-KHTUWLVZ.js.map +0 -1
- package/dist/chunk-ML35PIHX.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -2,24 +2,24 @@ import {
|
|
|
2
2
|
MemoryStorageAdapter,
|
|
3
3
|
PostgresStorageAdapter,
|
|
4
4
|
SQLiteStorageAdapter
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-5XJBAPXF.js";
|
|
6
6
|
import {
|
|
7
7
|
MemoryEventTransport,
|
|
8
8
|
SocketIOEventTransport,
|
|
9
9
|
WebhookEventTransport
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-U3PCVK6O.js";
|
|
11
11
|
import {
|
|
12
12
|
CronScheduler,
|
|
13
13
|
PostgresSchedulePersistence,
|
|
14
14
|
SQLiteSchedulePersistence
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-FMPJBK2N.js";
|
|
16
16
|
import {
|
|
17
17
|
ConsoleLogger,
|
|
18
18
|
SilentLogger,
|
|
19
19
|
createScopedLogger,
|
|
20
20
|
generateId,
|
|
21
21
|
sanitizeErrorForStorage
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-JPXPB7U5.js";
|
|
23
23
|
|
|
24
24
|
// src/utils/errors.ts
|
|
25
25
|
var WorkflowEngineError = class _WorkflowEngineError extends Error {
|
|
@@ -134,6 +134,96 @@ var WorkflowTimeoutError = class extends WorkflowEngineError {
|
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
// src/core/orchestrator/persistence.ts
|
|
138
|
+
async function markRunRunning(storage, runId, isResume) {
|
|
139
|
+
await storage.updateRun(runId, {
|
|
140
|
+
status: "running",
|
|
141
|
+
...isResume ? {} : { startedAt: /* @__PURE__ */ new Date() }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function saveCheckpoint(storage, runId, results, completedSteps) {
|
|
145
|
+
await storage.updateRun(runId, {
|
|
146
|
+
context: { ...results },
|
|
147
|
+
completedSteps: [...completedSteps]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async function markRunSucceeded(storage, runId, results) {
|
|
151
|
+
await storage.updateRun(runId, {
|
|
152
|
+
status: "succeeded",
|
|
153
|
+
context: results,
|
|
154
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function markRunFinal(storage, runId, status, results, error) {
|
|
158
|
+
await storage.updateRun(runId, {
|
|
159
|
+
status,
|
|
160
|
+
context: results,
|
|
161
|
+
error,
|
|
162
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async function createStepRecord(storage, runId, stepKey, stepName, attempt) {
|
|
166
|
+
return storage.createStep({
|
|
167
|
+
runId,
|
|
168
|
+
stepKey,
|
|
169
|
+
stepName,
|
|
170
|
+
status: "running",
|
|
171
|
+
attempt,
|
|
172
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function markStepSucceeded(storage, stepId, result) {
|
|
176
|
+
await storage.updateStep(stepId, {
|
|
177
|
+
status: "succeeded",
|
|
178
|
+
result,
|
|
179
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async function markStepFailed(storage, stepId, error, isCanceled) {
|
|
183
|
+
await storage.updateStep(stepId, {
|
|
184
|
+
status: isCanceled ? "canceled" : "failed",
|
|
185
|
+
error: WorkflowEngineError.fromError(error),
|
|
186
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function markStepCanceled(storage, stepId) {
|
|
190
|
+
await storage.updateStep(stepId, {
|
|
191
|
+
status: "canceled",
|
|
192
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function safeEmitEvent(events, logger, event) {
|
|
196
|
+
try {
|
|
197
|
+
events.emit(event);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.error("Failed to emit event:", error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/core/orchestrator/state-transitions.ts
|
|
204
|
+
function decideRunFinalState(error) {
|
|
205
|
+
if (error instanceof WorkflowTimeoutError) {
|
|
206
|
+
return { status: "failed", eventType: "run.timeout" };
|
|
207
|
+
}
|
|
208
|
+
if (error instanceof WorkflowCanceledError) {
|
|
209
|
+
return { status: "canceled", eventType: "run.canceled" };
|
|
210
|
+
}
|
|
211
|
+
return { status: "failed", eventType: "run.failed" };
|
|
212
|
+
}
|
|
213
|
+
function resolveStepErrorPolicy(step, definition) {
|
|
214
|
+
return {
|
|
215
|
+
onError: step.onError ?? definition.defaultOnError ?? "fail",
|
|
216
|
+
maxRetries: step.maxRetries ?? 3,
|
|
217
|
+
retryDelay: step.retryDelay ?? 1e3,
|
|
218
|
+
retryBackoff: step.retryBackoff ?? 2
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function decideStepFailureAction(policy, attempt) {
|
|
222
|
+
if (policy.onError === "skip") return "skip";
|
|
223
|
+
if (policy.onError === "retry" && attempt <= policy.maxRetries) return "retry";
|
|
224
|
+
return "fail";
|
|
225
|
+
}
|
|
226
|
+
|
|
137
227
|
// src/utils/retry.ts
|
|
138
228
|
var DEFAULT_RETRY_OPTIONS = {
|
|
139
229
|
maxRetries: 3,
|
|
@@ -190,47 +280,241 @@ function calculateRetryDelay(attempt, baseDelay, backoff) {
|
|
|
190
280
|
return Math.round(baseDelay * Math.pow(backoff, attempt - 1));
|
|
191
281
|
}
|
|
192
282
|
|
|
193
|
-
// src/core/orchestrator.ts
|
|
194
|
-
async function
|
|
283
|
+
// src/core/orchestrator/step-runner.ts
|
|
284
|
+
async function executeStep(step, context, options) {
|
|
285
|
+
const { definition, storage, events, logger, abortController } = options;
|
|
286
|
+
const policy = resolveStepErrorPolicy(step, definition);
|
|
287
|
+
let attempt = 0;
|
|
288
|
+
while (true) {
|
|
289
|
+
attempt++;
|
|
290
|
+
throwIfCanceled(abortController, context.runId);
|
|
291
|
+
const stepRecord = await beginStepAttempt(
|
|
292
|
+
storage,
|
|
293
|
+
events,
|
|
294
|
+
logger,
|
|
295
|
+
context,
|
|
296
|
+
step,
|
|
297
|
+
attempt
|
|
298
|
+
);
|
|
299
|
+
if (definition.hooks?.beforeStep) {
|
|
300
|
+
await definition.hooks.beforeStep(context, step);
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const result = await runStepHandler(step, context, abortController);
|
|
304
|
+
await completeStepAttempt(
|
|
305
|
+
storage,
|
|
306
|
+
events,
|
|
307
|
+
logger,
|
|
308
|
+
stepRecord,
|
|
309
|
+
context,
|
|
310
|
+
step,
|
|
311
|
+
result,
|
|
312
|
+
definition
|
|
313
|
+
);
|
|
314
|
+
return result;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const lastError = toError(error);
|
|
317
|
+
const isCanceled = error instanceof WorkflowCanceledError || abortController.signal.aborted;
|
|
318
|
+
await markStepFailed(storage, stepRecord.id, error, isCanceled);
|
|
319
|
+
if (isCanceled) {
|
|
320
|
+
emitStepFailedEvent(events, logger, context, step, lastError, attempt);
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
await runOnStepErrorHook(definition, context, step, lastError, logger);
|
|
324
|
+
emitStepFailedEvent(events, logger, context, step, lastError, attempt);
|
|
325
|
+
const action = decideStepFailureAction(policy, attempt);
|
|
326
|
+
if (action === "skip") {
|
|
327
|
+
handleSkipAction(events, logger, context, step, lastError);
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
if (action === "retry") {
|
|
331
|
+
await handleRetryAction(
|
|
332
|
+
storage,
|
|
333
|
+
events,
|
|
334
|
+
logger,
|
|
335
|
+
abortController,
|
|
336
|
+
context,
|
|
337
|
+
step,
|
|
338
|
+
attempt,
|
|
339
|
+
policy,
|
|
340
|
+
lastError,
|
|
341
|
+
stepRecord.id
|
|
342
|
+
);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
throw new StepError(step.key, lastError.message, attempt, lastError);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function throwIfCanceled(abortController, runId) {
|
|
350
|
+
if (abortController.signal.aborted) {
|
|
351
|
+
throw new WorkflowCanceledError(runId);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function beginStepAttempt(storage, events, logger, context, step, attempt) {
|
|
355
|
+
const stepRecord = await createStepRecord(
|
|
356
|
+
storage,
|
|
357
|
+
context.runId,
|
|
358
|
+
step.key,
|
|
359
|
+
step.name,
|
|
360
|
+
attempt
|
|
361
|
+
);
|
|
362
|
+
context.stepId = stepRecord.id;
|
|
363
|
+
safeEmitEvent(events, logger, {
|
|
364
|
+
runId: context.runId,
|
|
365
|
+
kind: context.kind,
|
|
366
|
+
eventType: "step.started",
|
|
367
|
+
stepKey: step.key,
|
|
368
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
369
|
+
payload: { attempt }
|
|
370
|
+
});
|
|
371
|
+
return stepRecord;
|
|
372
|
+
}
|
|
373
|
+
async function runStepHandler(step, context, abortController) {
|
|
374
|
+
if (step.timeout) {
|
|
375
|
+
return executeWithTimeout(
|
|
376
|
+
() => step.handler(context),
|
|
377
|
+
step.timeout,
|
|
378
|
+
abortController.signal,
|
|
379
|
+
step.key
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return raceWithAbort(step.handler(context), abortController.signal);
|
|
383
|
+
}
|
|
384
|
+
async function completeStepAttempt(storage, events, logger, stepRecord, context, step, result, definition) {
|
|
385
|
+
await markStepSucceeded(storage, stepRecord.id, result);
|
|
386
|
+
if (definition.hooks?.afterStep) {
|
|
387
|
+
await definition.hooks.afterStep(context, step, result);
|
|
388
|
+
}
|
|
389
|
+
safeEmitEvent(events, logger, {
|
|
390
|
+
runId: context.runId,
|
|
391
|
+
kind: context.kind,
|
|
392
|
+
eventType: "step.completed",
|
|
393
|
+
stepKey: step.key,
|
|
394
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
395
|
+
payload: { result }
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function toError(error) {
|
|
399
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
400
|
+
}
|
|
401
|
+
async function runOnStepErrorHook(definition, context, step, lastError, logger) {
|
|
402
|
+
if (!definition.hooks?.onStepError) return;
|
|
403
|
+
try {
|
|
404
|
+
await definition.hooks.onStepError(context, step, lastError);
|
|
405
|
+
} catch (hookError) {
|
|
406
|
+
logger.error("onStepError hook failed for run " + context.runId + ":", hookError);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function emitStepFailedEvent(events, logger, context, step, lastError, attempt) {
|
|
410
|
+
safeEmitEvent(events, logger, {
|
|
411
|
+
runId: context.runId,
|
|
412
|
+
kind: context.kind,
|
|
413
|
+
eventType: "step.failed",
|
|
414
|
+
stepKey: step.key,
|
|
415
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
416
|
+
payload: { error: lastError.message, attempt }
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function handleSkipAction(events, logger, context, step, lastError) {
|
|
420
|
+
logger.warn(`Step "${step.key}" failed, skipping (strategy: skip)`);
|
|
421
|
+
safeEmitEvent(events, logger, {
|
|
422
|
+
runId: context.runId,
|
|
423
|
+
kind: context.kind,
|
|
424
|
+
eventType: "step.skipped",
|
|
425
|
+
stepKey: step.key,
|
|
426
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
427
|
+
payload: { reason: "error", error: lastError.message }
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async function handleRetryAction(storage, events, logger, abortController, context, step, attempt, policy, lastError, stepRecordId) {
|
|
431
|
+
const delay = calculateRetryDelay(attempt, policy.retryDelay, policy.retryBackoff);
|
|
432
|
+
logger.warn(
|
|
433
|
+
`Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${policy.maxRetries})`
|
|
434
|
+
);
|
|
435
|
+
safeEmitEvent(events, logger, {
|
|
436
|
+
runId: context.runId,
|
|
437
|
+
kind: context.kind,
|
|
438
|
+
eventType: "step.retry",
|
|
439
|
+
stepKey: step.key,
|
|
440
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
441
|
+
payload: { attempt, maxRetries: policy.maxRetries, delay, error: lastError.message }
|
|
442
|
+
});
|
|
443
|
+
try {
|
|
444
|
+
await sleep(delay, abortController.signal);
|
|
445
|
+
} catch (sleepError) {
|
|
446
|
+
if (sleepError instanceof WorkflowCanceledError) {
|
|
447
|
+
await markStepCanceled(storage, stepRecordId);
|
|
448
|
+
}
|
|
449
|
+
throw sleepError;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function executeWithTimeout(fn, timeoutMs, signal, stepKey) {
|
|
453
|
+
let timeoutId;
|
|
454
|
+
let onAbort;
|
|
455
|
+
try {
|
|
456
|
+
return await Promise.race([
|
|
457
|
+
fn(),
|
|
458
|
+
new Promise((_, reject) => {
|
|
459
|
+
timeoutId = setTimeout(() => {
|
|
460
|
+
reject(new StepTimeoutError(stepKey, timeoutMs));
|
|
461
|
+
}, timeoutMs);
|
|
462
|
+
onAbort = () => {
|
|
463
|
+
clearTimeout(timeoutId);
|
|
464
|
+
reject(new WorkflowCanceledError("run"));
|
|
465
|
+
};
|
|
466
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
467
|
+
})
|
|
468
|
+
]);
|
|
469
|
+
} finally {
|
|
470
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
471
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function raceWithAbort(promise, signal) {
|
|
475
|
+
if (signal.aborted) {
|
|
476
|
+
throw new WorkflowCanceledError("run");
|
|
477
|
+
}
|
|
478
|
+
let onAbort;
|
|
479
|
+
try {
|
|
480
|
+
return await Promise.race([
|
|
481
|
+
promise,
|
|
482
|
+
new Promise((_, reject) => {
|
|
483
|
+
onAbort = () => reject(new WorkflowCanceledError("run"));
|
|
484
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
485
|
+
})
|
|
486
|
+
]);
|
|
487
|
+
} finally {
|
|
488
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/core/orchestrator/plan-resolution.ts
|
|
493
|
+
function buildContext(options) {
|
|
195
494
|
const {
|
|
196
495
|
runId,
|
|
197
496
|
definition,
|
|
198
497
|
input,
|
|
199
|
-
metadata
|
|
200
|
-
storage,
|
|
498
|
+
metadata,
|
|
201
499
|
events,
|
|
202
500
|
logger,
|
|
203
501
|
abortController,
|
|
204
502
|
spawnChild,
|
|
205
|
-
|
|
503
|
+
checkpointResults
|
|
206
504
|
} = options;
|
|
207
|
-
|
|
208
|
-
const isResume = !!checkpoint;
|
|
209
|
-
await storage.updateRun(runId, {
|
|
210
|
-
status: "running",
|
|
211
|
-
...isResume ? {} : { startedAt: /* @__PURE__ */ new Date() }
|
|
212
|
-
});
|
|
213
|
-
emitEvent(events, logger, {
|
|
214
|
-
runId,
|
|
215
|
-
kind: definition.kind,
|
|
216
|
-
eventType: isResume ? "run.resumed" : "run.started",
|
|
217
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
218
|
-
payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : void 0
|
|
219
|
-
});
|
|
220
|
-
const completedSteps = checkpoint ? Array.from(checkpoint.completedStepKeys) : [];
|
|
221
|
-
const context = {
|
|
505
|
+
return {
|
|
222
506
|
runId,
|
|
223
507
|
stepId: "",
|
|
224
|
-
//
|
|
508
|
+
// set per-step by the step-runner before each handler call
|
|
225
509
|
kind: definition.kind,
|
|
226
510
|
input,
|
|
227
|
-
results:
|
|
511
|
+
results: checkpointResults ? { ...checkpointResults } : {},
|
|
228
512
|
metadata,
|
|
229
513
|
logger: createScopedLogger(logger, runId),
|
|
230
514
|
signal: abortController.signal,
|
|
231
515
|
spawnChild,
|
|
232
516
|
emit: (eventType, payload) => {
|
|
233
|
-
|
|
517
|
+
safeEmitEvent(events, logger, {
|
|
234
518
|
runId,
|
|
235
519
|
kind: definition.kind,
|
|
236
520
|
eventType,
|
|
@@ -239,14 +523,66 @@ async function executeWorkflow(options) {
|
|
|
239
523
|
});
|
|
240
524
|
}
|
|
241
525
|
};
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (definition.timeout) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
526
|
+
}
|
|
527
|
+
function setupWorkflowTimeout(runId, definition, abortController) {
|
|
528
|
+
if (!definition.timeout) {
|
|
529
|
+
return {
|
|
530
|
+
clear: () => {
|
|
531
|
+
},
|
|
532
|
+
getTimeoutError: () => void 0
|
|
533
|
+
};
|
|
249
534
|
}
|
|
535
|
+
let timeoutError;
|
|
536
|
+
const timeoutId = setTimeout(() => {
|
|
537
|
+
timeoutError = new WorkflowTimeoutError(runId, definition.timeout);
|
|
538
|
+
abortController.abort();
|
|
539
|
+
}, definition.timeout);
|
|
540
|
+
return {
|
|
541
|
+
clear: () => clearTimeout(timeoutId),
|
|
542
|
+
getTimeoutError: () => timeoutError
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function initialCompletedSteps(checkpoint) {
|
|
546
|
+
return checkpoint ? Array.from(checkpoint.completedStepKeys) : [];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/core/orchestrator.ts
|
|
550
|
+
async function executeWorkflow(options) {
|
|
551
|
+
const {
|
|
552
|
+
runId,
|
|
553
|
+
definition,
|
|
554
|
+
input,
|
|
555
|
+
metadata = {},
|
|
556
|
+
storage,
|
|
557
|
+
events,
|
|
558
|
+
logger,
|
|
559
|
+
abortController,
|
|
560
|
+
spawnChild,
|
|
561
|
+
checkpoint
|
|
562
|
+
} = options;
|
|
563
|
+
const startTime = Date.now();
|
|
564
|
+
const isResume = !!checkpoint;
|
|
565
|
+
await markRunRunning(storage, runId, isResume);
|
|
566
|
+
safeEmitEvent(events, logger, {
|
|
567
|
+
runId,
|
|
568
|
+
kind: definition.kind,
|
|
569
|
+
eventType: isResume ? "run.resumed" : "run.started",
|
|
570
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
571
|
+
payload: isResume ? { resumedFrom: Array.from(checkpoint.completedStepKeys) } : void 0
|
|
572
|
+
});
|
|
573
|
+
const completedSteps = initialCompletedSteps(checkpoint);
|
|
574
|
+
const context = buildContext({
|
|
575
|
+
runId,
|
|
576
|
+
definition,
|
|
577
|
+
input,
|
|
578
|
+
metadata,
|
|
579
|
+
events,
|
|
580
|
+
logger,
|
|
581
|
+
abortController,
|
|
582
|
+
spawnChild,
|
|
583
|
+
checkpointResults: checkpoint?.results
|
|
584
|
+
});
|
|
585
|
+
const timeoutHandle = setupWorkflowTimeout(runId, definition, abortController);
|
|
250
586
|
try {
|
|
251
587
|
if (definition.hooks?.beforeRun) {
|
|
252
588
|
await definition.hooks.beforeRun(context);
|
|
@@ -258,7 +594,7 @@ async function executeWorkflow(options) {
|
|
|
258
594
|
context.currentStep = step.key;
|
|
259
595
|
if (checkpoint?.completedStepKeys.has(step.key)) {
|
|
260
596
|
logger.info(`Skipping step "${step.key}" (already completed in previous run)`);
|
|
261
|
-
|
|
597
|
+
safeEmitEvent(events, logger, {
|
|
262
598
|
runId,
|
|
263
599
|
kind: definition.kind,
|
|
264
600
|
eventType: "step.skipped",
|
|
@@ -272,7 +608,7 @@ async function executeWorkflow(options) {
|
|
|
272
608
|
const shouldSkip = await step.skipIf(context);
|
|
273
609
|
if (shouldSkip) {
|
|
274
610
|
logger.info(`Skipping step "${step.key}" (condition met)`);
|
|
275
|
-
|
|
611
|
+
safeEmitEvent(events, logger, {
|
|
276
612
|
runId,
|
|
277
613
|
kind: definition.kind,
|
|
278
614
|
eventType: "step.skipped",
|
|
@@ -291,14 +627,9 @@ async function executeWorkflow(options) {
|
|
|
291
627
|
});
|
|
292
628
|
context.results[step.key] = result;
|
|
293
629
|
completedSteps.push(step.key);
|
|
294
|
-
await storage
|
|
295
|
-
context: { ...context.results },
|
|
296
|
-
completedSteps: [...completedSteps]
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
if (workflowTimeoutId) {
|
|
300
|
-
clearTimeout(workflowTimeoutId);
|
|
630
|
+
await saveCheckpoint(storage, runId, context.results, completedSteps);
|
|
301
631
|
}
|
|
632
|
+
timeoutHandle.clear();
|
|
302
633
|
const duration = Date.now() - startTime;
|
|
303
634
|
const runResult = {
|
|
304
635
|
status: "succeeded",
|
|
@@ -308,12 +639,8 @@ async function executeWorkflow(options) {
|
|
|
308
639
|
if (definition.hooks?.afterRun) {
|
|
309
640
|
await definition.hooks.afterRun(context, runResult);
|
|
310
641
|
}
|
|
311
|
-
await storage
|
|
312
|
-
|
|
313
|
-
context: context.results,
|
|
314
|
-
finishedAt: /* @__PURE__ */ new Date()
|
|
315
|
-
});
|
|
316
|
-
emitEvent(events, logger, {
|
|
642
|
+
await markRunSucceeded(storage, runId, context.results);
|
|
643
|
+
safeEmitEvent(events, logger, {
|
|
317
644
|
runId,
|
|
318
645
|
kind: definition.kind,
|
|
319
646
|
eventType: "run.completed",
|
|
@@ -322,16 +649,11 @@ async function executeWorkflow(options) {
|
|
|
322
649
|
});
|
|
323
650
|
return runResult;
|
|
324
651
|
} catch (error) {
|
|
325
|
-
|
|
326
|
-
clearTimeout(workflowTimeoutId);
|
|
327
|
-
}
|
|
652
|
+
timeoutHandle.clear();
|
|
328
653
|
const duration = Date.now() - startTime;
|
|
329
|
-
const actualError =
|
|
654
|
+
const actualError = timeoutHandle.getTimeoutError() ?? error;
|
|
330
655
|
const workflowError = WorkflowEngineError.fromError(actualError);
|
|
331
|
-
const
|
|
332
|
-
const isCanceled = !isTimeout && actualError instanceof WorkflowCanceledError;
|
|
333
|
-
const status = isCanceled ? "canceled" : "failed";
|
|
334
|
-
const eventType = isTimeout ? "run.timeout" : isCanceled ? "run.canceled" : "run.failed";
|
|
656
|
+
const { status, eventType } = decideRunFinalState(actualError);
|
|
335
657
|
const runResult = {
|
|
336
658
|
status,
|
|
337
659
|
results: context.results,
|
|
@@ -345,13 +667,8 @@ async function executeWorkflow(options) {
|
|
|
345
667
|
logger.error("afterRun hook failed for run " + runId + ":", hookError);
|
|
346
668
|
}
|
|
347
669
|
}
|
|
348
|
-
await storage
|
|
349
|
-
|
|
350
|
-
context: context.results,
|
|
351
|
-
error: workflowError,
|
|
352
|
-
finishedAt: /* @__PURE__ */ new Date()
|
|
353
|
-
});
|
|
354
|
-
emitEvent(events, logger, {
|
|
670
|
+
await markRunFinal(storage, runId, status, context.results, workflowError);
|
|
671
|
+
safeEmitEvent(events, logger, {
|
|
355
672
|
runId,
|
|
356
673
|
kind: definition.kind,
|
|
357
674
|
eventType,
|
|
@@ -361,191 +678,272 @@ async function executeWorkflow(options) {
|
|
|
361
678
|
return runResult;
|
|
362
679
|
}
|
|
363
680
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
startedAt: /* @__PURE__ */ new Date()
|
|
384
|
-
});
|
|
385
|
-
context.stepId = stepRecord.id;
|
|
386
|
-
emitEvent(events, logger, {
|
|
387
|
-
runId: context.runId,
|
|
388
|
-
kind: context.kind,
|
|
389
|
-
eventType: "step.started",
|
|
390
|
-
stepKey: step.key,
|
|
391
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
392
|
-
payload: { attempt }
|
|
393
|
-
});
|
|
394
|
-
if (definition.hooks?.beforeStep) {
|
|
395
|
-
await definition.hooks.beforeStep(context, step);
|
|
681
|
+
|
|
682
|
+
// src/core/engine/concurrency.ts
|
|
683
|
+
var ConcurrencyManager = class {
|
|
684
|
+
activeRuns = /* @__PURE__ */ new Map();
|
|
685
|
+
runQueue = [];
|
|
686
|
+
timerHandles = /* @__PURE__ */ new Set();
|
|
687
|
+
storage;
|
|
688
|
+
events;
|
|
689
|
+
logger;
|
|
690
|
+
maxConcurrency;
|
|
691
|
+
constructor(deps) {
|
|
692
|
+
this.storage = deps.storage;
|
|
693
|
+
this.events = deps.events;
|
|
694
|
+
this.logger = deps.logger;
|
|
695
|
+
this.maxConcurrency = deps.maxConcurrency;
|
|
696
|
+
}
|
|
697
|
+
hasCapacity() {
|
|
698
|
+
if (this.maxConcurrency === void 0 || this.maxConcurrency <= 0) {
|
|
699
|
+
return true;
|
|
396
700
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
701
|
+
return this.activeRuns.size < this.maxConcurrency;
|
|
702
|
+
}
|
|
703
|
+
getActiveCount() {
|
|
704
|
+
return this.activeRuns.size;
|
|
705
|
+
}
|
|
706
|
+
getQueuedCount() {
|
|
707
|
+
return this.runQueue.length;
|
|
708
|
+
}
|
|
709
|
+
isActive(runId) {
|
|
710
|
+
return this.activeRuns.has(runId);
|
|
711
|
+
}
|
|
712
|
+
/** Insert a run in priority order (higher priority first, FIFO within same). */
|
|
713
|
+
enqueue(run) {
|
|
714
|
+
let inserted = false;
|
|
715
|
+
for (let i = 0; i < this.runQueue.length; i++) {
|
|
716
|
+
if (run.priority > this.runQueue[i].priority) {
|
|
717
|
+
this.runQueue.splice(i, 0, run);
|
|
718
|
+
inserted = true;
|
|
719
|
+
break;
|
|
411
720
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
721
|
+
}
|
|
722
|
+
if (!inserted) {
|
|
723
|
+
this.runQueue.push(run);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/** Remove a run from the queue if present. Returns true if it was queued. */
|
|
727
|
+
removeFromQueue(runId) {
|
|
728
|
+
const idx = this.runQueue.findIndex((q) => q.runId === runId);
|
|
729
|
+
if (idx === -1) return false;
|
|
730
|
+
this.runQueue.splice(idx, 1);
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
registerActive(runId, controller) {
|
|
734
|
+
this.activeRuns.set(runId, controller);
|
|
735
|
+
}
|
|
736
|
+
unregisterActive(runId) {
|
|
737
|
+
this.activeRuns.delete(runId);
|
|
738
|
+
}
|
|
739
|
+
abortActive(runId) {
|
|
740
|
+
const controller = this.activeRuns.get(runId);
|
|
741
|
+
if (!controller) return false;
|
|
742
|
+
controller.abort();
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Abort every in-flight run. `onAbort` is called for each run id before its
|
|
747
|
+
* controller is signaled (so callers can log, emit, etc.).
|
|
748
|
+
*/
|
|
749
|
+
abortAllActive(onAbort) {
|
|
750
|
+
for (const [runId, controller] of this.activeRuns) {
|
|
751
|
+
if (onAbort) onAbort(runId);
|
|
752
|
+
controller.abort();
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/** Forget all active-run bookkeeping without aborting (used during shutdown). */
|
|
756
|
+
clearActive() {
|
|
757
|
+
this.activeRuns.clear();
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Schedule a launch via setTimeout (when `delay` is truthy) or setImmediate
|
|
761
|
+
* (otherwise). Tracks the handle so it can be cleared during shutdown.
|
|
762
|
+
*/
|
|
763
|
+
scheduleLaunch(fn, delay) {
|
|
764
|
+
if (delay) {
|
|
765
|
+
const handle = setTimeout(fn, delay);
|
|
766
|
+
this.timerHandles.add(handle);
|
|
767
|
+
} else {
|
|
768
|
+
const handle = setImmediate(fn);
|
|
769
|
+
this.timerHandles.add(handle);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
clearTimers() {
|
|
773
|
+
for (const handle of this.timerHandles) {
|
|
774
|
+
if (typeof handle === "object" && "ref" in handle) {
|
|
775
|
+
clearImmediate(handle);
|
|
776
|
+
} else {
|
|
777
|
+
clearTimeout(handle);
|
|
419
778
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
444
|
-
payload: { error: lastError.message, attempt }
|
|
779
|
+
}
|
|
780
|
+
this.timerHandles.clear();
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Drain the queue while capacity is available, dispatching each item via
|
|
784
|
+
* `dispatch(run)`. Each iteration is wrapped in try/catch so that a failure
|
|
785
|
+
* dispatching one queued item (e.g., an event-transport that throws) cannot
|
|
786
|
+
* tear down the loop and leave remaining items stuck in the queue. Failed
|
|
787
|
+
* dispatches are logged, the run is marked as failed in storage as
|
|
788
|
+
* best-effort telemetry, and the loop advances to the next item.
|
|
789
|
+
*
|
|
790
|
+
* Emits `run.dequeued` for each dispatched run (this is what the C3
|
|
791
|
+
* resilience test exercises by having the emit throw).
|
|
792
|
+
*/
|
|
793
|
+
processQueue(dispatch) {
|
|
794
|
+
while (this.hasCapacity() && this.runQueue.length > 0) {
|
|
795
|
+
const next = this.runQueue.shift();
|
|
796
|
+
try {
|
|
797
|
+
this.logger.debug(`Starting queued run ${next.runId}`);
|
|
798
|
+
this.events.emit({
|
|
799
|
+
runId: next.runId,
|
|
800
|
+
kind: next.definition.kind,
|
|
801
|
+
eventType: "run.dequeued",
|
|
802
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
445
803
|
});
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
await definition.hooks.onStepError(context, step, lastError);
|
|
451
|
-
} catch (hookError) {
|
|
452
|
-
logger.error("onStepError hook failed for run " + context.runId + ":", hookError);
|
|
453
|
-
}
|
|
804
|
+
dispatch(next);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
this.recordQueueDispatchFailure(next, err);
|
|
454
807
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Record a queue-dispatch failure: log it, emit a failure event, and
|
|
812
|
+
* best-effort mark the run as failed in storage. All side-effects are
|
|
813
|
+
* defensively wrapped so this method itself never throws.
|
|
814
|
+
*/
|
|
815
|
+
recordQueueDispatchFailure(queued, err) {
|
|
816
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
817
|
+
this.logger.error(`Failed to dispatch queued run ${queued.runId}`, err);
|
|
818
|
+
try {
|
|
819
|
+
this.events.emit({
|
|
820
|
+
runId: queued.runId,
|
|
821
|
+
kind: queued.definition.kind,
|
|
822
|
+
eventType: "run.failed",
|
|
460
823
|
timestamp: /* @__PURE__ */ new Date(),
|
|
461
|
-
payload: { error:
|
|
824
|
+
payload: { error: { code: "QUEUE_DISPATCH_FAILED", message } }
|
|
462
825
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return void 0;
|
|
474
|
-
}
|
|
475
|
-
if (onError === "retry" && attempt <= maxRetries) {
|
|
476
|
-
const delay = calculateRetryDelay(attempt, retryDelay, retryBackoff);
|
|
477
|
-
logger.warn(`Step "${step.key}" failed, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})`);
|
|
478
|
-
emitEvent(events, logger, {
|
|
479
|
-
runId: context.runId,
|
|
480
|
-
kind: context.kind,
|
|
481
|
-
eventType: "step.retry",
|
|
482
|
-
stepKey: step.key,
|
|
483
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
484
|
-
payload: { attempt, maxRetries, delay, error: lastError.message }
|
|
485
|
-
});
|
|
486
|
-
try {
|
|
487
|
-
await sleep(delay, abortController.signal);
|
|
488
|
-
} catch (sleepError) {
|
|
489
|
-
if (sleepError instanceof WorkflowCanceledError) {
|
|
490
|
-
await storage.updateStep(stepRecord.id, {
|
|
491
|
-
status: "canceled",
|
|
492
|
-
finishedAt: /* @__PURE__ */ new Date()
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
throw sleepError;
|
|
496
|
-
}
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
throw new StepError(step.key, lastError.message, attempt, lastError);
|
|
500
|
-
}
|
|
826
|
+
} catch (emitErr) {
|
|
827
|
+
this.logger.error(`Failed to emit dispatch-failure event for ${queued.runId}`, emitErr);
|
|
828
|
+
}
|
|
829
|
+
this.storage.updateRun(queued.runId, {
|
|
830
|
+
status: "failed",
|
|
831
|
+
finishedAt: /* @__PURE__ */ new Date(),
|
|
832
|
+
error: { code: "QUEUE_DISPATCH_FAILED", message }
|
|
833
|
+
}).catch((updateErr) => {
|
|
834
|
+
this.logger.error(`Failed to record dispatch failure for ${queued.runId}`, updateErr);
|
|
835
|
+
});
|
|
501
836
|
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
]);
|
|
520
|
-
} finally {
|
|
521
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
522
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// src/core/engine/lifecycle.ts
|
|
840
|
+
var LifecycleManager = class {
|
|
841
|
+
storage;
|
|
842
|
+
events;
|
|
843
|
+
logger;
|
|
844
|
+
registry;
|
|
845
|
+
concurrency;
|
|
846
|
+
launchRun;
|
|
847
|
+
constructor(deps) {
|
|
848
|
+
this.storage = deps.storage;
|
|
849
|
+
this.events = deps.events;
|
|
850
|
+
this.logger = deps.logger;
|
|
851
|
+
this.registry = deps.registry;
|
|
852
|
+
this.concurrency = deps.concurrency;
|
|
853
|
+
this.launchRun = deps.launchRun;
|
|
523
854
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
855
|
+
/**
|
|
856
|
+
* Initialize the engine's storage adapter if it requires it.
|
|
857
|
+
* Safe to call when the adapter has no `initialize` hook.
|
|
858
|
+
*/
|
|
859
|
+
async initialize() {
|
|
860
|
+
if (this.storage.initialize) {
|
|
861
|
+
await this.storage.initialize();
|
|
862
|
+
}
|
|
528
863
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
864
|
+
/**
|
|
865
|
+
* Shutdown the engine gracefully:
|
|
866
|
+
* 1. clear pending timer handles (so no further launches fire)
|
|
867
|
+
* 2. abort all in-flight runs
|
|
868
|
+
* 3. close the event transport
|
|
869
|
+
* 4. close the storage adapter
|
|
870
|
+
* 5. drop active-run bookkeeping
|
|
871
|
+
*/
|
|
872
|
+
async shutdown() {
|
|
873
|
+
this.logger.info("Shutting down workflow engine...");
|
|
874
|
+
this.concurrency.clearTimers();
|
|
875
|
+
this.concurrency.abortAllActive((runId) => {
|
|
876
|
+
this.logger.debug(`Canceling active run: ${runId}`);
|
|
877
|
+
});
|
|
878
|
+
if (this.events.close) {
|
|
879
|
+
this.events.close();
|
|
880
|
+
}
|
|
881
|
+
if (this.storage.close) {
|
|
882
|
+
await this.storage.close();
|
|
883
|
+
}
|
|
884
|
+
this.concurrency.clearActive();
|
|
885
|
+
this.logger.info("Workflow engine shutdown complete");
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Resume an interrupted workflow run from its checkpoint.
|
|
889
|
+
* The run must be in 'queued' or 'running' status.
|
|
890
|
+
*/
|
|
891
|
+
async resumeRun(runId) {
|
|
892
|
+
const run = await this.storage.getRun(runId);
|
|
893
|
+
if (!run) {
|
|
894
|
+
throw new RunNotFoundError(runId);
|
|
895
|
+
}
|
|
896
|
+
if (!["queued", "running"].includes(run.status)) {
|
|
897
|
+
throw new Error(`Cannot resume run ${runId}: status is "${run.status}"`);
|
|
898
|
+
}
|
|
899
|
+
const definition = this.registry.get(run.kind);
|
|
900
|
+
if (!definition) {
|
|
901
|
+
throw new WorkflowNotFoundError(run.kind);
|
|
902
|
+
}
|
|
903
|
+
if (this.concurrency.isActive(runId)) {
|
|
904
|
+
this.logger.warn(`Run ${runId} is already active, skipping resume`);
|
|
905
|
+
return runId;
|
|
906
|
+
}
|
|
907
|
+
this.logger.info(`Resuming run ${runId} from checkpoint`);
|
|
908
|
+
const completedStepKeys = new Set(run.completedSteps ?? []);
|
|
909
|
+
this.launchRun(runId, definition, run.input, run.metadata, void 0, {
|
|
910
|
+
completedStepKeys,
|
|
911
|
+
results: run.context
|
|
912
|
+
});
|
|
913
|
+
return runId;
|
|
540
914
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
915
|
+
/**
|
|
916
|
+
* Get all runs that were interrupted and can be resumed.
|
|
917
|
+
* Returns runs with status 'queued' or 'running'.
|
|
918
|
+
*/
|
|
919
|
+
async getResumableRuns() {
|
|
920
|
+
const result = await this.storage.listRuns({
|
|
921
|
+
status: ["queued", "running"]
|
|
922
|
+
});
|
|
923
|
+
return result.items;
|
|
547
924
|
}
|
|
548
|
-
|
|
925
|
+
/**
|
|
926
|
+
* Resume every interrupted run whose workflow kind is currently registered.
|
|
927
|
+
* Useful for recovering after a server restart.
|
|
928
|
+
*/
|
|
929
|
+
async resumeAllInterrupted() {
|
|
930
|
+
const resumableRuns = await this.getResumableRuns();
|
|
931
|
+
const resumedIds = [];
|
|
932
|
+
for (const run of resumableRuns) {
|
|
933
|
+
if (this.registry.has(run.kind)) {
|
|
934
|
+
try {
|
|
935
|
+
await this.resumeRun(run.id);
|
|
936
|
+
resumedIds.push(run.id);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
this.logger.error(`Failed to resume run ${run.id}:`, error);
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
this.logger.warn(`Cannot resume run ${run.id}: workflow "${run.kind}" not registered`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return resumedIds;
|
|
945
|
+
}
|
|
946
|
+
};
|
|
549
947
|
|
|
550
948
|
// src/core/engine.ts
|
|
551
949
|
var WorkflowEngine = class _WorkflowEngine {
|
|
@@ -553,14 +951,28 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
553
951
|
storage;
|
|
554
952
|
events;
|
|
555
953
|
logger;
|
|
556
|
-
activeRuns = /* @__PURE__ */ new Map();
|
|
557
954
|
settings;
|
|
558
|
-
|
|
955
|
+
concurrency;
|
|
956
|
+
lifecycle;
|
|
559
957
|
constructor(config = {}) {
|
|
560
958
|
this.storage = config.storage ?? new MemoryStorageAdapter();
|
|
561
959
|
this.events = config.events ?? new MemoryEventTransport();
|
|
562
960
|
this.logger = config.logger ?? new ConsoleLogger();
|
|
563
961
|
this.settings = config.settings ?? {};
|
|
962
|
+
this.concurrency = new ConcurrencyManager({
|
|
963
|
+
storage: this.storage,
|
|
964
|
+
events: this.events,
|
|
965
|
+
logger: this.logger,
|
|
966
|
+
maxConcurrency: this.settings.maxConcurrency
|
|
967
|
+
});
|
|
968
|
+
this.lifecycle = new LifecycleManager({
|
|
969
|
+
storage: this.storage,
|
|
970
|
+
events: this.events,
|
|
971
|
+
logger: this.logger,
|
|
972
|
+
registry: this.registry,
|
|
973
|
+
concurrency: this.concurrency,
|
|
974
|
+
launchRun: (runId, def, input, metadata, delay, checkpoint) => this.launchRun(runId, def, input, metadata, delay, checkpoint)
|
|
975
|
+
});
|
|
564
976
|
}
|
|
565
977
|
/**
|
|
566
978
|
* Initialize the engine and its storage/event adapters.
|
|
@@ -568,31 +980,19 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
568
980
|
* (e.g., PostgresStorageAdapter).
|
|
569
981
|
*/
|
|
570
982
|
async initialize() {
|
|
571
|
-
|
|
572
|
-
await this.storage.initialize();
|
|
573
|
-
}
|
|
983
|
+
return this.lifecycle.initialize();
|
|
574
984
|
}
|
|
575
985
|
/**
|
|
576
986
|
* Get the current number of active runs.
|
|
577
987
|
*/
|
|
578
988
|
getActiveRunCount() {
|
|
579
|
-
return this.
|
|
989
|
+
return this.concurrency.getActiveCount();
|
|
580
990
|
}
|
|
581
991
|
/**
|
|
582
992
|
* Get the number of queued runs waiting for capacity.
|
|
583
993
|
*/
|
|
584
994
|
getQueuedRunCount() {
|
|
585
|
-
return this.
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Check if capacity is available for a new run.
|
|
589
|
-
*/
|
|
590
|
-
hasCapacity() {
|
|
591
|
-
const maxConcurrency = this.settings.maxConcurrency;
|
|
592
|
-
if (maxConcurrency === void 0 || maxConcurrency <= 0) {
|
|
593
|
-
return true;
|
|
594
|
-
}
|
|
595
|
-
return this.activeRuns.size < maxConcurrency;
|
|
995
|
+
return this.concurrency.getQueuedCount();
|
|
596
996
|
}
|
|
597
997
|
// ============================================================================
|
|
598
998
|
// Workflow Registration
|
|
@@ -672,7 +1072,7 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
672
1072
|
timestamp: /* @__PURE__ */ new Date(),
|
|
673
1073
|
payload: { input: options.input, metadata: options.metadata, priority: options.priority ?? 0 }
|
|
674
1074
|
});
|
|
675
|
-
if (this.hasCapacity()) {
|
|
1075
|
+
if (this.concurrency.hasCapacity()) {
|
|
676
1076
|
this.executeRun(
|
|
677
1077
|
runId,
|
|
678
1078
|
definition,
|
|
@@ -681,7 +1081,7 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
681
1081
|
options.delay
|
|
682
1082
|
);
|
|
683
1083
|
} else {
|
|
684
|
-
this.
|
|
1084
|
+
this.concurrency.enqueue({
|
|
685
1085
|
runId,
|
|
686
1086
|
definition,
|
|
687
1087
|
input: options.input ?? {},
|
|
@@ -689,33 +1089,17 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
689
1089
|
priority: options.priority ?? 0,
|
|
690
1090
|
queuedAt: /* @__PURE__ */ new Date()
|
|
691
1091
|
});
|
|
692
|
-
this.logger.debug(`Run ${runId} queued (${this.
|
|
1092
|
+
this.logger.debug(`Run ${runId} queued (${this.concurrency.getQueuedCount()} in queue)`);
|
|
693
1093
|
this.events.emit({
|
|
694
1094
|
runId,
|
|
695
1095
|
kind: options.kind,
|
|
696
1096
|
eventType: "run.queued",
|
|
697
1097
|
timestamp: /* @__PURE__ */ new Date(),
|
|
698
|
-
payload: { queuePosition: this.
|
|
1098
|
+
payload: { queuePosition: this.concurrency.getQueuedCount() }
|
|
699
1099
|
});
|
|
700
1100
|
}
|
|
701
1101
|
return runId;
|
|
702
1102
|
}
|
|
703
|
-
/**
|
|
704
|
-
* Queue a run in priority order.
|
|
705
|
-
*/
|
|
706
|
-
queueRun(queuedRun) {
|
|
707
|
-
let inserted = false;
|
|
708
|
-
for (let i = 0; i < this.runQueue.length; i++) {
|
|
709
|
-
if (queuedRun.priority > this.runQueue[i].priority) {
|
|
710
|
-
this.runQueue.splice(i, 0, queuedRun);
|
|
711
|
-
inserted = true;
|
|
712
|
-
break;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if (!inserted) {
|
|
716
|
-
this.runQueue.push(queuedRun);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
1103
|
/**
|
|
720
1104
|
* Execute a run (internal method).
|
|
721
1105
|
*/
|
|
@@ -728,7 +1112,7 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
728
1112
|
*/
|
|
729
1113
|
launchRun(runId, definition, input, metadata, delay, checkpoint) {
|
|
730
1114
|
const abortController = new AbortController();
|
|
731
|
-
this.
|
|
1115
|
+
this.concurrency.registerActive(runId, abortController);
|
|
732
1116
|
const execute = async () => {
|
|
733
1117
|
try {
|
|
734
1118
|
await executeWorkflow({
|
|
@@ -744,36 +1128,26 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
744
1128
|
checkpoint
|
|
745
1129
|
});
|
|
746
1130
|
} finally {
|
|
747
|
-
this.
|
|
1131
|
+
this.concurrency.unregisterActive(runId);
|
|
748
1132
|
this.processQueue();
|
|
749
1133
|
}
|
|
750
1134
|
};
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1135
|
+
const guarded = () => {
|
|
1136
|
+
execute().catch((err) => {
|
|
1137
|
+
this.concurrency.unregisterActive(runId);
|
|
1138
|
+
this.logger.error(`Unhandled error launching run ${runId}`, err);
|
|
1139
|
+
});
|
|
1140
|
+
};
|
|
1141
|
+
this.concurrency.scheduleLaunch(guarded, delay);
|
|
756
1142
|
}
|
|
757
1143
|
/**
|
|
758
|
-
*
|
|
1144
|
+
* Drain the queue, dispatching each ready run via `executeRun`. Resilience
|
|
1145
|
+
* against per-item dispatch failures lives in `ConcurrencyManager.processQueue`.
|
|
759
1146
|
*/
|
|
760
1147
|
processQueue() {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
this.events.emit({
|
|
765
|
-
runId: next.runId,
|
|
766
|
-
kind: next.definition.kind,
|
|
767
|
-
eventType: "run.dequeued",
|
|
768
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
769
|
-
});
|
|
770
|
-
this.executeRun(
|
|
771
|
-
next.runId,
|
|
772
|
-
next.definition,
|
|
773
|
-
next.input,
|
|
774
|
-
next.metadata
|
|
775
|
-
);
|
|
776
|
-
}
|
|
1148
|
+
this.concurrency.processQueue((next) => {
|
|
1149
|
+
this.executeRun(next.runId, next.definition, next.input, next.metadata);
|
|
1150
|
+
});
|
|
777
1151
|
}
|
|
778
1152
|
/**
|
|
779
1153
|
* Start a child workflow from within a parent workflow.
|
|
@@ -802,14 +1176,8 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
802
1176
|
if (["succeeded", "failed", "canceled", "timeout"].includes(run.status)) {
|
|
803
1177
|
return;
|
|
804
1178
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
this.runQueue.splice(queueIndex, 1);
|
|
808
|
-
}
|
|
809
|
-
const controller = this.activeRuns.get(runId);
|
|
810
|
-
if (controller) {
|
|
811
|
-
controller.abort();
|
|
812
|
-
}
|
|
1179
|
+
this.concurrency.removeFromQueue(runId);
|
|
1180
|
+
this.concurrency.abortActive(runId);
|
|
813
1181
|
await this.storage.updateRun(runId, {
|
|
814
1182
|
status: "canceled",
|
|
815
1183
|
finishedAt: /* @__PURE__ */ new Date()
|
|
@@ -843,39 +1211,52 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
843
1211
|
*/
|
|
844
1212
|
async waitForRun(runId, options = {}) {
|
|
845
1213
|
const timeout = options.timeout ?? 6e4;
|
|
846
|
-
|
|
847
|
-
if (!existingRun) {
|
|
848
|
-
throw new RunNotFoundError(runId);
|
|
849
|
-
}
|
|
850
|
-
if (_WorkflowEngine.TERMINAL_STATUSES.includes(existingRun.status)) {
|
|
851
|
-
return existingRun;
|
|
852
|
-
}
|
|
853
|
-
return new Promise((resolve, reject) => {
|
|
1214
|
+
return new Promise(async (resolve, reject) => {
|
|
854
1215
|
let timeoutId;
|
|
855
1216
|
let unsubscribe;
|
|
1217
|
+
let settled = false;
|
|
856
1218
|
const cleanup = () => {
|
|
857
1219
|
if (timeoutId) clearTimeout(timeoutId);
|
|
858
1220
|
if (unsubscribe) unsubscribe();
|
|
859
1221
|
};
|
|
860
|
-
|
|
1222
|
+
const settle = (fn) => {
|
|
1223
|
+
if (settled) return;
|
|
1224
|
+
settled = true;
|
|
861
1225
|
cleanup();
|
|
862
|
-
|
|
1226
|
+
fn();
|
|
1227
|
+
};
|
|
1228
|
+
timeoutId = setTimeout(() => {
|
|
1229
|
+
settle(() => reject(new WaitForRunTimeoutError(runId, timeout)));
|
|
863
1230
|
}, timeout);
|
|
864
1231
|
unsubscribe = this.events.subscribe(runId, async (event) => {
|
|
865
1232
|
if (_WorkflowEngine.TERMINAL_EVENT_TYPES.includes(event.eventType)) {
|
|
866
|
-
cleanup();
|
|
867
1233
|
try {
|
|
868
1234
|
const run = await this.storage.getRun(runId);
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1235
|
+
settle(() => {
|
|
1236
|
+
if (!run) {
|
|
1237
|
+
reject(new RunNotFoundError(runId));
|
|
1238
|
+
} else {
|
|
1239
|
+
resolve(run);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
874
1242
|
} catch (err) {
|
|
875
|
-
reject(err);
|
|
1243
|
+
settle(() => reject(err));
|
|
876
1244
|
}
|
|
877
1245
|
}
|
|
878
1246
|
});
|
|
1247
|
+
try {
|
|
1248
|
+
const existingRun = await this.storage.getRun(runId);
|
|
1249
|
+
if (!existingRun) {
|
|
1250
|
+
settle(() => reject(new RunNotFoundError(runId)));
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (_WorkflowEngine.TERMINAL_STATUSES.includes(existingRun.status)) {
|
|
1254
|
+
settle(() => resolve(existingRun));
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
settle(() => reject(err));
|
|
1259
|
+
}
|
|
879
1260
|
});
|
|
880
1261
|
}
|
|
881
1262
|
// ============================================================================
|
|
@@ -892,38 +1273,14 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
892
1273
|
* @throws {WorkflowNotFoundError} If the workflow kind is not registered
|
|
893
1274
|
*/
|
|
894
1275
|
async resumeRun(runId) {
|
|
895
|
-
|
|
896
|
-
if (!run) {
|
|
897
|
-
throw new RunNotFoundError(runId);
|
|
898
|
-
}
|
|
899
|
-
if (!["queued", "running"].includes(run.status)) {
|
|
900
|
-
throw new Error(`Cannot resume run ${runId}: status is "${run.status}"`);
|
|
901
|
-
}
|
|
902
|
-
const definition = this.registry.get(run.kind);
|
|
903
|
-
if (!definition) {
|
|
904
|
-
throw new WorkflowNotFoundError(run.kind);
|
|
905
|
-
}
|
|
906
|
-
if (this.activeRuns.has(runId)) {
|
|
907
|
-
this.logger.warn(`Run ${runId} is already active, skipping resume`);
|
|
908
|
-
return runId;
|
|
909
|
-
}
|
|
910
|
-
this.logger.info(`Resuming run ${runId} from checkpoint`);
|
|
911
|
-
const completedStepKeys = new Set(run.completedSteps ?? []);
|
|
912
|
-
this.launchRun(runId, definition, run.input, run.metadata, void 0, {
|
|
913
|
-
completedStepKeys,
|
|
914
|
-
results: run.context
|
|
915
|
-
});
|
|
916
|
-
return runId;
|
|
1276
|
+
return this.lifecycle.resumeRun(runId);
|
|
917
1277
|
}
|
|
918
1278
|
/**
|
|
919
1279
|
* Get all runs that were interrupted and can be resumed.
|
|
920
1280
|
* Returns runs with status 'queued' or 'running'.
|
|
921
1281
|
*/
|
|
922
1282
|
async getResumableRuns() {
|
|
923
|
-
|
|
924
|
-
status: ["queued", "running"]
|
|
925
|
-
});
|
|
926
|
-
return result.items;
|
|
1283
|
+
return this.lifecycle.getResumableRuns();
|
|
927
1284
|
}
|
|
928
1285
|
/**
|
|
929
1286
|
* Resume all interrupted runs.
|
|
@@ -932,21 +1289,7 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
932
1289
|
* @returns Array of resumed run IDs
|
|
933
1290
|
*/
|
|
934
1291
|
async resumeAllInterrupted() {
|
|
935
|
-
|
|
936
|
-
const resumedIds = [];
|
|
937
|
-
for (const run of resumableRuns) {
|
|
938
|
-
if (this.registry.has(run.kind)) {
|
|
939
|
-
try {
|
|
940
|
-
await this.resumeRun(run.id);
|
|
941
|
-
resumedIds.push(run.id);
|
|
942
|
-
} catch (error) {
|
|
943
|
-
this.logger.error(`Failed to resume run ${run.id}:`, error);
|
|
944
|
-
}
|
|
945
|
-
} else {
|
|
946
|
-
this.logger.warn(`Cannot resume run ${run.id}: workflow "${run.kind}" not registered`);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
return resumedIds;
|
|
1292
|
+
return this.lifecycle.resumeAllInterrupted();
|
|
950
1293
|
}
|
|
951
1294
|
// ============================================================================
|
|
952
1295
|
// Event Subscription
|
|
@@ -995,24 +1338,65 @@ var WorkflowEngine = class _WorkflowEngine {
|
|
|
995
1338
|
* Cancels all active runs and closes resources.
|
|
996
1339
|
*/
|
|
997
1340
|
async shutdown() {
|
|
998
|
-
this.
|
|
999
|
-
for (const [runId, controller] of this.activeRuns) {
|
|
1000
|
-
this.logger.debug(`Canceling active run: ${runId}`);
|
|
1001
|
-
controller.abort();
|
|
1002
|
-
}
|
|
1003
|
-
if (this.events.close) {
|
|
1004
|
-
this.events.close();
|
|
1005
|
-
}
|
|
1006
|
-
if (this.storage.close) {
|
|
1007
|
-
await this.storage.close();
|
|
1008
|
-
}
|
|
1009
|
-
this.activeRuns.clear();
|
|
1010
|
-
this.logger.info("Workflow engine shutdown complete");
|
|
1341
|
+
return this.lifecycle.shutdown();
|
|
1011
1342
|
}
|
|
1012
1343
|
};
|
|
1013
1344
|
|
|
1014
|
-
// src/planning/
|
|
1345
|
+
// src/planning/evaluate-condition.ts
|
|
1015
1346
|
import RE2 from "re2";
|
|
1347
|
+
var MAX_REGEX_PATTERN_LENGTH = 512;
|
|
1348
|
+
function getNestedValue(obj, path) {
|
|
1349
|
+
return path.split(".").reduce((current, key) => {
|
|
1350
|
+
if (current && typeof current === "object" && key in current) {
|
|
1351
|
+
return current[key];
|
|
1352
|
+
}
|
|
1353
|
+
return void 0;
|
|
1354
|
+
}, obj);
|
|
1355
|
+
}
|
|
1356
|
+
function evaluateCondition(operator, fieldValue, conditionValue) {
|
|
1357
|
+
switch (operator) {
|
|
1358
|
+
case "eq":
|
|
1359
|
+
return fieldValue === conditionValue;
|
|
1360
|
+
case "neq":
|
|
1361
|
+
return fieldValue !== conditionValue;
|
|
1362
|
+
case "gt":
|
|
1363
|
+
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue > conditionValue : false;
|
|
1364
|
+
case "gte":
|
|
1365
|
+
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue >= conditionValue : false;
|
|
1366
|
+
case "lt":
|
|
1367
|
+
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue < conditionValue : false;
|
|
1368
|
+
case "lte":
|
|
1369
|
+
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue <= conditionValue : false;
|
|
1370
|
+
case "contains":
|
|
1371
|
+
if (typeof fieldValue === "string" && typeof conditionValue === "string") {
|
|
1372
|
+
return fieldValue.includes(conditionValue);
|
|
1373
|
+
}
|
|
1374
|
+
if (Array.isArray(fieldValue)) {
|
|
1375
|
+
return fieldValue.includes(conditionValue);
|
|
1376
|
+
}
|
|
1377
|
+
return false;
|
|
1378
|
+
case "matches":
|
|
1379
|
+
if (typeof fieldValue === "string" && typeof conditionValue === "string") {
|
|
1380
|
+
if (conditionValue.length > MAX_REGEX_PATTERN_LENGTH) {
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
return new RE2(conditionValue).test(fieldValue);
|
|
1385
|
+
} catch {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return false;
|
|
1390
|
+
case "exists":
|
|
1391
|
+
return fieldValue !== void 0 && fieldValue !== null && fieldValue !== "";
|
|
1392
|
+
case "notExists":
|
|
1393
|
+
return fieldValue === void 0 || fieldValue === null || fieldValue === "";
|
|
1394
|
+
default:
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/planning/registry.ts
|
|
1016
1400
|
var MemoryStepHandlerRegistry = class {
|
|
1017
1401
|
handlers = /* @__PURE__ */ new Map();
|
|
1018
1402
|
tagIndex = /* @__PURE__ */ new Map();
|
|
@@ -1183,71 +1567,14 @@ var MemoryRecipeRegistry = class {
|
|
|
1183
1567
|
/**
|
|
1184
1568
|
* Evaluate recipe conditions against an input.
|
|
1185
1569
|
* Returns true if all conditions match.
|
|
1186
|
-
*
|
|
1187
|
-
* Note: This intentionally mirrors the condition evaluation in planner.ts
|
|
1188
|
-
* to keep registry queries independent of the planner implementation.
|
|
1189
1570
|
*/
|
|
1190
1571
|
evaluateConditions(conditions, input) {
|
|
1191
1572
|
if (!conditions || conditions.length === 0) return true;
|
|
1192
1573
|
return conditions.every((condition) => {
|
|
1193
|
-
const fieldValue =
|
|
1194
|
-
return
|
|
1574
|
+
const fieldValue = getNestedValue(input, condition.field);
|
|
1575
|
+
return evaluateCondition(condition.operator, fieldValue, condition.value);
|
|
1195
1576
|
});
|
|
1196
1577
|
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Get a nested value from an object using dot notation.
|
|
1199
|
-
*/
|
|
1200
|
-
getNestedValue(obj, path) {
|
|
1201
|
-
return path.split(".").reduce((current, key) => {
|
|
1202
|
-
if (current && typeof current === "object" && key in current) {
|
|
1203
|
-
return current[key];
|
|
1204
|
-
}
|
|
1205
|
-
return void 0;
|
|
1206
|
-
}, obj);
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Evaluate a single condition.
|
|
1210
|
-
*/
|
|
1211
|
-
evaluateCondition(condition, fieldValue) {
|
|
1212
|
-
const { operator, value } = condition;
|
|
1213
|
-
switch (operator) {
|
|
1214
|
-
case "eq":
|
|
1215
|
-
return fieldValue === value;
|
|
1216
|
-
case "neq":
|
|
1217
|
-
return fieldValue !== value;
|
|
1218
|
-
case "gt":
|
|
1219
|
-
return typeof fieldValue === "number" && typeof value === "number" ? fieldValue > value : false;
|
|
1220
|
-
case "gte":
|
|
1221
|
-
return typeof fieldValue === "number" && typeof value === "number" ? fieldValue >= value : false;
|
|
1222
|
-
case "lt":
|
|
1223
|
-
return typeof fieldValue === "number" && typeof value === "number" ? fieldValue < value : false;
|
|
1224
|
-
case "lte":
|
|
1225
|
-
return typeof fieldValue === "number" && typeof value === "number" ? fieldValue <= value : false;
|
|
1226
|
-
case "contains":
|
|
1227
|
-
if (typeof fieldValue === "string" && typeof value === "string") {
|
|
1228
|
-
return fieldValue.includes(value);
|
|
1229
|
-
}
|
|
1230
|
-
if (Array.isArray(fieldValue)) {
|
|
1231
|
-
return fieldValue.includes(value);
|
|
1232
|
-
}
|
|
1233
|
-
return false;
|
|
1234
|
-
case "matches":
|
|
1235
|
-
if (typeof fieldValue === "string" && typeof value === "string") {
|
|
1236
|
-
try {
|
|
1237
|
-
return new RE2(value).test(fieldValue);
|
|
1238
|
-
} catch {
|
|
1239
|
-
return false;
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
return false;
|
|
1243
|
-
case "exists":
|
|
1244
|
-
return fieldValue !== void 0 && fieldValue !== null && fieldValue !== "";
|
|
1245
|
-
case "notExists":
|
|
1246
|
-
return fieldValue === void 0 || fieldValue === null || fieldValue === "";
|
|
1247
|
-
default:
|
|
1248
|
-
return false;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
1578
|
};
|
|
1252
1579
|
function createRegistry() {
|
|
1253
1580
|
return {
|
|
@@ -1257,54 +1584,6 @@ function createRegistry() {
|
|
|
1257
1584
|
}
|
|
1258
1585
|
|
|
1259
1586
|
// src/planning/planner.ts
|
|
1260
|
-
import RE22 from "re2";
|
|
1261
|
-
function getNestedValue(obj, path) {
|
|
1262
|
-
return path.split(".").reduce((current, key) => {
|
|
1263
|
-
if (current && typeof current === "object" && key in current) {
|
|
1264
|
-
return current[key];
|
|
1265
|
-
}
|
|
1266
|
-
return void 0;
|
|
1267
|
-
}, obj);
|
|
1268
|
-
}
|
|
1269
|
-
function evaluateCondition(operator, fieldValue, conditionValue) {
|
|
1270
|
-
switch (operator) {
|
|
1271
|
-
case "eq":
|
|
1272
|
-
return fieldValue === conditionValue;
|
|
1273
|
-
case "neq":
|
|
1274
|
-
return fieldValue !== conditionValue;
|
|
1275
|
-
case "gt":
|
|
1276
|
-
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue > conditionValue : false;
|
|
1277
|
-
case "gte":
|
|
1278
|
-
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue >= conditionValue : false;
|
|
1279
|
-
case "lt":
|
|
1280
|
-
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue < conditionValue : false;
|
|
1281
|
-
case "lte":
|
|
1282
|
-
return typeof fieldValue === "number" && typeof conditionValue === "number" ? fieldValue <= conditionValue : false;
|
|
1283
|
-
case "contains":
|
|
1284
|
-
if (typeof fieldValue === "string" && typeof conditionValue === "string") {
|
|
1285
|
-
return fieldValue.includes(conditionValue);
|
|
1286
|
-
}
|
|
1287
|
-
if (Array.isArray(fieldValue)) {
|
|
1288
|
-
return fieldValue.includes(conditionValue);
|
|
1289
|
-
}
|
|
1290
|
-
return false;
|
|
1291
|
-
case "matches":
|
|
1292
|
-
if (typeof fieldValue === "string" && typeof conditionValue === "string") {
|
|
1293
|
-
try {
|
|
1294
|
-
return new RE22(conditionValue).test(fieldValue);
|
|
1295
|
-
} catch {
|
|
1296
|
-
return false;
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
return false;
|
|
1300
|
-
case "exists":
|
|
1301
|
-
return fieldValue !== void 0 && fieldValue !== null && fieldValue !== "";
|
|
1302
|
-
case "notExists":
|
|
1303
|
-
return fieldValue === void 0 || fieldValue === null || fieldValue === "";
|
|
1304
|
-
default:
|
|
1305
|
-
return false;
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
1587
|
function scoreConditions(conditions, input) {
|
|
1309
1588
|
if (!conditions || conditions.length === 0) {
|
|
1310
1589
|
return 10;
|