@bratsos/workflow-engine 0.0.11 → 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 (49) hide show
  1. package/README.md +270 -513
  2. package/dist/chunk-D7RVRRM2.js +3 -0
  3. package/dist/chunk-D7RVRRM2.js.map +1 -0
  4. package/dist/chunk-HL3OJG7W.js +1033 -0
  5. package/dist/chunk-HL3OJG7W.js.map +1 -0
  6. package/dist/chunk-MUWP5SF2.js +33 -0
  7. package/dist/chunk-MUWP5SF2.js.map +1 -0
  8. package/dist/chunk-NYKMT46J.js +1143 -0
  9. package/dist/chunk-NYKMT46J.js.map +1 -0
  10. package/dist/chunk-P4KMGCT3.js +2292 -0
  11. package/dist/chunk-P4KMGCT3.js.map +1 -0
  12. package/dist/chunk-SPXBCZLB.js +17 -0
  13. package/dist/chunk-SPXBCZLB.js.map +1 -0
  14. package/dist/cli/sync-models.d.ts +1 -0
  15. package/dist/cli/sync-models.js +210 -0
  16. package/dist/cli/sync-models.js.map +1 -0
  17. package/dist/client-D4PoxADF.d.ts +798 -0
  18. package/dist/client.d.ts +5 -0
  19. package/dist/client.js +4 -0
  20. package/dist/client.js.map +1 -0
  21. package/dist/index-DAzCfO1R.d.ts +217 -0
  22. package/dist/index.d.ts +569 -0
  23. package/dist/index.js +399 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/interface-MMqhfQQK.d.ts +411 -0
  26. package/dist/kernel/index.d.ts +26 -0
  27. package/dist/kernel/index.js +3 -0
  28. package/dist/kernel/index.js.map +1 -0
  29. package/dist/kernel/testing/index.d.ts +44 -0
  30. package/dist/kernel/testing/index.js +85 -0
  31. package/dist/kernel/testing/index.js.map +1 -0
  32. package/dist/persistence/index.d.ts +2 -0
  33. package/dist/persistence/index.js +6 -0
  34. package/dist/persistence/index.js.map +1 -0
  35. package/dist/persistence/prisma/index.d.ts +37 -0
  36. package/dist/persistence/prisma/index.js +5 -0
  37. package/dist/persistence/prisma/index.js.map +1 -0
  38. package/dist/plugins-BCnDUwIc.d.ts +415 -0
  39. package/dist/ports-tU3rzPXJ.d.ts +245 -0
  40. package/dist/stage-BPw7m9Wx.d.ts +144 -0
  41. package/dist/testing/index.d.ts +264 -0
  42. package/dist/testing/index.js +920 -0
  43. package/dist/testing/index.js.map +1 -0
  44. package/package.json +11 -1
  45. package/skills/workflow-engine/SKILL.md +234 -348
  46. package/skills/workflow-engine/references/03-runtime-setup.md +111 -426
  47. package/skills/workflow-engine/references/05-persistence-setup.md +32 -0
  48. package/skills/workflow-engine/references/07-testing-patterns.md +141 -474
  49. package/skills/workflow-engine/references/08-common-patterns.md +118 -431
