@bratsos/workflow-engine 0.1.0 → 0.2.0

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 (37) hide show
  1. package/README.md +270 -513
  2. package/dist/chunk-HL3OJG7W.js +1033 -0
  3. package/dist/chunk-HL3OJG7W.js.map +1 -0
  4. package/dist/{chunk-7IITBLFY.js → chunk-NYKMT46J.js} +268 -25
  5. package/dist/chunk-NYKMT46J.js.map +1 -0
  6. package/dist/chunk-SPXBCZLB.js +17 -0
  7. package/dist/chunk-SPXBCZLB.js.map +1 -0
  8. package/dist/{client-5vz5Vv4A.d.ts → client-D4PoxADF.d.ts} +3 -143
  9. package/dist/client.d.ts +3 -2
  10. package/dist/{index-DmR3E8D7.d.ts → index-DAzCfO1R.d.ts} +20 -1
  11. package/dist/index.d.ts +234 -601
  12. package/dist/index.js +46 -2034
  13. package/dist/index.js.map +1 -1
  14. package/dist/{interface-Cv22wvLG.d.ts → interface-MMqhfQQK.d.ts} +69 -2
  15. package/dist/kernel/index.d.ts +26 -0
  16. package/dist/kernel/index.js +3 -0
  17. package/dist/kernel/index.js.map +1 -0
  18. package/dist/kernel/testing/index.d.ts +44 -0
  19. package/dist/kernel/testing/index.js +85 -0
  20. package/dist/kernel/testing/index.js.map +1 -0
  21. package/dist/persistence/index.d.ts +2 -2
  22. package/dist/persistence/index.js +2 -1
  23. package/dist/persistence/prisma/index.d.ts +2 -2
  24. package/dist/persistence/prisma/index.js +2 -1
  25. package/dist/plugins-BCnDUwIc.d.ts +415 -0
  26. package/dist/ports-tU3rzPXJ.d.ts +245 -0
  27. package/dist/stage-BPw7m9Wx.d.ts +144 -0
  28. package/dist/testing/index.d.ts +23 -1
  29. package/dist/testing/index.js +156 -13
  30. package/dist/testing/index.js.map +1 -1
  31. package/package.json +11 -1
  32. package/skills/workflow-engine/SKILL.md +234 -348
  33. package/skills/workflow-engine/references/03-runtime-setup.md +111 -426
  34. package/skills/workflow-engine/references/05-persistence-setup.md +32 -0
  35. package/skills/workflow-engine/references/07-testing-patterns.md +141 -474
  36. package/skills/workflow-engine/references/08-common-patterns.md +118 -431
  37. package/dist/chunk-7IITBLFY.js.map +0 -1
