@bratsos/workflow-engine 0.0.11 → 0.1.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.
@@ -0,0 +1,777 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ // src/testing/in-memory-ai-logger.ts
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
+ stageId: options.stageId,
194
+ status: "PENDING",
195
+ priority: options.priority ?? 5,
196
+ workerId: null,
197
+ lockedAt: null,
198
+ startedAt: null,
199
+ completedAt: null,
200
+ attempt: 1,
201
+ maxAttempts: this.defaultMaxAttempts,
202
+ lastError: null,
203
+ nextPollAt: options.scheduledFor ?? null,
204
+ payload: options.payload ?? {}
205
+ };
206
+ this.jobs.set(id, job);
207
+ return id;
208
+ }
209
+ async enqueueParallel(jobs) {
210
+ const ids = [];
211
+ for (const job of jobs) {
212
+ const id = await this.enqueue(job);
213
+ ids.push(id);
214
+ }
215
+ return ids;
216
+ }
217
+ async dequeue() {
218
+ const now = /* @__PURE__ */ new Date();
219
+ const pendingJobs = Array.from(this.jobs.values()).filter(
220
+ (j) => j.status === "PENDING" && (j.nextPollAt === null || j.nextPollAt <= now)
221
+ ).sort((a, b) => {
222
+ if (b.priority !== a.priority) {
223
+ return b.priority - a.priority;
224
+ }
225
+ return a.createdAt.getTime() - b.createdAt.getTime();
226
+ });
227
+ if (pendingJobs.length === 0) {
228
+ return null;
229
+ }
230
+ const job = pendingJobs[0];
231
+ const updated = {
232
+ ...job,
233
+ status: "RUNNING",
234
+ workerId: this.workerId,
235
+ lockedAt: now,
236
+ startedAt: now,
237
+ updatedAt: now
238
+ };
239
+ this.jobs.set(job.id, updated);
240
+ return {
241
+ jobId: job.id,
242
+ workflowRunId: job.workflowRunId,
243
+ stageId: job.stageId,
244
+ priority: job.priority,
245
+ attempt: job.attempt,
246
+ payload: job.payload
247
+ };
248
+ }
249
+ async complete(jobId) {
250
+ const job = this.jobs.get(jobId);
251
+ if (!job) {
252
+ throw new Error(`Job not found: ${jobId}`);
253
+ }
254
+ const now = /* @__PURE__ */ new Date();
255
+ const updated = {
256
+ ...job,
257
+ status: "COMPLETED",
258
+ completedAt: now,
259
+ updatedAt: now
260
+ };
261
+ this.jobs.set(jobId, updated);
262
+ }
263
+ async suspend(jobId, nextPollAt) {
264
+ const job = this.jobs.get(jobId);
265
+ if (!job) {
266
+ throw new Error(`Job not found: ${jobId}`);
267
+ }
268
+ const updated = {
269
+ ...job,
270
+ status: "SUSPENDED",
271
+ nextPollAt,
272
+ workerId: null,
273
+ lockedAt: null,
274
+ updatedAt: /* @__PURE__ */ new Date()
275
+ };
276
+ this.jobs.set(jobId, updated);
277
+ }
278
+ async fail(jobId, error, shouldRetry = true) {
279
+ const job = this.jobs.get(jobId);
280
+ if (!job) {
281
+ throw new Error(`Job not found: ${jobId}`);
282
+ }
283
+ const now = /* @__PURE__ */ new Date();
284
+ if (shouldRetry && job.attempt < job.maxAttempts) {
285
+ const updated = {
286
+ ...job,
287
+ status: "PENDING",
288
+ attempt: job.attempt + 1,
289
+ lastError: error,
290
+ workerId: null,
291
+ lockedAt: null,
292
+ updatedAt: now
293
+ };
294
+ this.jobs.set(jobId, updated);
295
+ } else {
296
+ const updated = {
297
+ ...job,
298
+ status: "FAILED",
299
+ lastError: error,
300
+ completedAt: now,
301
+ updatedAt: now
302
+ };
303
+ this.jobs.set(jobId, updated);
304
+ }
305
+ }
306
+ async getSuspendedJobsReadyToPoll() {
307
+ const now = /* @__PURE__ */ new Date();
308
+ return Array.from(this.jobs.values()).filter(
309
+ (j) => j.status === "SUSPENDED" && j.nextPollAt && j.nextPollAt <= now
310
+ ).map((j) => ({
311
+ jobId: j.id,
312
+ stageId: j.stageId,
313
+ workflowRunId: j.workflowRunId
314
+ }));
315
+ }
316
+ async releaseStaleJobs(staleThresholdMs = 6e4) {
317
+ const now = /* @__PURE__ */ new Date();
318
+ const threshold = new Date(now.getTime() - staleThresholdMs);
319
+ let released = 0;
320
+ for (const job of this.jobs.values()) {
321
+ if (job.status === "RUNNING" && job.lockedAt && job.lockedAt < threshold) {
322
+ const updated = {
323
+ ...job,
324
+ status: "PENDING",
325
+ workerId: null,
326
+ lockedAt: null,
327
+ updatedAt: now
328
+ };
329
+ this.jobs.set(job.id, updated);
330
+ released++;
331
+ }
332
+ }
333
+ return released;
334
+ }
335
+ // ============================================================================
336
+ // Test Helpers
337
+ // ============================================================================
338
+ /**
339
+ * Clear all jobs - useful between tests
340
+ */
341
+ clear() {
342
+ this.jobs.clear();
343
+ }
344
+ /**
345
+ * Get all jobs for inspection
346
+ */
347
+ getAllJobs() {
348
+ return Array.from(this.jobs.values()).map((j) => ({ ...j }));
349
+ }
350
+ /**
351
+ * Get jobs by status for inspection
352
+ */
353
+ getJobsByStatus(status) {
354
+ return Array.from(this.jobs.values()).filter((j) => j.status === status).map((j) => ({ ...j }));
355
+ }
356
+ /**
357
+ * Get a specific job by ID
358
+ */
359
+ getJob(jobId) {
360
+ const job = this.jobs.get(jobId);
361
+ return job ? { ...job } : null;
362
+ }
363
+ /**
364
+ * Get the worker ID for this queue instance
365
+ */
366
+ getWorkerId() {
367
+ return this.workerId;
368
+ }
369
+ /**
370
+ * Set max attempts for new jobs
371
+ */
372
+ setDefaultMaxAttempts(maxAttempts) {
373
+ this.defaultMaxAttempts = maxAttempts;
374
+ }
375
+ /**
376
+ * Simulate a worker crash by releasing a job's lock without completing it
377
+ */
378
+ simulateCrash(jobId) {
379
+ const job = this.jobs.get(jobId);
380
+ if (job && job.status === "RUNNING") ;
381
+ }
382
+ /**
383
+ * Move a suspended job back to pending (for manual resume testing)
384
+ */
385
+ resumeJob(jobId) {
386
+ const job = this.jobs.get(jobId);
387
+ if (job && job.status === "SUSPENDED") {
388
+ const updated = {
389
+ ...job,
390
+ status: "PENDING",
391
+ nextPollAt: null,
392
+ updatedAt: /* @__PURE__ */ new Date()
393
+ };
394
+ this.jobs.set(jobId, updated);
395
+ }
396
+ }
397
+ /**
398
+ * Set lockedAt for testing stale job scenarios
399
+ */
400
+ setJobLockedAt(jobId, lockedAt) {
401
+ const job = this.jobs.get(jobId);
402
+ if (job) {
403
+ const updated = {
404
+ ...job,
405
+ lockedAt
406
+ };
407
+ this.jobs.set(jobId, updated);
408
+ }
409
+ }
410
+ /**
411
+ * Set nextPollAt for testing suspended job polling
412
+ */
413
+ setJobNextPollAt(jobId, nextPollAt) {
414
+ const job = this.jobs.get(jobId);
415
+ if (job) {
416
+ const updated = {
417
+ ...job,
418
+ nextPollAt
419
+ };
420
+ this.jobs.set(jobId, updated);
421
+ }
422
+ }
423
+ };
424
+ var InMemoryWorkflowPersistence = class {
425
+ runs = /* @__PURE__ */ new Map();
426
+ stages = /* @__PURE__ */ new Map();
427
+ logs = /* @__PURE__ */ new Map();
428
+ artifacts = /* @__PURE__ */ new Map();
429
+ // Helper to generate composite keys for stages
430
+ stageKey(runId, stageId) {
431
+ return `${runId}:${stageId}`;
432
+ }
433
+ // Helper to generate composite keys for artifacts
434
+ artifactKey(runId, key) {
435
+ return `${runId}:${key}`;
436
+ }
437
+ // ============================================================================
438
+ // WorkflowRun Operations
439
+ // ============================================================================
440
+ async createRun(data) {
441
+ const now = /* @__PURE__ */ new Date();
442
+ const record = {
443
+ id: data.id ?? randomUUID(),
444
+ createdAt: now,
445
+ updatedAt: now,
446
+ workflowId: data.workflowId,
447
+ workflowName: data.workflowName,
448
+ workflowType: data.workflowType,
449
+ status: "PENDING",
450
+ startedAt: null,
451
+ completedAt: null,
452
+ duration: null,
453
+ input: data.input,
454
+ output: null,
455
+ config: data.config ?? {},
456
+ totalCost: 0,
457
+ totalTokens: 0,
458
+ priority: data.priority ?? 5
459
+ };
460
+ this.runs.set(record.id, record);
461
+ return { ...record };
462
+ }
463
+ async updateRun(id, data) {
464
+ const run = this.runs.get(id);
465
+ if (!run) {
466
+ throw new Error(`WorkflowRun not found: ${id}`);
467
+ }
468
+ const updated = {
469
+ ...run,
470
+ ...data,
471
+ updatedAt: /* @__PURE__ */ new Date()
472
+ };
473
+ this.runs.set(id, updated);
474
+ }
475
+ async getRun(id) {
476
+ const run = this.runs.get(id);
477
+ return run ? { ...run } : null;
478
+ }
479
+ async getRunStatus(id) {
480
+ const run = this.runs.get(id);
481
+ return run?.status ?? null;
482
+ }
483
+ async getRunsByStatus(status) {
484
+ return Array.from(this.runs.values()).filter((run) => run.status === status).map((run) => ({ ...run }));
485
+ }
486
+ async claimPendingRun(id) {
487
+ const run = this.runs.get(id);
488
+ if (!run || run.status !== "PENDING") {
489
+ return false;
490
+ }
491
+ const updated = {
492
+ ...run,
493
+ status: "RUNNING",
494
+ startedAt: /* @__PURE__ */ new Date(),
495
+ updatedAt: /* @__PURE__ */ new Date()
496
+ };
497
+ this.runs.set(id, updated);
498
+ return true;
499
+ }
500
+ async claimNextPendingRun() {
501
+ const pendingRuns = Array.from(this.runs.values()).filter((run) => run.status === "PENDING").sort((a, b) => {
502
+ if (a.priority !== b.priority) {
503
+ return b.priority - a.priority;
504
+ }
505
+ return a.createdAt.getTime() - b.createdAt.getTime();
506
+ });
507
+ if (pendingRuns.length === 0) {
508
+ return null;
509
+ }
510
+ const runToClaim = pendingRuns[0];
511
+ const currentRun = this.runs.get(runToClaim.id);
512
+ if (!currentRun || currentRun.status !== "PENDING") {
513
+ return this.claimNextPendingRun();
514
+ }
515
+ const claimed = {
516
+ ...currentRun,
517
+ status: "RUNNING",
518
+ startedAt: /* @__PURE__ */ new Date(),
519
+ updatedAt: /* @__PURE__ */ new Date()
520
+ };
521
+ this.runs.set(claimed.id, claimed);
522
+ return { ...claimed };
523
+ }
524
+ // ============================================================================
525
+ // WorkflowStage Operations
526
+ // ============================================================================
527
+ async createStage(data) {
528
+ const now = /* @__PURE__ */ new Date();
529
+ const id = randomUUID();
530
+ const record = {
531
+ id,
532
+ createdAt: now,
533
+ updatedAt: now,
534
+ workflowRunId: data.workflowRunId,
535
+ stageId: data.stageId,
536
+ stageName: data.stageName,
537
+ stageNumber: data.stageNumber,
538
+ executionGroup: data.executionGroup,
539
+ status: data.status ?? "PENDING",
540
+ startedAt: data.startedAt ?? null,
541
+ completedAt: null,
542
+ duration: null,
543
+ inputData: data.inputData ?? null,
544
+ outputData: null,
545
+ config: data.config ?? null,
546
+ suspendedState: null,
547
+ resumeData: null,
548
+ nextPollAt: null,
549
+ pollInterval: null,
550
+ maxWaitUntil: null,
551
+ metrics: null,
552
+ embeddingInfo: null,
553
+ errorMessage: null
554
+ };
555
+ this.stages.set(id, record);
556
+ this.stages.set(this.stageKey(data.workflowRunId, data.stageId), record);
557
+ return { ...record };
558
+ }
559
+ async upsertStage(data) {
560
+ const key = this.stageKey(data.workflowRunId, data.stageId);
561
+ const existing = this.stages.get(key);
562
+ if (existing) {
563
+ const updated = {
564
+ ...existing,
565
+ ...data.update,
566
+ updatedAt: /* @__PURE__ */ new Date()
567
+ };
568
+ this.stages.set(existing.id, updated);
569
+ this.stages.set(key, updated);
570
+ return { ...updated };
571
+ } else {
572
+ return this.createStage(data.create);
573
+ }
574
+ }
575
+ async updateStage(id, data) {
576
+ const stage = this.stages.get(id);
577
+ if (!stage) {
578
+ throw new Error(`WorkflowStage not found: ${id}`);
579
+ }
580
+ const updated = {
581
+ ...stage,
582
+ ...data,
583
+ updatedAt: /* @__PURE__ */ new Date()
584
+ };
585
+ this.stages.set(id, updated);
586
+ this.stages.set(this.stageKey(stage.workflowRunId, stage.stageId), updated);
587
+ }
588
+ async updateStageByRunAndStageId(workflowRunId, stageId, data) {
589
+ const key = this.stageKey(workflowRunId, stageId);
590
+ const stage = this.stages.get(key);
591
+ if (!stage) {
592
+ throw new Error(`WorkflowStage not found: ${workflowRunId}/${stageId}`);
593
+ }
594
+ const updated = {
595
+ ...stage,
596
+ ...data,
597
+ updatedAt: /* @__PURE__ */ new Date()
598
+ };
599
+ this.stages.set(stage.id, updated);
600
+ this.stages.set(key, updated);
601
+ }
602
+ async getStage(runId, stageId) {
603
+ const key = this.stageKey(runId, stageId);
604
+ const stage = this.stages.get(key);
605
+ return stage ? { ...stage } : null;
606
+ }
607
+ async getStageById(id) {
608
+ const stage = this.stages.get(id);
609
+ return stage ? { ...stage } : null;
610
+ }
611
+ async getStagesByRun(runId, options) {
612
+ const seenIds = /* @__PURE__ */ new Set();
613
+ let stages = Array.from(this.stages.values()).filter((s) => {
614
+ if (s.workflowRunId !== runId) return false;
615
+ if (seenIds.has(s.id)) return false;
616
+ seenIds.add(s.id);
617
+ return true;
618
+ });
619
+ if (options?.status) {
620
+ stages = stages.filter((s) => s.status === options.status);
621
+ }
622
+ stages.sort((a, b) => {
623
+ const diff = a.stageNumber - b.stageNumber;
624
+ return options?.orderBy === "desc" ? -diff : diff;
625
+ });
626
+ return stages.map((s) => ({ ...s }));
627
+ }
628
+ async getSuspendedStages(beforeDate) {
629
+ return Array.from(this.stages.values()).filter(
630
+ (s) => s.status === "SUSPENDED" && s.nextPollAt && s.nextPollAt <= beforeDate && this.stages.get(s.id) === s
631
+ ).map((s) => ({ ...s }));
632
+ }
633
+ async getFirstSuspendedStageReadyToResume(runId) {
634
+ const stages = await this.getStagesByRun(runId, { status: "SUSPENDED" });
635
+ const now = /* @__PURE__ */ new Date();
636
+ const ready = stages.find((s) => s.nextPollAt && s.nextPollAt <= now);
637
+ return ready ?? null;
638
+ }
639
+ async getFirstFailedStage(runId) {
640
+ const stages = await this.getStagesByRun(runId, { status: "FAILED" });
641
+ return stages[0] ?? null;
642
+ }
643
+ async getLastCompletedStage(runId) {
644
+ const stages = await this.getStagesByRun(runId, {
645
+ status: "COMPLETED",
646
+ orderBy: "desc"
647
+ });
648
+ return stages[0] ?? null;
649
+ }
650
+ async getLastCompletedStageBefore(runId, executionGroup) {
651
+ const stages = await this.getStagesByRun(runId, {
652
+ status: "COMPLETED",
653
+ orderBy: "desc"
654
+ });
655
+ const before = stages.filter((s) => s.executionGroup < executionGroup);
656
+ return before[0] ?? null;
657
+ }
658
+ async deleteStage(id) {
659
+ const stage = this.stages.get(id);
660
+ if (stage) {
661
+ this.stages.delete(id);
662
+ this.stages.delete(this.stageKey(stage.workflowRunId, stage.stageId));
663
+ }
664
+ }
665
+ // ============================================================================
666
+ // WorkflowLog Operations
667
+ // ============================================================================
668
+ async createLog(data) {
669
+ const record = {
670
+ id: randomUUID(),
671
+ createdAt: /* @__PURE__ */ new Date(),
672
+ workflowRunId: data.workflowRunId ?? null,
673
+ workflowStageId: data.workflowStageId ?? null,
674
+ level: data.level,
675
+ message: data.message,
676
+ metadata: data.metadata ?? null
677
+ };
678
+ this.logs.set(record.id, record);
679
+ }
680
+ // ============================================================================
681
+ // WorkflowArtifact Operations
682
+ // ============================================================================
683
+ async saveArtifact(data) {
684
+ const now = /* @__PURE__ */ new Date();
685
+ const key = this.artifactKey(data.workflowRunId, data.key);
686
+ const existing = this.artifacts.get(key);
687
+ const record = {
688
+ id: existing?.id ?? randomUUID(),
689
+ createdAt: existing?.createdAt ?? now,
690
+ updatedAt: now,
691
+ workflowRunId: data.workflowRunId,
692
+ workflowStageId: data.workflowStageId ?? null,
693
+ key: data.key,
694
+ type: data.type,
695
+ data: data.data,
696
+ size: data.size,
697
+ metadata: data.metadata ?? null
698
+ };
699
+ this.artifacts.set(key, record);
700
+ }
701
+ async loadArtifact(runId, key) {
702
+ const artifact = this.artifacts.get(this.artifactKey(runId, key));
703
+ if (!artifact) {
704
+ throw new Error(`Artifact not found: ${runId}/${key}`);
705
+ }
706
+ return artifact.data;
707
+ }
708
+ async hasArtifact(runId, key) {
709
+ return this.artifacts.has(this.artifactKey(runId, key));
710
+ }
711
+ async deleteArtifact(runId, key) {
712
+ this.artifacts.delete(this.artifactKey(runId, key));
713
+ }
714
+ async listArtifacts(runId) {
715
+ return Array.from(this.artifacts.values()).filter((a) => a.workflowRunId === runId).map((a) => ({ ...a }));
716
+ }
717
+ async getStageIdForArtifact(runId, stageId) {
718
+ const stage = await this.getStage(runId, stageId);
719
+ return stage?.id ?? null;
720
+ }
721
+ // ============================================================================
722
+ // Stage Output Convenience Method
723
+ // ============================================================================
724
+ async saveStageOutput(runId, workflowType, stageId, output) {
725
+ const key = `workflow-v2/${workflowType}/${runId}/${stageId}/output.json`;
726
+ const stageDbId = await this.getStageIdForArtifact(runId, stageId);
727
+ await this.saveArtifact({
728
+ workflowRunId: runId,
729
+ workflowStageId: stageDbId ?? void 0,
730
+ key,
731
+ type: "STAGE_OUTPUT",
732
+ data: output,
733
+ size: JSON.stringify(output).length
734
+ });
735
+ return key;
736
+ }
737
+ // ============================================================================
738
+ // Test Helpers
739
+ // ============================================================================
740
+ /**
741
+ * Clear all data - useful between tests
742
+ */
743
+ clear() {
744
+ this.runs.clear();
745
+ this.stages.clear();
746
+ this.logs.clear();
747
+ this.artifacts.clear();
748
+ }
749
+ /**
750
+ * Get all runs for inspection
751
+ */
752
+ getAllRuns() {
753
+ return Array.from(this.runs.values()).map((r) => ({ ...r }));
754
+ }
755
+ /**
756
+ * Get all stages for inspection
757
+ */
758
+ getAllStages() {
759
+ return Array.from(this.stages.values()).filter((s) => this.stages.get(s.id) === s).map((s) => ({ ...s }));
760
+ }
761
+ /**
762
+ * Get all logs for inspection
763
+ */
764
+ getAllLogs() {
765
+ return Array.from(this.logs.values()).map((l) => ({ ...l }));
766
+ }
767
+ /**
768
+ * Get all artifacts for inspection
769
+ */
770
+ getAllArtifacts() {
771
+ return Array.from(this.artifacts.values()).map((a) => ({ ...a }));
772
+ }
773
+ };
774
+
775
+ export { InMemoryAICallLogger, InMemoryJobQueue, InMemoryWorkflowPersistence };
776
+ //# sourceMappingURL=index.js.map
777
+ //# sourceMappingURL=index.js.map