@@ -0,0 +1,920 @@
1
+ import { StaleVersionError } from '../chunk-SPXBCZLB.js';
2
+ import { randomUUID } from 'crypto';
3
+
4
+ var InMemoryAICallLogger = class {
5
+ calls = /* @__PURE__ */ new Map();
6
+ recordedBatches = /* @__PURE__ */ new Set();
7
+ // ============================================================================
8
+ // Core Operations
9
+ // ============================================================================
10
+ /**
11
+ * Log a single AI call (fire and forget)
12
+ */
13
+ logCall(call) {
14
+ const id = randomUUID();
15
+ const record = {
16
+ id,
17
+ createdAt: /* @__PURE__ */ new Date(),
18
+ topic: call.topic,
19
+ callType: call.callType,
20
+ modelKey: call.modelKey,
21
+ modelId: call.modelId,
22
+ prompt: call.prompt,
23
+ response: call.response,
24
+ inputTokens: call.inputTokens,
25
+ outputTokens: call.outputTokens,
26
+ cost: call.cost,
27
+ metadata: call.metadata ?? null
28
+ };
29
+ this.calls.set(id, record);
30
+ }
31
+ /**
32
+ * Log batch results (for recording batch API results)
33
+ */
34
+ async logBatchResults(batchId, results) {
35
+ this.recordedBatches.add(batchId);
36
+ for (const result of results) {
37
+ this.logCall({
38
+ ...result,
39
+ metadata: {
40
+ ...result.metadata,
41
+ batchId
42
+ }
43
+ });
44
+ }
45
+ }
46
+ /**
47
+ * Get aggregated stats for a topic prefix
48
+ */
49
+ async getStats(topicPrefix) {
50
+ const matchingCalls = Array.from(this.calls.values()).filter(
51
+ (call) => call.topic.startsWith(topicPrefix)
52
+ );
53
+ const stats = {
54
+ totalCalls: matchingCalls.length,
55
+ totalInputTokens: 0,
56
+ totalOutputTokens: 0,
57
+ totalCost: 0,
58
+ perModel: {}
59
+ };
60
+ for (const call of matchingCalls) {
61
+ stats.totalInputTokens += call.inputTokens;
62
+ stats.totalOutputTokens += call.outputTokens;
63
+ stats.totalCost += call.cost;
64
+ if (!stats.perModel[call.modelKey]) {
65
+ stats.perModel[call.modelKey] = {
66
+ calls: 0,
67
+ inputTokens: 0,
68
+ outputTokens: 0,
69
+ cost: 0
70
+ };
71
+ }
72
+ stats.perModel[call.modelKey].calls++;
73
+ stats.perModel[call.modelKey].inputTokens += call.inputTokens;
74
+ stats.perModel[call.modelKey].outputTokens += call.outputTokens;
75
+ stats.perModel[call.modelKey].cost += call.cost;
76
+ }
77
+ return stats;
78
+ }
79
+ /**
80
+ * Check if batch results are already recorded
81
+ */
82
+ async isRecorded(batchId) {
83
+ return this.recordedBatches.has(batchId);
84
+ }
85
+ // ============================================================================
86
+ // Test Helpers
87
+ // ============================================================================
88
+ /**
89
+ * Clear all data - useful between tests
90
+ */
91
+ clear() {
92
+ this.calls.clear();
93
+ this.recordedBatches.clear();
94
+ }
95
+ /**
96
+ * Get all calls for inspection
97
+ */
98
+ getAllCalls() {
99
+ return Array.from(this.calls.values()).map((c) => ({ ...c }));
100
+ }
101
+ /**
102
+ * Get calls by topic for inspection
103
+ */
104
+ getCallsByTopic(topic) {
105
+ return Array.from(this.calls.values()).filter((c) => c.topic === topic).map((c) => ({ ...c }));
106
+ }
107
+ /**
108
+ * Get calls by topic prefix for inspection
109
+ */
110
+ getCallsByTopicPrefix(prefix) {
111
+ return Array.from(this.calls.values()).filter((c) => c.topic.startsWith(prefix)).map((c) => ({ ...c }));
112
+ }
113
+ /**
114
+ * Get calls by model for inspection
115
+ */
116
+ getCallsByModel(modelKey) {
117
+ return Array.from(this.calls.values()).filter((c) => c.modelKey === modelKey).map((c) => ({ ...c }));
118
+ }
119
+ /**
120
+ * Get calls by call type for inspection
121
+ */
122
+ getCallsByType(callType) {
123
+ return Array.from(this.calls.values()).filter((c) => c.callType === callType).map((c) => ({ ...c }));
124
+ }
125
+ /**
126
+ * Get total cost across all calls
127
+ */
128
+ getTotalCost() {
129
+ let total = 0;
130
+ for (const call of this.calls.values()) {
131
+ total += call.cost;
132
+ }
133
+ return total;
134
+ }
135
+ /**
136
+ * Get total tokens across all calls
137
+ */
138
+ getTotalTokens() {
139
+ let input = 0;
140
+ let output = 0;
141
+ for (const call of this.calls.values()) {
142
+ input += call.inputTokens;
143
+ output += call.outputTokens;
144
+ }
145
+ return { input, output };
146
+ }
147
+ /**
148
+ * Get call count
149
+ */
150
+ getCallCount() {
151
+ return this.calls.size;
152
+ }
153
+ /**
154
+ * Get all recorded batch IDs
155
+ */
156
+ getRecordedBatchIds() {
157
+ return Array.from(this.recordedBatches);
158
+ }
159
+ /**
160
+ * Get the last call made (useful for assertions)
161
+ */
162
+ getLastCall() {
163
+ const calls = Array.from(this.calls.values());
164
+ if (calls.length === 0) return null;
165
+ calls.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
166
+ return { ...calls[0] };
167
+ }
168
+ /**
169
+ * Assert a call was made with specific properties
170
+ */
171
+ hasCallMatching(predicate) {
172
+ return Array.from(this.calls.values()).some(predicate);
173
+ }
174
+ };
175
+ var InMemoryJobQueue = class {
176
+ jobs = /* @__PURE__ */ new Map();
177
+ workerId;
178
+ defaultMaxAttempts = 3;
179
+ constructor(workerId) {
180
+ this.workerId = workerId ?? `worker-${randomUUID().slice(0, 8)}`;
181
+ }
182
+ // ============================================================================
183
+ // Core Operations
184
+ // ============================================================================
185
+ async enqueue(options) {
186
+ const now = /* @__PURE__ */ new Date();
187
+ const id = randomUUID();
188
+ const job = {
189
+ id,
190
+ createdAt: now,
191
+ updatedAt: now,
192
+ workflowRunId: options.workflowRunId,
193
+ workflowId: options.workflowId,
194
+ stageId: options.stageId,
195
+ status: "PENDING",
196
+ priority: options.priority ?? 5,
197
+ workerId: null,
198
+ lockedAt: null,
199
+ startedAt: null,
200
+ completedAt: null,
201
+ attempt: 1,
202
+ maxAttempts: this.defaultMaxAttempts,
203
+ lastError: null,
204
+ nextPollAt: options.scheduledFor ?? null,
205
+ payload: options.payload ?? {}
206
+ };
207
+ this.jobs.set(id, job);
208
+ return id;
209
+ }
210
+ async enqueueParallel(jobs) {
211
+ const ids = [];
212
+ for (const job of jobs) {
213
+ const id = await this.enqueue(job);
214
+ ids.push(id);
215
+ }
216
+ return ids;
217
+ }
218
+ async dequeue() {
219
+ const now = /* @__PURE__ */ new Date();
220
+ const pendingJobs = Array.from(this.jobs.values()).filter(
221
+ (j) => j.status === "PENDING" && (j.nextPollAt === null || j.nextPollAt <= now)
222
+ ).sort((a, b) => {
223
+ if (b.priority !== a.priority) {
224
+ return b.priority - a.priority;
225
+ }
226
+ return a.createdAt.getTime() - b.createdAt.getTime();
227
+ });
228
+ if (pendingJobs.length === 0) {
229
+ return null;
230
+ }
231
+ const job = pendingJobs[0];
232
+ const updated = {
233
+ ...job,
234
+ status: "RUNNING",
235
+ workerId: this.workerId,
236
+ lockedAt: now,
237
+ startedAt: now,
238
+ updatedAt: now
239
+ };
240
+ this.jobs.set(job.id, updated);
241
+ return {
242
+ jobId: job.id,
243
+ workflowRunId: job.workflowRunId,
244
+ workflowId: job.workflowId,
245
+ stageId: job.stageId,
246
+ priority: job.priority,
247
+ attempt: job.attempt,
248
+ maxAttempts: job.maxAttempts,
249
+ payload: job.payload
250
+ };
251
+ }
252
+ async complete(jobId) {
253
+ const job = this.jobs.get(jobId);
254
+ if (!job) {
255
+ throw new Error(`Job not found: ${jobId}`);
256
+ }
257
+ const now = /* @__PURE__ */ new Date();
258
+ const updated = {
259
+ ...job,
260
+ status: "COMPLETED",
261
+ completedAt: now,
262
+ updatedAt: now
263
+ };
264
+ this.jobs.set(jobId, updated);
265
+ }
266
+ async suspend(jobId, nextPollAt) {
267
+ const job = this.jobs.get(jobId);
268
+ if (!job) {
269
+ throw new Error(`Job not found: ${jobId}`);
270
+ }
271
+ const updated = {
272
+ ...job,
273
+ status: "SUSPENDED",
274
+ nextPollAt,
275
+ workerId: null,
276
+ lockedAt: null,
277
+ updatedAt: /* @__PURE__ */ new Date()
278
+ };
279
+ this.jobs.set(jobId, updated);
280
+ }
281
+ async fail(jobId, error, shouldRetry = true) {
282
+ const job = this.jobs.get(jobId);
283
+ if (!job) {
284
+ throw new Error(`Job not found: ${jobId}`);
285
+ }
286
+ const now = /* @__PURE__ */ new Date();
287
+ if (shouldRetry && job.attempt < job.maxAttempts) {
288
+ const updated = {
289
+ ...job,
290
+ status: "PENDING",
291
+ attempt: job.attempt + 1,
292
+ lastError: error,
293
+ workerId: null,
294
+ lockedAt: null,
295
+ updatedAt: now
296
+ };
297
+ this.jobs.set(jobId, updated);
298
+ } else {
299
+ const updated = {
300
+ ...job,
301
+ status: "FAILED",
302
+ lastError: error,
303
+ completedAt: now,
304
+ updatedAt: now
305
+ };
306
+ this.jobs.set(jobId, updated);
307
+ }
308
+ }
309
+ async getSuspendedJobsReadyToPoll() {
310
+ const now = /* @__PURE__ */ new Date();
311
+ return Array.from(this.jobs.values()).filter(
312
+ (j) => j.status === "SUSPENDED" && j.nextPollAt && j.nextPollAt <= now
313
+ ).map((j) => ({
314
+ jobId: j.id,
315
+ stageId: j.stageId,
316
+ workflowRunId: j.workflowRunId
317
+ }));
318
+ }
319
+ async releaseStaleJobs(staleThresholdMs = 6e4) {
320
+ const now = /* @__PURE__ */ new Date();
321
+ const threshold = new Date(now.getTime() - staleThresholdMs);
322
+ let released = 0;
323
+ for (const job of this.jobs.values()) {
324
+ if (job.status === "RUNNING" && job.lockedAt && job.lockedAt < threshold) {
325
+ const updated = {
326
+ ...job,
327
+ status: "PENDING",
328
+ workerId: null,
329
+ lockedAt: null,
330
+ updatedAt: now
331
+ };
332
+ this.jobs.set(job.id, updated);
333
+ released++;
334
+ }
335
+ }
336
+ return released;
337
+ }
338
+ // ============================================================================
339
+ // Test Helpers
340
+ // ============================================================================
341
+ /**
342
+ * Clear all jobs - useful between tests
343
+ */
344
+ clear() {
345
+ this.jobs.clear();
346
+ }
347
+ /**
348
+ * Get all jobs for inspection
349
+ */
350
+ getAllJobs() {
351
+ return Array.from(this.jobs.values()).map((j) => ({ ...j }));
352
+ }
353
+ /**
354
+ * Get jobs by status for inspection
355
+ */
356
+ getJobsByStatus(status) {
357
+ return Array.from(this.jobs.values()).filter((j) => j.status === status).map((j) => ({ ...j }));
358
+ }
359
+ /**
360
+ * Get a specific job by ID
361
+ */
362
+ getJob(jobId) {
363
+ const job = this.jobs.get(jobId);
364
+ return job ? { ...job } : null;
365
+ }
366
+ /**
367
+ * Get the worker ID for this queue instance
368
+ */
369
+ getWorkerId() {
370
+ return this.workerId;
371
+ }
372
+ /**
373
+ * Set max attempts for new jobs
374
+ */
375
+ setDefaultMaxAttempts(maxAttempts) {
376
+ this.defaultMaxAttempts = maxAttempts;
377
+ }
378
+ /**
379
+ * Simulate a worker crash by releasing a job's lock without completing it
380
+ */
381
+ simulateCrash(jobId) {
382
+ const job = this.jobs.get(jobId);
383
+ if (job && job.status === "RUNNING") ;
384
+ }
385
+ /**
386
+ * Move a suspended job back to pending (for manual resume testing)
387
+ */
388
+ resumeJob(jobId) {
389
+ const job = this.jobs.get(jobId);
390
+ if (job && job.status === "SUSPENDED") {
391
+ const updated = {
392
+ ...job,
393
+ status: "PENDING",
394
+ nextPollAt: null,
395
+ updatedAt: /* @__PURE__ */ new Date()
396
+ };
397
+ this.jobs.set(jobId, updated);
398
+ }
399
+ }
400
+ /**
401
+ * Set lockedAt for testing stale job scenarios
402
+ */
403
+ setJobLockedAt(jobId, lockedAt) {
404
+ const job = this.jobs.get(jobId);
405
+ if (job) {
406
+ const updated = {
407
+ ...job,
408
+ lockedAt
409
+ };
410
+ this.jobs.set(jobId, updated);
411
+ }
412
+ }
413
+ /**
414
+ * Set nextPollAt for testing suspended job polling
415
+ */
416
+ setJobNextPollAt(jobId, nextPollAt) {
417
+ const job = this.jobs.get(jobId);
418
+ if (job) {
419
+ const updated = {
420
+ ...job,
421
+ nextPollAt
422
+ };
423
+ this.jobs.set(jobId, updated);
424
+ }
425
+ }
426
+ };
427
+ var InMemoryWorkflowPersistence = class {
428
+ runs = /* @__PURE__ */ new Map();
429
+ stages = /* @__PURE__ */ new Map();
430
+ logs = /* @__PURE__ */ new Map();
431
+ artifacts = /* @__PURE__ */ new Map();
432
+ outbox = [];
433
+ idempotencyKeys = /* @__PURE__ */ new Map();
434
+ idempotencyInProgress = /* @__PURE__ */ new Set();
435
+ outboxSequences = /* @__PURE__ */ new Map();
436
+ // Helper to generate composite keys for stages
437
+ stageKey(runId, stageId) {
438
+ return `${runId}:${stageId}`;
439
+ }
440
+ // Helper to generate composite keys for artifacts
441
+ artifactKey(runId, key) {
442
+ return `${runId}:${key}`;
443
+ }
444
+ idempotencyCompositeKey(commandType, key) {
445
+ return `${commandType}:${key}`;
446
+ }
447
+ async withTransaction(fn) {
448
+ return fn(this);
449
+ }
450
+ // ============================================================================
451
+ // WorkflowRun Operations
452
+ // ============================================================================
453
+ async createRun(data) {
454
+ const now = /* @__PURE__ */ new Date();
455
+ const record = {
456
+ id: data.id ?? randomUUID(),
457
+ createdAt: now,
458
+ updatedAt: now,
459
+ version: 1,
460
+ workflowId: data.workflowId,
461
+ workflowName: data.workflowName,
462
+ workflowType: data.workflowType,
463
+ status: "PENDING",
464
+ startedAt: null,
465
+ completedAt: null,
466
+ duration: null,
467
+ input: data.input,
468
+ output: null,
469
+ config: data.config ?? {},
470
+ totalCost: 0,
471
+ totalTokens: 0,
472
+ priority: data.priority ?? 5
473
+ };
474
+ this.runs.set(record.id, record);
475
+ return { ...record };
476
+ }
477
+ async updateRun(id, data) {
478
+ const run = this.runs.get(id);
479
+ if (!run) {
480
+ throw new Error(`WorkflowRun not found: ${id}`);
481
+ }
482
+ if (data.expectedVersion !== void 0 && run.version !== data.expectedVersion) {
483
+ throw new StaleVersionError(
484
+ "WorkflowRun",
485
+ id,
486
+ data.expectedVersion,
487
+ run.version
488
+ );
489
+ }
490
+ const { expectedVersion: _, ...rest } = data;
491
+ const updated = {
492
+ ...run,
493
+ ...rest,
494
+ updatedAt: /* @__PURE__ */ new Date(),
495
+ version: run.version + 1
496
+ };
497
+ this.runs.set(id, updated);
498
+ }
499
+ async getRun(id) {
500
+ const run = this.runs.get(id);
501
+ return run ? { ...run } : null;
502
+ }
503
+ async getRunStatus(id) {
504
+ const run = this.runs.get(id);
505
+ return run?.status ?? null;
506
+ }
507
+ async getRunsByStatus(status) {
508
+ return Array.from(this.runs.values()).filter((run) => run.status === status).map((run) => ({ ...run }));
509
+ }
510
+ async claimPendingRun(id) {
511
+ const run = this.runs.get(id);
512
+ if (!run || run.status !== "PENDING") {
513
+ return false;
514
+ }
515
+ const updated = {
516
+ ...run,
517
+ status: "RUNNING",
518
+ startedAt: /* @__PURE__ */ new Date(),
519
+ updatedAt: /* @__PURE__ */ new Date(),
520
+ version: run.version + 1
521
+ };
522
+ this.runs.set(id, updated);
523
+ return true;
524
+ }
525
+ async claimNextPendingRun() {
526
+ const pendingRuns = Array.from(this.runs.values()).filter((run) => run.status === "PENDING").sort((a, b) => {
527
+ if (a.priority !== b.priority) {
528
+ return b.priority - a.priority;
529
+ }
530
+ return a.createdAt.getTime() - b.createdAt.getTime();
531
+ });
532
+ if (pendingRuns.length === 0) {
533
+ return null;
534
+ }
535
+ const runToClaim = pendingRuns[0];
536
+ const currentRun = this.runs.get(runToClaim.id);
537
+ if (!currentRun || currentRun.status !== "PENDING") {
538
+ return this.claimNextPendingRun();
539
+ }
540
+ const claimed = {
541
+ ...currentRun,
542
+ status: "RUNNING",
543
+ startedAt: /* @__PURE__ */ new Date(),
544
+ updatedAt: /* @__PURE__ */ new Date(),
545
+ version: currentRun.version + 1
546
+ };
547
+ this.runs.set(claimed.id, claimed);
548
+ return { ...claimed };
549
+ }
550
+ // ============================================================================
551
+ // WorkflowStage Operations
552
+ // ============================================================================
553
+ async createStage(data) {
554
+ const now = /* @__PURE__ */ new Date();
555
+ const id = randomUUID();
556
+ const record = {
557
+ id,
558
+ createdAt: now,
559
+ updatedAt: now,
560
+ version: 1,
561
+ workflowRunId: data.workflowRunId,
562
+ stageId: data.stageId,
563
+ stageName: data.stageName,
564
+ stageNumber: data.stageNumber,
565
+ executionGroup: data.executionGroup,
566
+ status: data.status ?? "PENDING",
567
+ startedAt: data.startedAt ?? null,
568
+ completedAt: null,
569
+ duration: null,
570
+ inputData: data.inputData ?? null,
571
+ outputData: null,
572
+ config: data.config ?? null,
573
+ suspendedState: null,
574
+ resumeData: null,
575
+ nextPollAt: null,
576
+ pollInterval: null,
577
+ maxWaitUntil: null,
578
+ metrics: null,
579
+ embeddingInfo: null,
580
+ errorMessage: null
581
+ };
582
+ this.stages.set(id, record);
583
+ this.stages.set(this.stageKey(data.workflowRunId, data.stageId), record);
584
+ return { ...record };
585
+ }
586
+ async upsertStage(data) {
587
+ const key = this.stageKey(data.workflowRunId, data.stageId);
588
+ const existing = this.stages.get(key);
589
+ if (existing) {
590
+ const updated = {
591
+ ...existing,
592
+ ...data.update,
593
+ updatedAt: /* @__PURE__ */ new Date(),
594
+ version: existing.version + 1
595
+ };
596
+ this.stages.set(existing.id, updated);
597
+ this.stages.set(key, updated);
598
+ return { ...updated };
599
+ } else {
600
+ return this.createStage(data.create);
601
+ }
602
+ }
603
+ async updateStage(id, data) {
604
+ const stage = this.stages.get(id);
605
+ if (!stage) {
606
+ throw new Error(`WorkflowStage not found: ${id}`);
607
+ }
608
+ if (data.expectedVersion !== void 0 && stage.version !== data.expectedVersion) {
609
+ throw new StaleVersionError(
610
+ "WorkflowStage",
611
+ id,
612
+ data.expectedVersion,
613
+ stage.version
614
+ );
615
+ }
616
+ const { expectedVersion: _, ...rest } = data;
617
+ const updated = {
618
+ ...stage,
619
+ ...rest,
620
+ updatedAt: /* @__PURE__ */ new Date(),
621
+ version: stage.version + 1
622
+ };
623
+ this.stages.set(id, updated);
624
+ this.stages.set(this.stageKey(stage.workflowRunId, stage.stageId), updated);
625
+ }
626
+ async updateStageByRunAndStageId(workflowRunId, stageId, data) {
627
+ const key = this.stageKey(workflowRunId, stageId);
628
+ const stage = this.stages.get(key);
629
+ if (!stage) {
630
+ throw new Error(`WorkflowStage not found: ${workflowRunId}/${stageId}`);
631
+ }
632
+ if (data.expectedVersion !== void 0 && stage.version !== data.expectedVersion) {
633
+ throw new StaleVersionError(
634
+ "WorkflowStage",
635
+ `${workflowRunId}/${stageId}`,
636
+ data.expectedVersion,
637
+ stage.version
638
+ );
639
+ }
640
+ const { expectedVersion: _, ...rest } = data;
641
+ const updated = {
642
+ ...stage,
643
+ ...rest,
644
+ updatedAt: /* @__PURE__ */ new Date(),
645
+ version: stage.version + 1
646
+ };
647
+ this.stages.set(stage.id, updated);
648
+ this.stages.set(key, updated);
649
+ }
650
+ async getStage(runId, stageId) {
651
+ const key = this.stageKey(runId, stageId);
652
+ const stage = this.stages.get(key);
653
+ return stage ? { ...stage } : null;
654
+ }
655
+ async getStageById(id) {
656
+ const stage = this.stages.get(id);
657
+ return stage ? { ...stage } : null;
658
+ }
659
+ async getStagesByRun(runId, options) {
660
+ const seenIds = /* @__PURE__ */ new Set();
661
+ let stages = Array.from(this.stages.values()).filter((s) => {
662
+ if (s.workflowRunId !== runId) return false;
663
+ if (seenIds.has(s.id)) return false;
664
+ seenIds.add(s.id);
665
+ return true;
666
+ });
667
+ if (options?.status) {
668
+ stages = stages.filter((s) => s.status === options.status);
669
+ }
670
+ stages.sort((a, b) => {
671
+ const diff = a.stageNumber - b.stageNumber;
672
+ return options?.orderBy === "desc" ? -diff : diff;
673
+ });
674
+ return stages.map((s) => ({ ...s }));
675
+ }
676
+ async getSuspendedStages(beforeDate) {
677
+ const seenIds = /* @__PURE__ */ new Set();
678
+ return Array.from(this.stages.values()).filter((s) => {
679
+ if (seenIds.has(s.id)) return false;
680
+ seenIds.add(s.id);
681
+ return s.status === "SUSPENDED" && s.nextPollAt !== null && s.nextPollAt <= beforeDate;
682
+ }).map((s) => ({ ...s }));
683
+ }
684
+ async getFirstSuspendedStageReadyToResume(runId) {
685
+ const stages = await this.getStagesByRun(runId, { status: "SUSPENDED" });
686
+ const now = /* @__PURE__ */ new Date();
687
+ const ready = stages.find((s) => s.nextPollAt && s.nextPollAt <= now);
688
+ return ready ?? null;
689
+ }
690
+ async getFirstFailedStage(runId) {
691
+ const stages = await this.getStagesByRun(runId, { status: "FAILED" });
692
+ return stages[0] ?? null;
693
+ }
694
+ async getLastCompletedStage(runId) {
695
+ const stages = await this.getStagesByRun(runId, {
696
+ status: "COMPLETED",
697
+ orderBy: "desc"
698
+ });
699
+ return stages[0] ?? null;
700
+ }
701
+ async getLastCompletedStageBefore(runId, executionGroup) {
702
+ const stages = await this.getStagesByRun(runId, {
703
+ status: "COMPLETED",
704
+ orderBy: "desc"
705
+ });
706
+ const before = stages.filter((s) => s.executionGroup < executionGroup);
707
+ return before[0] ?? null;
708
+ }
709
+ async deleteStage(id) {
710
+ const stage = this.stages.get(id);
711
+ if (stage) {
712
+ this.stages.delete(id);
713
+ this.stages.delete(this.stageKey(stage.workflowRunId, stage.stageId));
714
+ }
715
+ }
716
+ // ============================================================================
717
+ // WorkflowLog Operations
718
+ // ============================================================================
719
+ async createLog(data) {
720
+ const record = {
721
+ id: randomUUID(),
722
+ createdAt: /* @__PURE__ */ new Date(),
723
+ workflowRunId: data.workflowRunId ?? null,
724
+ workflowStageId: data.workflowStageId ?? null,
725
+ level: data.level,
726
+ message: data.message,
727
+ metadata: data.metadata ?? null
728
+ };
729
+ this.logs.set(record.id, record);
730
+ }
731
+ // ============================================================================
732
+ // Outbox Operations
733
+ // ============================================================================
734
+ async appendOutboxEvents(events) {
735
+ for (const event of events) {
736
+ const currentSeq = this.outboxSequences.get(event.workflowRunId) ?? 0;
737
+ const nextSeq = currentSeq + 1;
738
+ this.outboxSequences.set(event.workflowRunId, nextSeq);
739
+ const record = {
740
+ id: randomUUID(),
741
+ workflowRunId: event.workflowRunId,
742
+ sequence: nextSeq,
743
+ eventType: event.eventType,
744
+ payload: event.payload,
745
+ causationId: event.causationId,
746
+ occurredAt: event.occurredAt,
747
+ publishedAt: null,
748
+ retryCount: 0,
749
+ dlqAt: null
750
+ };
751
+ this.outbox.push(record);
752
+ }
753
+ }
754
+ async getUnpublishedOutboxEvents(limit) {
755
+ const effectiveLimit = limit ?? 100;
756
+ return this.outbox.filter((r) => r.publishedAt === null && r.dlqAt === null).sort((a, b) => {
757
+ const runCmp = a.workflowRunId.localeCompare(b.workflowRunId);
758
+ if (runCmp !== 0) return runCmp;
759
+ return a.sequence - b.sequence;
760
+ }).slice(0, effectiveLimit).map((r) => ({ ...r }));
761
+ }
762
+ async markOutboxEventsPublished(ids) {
763
+ const idSet = new Set(ids);
764
+ for (const record of this.outbox) {
765
+ if (idSet.has(record.id)) {
766
+ record.publishedAt = /* @__PURE__ */ new Date();
767
+ }
768
+ }
769
+ }
770
+ async incrementOutboxRetryCount(id) {
771
+ const record = this.outbox.find((r) => r.id === id);
772
+ if (!record) throw new Error(`Outbox event not found: ${id}`);
773
+ record.retryCount++;
774
+ return record.retryCount;
775
+ }
776
+ async moveOutboxEventToDLQ(id) {
777
+ const record = this.outbox.find((r) => r.id === id);
778
+ if (!record) throw new Error(`Outbox event not found: ${id}`);
779
+ record.dlqAt = /* @__PURE__ */ new Date();
780
+ }
781
+ async replayDLQEvents(maxEvents) {
782
+ const dlqEvents = this.outbox.filter((r) => r.dlqAt !== null).slice(0, maxEvents);
783
+ for (const record of dlqEvents) {
784
+ record.dlqAt = null;
785
+ record.retryCount = 0;
786
+ }
787
+ return dlqEvents.length;
788
+ }
789
+ // ============================================================================
790
+ // Idempotency Operations
791
+ // ============================================================================
792
+ async acquireIdempotencyKey(key, commandType) {
793
+ const compositeKey = this.idempotencyCompositeKey(commandType, key);
794
+ const record = this.idempotencyKeys.get(compositeKey);
795
+ if (record) {
796
+ return { status: "replay", result: record.result };
797
+ }
798
+ if (this.idempotencyInProgress.has(compositeKey)) {
799
+ return { status: "in_progress" };
800
+ }
801
+ this.idempotencyInProgress.add(compositeKey);
802
+ return { status: "acquired" };
803
+ }
804
+ async completeIdempotencyKey(key, commandType, result) {
805
+ const compositeKey = this.idempotencyCompositeKey(commandType, key);
806
+ this.idempotencyInProgress.delete(compositeKey);
807
+ this.idempotencyKeys.set(compositeKey, {
808
+ key,
809
+ commandType,
810
+ result,
811
+ createdAt: /* @__PURE__ */ new Date()
812
+ });
813
+ }
814
+ async releaseIdempotencyKey(key, commandType) {
815
+ this.idempotencyInProgress.delete(
816
+ this.idempotencyCompositeKey(commandType, key)
817
+ );
818
+ }
819
+ // ============================================================================
820
+ // WorkflowArtifact Operations
821
+ // ============================================================================
822
+ async saveArtifact(data) {
823
+ const now = /* @__PURE__ */ new Date();
824
+ const key = this.artifactKey(data.workflowRunId, data.key);
825
+ const existing = this.artifacts.get(key);
826
+ const record = {
827
+ id: existing?.id ?? randomUUID(),
828
+ createdAt: existing?.createdAt ?? now,
829
+ updatedAt: now,
830
+ workflowRunId: data.workflowRunId,
831
+ workflowStageId: data.workflowStageId ?? null,
832
+ key: data.key,
833
+ type: data.type,
834
+ data: data.data,
835
+ size: data.size,
836
+ metadata: data.metadata ?? null
837
+ };
838
+ this.artifacts.set(key, record);
839
+ }
840
+ async loadArtifact(runId, key) {
841
+ const artifact = this.artifacts.get(this.artifactKey(runId, key));
842
+ if (!artifact) {
843
+ throw new Error(`Artifact not found: ${runId}/${key}`);
844
+ }
845
+ return artifact.data;
846
+ }
847
+ async hasArtifact(runId, key) {
848
+ return this.artifacts.has(this.artifactKey(runId, key));
849
+ }
850
+ async deleteArtifact(runId, key) {
851
+ this.artifacts.delete(this.artifactKey(runId, key));
852
+ }
853
+ async listArtifacts(runId) {
854
+ return Array.from(this.artifacts.values()).filter((a) => a.workflowRunId === runId).map((a) => ({ ...a }));
855
+ }
856
+ async getStageIdForArtifact(runId, stageId) {
857
+ const stage = await this.getStage(runId, stageId);
858
+ return stage?.id ?? null;
859
+ }
860
+ // ============================================================================
861
+ // Stage Output Convenience Method
862
+ // ============================================================================
863
+ async saveStageOutput(runId, workflowType, stageId, output) {
864
+ const key = `workflow-v2/${workflowType}/${runId}/${stageId}/output.json`;
865
+ const stageDbId = await this.getStageIdForArtifact(runId, stageId);
866
+ await this.saveArtifact({
867
+ workflowRunId: runId,
868
+ workflowStageId: stageDbId ?? void 0,
869
+ key,
870
+ type: "STAGE_OUTPUT",
871
+ data: output,
872
+ size: JSON.stringify(output).length
873
+ });
874
+ return key;
875
+ }
876
+ // ============================================================================
877
+ // Test Helpers
878
+ // ============================================================================
879
+ /**
880
+ * Clear all data - useful between tests
881
+ */
882
+ clear() {
883
+ this.runs.clear();
884
+ this.stages.clear();
885
+ this.logs.clear();
886
+ this.artifacts.clear();
887
+ this.outbox = [];
888
+ this.idempotencyKeys.clear();
889
+ this.idempotencyInProgress.clear();
890
+ this.outboxSequences.clear();
891
+ }
892
+ /**
893
+ * Get all runs for inspection
894
+ */
895
+ getAllRuns() {
896
+ return Array.from(this.runs.values()).map((r) => ({ ...r }));
897
+ }
898
+ /**
899
+ * Get all stages for inspection
900
+ */
901
+ getAllStages() {
902
+ return Array.from(this.stages.values()).filter((s) => this.stages.get(s.id) === s).map((s) => ({ ...s }));
903
+ }
904
+ /**
905
+ * Get all logs for inspection
906
+ */
907
+ getAllLogs() {
908
+ return Array.from(this.logs.values()).map((l) => ({ ...l }));
909
+ }
910
+ /**
911
+ * Get all artifacts for inspection
912
+ */
913
+ getAllArtifacts() {
914
+ return Array.from(this.artifacts.values()).map((a) => ({ ...a }));
915
+ }
916
+ };
917
+
918
+ export { InMemoryAICallLogger, InMemoryJobQueue, InMemoryWorkflowPersistence };
919
+ //# sourceMappingURL=index.js.map
920
+ //# sourceMappingURL=index.js.map