@@ -0,0 +1,1033 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/core/types.ts
4
+ z.object({
5
+ batchId: z.string(),
6
+ statusUrl: z.string().optional(),
7
+ apiKey: z.string().optional(),
8
+ submittedAt: z.string(),
9
+ // ISO date string
10
+ pollInterval: z.number(),
11
+ // milliseconds
12
+ maxWaitTime: z.number(),
13
+ // milliseconds
14
+ metadata: z.record(z.string(), z.unknown()).optional()
15
+ });
16
+ function isSuspendedResult(result) {
17
+ return "suspended" in result && result.suspended === true;
18
+ }
19
+
20
+ // src/kernel/errors.ts
21
+ var IdempotencyInProgressError = class extends Error {
22
+ constructor(key, commandType) {
23
+ super(
24
+ `Command "${commandType}" with idempotency key "${key}" is already in progress`
25
+ );
26
+ this.key = key;
27
+ this.commandType = commandType;
28
+ this.name = "IdempotencyInProgressError";
29
+ }
30
+ };
31
+
32
+ // src/kernel/helpers/load-workflow-context.ts
33
+ async function loadWorkflowContext(workflowRunId, deps) {
34
+ const completedStages = await deps.persistence.getStagesByRun(workflowRunId, {
35
+ status: "COMPLETED",
36
+ orderBy: "asc"
37
+ });
38
+ const context = {};
39
+ for (const stage of completedStages) {
40
+ const outputData = stage.outputData;
41
+ if (outputData?._artifactKey) {
42
+ context[stage.stageId] = await deps.blobStore.get(
43
+ outputData._artifactKey
44
+ );
45
+ } else if (outputData && typeof outputData === "object") {
46
+ context[stage.stageId] = outputData;
47
+ }
48
+ }
49
+ return context;
50
+ }
51
+
52
+ // src/kernel/helpers/save-stage-output.ts
53
+ async function saveStageOutput(runId, workflowType, stageId, output, deps) {
54
+ const key = `workflow-v2/${workflowType}/${runId}/${stageId}/output.json`;
55
+ await deps.blobStore.put(key, output);
56
+ return key;
57
+ }
58
+
59
+ // src/kernel/helpers/create-storage-shim.ts
60
+ function createStorageShim(workflowRunId, workflowType, deps) {
61
+ return {
62
+ async save(key, data) {
63
+ await deps.blobStore.put(key, data);
64
+ },
65
+ async load(key) {
66
+ return deps.blobStore.get(key);
67
+ },
68
+ async exists(key) {
69
+ return deps.blobStore.has(key);
70
+ },
71
+ async delete(key) {
72
+ return deps.blobStore.delete(key);
73
+ },
74
+ getStageKey(stageId, suffix) {
75
+ const base = `workflow-v2/${workflowType}/${workflowRunId}/${stageId}`;
76
+ return suffix ? `${base}/${suffix}` : `${base}/output.json`;
77
+ }
78
+ };
79
+ }
80
+
81
+ // src/kernel/handlers/job-execute.ts
82
+ function resolveStageInput(workflow, stageId, workflowRun, workflowContext) {
83
+ const groupIndex = workflow.getExecutionGroupIndex(stageId);
84
+ if (groupIndex === 0) return workflowRun.input;
85
+ const prevStageId = workflow.getPreviousStageId(stageId);
86
+ if (prevStageId && workflowContext[prevStageId] !== void 0) {
87
+ return workflowContext[prevStageId];
88
+ }
89
+ return workflowRun.input;
90
+ }
91
+ async function handleJobExecute(command, deps) {
92
+ const { workflowRunId, workflowId, stageId, config } = command;
93
+ const events = [];
94
+ const startTime = deps.clock.now().getTime();
95
+ const workflow = deps.registry.getWorkflow(workflowId);
96
+ if (!workflow)
97
+ throw new Error(`Workflow ${workflowId} not found in registry`);
98
+ const stageDef = workflow.getStage(stageId);
99
+ if (!stageDef)
100
+ throw new Error(`Stage ${stageId} not found in workflow ${workflowId}`);
101
+ const workflowRun = await deps.persistence.getRun(workflowRunId);
102
+ if (!workflowRun) throw new Error(`WorkflowRun ${workflowRunId} not found`);
103
+ const workflowContext = await loadWorkflowContext(workflowRunId, deps);
104
+ const stageRecord = await deps.persistence.upsertStage({
105
+ workflowRunId,
106
+ stageId,
107
+ create: {
108
+ workflowRunId,
109
+ stageId,
110
+ stageName: stageDef.name,
111
+ stageNumber: workflow.getStageIndex(stageId) + 1,
112
+ executionGroup: workflow.getExecutionGroupIndex(stageId),
113
+ status: "RUNNING",
114
+ startedAt: deps.clock.now(),
115
+ config
116
+ },
117
+ update: {
118
+ status: "RUNNING",
119
+ startedAt: deps.clock.now()
120
+ }
121
+ });
122
+ events.push({
123
+ type: "stage:started",
124
+ timestamp: deps.clock.now(),
125
+ workflowRunId,
126
+ stageId,
127
+ stageName: stageDef.name,
128
+ stageNumber: stageRecord.stageNumber
129
+ });
130
+ try {
131
+ const rawInput = resolveStageInput(
132
+ workflow,
133
+ stageId,
134
+ workflowRun,
135
+ workflowContext
136
+ );
137
+ const validatedInput = stageDef.inputSchema.parse(rawInput);
138
+ let stageConfig = config[stageId] || {};
139
+ try {
140
+ if (stageDef.configSchema) {
141
+ stageConfig = stageDef.configSchema.parse(stageConfig);
142
+ }
143
+ } catch {
144
+ }
145
+ const logFn = async (level, message, meta) => {
146
+ await deps.persistence.createLog({
147
+ workflowRunId,
148
+ workflowStageId: stageRecord.id,
149
+ level,
150
+ message,
151
+ metadata: meta
152
+ }).catch(() => {
153
+ });
154
+ };
155
+ const context = {
156
+ workflowRunId,
157
+ stageId,
158
+ stageNumber: stageRecord.stageNumber,
159
+ stageName: stageDef.name,
160
+ stageRecordId: stageRecord.id,
161
+ input: validatedInput,
162
+ config: stageConfig,
163
+ resumeState: stageRecord.suspendedState,
164
+ onProgress: (update) => {
165
+ events.push({
166
+ type: "stage:progress",
167
+ timestamp: deps.clock.now(),
168
+ workflowRunId,
169
+ stageId,
170
+ progress: update.progress,
171
+ message: update.message,
172
+ details: update.details
173
+ });
174
+ },
175
+ onLog: logFn,
176
+ log: logFn,
177
+ storage: createStorageShim(workflowRunId, workflowRun.workflowType, deps),
178
+ workflowContext
179
+ };
180
+ const result = await stageDef.execute(context);
181
+ if (isSuspendedResult(result)) {
182
+ const { state, pollConfig, metrics } = result;
183
+ const nextPollAt = new Date(
184
+ pollConfig.nextPollAt?.getTime() ?? deps.clock.now().getTime() + (pollConfig.pollInterval || 6e4)
185
+ );
186
+ await deps.persistence.updateStage(stageRecord.id, {
187
+ status: "SUSPENDED",
188
+ suspendedState: state,
189
+ nextPollAt,
190
+ pollInterval: pollConfig.pollInterval,
191
+ maxWaitUntil: pollConfig.maxWaitTime ? new Date(deps.clock.now().getTime() + pollConfig.maxWaitTime) : void 0,
192
+ metrics
193
+ });
194
+ events.push({
195
+ type: "stage:suspended",
196
+ timestamp: deps.clock.now(),
197
+ workflowRunId,
198
+ stageId,
199
+ stageName: stageDef.name,
200
+ nextPollAt
201
+ });
202
+ return { outcome: "suspended", nextPollAt, _events: events };
203
+ } else {
204
+ const duration = deps.clock.now().getTime() - startTime;
205
+ const outputKey = await saveStageOutput(
206
+ workflowRunId,
207
+ workflowRun.workflowType,
208
+ stageId,
209
+ result.output,
210
+ deps
211
+ );
212
+ await deps.persistence.updateStage(stageRecord.id, {
213
+ status: "COMPLETED",
214
+ completedAt: deps.clock.now(),
215
+ duration,
216
+ outputData: { _artifactKey: outputKey },
217
+ metrics: result.metrics,
218
+ embeddingInfo: result.embeddings
219
+ });
220
+ events.push({
221
+ type: "stage:completed",
222
+ timestamp: deps.clock.now(),
223
+ workflowRunId,
224
+ stageId,
225
+ stageName: stageDef.name,
226
+ duration
227
+ });
228
+ return {
229
+ outcome: "completed",
230
+ output: result.output,
231
+ _events: events
232
+ };
233
+ }
234
+ } catch (error) {
235
+ const errorMessage = error instanceof Error ? error.message : String(error);
236
+ const duration = deps.clock.now().getTime() - startTime;
237
+ await deps.persistence.updateStage(stageRecord.id, {
238
+ status: "FAILED",
239
+ completedAt: deps.clock.now(),
240
+ duration,
241
+ errorMessage
242
+ });
243
+ await deps.persistence.createLog({
244
+ workflowRunId,
245
+ workflowStageId: stageRecord.id,
246
+ level: "ERROR",
247
+ message: errorMessage
248
+ }).catch(() => {
249
+ });
250
+ events.push({
251
+ type: "stage:failed",
252
+ timestamp: deps.clock.now(),
253
+ workflowRunId,
254
+ stageId,
255
+ stageName: stageDef.name,
256
+ error: errorMessage
257
+ });
258
+ return { outcome: "failed", error: errorMessage, _events: events };
259
+ }
260
+ }
261
+
262
+ // src/kernel/handlers/lease-reap-stale.ts
263
+ async function handleLeaseReapStale(command, deps) {
264
+ const released = await deps.jobTransport.releaseStaleJobs(
265
+ command.staleThresholdMs
266
+ );
267
+ return { released, _events: [] };
268
+ }
269
+
270
+ // src/kernel/handlers/outbox-flush.ts
271
+ async function handleOutboxFlush(command, deps) {
272
+ const limit = command.maxEvents ?? 100;
273
+ const events = await deps.persistence.getUnpublishedOutboxEvents(limit);
274
+ const maxRetries = deps.eventSink.maxRetries ?? 3;
275
+ const publishedIds = [];
276
+ for (const outboxEvent of events) {
277
+ try {
278
+ await deps.eventSink.emit(outboxEvent.payload);
279
+ publishedIds.push(outboxEvent.id);
280
+ } catch {
281
+ const newCount = await deps.persistence.incrementOutboxRetryCount(
282
+ outboxEvent.id
283
+ );
284
+ if (newCount >= maxRetries) {
285
+ await deps.persistence.moveOutboxEventToDLQ(outboxEvent.id);
286
+ }
287
+ }
288
+ }
289
+ if (publishedIds.length > 0) {
290
+ await deps.persistence.markOutboxEventsPublished(publishedIds);
291
+ }
292
+ return { published: publishedIds.length, _events: [] };
293
+ }
294
+
295
+ // src/kernel/handlers/plugin-replay-dlq.ts
296
+ async function handlePluginReplayDLQ(command, deps) {
297
+ const maxEvents = command.maxEvents ?? 100;
298
+ const replayed = await deps.persistence.replayDLQEvents(maxEvents);
299
+ return { replayed, _events: [] };
300
+ }
301
+
302
+ // src/kernel/handlers/run-cancel.ts
303
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "CANCELLED"]);
304
+ async function handleRunCancel(command, deps) {
305
+ const run = await deps.persistence.getRun(command.workflowRunId);
306
+ if (!run || TERMINAL_STATUSES.has(run.status)) {
307
+ return { cancelled: false, _events: [] };
308
+ }
309
+ await deps.persistence.updateRun(command.workflowRunId, {
310
+ status: "CANCELLED",
311
+ completedAt: deps.clock.now()
312
+ });
313
+ return {
314
+ cancelled: true,
315
+ _events: [
316
+ {
317
+ type: "workflow:cancelled",
318
+ timestamp: deps.clock.now(),
319
+ workflowRunId: command.workflowRunId,
320
+ reason: command.reason
321
+ }
322
+ ]
323
+ };
324
+ }
325
+
326
+ // src/kernel/handlers/run-claim-pending.ts
327
+ async function handleRunClaimPending(command, deps) {
328
+ const maxClaims = command.maxClaims ?? 10;
329
+ const claimed = [];
330
+ const events = [];
331
+ for (let i = 0; i < maxClaims; i++) {
332
+ const run = await deps.persistence.claimNextPendingRun();
333
+ if (!run) break;
334
+ const workflow = deps.registry.getWorkflow(run.workflowId);
335
+ if (!workflow) {
336
+ const error = `Workflow ${run.workflowId} not found in registry`;
337
+ const failedAt = deps.clock.now();
338
+ await deps.persistence.updateRun(run.id, {
339
+ status: "FAILED",
340
+ completedAt: failedAt,
341
+ output: {
342
+ error: {
343
+ code: "WORKFLOW_NOT_FOUND",
344
+ message: error,
345
+ workerId: command.workerId
346
+ }
347
+ }
348
+ });
349
+ await deps.persistence.createLog({
350
+ workflowRunId: run.id,
351
+ level: "ERROR",
352
+ message: error,
353
+ metadata: {
354
+ workerId: command.workerId,
355
+ code: "WORKFLOW_NOT_FOUND"
356
+ }
357
+ }).catch(() => {
358
+ });
359
+ events.push({
360
+ type: "workflow:failed",
361
+ timestamp: failedAt,
362
+ workflowRunId: run.id,
363
+ error
364
+ });
365
+ continue;
366
+ }
367
+ const stages = workflow.getStagesInExecutionGroup(1);
368
+ if (stages.length === 0) {
369
+ const error = `Workflow ${run.workflowId} has no stages in execution group 1`;
370
+ const failedAt = deps.clock.now();
371
+ await deps.persistence.updateRun(run.id, {
372
+ status: "FAILED",
373
+ completedAt: failedAt,
374
+ output: {
375
+ error: {
376
+ code: "EMPTY_STAGE_GRAPH",
377
+ message: error,
378
+ workerId: command.workerId
379
+ }
380
+ }
381
+ });
382
+ await deps.persistence.createLog({
383
+ workflowRunId: run.id,
384
+ level: "ERROR",
385
+ message: error,
386
+ metadata: {
387
+ workerId: command.workerId,
388
+ code: "EMPTY_STAGE_GRAPH"
389
+ }
390
+ }).catch(() => {
391
+ });
392
+ events.push({
393
+ type: "workflow:failed",
394
+ timestamp: failedAt,
395
+ workflowRunId: run.id,
396
+ error
397
+ });
398
+ continue;
399
+ }
400
+ for (const stage of stages) {
401
+ await deps.persistence.createStage({
402
+ workflowRunId: run.id,
403
+ stageId: stage.id,
404
+ stageName: stage.name,
405
+ stageNumber: workflow.getStageIndex(stage.id) + 1,
406
+ executionGroup: 1,
407
+ status: "PENDING",
408
+ config: run.config?.[stage.id] || {}
409
+ });
410
+ }
411
+ const jobIds = await deps.jobTransport.enqueueParallel(
412
+ stages.map((stage) => ({
413
+ workflowRunId: run.id,
414
+ workflowId: run.workflowId,
415
+ stageId: stage.id,
416
+ priority: run.priority,
417
+ payload: { config: run.config || {} }
418
+ }))
419
+ );
420
+ events.push({
421
+ type: "workflow:started",
422
+ timestamp: deps.clock.now(),
423
+ workflowRunId: run.id
424
+ });
425
+ claimed.push({
426
+ workflowRunId: run.id,
427
+ workflowId: run.workflowId,
428
+ jobIds
429
+ });
430
+ }
431
+ return { claimed, _events: events };
432
+ }
433
+
434
+ // src/kernel/handlers/run-create.ts
435
+ async function handleRunCreate(command, deps) {
436
+ const workflow = deps.registry.getWorkflow(command.workflowId);
437
+ if (!workflow) {
438
+ throw new Error(`Workflow ${command.workflowId} not found in registry`);
439
+ }
440
+ try {
441
+ workflow.inputSchema.parse(command.input);
442
+ } catch (error) {
443
+ throw new Error(`Invalid workflow input: ${error}`);
444
+ }
445
+ const defaultConfig = workflow.getDefaultConfig?.() ?? {};
446
+ const mergedConfig = { ...defaultConfig, ...command.config };
447
+ const configValidation = workflow.validateConfig(mergedConfig);
448
+ if (!configValidation.valid) {
449
+ const errors = configValidation.errors.map((e) => `${e.stageId}: ${e.error}`).join(", ");
450
+ throw new Error(`Invalid workflow config: ${errors}`);
451
+ }
452
+ const priority = command.priority ?? 5;
453
+ const run = await deps.persistence.createRun({
454
+ workflowId: command.workflowId,
455
+ workflowName: workflow.name,
456
+ workflowType: command.workflowId,
457
+ input: command.input,
458
+ config: mergedConfig,
459
+ priority,
460
+ metadata: command.metadata
461
+ });
462
+ return {
463
+ workflowRunId: run.id,
464
+ status: "PENDING",
465
+ _events: [
466
+ {
467
+ type: "workflow:created",
468
+ timestamp: deps.clock.now(),
469
+ workflowRunId: run.id,
470
+ workflowId: command.workflowId
471
+ }
472
+ ]
473
+ };
474
+ }
475
+
476
+ // src/kernel/handlers/run-rerun-from.ts
477
+ async function handleRunRerunFrom(command, deps) {
478
+ const { workflowRunId, fromStageId } = command;
479
+ const events = [];
480
+ const run = await deps.persistence.getRun(workflowRunId);
481
+ if (!run) throw new Error(`WorkflowRun ${workflowRunId} not found`);
482
+ if (run.status !== "COMPLETED" && run.status !== "FAILED") {
483
+ throw new Error(
484
+ `Cannot rerun workflow in ${run.status} state. Must be COMPLETED or FAILED.`
485
+ );
486
+ }
487
+ const workflow = deps.registry.getWorkflow(run.workflowId);
488
+ if (!workflow)
489
+ throw new Error(`Workflow ${run.workflowId} not found in registry`);
490
+ const stageDef = workflow.getStage(fromStageId);
491
+ if (!stageDef) {
492
+ throw new Error(
493
+ `Stage ${fromStageId} not found in workflow ${run.workflowId}`
494
+ );
495
+ }
496
+ const targetGroup = workflow.getExecutionGroupIndex(fromStageId);
497
+ const existingStages = await deps.persistence.getStagesByRun(workflowRunId);
498
+ if (targetGroup > 1) {
499
+ const priorStages = existingStages.filter(
500
+ (s) => s.executionGroup < targetGroup
501
+ );
502
+ if (priorStages.length === 0) {
503
+ throw new Error(
504
+ `Cannot rerun from stage ${fromStageId}: previous stages have not been executed`
505
+ );
506
+ }
507
+ }
508
+ const stagesToDelete = existingStages.filter(
509
+ (s) => s.executionGroup >= targetGroup
510
+ );
511
+ const deletedStageIds = stagesToDelete.map((s) => s.stageId);
512
+ for (const stage of stagesToDelete) {
513
+ const outputRef = stage.outputData;
514
+ if (outputRef?._artifactKey) {
515
+ await deps.blobStore.delete(outputRef._artifactKey).catch(() => {
516
+ });
517
+ }
518
+ }
519
+ for (const stage of stagesToDelete) {
520
+ await deps.persistence.deleteStage(stage.id);
521
+ }
522
+ await deps.persistence.updateRun(workflowRunId, {
523
+ status: "RUNNING",
524
+ completedAt: null
525
+ });
526
+ const targetStages = workflow.getStagesInExecutionGroup(targetGroup);
527
+ for (const stage of targetStages) {
528
+ await deps.persistence.createStage({
529
+ workflowRunId,
530
+ stageId: stage.id,
531
+ stageName: stage.name,
532
+ stageNumber: workflow.getStageIndex(stage.id) + 1,
533
+ executionGroup: targetGroup,
534
+ status: "PENDING",
535
+ config: run.config?.[stage.id] || {}
536
+ });
537
+ }
538
+ await deps.jobTransport.enqueueParallel(
539
+ targetStages.map((stage) => ({
540
+ workflowRunId,
541
+ workflowId: run.workflowId,
542
+ stageId: stage.id,
543
+ priority: run.priority,
544
+ payload: { config: run.config || {} }
545
+ }))
546
+ );
547
+ events.push({
548
+ type: "workflow:started",
549
+ timestamp: deps.clock.now(),
550
+ workflowRunId
551
+ });
552
+ return {
553
+ workflowRunId,
554
+ fromStageId,
555
+ deletedStages: deletedStageIds,
556
+ _events: events
557
+ };
558
+ }
559
+
560
+ // src/kernel/handlers/run-transition.ts
561
+ var TERMINAL_STATUSES2 = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "CANCELLED"]);
562
+ var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["RUNNING", "PENDING", "SUSPENDED"]);
563
+ async function enqueueExecutionGroup(run, workflow, groupIndex, deps) {
564
+ const stages = workflow.getStagesInExecutionGroup(groupIndex);
565
+ for (const stage of stages) {
566
+ await deps.persistence.createStage({
567
+ workflowRunId: run.id,
568
+ stageId: stage.id,
569
+ stageName: stage.name,
570
+ stageNumber: workflow.getStageIndex(stage.id) + 1,
571
+ executionGroup: groupIndex,
572
+ status: "PENDING",
573
+ config: run.config?.[stage.id] || {}
574
+ });
575
+ }
576
+ return deps.jobTransport.enqueueParallel(
577
+ stages.map((stage) => ({
578
+ workflowRunId: run.id,
579
+ workflowId: run.workflowId,
580
+ stageId: stage.id,
581
+ priority: run.priority,
582
+ payload: { config: run.config || {} }
583
+ }))
584
+ );
585
+ }
586
+ async function handleRunTransition(command, deps) {
587
+ const events = [];
588
+ const run = await deps.persistence.getRun(command.workflowRunId);
589
+ if (!run) {
590
+ return { action: "noop", _events: [] };
591
+ }
592
+ if (TERMINAL_STATUSES2.has(run.status)) {
593
+ return { action: "noop", _events: [] };
594
+ }
595
+ const workflow = deps.registry.getWorkflow(run.workflowId);
596
+ if (!workflow) {
597
+ return { action: "noop", _events: [] };
598
+ }
599
+ const stages = await deps.persistence.getStagesByRun(command.workflowRunId);
600
+ if (stages.length === 0) {
601
+ await enqueueExecutionGroup(run, workflow, 1, deps);
602
+ events.push({
603
+ type: "workflow:started",
604
+ timestamp: deps.clock.now(),
605
+ workflowRunId: run.id
606
+ });
607
+ return { action: "advanced", nextGroup: 1, _events: events };
608
+ }
609
+ const hasActive = stages.some((s) => ACTIVE_STATUSES.has(s.status));
610
+ if (hasActive) {
611
+ return { action: "noop", _events: [] };
612
+ }
613
+ const failedStage = stages.find((s) => s.status === "FAILED");
614
+ if (failedStage) {
615
+ await deps.persistence.updateRun(command.workflowRunId, {
616
+ status: "FAILED",
617
+ completedAt: deps.clock.now()
618
+ });
619
+ events.push({
620
+ type: "workflow:failed",
621
+ timestamp: deps.clock.now(),
622
+ workflowRunId: command.workflowRunId,
623
+ error: failedStage.errorMessage || "Stage failed"
624
+ });
625
+ return { action: "failed", _events: events };
626
+ }
627
+ const maxGroup = stages.reduce(
628
+ (max, s) => s.executionGroup > max ? s.executionGroup : max,
629
+ 0
630
+ );
631
+ const nextGroupStages = workflow.getStagesInExecutionGroup(maxGroup + 1);
632
+ if (nextGroupStages.length > 0) {
633
+ await enqueueExecutionGroup(run, workflow, maxGroup + 1, deps);
634
+ return {
635
+ action: "advanced",
636
+ nextGroup: maxGroup + 1,
637
+ _events: events
638
+ };
639
+ }
640
+ let totalCost = 0;
641
+ let totalTokens = 0;
642
+ for (const stage of stages) {
643
+ const metrics = stage.metrics;
644
+ if (metrics) {
645
+ totalCost += metrics.totalCost ?? 0;
646
+ totalTokens += metrics.totalTokens ?? 0;
647
+ }
648
+ }
649
+ const duration = deps.clock.now().getTime() - run.createdAt.getTime();
650
+ await deps.persistence.updateRun(command.workflowRunId, {
651
+ status: "COMPLETED",
652
+ completedAt: deps.clock.now(),
653
+ duration,
654
+ totalCost,
655
+ totalTokens
656
+ });
657
+ events.push({
658
+ type: "workflow:completed",
659
+ timestamp: deps.clock.now(),
660
+ workflowRunId: command.workflowRunId,
661
+ duration,
662
+ totalCost,
663
+ totalTokens
664
+ });
665
+ return { action: "completed", _events: events };
666
+ }
667
+
668
+ // src/kernel/handlers/stage-poll-suspended.ts
669
+ async function handleStagePollSuspended(command, deps) {
670
+ const events = [];
671
+ const maxChecks = command.maxChecks ?? 50;
672
+ const suspendedStages = await deps.persistence.getSuspendedStages(
673
+ deps.clock.now()
674
+ );
675
+ const stagesToCheck = suspendedStages.slice(0, maxChecks);
676
+ let checked = 0;
677
+ let resumed = 0;
678
+ let failed = 0;
679
+ const resumedWorkflowRunIds = /* @__PURE__ */ new Set();
680
+ for (const stageRecord of stagesToCheck) {
681
+ checked++;
682
+ const run = await deps.persistence.getRun(stageRecord.workflowRunId);
683
+ if (!run) continue;
684
+ const workflow = deps.registry.getWorkflow(run.workflowId);
685
+ if (!workflow) {
686
+ await deps.persistence.updateStage(stageRecord.id, {
687
+ status: "FAILED",
688
+ completedAt: deps.clock.now(),
689
+ errorMessage: `Workflow ${run.workflowId} not found in registry`
690
+ });
691
+ failed++;
692
+ events.push({
693
+ type: "stage:failed",
694
+ timestamp: deps.clock.now(),
695
+ workflowRunId: stageRecord.workflowRunId,
696
+ stageId: stageRecord.stageId,
697
+ stageName: stageRecord.stageName,
698
+ error: `Workflow ${run.workflowId} not found in registry`
699
+ });
700
+ continue;
701
+ }
702
+ const stageDef = workflow.getStage(stageRecord.stageId);
703
+ if (!stageDef || !stageDef.checkCompletion) {
704
+ const errorMsg = !stageDef ? `Stage ${stageRecord.stageId} not found in workflow ${run.workflowId}` : `Stage ${stageRecord.stageId} does not support checkCompletion`;
705
+ await deps.persistence.updateStage(stageRecord.id, {
706
+ status: "FAILED",
707
+ completedAt: deps.clock.now(),
708
+ errorMessage: errorMsg
709
+ });
710
+ failed++;
711
+ events.push({
712
+ type: "stage:failed",
713
+ timestamp: deps.clock.now(),
714
+ workflowRunId: stageRecord.workflowRunId,
715
+ stageId: stageRecord.stageId,
716
+ stageName: stageRecord.stageName,
717
+ error: errorMsg
718
+ });
719
+ continue;
720
+ }
721
+ const storage = createStorageShim(
722
+ stageRecord.workflowRunId,
723
+ run.workflowType,
724
+ deps
725
+ );
726
+ const logFn = async (level, message, meta) => {
727
+ await deps.persistence.createLog({
728
+ workflowRunId: stageRecord.workflowRunId,
729
+ workflowStageId: stageRecord.id,
730
+ level,
731
+ message,
732
+ metadata: meta
733
+ }).catch(() => {
734
+ });
735
+ };
736
+ const checkContext = {
737
+ workflowRunId: run.id,
738
+ stageId: stageRecord.stageId,
739
+ stageRecordId: stageRecord.id,
740
+ config: stageRecord.config || {},
741
+ log: logFn,
742
+ onLog: logFn,
743
+ storage
744
+ };
745
+ try {
746
+ const checkResult = await stageDef.checkCompletion(
747
+ stageRecord.suspendedState,
748
+ checkContext
749
+ );
750
+ if (checkResult.error) {
751
+ await deps.persistence.updateStage(stageRecord.id, {
752
+ status: "FAILED",
753
+ completedAt: deps.clock.now(),
754
+ errorMessage: checkResult.error,
755
+ nextPollAt: null
756
+ });
757
+ await deps.persistence.updateRun(stageRecord.workflowRunId, {
758
+ status: "FAILED",
759
+ completedAt: deps.clock.now()
760
+ });
761
+ failed++;
762
+ events.push({
763
+ type: "stage:failed",
764
+ timestamp: deps.clock.now(),
765
+ workflowRunId: stageRecord.workflowRunId,
766
+ stageId: stageRecord.stageId,
767
+ stageName: stageRecord.stageName,
768
+ error: checkResult.error
769
+ });
770
+ events.push({
771
+ type: "workflow:failed",
772
+ timestamp: deps.clock.now(),
773
+ workflowRunId: stageRecord.workflowRunId,
774
+ error: checkResult.error
775
+ });
776
+ } else if (checkResult.ready) {
777
+ let outputRef;
778
+ if (checkResult.output !== void 0) {
779
+ let validatedOutput = checkResult.output;
780
+ try {
781
+ validatedOutput = stageDef.outputSchema.parse(checkResult.output);
782
+ } catch {
783
+ }
784
+ const outputKey = await saveStageOutput(
785
+ stageRecord.workflowRunId,
786
+ run.workflowType,
787
+ stageRecord.stageId,
788
+ validatedOutput,
789
+ deps
790
+ );
791
+ outputRef = { _artifactKey: outputKey };
792
+ }
793
+ const duration = deps.clock.now().getTime() - (stageRecord.startedAt?.getTime() ?? deps.clock.now().getTime());
794
+ await deps.persistence.updateStage(stageRecord.id, {
795
+ status: "COMPLETED",
796
+ completedAt: deps.clock.now(),
797
+ duration,
798
+ outputData: outputRef,
799
+ nextPollAt: null,
800
+ metrics: checkResult.metrics,
801
+ embeddingInfo: checkResult.embeddings
802
+ });
803
+ resumed++;
804
+ resumedWorkflowRunIds.add(stageRecord.workflowRunId);
805
+ events.push({
806
+ type: "stage:completed",
807
+ timestamp: deps.clock.now(),
808
+ workflowRunId: stageRecord.workflowRunId,
809
+ stageId: stageRecord.stageId,
810
+ stageName: stageRecord.stageName,
811
+ duration
812
+ });
813
+ } else {
814
+ const pollInterval = checkResult.nextCheckIn ?? stageRecord.pollInterval ?? 6e4;
815
+ const nextPollAt = new Date(deps.clock.now().getTime() + pollInterval);
816
+ await deps.persistence.updateStage(stageRecord.id, {
817
+ nextPollAt
818
+ });
819
+ }
820
+ } catch (error) {
821
+ const errorMessage = error instanceof Error ? error.message : String(error);
822
+ await deps.persistence.updateStage(stageRecord.id, {
823
+ status: "FAILED",
824
+ completedAt: deps.clock.now(),
825
+ errorMessage,
826
+ nextPollAt: null
827
+ });
828
+ await deps.persistence.updateRun(stageRecord.workflowRunId, {
829
+ status: "FAILED",
830
+ completedAt: deps.clock.now()
831
+ });
832
+ failed++;
833
+ events.push({
834
+ type: "stage:failed",
835
+ timestamp: deps.clock.now(),
836
+ workflowRunId: stageRecord.workflowRunId,
837
+ stageId: stageRecord.stageId,
838
+ stageName: stageRecord.stageName,
839
+ error: errorMessage
840
+ });
841
+ events.push({
842
+ type: "workflow:failed",
843
+ timestamp: deps.clock.now(),
844
+ workflowRunId: stageRecord.workflowRunId,
845
+ error: errorMessage
846
+ });
847
+ }
848
+ }
849
+ return {
850
+ checked,
851
+ resumed,
852
+ failed,
853
+ resumedWorkflowRunIds: [...resumedWorkflowRunIds],
854
+ _events: events
855
+ };
856
+ }
857
+
858
+ // src/kernel/kernel.ts
859
+ function getIdempotencyKey(command) {
860
+ if (command.type === "run.create") return command.idempotencyKey;
861
+ if (command.type === "job.execute") return command.idempotencyKey;
862
+ return void 0;
863
+ }
864
+ function createKernel(config) {
865
+ const {
866
+ persistence,
867
+ blobStore,
868
+ jobTransport,
869
+ eventSink,
870
+ scheduler,
871
+ clock,
872
+ registry
873
+ } = config;
874
+ const deps = {
875
+ persistence,
876
+ blobStore,
877
+ jobTransport,
878
+ eventSink,
879
+ scheduler,
880
+ clock,
881
+ registry
882
+ };
883
+ async function dispatch(command) {
884
+ if (command.type === "outbox.flush") {
885
+ const result = await handleOutboxFlush(
886
+ command,
887
+ deps
888
+ );
889
+ const { _events: _, ...publicResult } = result;
890
+ return publicResult;
891
+ }
892
+ if (command.type === "plugin.replayDLQ") {
893
+ const result = await handlePluginReplayDLQ(
894
+ command,
895
+ deps
896
+ );
897
+ const { _events: _, ...publicResult } = result;
898
+ return publicResult;
899
+ }
900
+ const idempotencyKey = getIdempotencyKey(command);
901
+ let idempotencyAcquired = false;
902
+ if (idempotencyKey) {
903
+ const acquired = await persistence.acquireIdempotencyKey(
904
+ idempotencyKey,
905
+ command.type
906
+ );
907
+ if (acquired.status === "replay") {
908
+ return acquired.result;
909
+ }
910
+ if (acquired.status === "in_progress") {
911
+ throw new IdempotencyInProgressError(idempotencyKey, command.type);
912
+ }
913
+ idempotencyAcquired = true;
914
+ }
915
+ try {
916
+ const publicResult = await persistence.withTransaction(async (tx) => {
917
+ const txDeps = { ...deps, persistence: tx };
918
+ let result;
919
+ switch (command.type) {
920
+ case "run.create":
921
+ result = await handleRunCreate(command, txDeps);
922
+ break;
923
+ case "run.claimPending":
924
+ result = await handleRunClaimPending(
925
+ command,
926
+ txDeps
927
+ );
928
+ break;
929
+ case "run.transition":
930
+ result = await handleRunTransition(
931
+ command,
932
+ txDeps
933
+ );
934
+ break;
935
+ case "run.cancel":
936
+ result = await handleRunCancel(command, txDeps);
937
+ break;
938
+ case "run.rerunFrom":
939
+ result = await handleRunRerunFrom(
940
+ command,
941
+ txDeps
942
+ );
943
+ break;
944
+ case "job.execute":
945
+ result = await handleJobExecute(
946
+ command,
947
+ txDeps
948
+ );
949
+ break;
950
+ case "stage.pollSuspended":
951
+ result = await handleStagePollSuspended(
952
+ command,
953
+ txDeps
954
+ );
955
+ break;
956
+ case "lease.reapStale":
957
+ result = await handleLeaseReapStale(
958
+ command,
959
+ txDeps
960
+ );
961
+ break;
962
+ default: {
963
+ const _exhaustive = command;
964
+ throw new Error(
965
+ `Unknown command type: ${_exhaustive.type}`
966
+ );
967
+ }
968
+ }
969
+ const events = result._events;
970
+ if (events.length > 0) {
971
+ const causationId = idempotencyKey ?? crypto.randomUUID();
972
+ const outboxEvents = events.map(
973
+ (event) => ({
974
+ workflowRunId: event.workflowRunId,
975
+ eventType: event.type,
976
+ payload: event,
977
+ causationId,
978
+ occurredAt: event.timestamp
979
+ })
980
+ );
981
+ await tx.appendOutboxEvents(outboxEvents);
982
+ }
983
+ const { _events: _, ...stripped } = result;
984
+ return stripped;
985
+ });
986
+ if (idempotencyKey && idempotencyAcquired) {
987
+ await persistence.completeIdempotencyKey(
988
+ idempotencyKey,
989
+ command.type,
990
+ publicResult
991
+ );
992
+ }
993
+ return publicResult;
994
+ } catch (error) {
995
+ if (idempotencyKey && idempotencyAcquired) {
996
+ await persistence.releaseIdempotencyKey(idempotencyKey, command.type).catch(() => {
997
+ });
998
+ }
999
+ throw error;
1000
+ }
1001
+ }
1002
+ return { dispatch };
1003
+ }
1004
+
1005
+ // src/kernel/plugins.ts
1006
+ function definePlugin(definition) {
1007
+ return definition;
1008
+ }
1009
+ function createPluginRunner(config) {
1010
+ const { plugins, maxRetries = 3 } = config;
1011
+ const handlersByType = /* @__PURE__ */ new Map();
1012
+ for (const plugin of plugins) {
1013
+ for (const eventType of plugin.on) {
1014
+ const existing = handlersByType.get(eventType) ?? [];
1015
+ existing.push(plugin);
1016
+ handlersByType.set(eventType, existing);
1017
+ }
1018
+ }
1019
+ return {
1020
+ maxRetries,
1021
+ async emit(event) {
1022
+ const matching = handlersByType.get(event.type);
1023
+ if (!matching || matching.length === 0) return;
1024
+ for (const plugin of matching) {
1025
+ await plugin.handle(event);
1026
+ }
1027
+ }
1028
+ };
1029
+ }
1030
+
1031
+ export { IdempotencyInProgressError, createKernel, createPluginRunner, definePlugin, loadWorkflowContext, saveStageOutput };
1032
+ //# sourceMappingURL=chunk-HL3OJG7W.js.map
1033
+ //# sourceMappingURL=chunk-HL3OJG7W.js.map