@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,900 @@
1
+ import { createLogger } from './chunk-MUWP5SF2.js';
2
+
3
+ // src/persistence/prisma/ai-logger.ts
4
+ var logger = createLogger("AICallLogger");
5
+ var PrismaAICallLogger = class {
6
+ constructor(prisma) {
7
+ this.prisma = prisma;
8
+ }
9
+ /**
10
+ * Log a single AI call (fire and forget)
11
+ * Does not await - logs asynchronously to avoid blocking AI operations
12
+ */
13
+ logCall(call) {
14
+ this.prisma.aICall.create({
15
+ data: {
16
+ topic: call.topic,
17
+ callType: call.callType,
18
+ modelKey: call.modelKey,
19
+ modelId: call.modelId,
20
+ prompt: call.prompt,
21
+ response: call.response,
22
+ inputTokens: call.inputTokens,
23
+ outputTokens: call.outputTokens,
24
+ cost: call.cost,
25
+ metadata: call.metadata
26
+ }
27
+ }).catch(
28
+ (error) => logger.error("Failed to persist AI call:", error)
29
+ );
30
+ }
31
+ /**
32
+ * Log batch results (for recording batch API results)
33
+ */
34
+ async logBatchResults(batchId, results) {
35
+ await this.prisma.aICall.createMany({
36
+ data: results.map((call) => ({
37
+ topic: call.topic,
38
+ callType: call.callType,
39
+ modelKey: call.modelKey,
40
+ modelId: call.modelId,
41
+ prompt: call.prompt,
42
+ response: call.response,
43
+ inputTokens: call.inputTokens,
44
+ outputTokens: call.outputTokens,
45
+ cost: call.cost,
46
+ metadata: {
47
+ ...call.metadata,
48
+ batchId
49
+ }
50
+ }))
51
+ });
52
+ }
53
+ /**
54
+ * Get aggregated stats for a topic prefix
55
+ */
56
+ async getStats(topicPrefix) {
57
+ const calls = await this.prisma.aICall.findMany({
58
+ where: {
59
+ topic: { startsWith: topicPrefix }
60
+ },
61
+ select: {
62
+ modelKey: true,
63
+ inputTokens: true,
64
+ outputTokens: true,
65
+ cost: true
66
+ }
67
+ });
68
+ const perModel = {};
69
+ for (const call of calls) {
70
+ if (!perModel[call.modelKey]) {
71
+ perModel[call.modelKey] = {
72
+ calls: 0,
73
+ inputTokens: 0,
74
+ outputTokens: 0,
75
+ cost: 0
76
+ };
77
+ }
78
+ perModel[call.modelKey].calls++;
79
+ perModel[call.modelKey].inputTokens += call.inputTokens;
80
+ perModel[call.modelKey].outputTokens += call.outputTokens;
81
+ perModel[call.modelKey].cost += call.cost;
82
+ }
83
+ return {
84
+ totalCalls: calls.length,
85
+ totalInputTokens: calls.reduce(
86
+ (sum, c) => sum + c.inputTokens,
87
+ 0
88
+ ),
89
+ totalOutputTokens: calls.reduce(
90
+ (sum, c) => sum + c.outputTokens,
91
+ 0
92
+ ),
93
+ totalCost: calls.reduce(
94
+ (sum, c) => sum + c.cost,
95
+ 0
96
+ ),
97
+ perModel
98
+ };
99
+ }
100
+ /**
101
+ * Check if batch results are already recorded
102
+ */
103
+ async isRecorded(batchId) {
104
+ const existing = await this.prisma.aICall.findFirst({
105
+ where: {
106
+ metadata: {
107
+ path: ["batchId"],
108
+ equals: batchId
109
+ }
110
+ },
111
+ select: { id: true }
112
+ });
113
+ return existing !== null;
114
+ }
115
+ };
116
+ function createPrismaAICallLogger(prisma) {
117
+ return new PrismaAICallLogger(prisma);
118
+ }
119
+
120
+ // src/persistence/prisma/enum-compat.ts
121
+ function createEnumHelper(prisma) {
122
+ const resolveEnum = (enumName, value) => {
123
+ try {
124
+ const enumObj = prisma.$Enums?.[enumName];
125
+ if (enumObj && value in enumObj) {
126
+ return enumObj[value];
127
+ }
128
+ } catch {
129
+ }
130
+ return value;
131
+ };
132
+ return {
133
+ status: (value) => resolveEnum("Status", value),
134
+ artifactType: (value) => resolveEnum("ArtifactType", value),
135
+ logLevel: (value) => resolveEnum("LogLevel", value)
136
+ };
137
+ }
138
+
139
+ // src/persistence/prisma/job-queue.ts
140
+ var logger2 = createLogger("JobQueue");
141
+ var PrismaJobQueue = class {
142
+ workerId;
143
+ prisma;
144
+ enums;
145
+ databaseType;
146
+ constructor(prisma, options = {}) {
147
+ this.prisma = prisma;
148
+ this.workerId = options.workerId || `worker-${process.pid}-${Date.now()}`;
149
+ this.enums = createEnumHelper(prisma);
150
+ this.databaseType = options.databaseType ?? "postgresql";
151
+ }
152
+ /**
153
+ * Add a new job to the queue
154
+ */
155
+ async enqueue(options) {
156
+ const job = await this.prisma.jobQueue.create({
157
+ data: {
158
+ workflowRunId: options.workflowRunId,
159
+ stageId: options.stageId,
160
+ priority: options.priority ?? 5,
161
+ payload: options.payload || {},
162
+ status: this.enums.status("PENDING"),
163
+ nextPollAt: options.scheduledFor
164
+ }
165
+ });
166
+ logger2.debug(
167
+ `Enqueued job ${job.id} for stage ${options.stageId} (run: ${options.workflowRunId})`
168
+ );
169
+ return job.id;
170
+ }
171
+ /**
172
+ * Enqueue multiple stages in parallel (same execution group)
173
+ */
174
+ async enqueueParallel(jobs) {
175
+ if (jobs.length === 0) return [];
176
+ const results = await this.prisma.$transaction(
177
+ jobs.map(
178
+ (job) => this.prisma.jobQueue.create({
179
+ data: {
180
+ workflowRunId: job.workflowRunId,
181
+ stageId: job.stageId,
182
+ priority: job.priority ?? 5,
183
+ payload: job.payload || {},
184
+ status: this.enums.status("PENDING")
185
+ }
186
+ })
187
+ )
188
+ );
189
+ return results.map((r) => r.id);
190
+ }
191
+ /**
192
+ * Atomically dequeue the next available job
193
+ * Uses FOR UPDATE SKIP LOCKED (PostgreSQL) or optimistic locking (SQLite)
194
+ */
195
+ async dequeue() {
196
+ if (this.databaseType === "sqlite") {
197
+ return this.dequeueSqlite();
198
+ }
199
+ return this.dequeuePostgres();
200
+ }
201
+ /**
202
+ * PostgreSQL implementation using FOR UPDATE SKIP LOCKED for safe concurrency
203
+ */
204
+ async dequeuePostgres() {
205
+ try {
206
+ const result = await this.prisma.$queryRaw`
207
+ UPDATE "job_queue"
208
+ SET
209
+ status = 'RUNNING',
210
+ "workerId" = ${this.workerId},
211
+ "lockedAt" = NOW(),
212
+ "startedAt" = NOW(),
213
+ attempt = attempt + 1
214
+ WHERE id = (
215
+ SELECT id FROM "job_queue"
216
+ WHERE status = 'PENDING'
217
+ AND ("nextPollAt" IS NULL OR "nextPollAt" <= NOW())
218
+ ORDER BY priority DESC, "createdAt" ASC
219
+ LIMIT 1
220
+ FOR UPDATE SKIP LOCKED
221
+ )
222
+ RETURNING id, "workflowRunId", "stageId", priority, attempt, payload
223
+ `;
224
+ if (result.length === 0) {
225
+ return null;
226
+ }
227
+ const job = result[0];
228
+ logger2.debug(
229
+ `Dequeued job ${job.id} (stage: ${job.stageId}, attempt: ${job.attempt})`
230
+ );
231
+ return {
232
+ jobId: job.id,
233
+ workflowRunId: job.workflowRunId,
234
+ stageId: job.stageId,
235
+ priority: job.priority,
236
+ attempt: job.attempt,
237
+ payload: job.payload
238
+ };
239
+ } catch (error) {
240
+ logger2.error("Error dequeuing job:", error);
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * SQLite implementation using optimistic locking.
246
+ * SQLite doesn't support FOR UPDATE SKIP LOCKED, so we use a two-step approach:
247
+ * 1. Find a PENDING job
248
+ * 2. Atomically update it (only succeeds if still PENDING)
249
+ * 3. If another worker claimed it, retry
250
+ */
251
+ async dequeueSqlite() {
252
+ try {
253
+ const now = /* @__PURE__ */ new Date();
254
+ const job = await this.prisma.jobQueue.findFirst({
255
+ where: {
256
+ status: this.enums.status("PENDING"),
257
+ OR: [{ nextPollAt: null }, { nextPollAt: { lte: now } }]
258
+ },
259
+ orderBy: [{ priority: "desc" }, { createdAt: "asc" }]
260
+ });
261
+ if (!job) {
262
+ return null;
263
+ }
264
+ const result = await this.prisma.jobQueue.updateMany({
265
+ where: {
266
+ id: job.id,
267
+ status: this.enums.status("PENDING")
268
+ // Optimistic lock
269
+ },
270
+ data: {
271
+ status: this.enums.status("RUNNING"),
272
+ workerId: this.workerId,
273
+ lockedAt: now,
274
+ startedAt: now,
275
+ attempt: { increment: 1 }
276
+ }
277
+ });
278
+ if (result.count === 0) {
279
+ return this.dequeueSqlite();
280
+ }
281
+ const claimedJob = await this.prisma.jobQueue.findUnique({
282
+ where: { id: job.id }
283
+ });
284
+ if (!claimedJob) {
285
+ return null;
286
+ }
287
+ logger2.debug(
288
+ `Dequeued job ${claimedJob.id} (stage: ${claimedJob.stageId}, attempt: ${claimedJob.attempt})`
289
+ );
290
+ return {
291
+ jobId: claimedJob.id,
292
+ workflowRunId: claimedJob.workflowRunId,
293
+ stageId: claimedJob.stageId,
294
+ priority: claimedJob.priority,
295
+ attempt: claimedJob.attempt,
296
+ payload: claimedJob.payload
297
+ };
298
+ } catch (error) {
299
+ logger2.error("Error dequeuing job:", error);
300
+ return null;
301
+ }
302
+ }
303
+ /**
304
+ * Mark job as completed
305
+ */
306
+ async complete(jobId) {
307
+ await this.prisma.jobQueue.update({
308
+ where: { id: jobId },
309
+ data: {
310
+ status: this.enums.status("COMPLETED"),
311
+ completedAt: /* @__PURE__ */ new Date()
312
+ }
313
+ });
314
+ logger2.debug(`Job ${jobId} completed`);
315
+ }
316
+ /**
317
+ * Mark job as suspended (for async-batch)
318
+ */
319
+ async suspend(jobId, nextPollAt) {
320
+ await this.prisma.jobQueue.update({
321
+ where: { id: jobId },
322
+ data: {
323
+ status: this.enums.status("SUSPENDED"),
324
+ nextPollAt,
325
+ workerId: null,
326
+ lockedAt: null
327
+ }
328
+ });
329
+ logger2.debug(`Job ${jobId} suspended until ${nextPollAt.toISOString()}`);
330
+ }
331
+ /**
332
+ * Mark job as failed
333
+ */
334
+ async fail(jobId, error, shouldRetry = false) {
335
+ const job = await this.prisma.jobQueue.findUnique({
336
+ where: { id: jobId },
337
+ select: { attempt: true, maxAttempts: true }
338
+ });
339
+ if (shouldRetry && job && job.attempt < job.maxAttempts) {
340
+ const backoffMs = 2 ** job.attempt * 1e3;
341
+ const nextPollAt = new Date(Date.now() + backoffMs);
342
+ await this.prisma.jobQueue.update({
343
+ where: { id: jobId },
344
+ data: {
345
+ status: this.enums.status("PENDING"),
346
+ lastError: error,
347
+ workerId: null,
348
+ lockedAt: null,
349
+ nextPollAt
350
+ }
351
+ });
352
+ logger2.debug(`Job ${jobId} failed, will retry in ${backoffMs}ms`);
353
+ } else {
354
+ await this.prisma.jobQueue.update({
355
+ where: { id: jobId },
356
+ data: {
357
+ status: this.enums.status("FAILED"),
358
+ completedAt: /* @__PURE__ */ new Date(),
359
+ lastError: error
360
+ }
361
+ });
362
+ logger2.debug(`Job ${jobId} failed permanently: ${error}`);
363
+ }
364
+ }
365
+ /**
366
+ * Get suspended jobs that are ready to be checked
367
+ */
368
+ async getSuspendedJobsReadyToPoll() {
369
+ const jobs = await this.prisma.jobQueue.findMany({
370
+ where: {
371
+ status: this.enums.status("SUSPENDED"),
372
+ nextPollAt: { lte: /* @__PURE__ */ new Date() }
373
+ },
374
+ select: {
375
+ id: true,
376
+ workflowRunId: true,
377
+ stageId: true
378
+ }
379
+ });
380
+ return jobs.map(
381
+ (j) => ({
382
+ jobId: j.id,
383
+ workflowRunId: j.workflowRunId,
384
+ stageId: j.stageId
385
+ })
386
+ );
387
+ }
388
+ /**
389
+ * Release stale locks (for crashed workers)
390
+ */
391
+ async releaseStaleJobs(staleThresholdMs = 3e5) {
392
+ const thresholdDate = new Date(Date.now() - staleThresholdMs);
393
+ const result = await this.prisma.jobQueue.updateMany({
394
+ where: {
395
+ status: this.enums.status("RUNNING"),
396
+ lockedAt: { lt: thresholdDate }
397
+ },
398
+ data: {
399
+ status: this.enums.status("PENDING"),
400
+ workerId: null,
401
+ lockedAt: null
402
+ }
403
+ });
404
+ if (result.count > 0) {
405
+ logger2.debug(
406
+ `Released ${result.count} stale jobs (locked before ${thresholdDate.toISOString()})`
407
+ );
408
+ }
409
+ return result.count;
410
+ }
411
+ };
412
+ function createPrismaJobQueue(prisma, optionsOrWorkerId) {
413
+ const options = typeof optionsOrWorkerId === "string" ? { workerId: optionsOrWorkerId } : optionsOrWorkerId ?? {};
414
+ return new PrismaJobQueue(prisma, options);
415
+ }
416
+
417
+ // src/persistence/prisma/persistence.ts
418
+ var PrismaWorkflowPersistence = class {
419
+ constructor(prisma, options = {}) {
420
+ this.prisma = prisma;
421
+ this.enums = createEnumHelper(prisma);
422
+ this.databaseType = options.databaseType ?? "postgresql";
423
+ }
424
+ enums;
425
+ databaseType;
426
+ // ============================================================================
427
+ // WorkflowRun Operations
428
+ // ============================================================================
429
+ async createRun(data) {
430
+ const run = await this.prisma.workflowRun.create({
431
+ data: {
432
+ id: data.id,
433
+ workflowId: data.workflowId,
434
+ workflowName: data.workflowName,
435
+ workflowType: data.workflowType,
436
+ input: data.input,
437
+ config: data.config ?? {},
438
+ priority: data.priority ?? 5,
439
+ // Spread metadata for domain-specific fields (certificateId, etc.)
440
+ ...data.metadata ?? {}
441
+ }
442
+ });
443
+ return this.mapWorkflowRun(run);
444
+ }
445
+ async updateRun(id, data) {
446
+ await this.prisma.workflowRun.update({
447
+ where: { id },
448
+ data: {
449
+ status: data.status ? this.enums.status(data.status) : void 0,
450
+ startedAt: data.startedAt,
451
+ completedAt: data.completedAt,
452
+ duration: data.duration,
453
+ output: data.output,
454
+ totalCost: data.totalCost,
455
+ totalTokens: data.totalTokens
456
+ }
457
+ });
458
+ }
459
+ async getRun(id) {
460
+ const run = await this.prisma.workflowRun.findUnique({ where: { id } });
461
+ return run ? this.mapWorkflowRun(run) : null;
462
+ }
463
+ async getRunStatus(id) {
464
+ const run = await this.prisma.workflowRun.findUnique({
465
+ where: { id },
466
+ select: { status: true }
467
+ });
468
+ return run?.status ?? null;
469
+ }
470
+ async getRunsByStatus(status) {
471
+ const runs = await this.prisma.workflowRun.findMany({
472
+ where: { status: this.enums.status(status) },
473
+ orderBy: { createdAt: "asc" }
474
+ });
475
+ return runs.map((run) => this.mapWorkflowRun(run));
476
+ }
477
+ async claimPendingRun(id) {
478
+ const result = await this.prisma.workflowRun.updateMany({
479
+ where: {
480
+ id,
481
+ status: this.enums.status("PENDING")
482
+ },
483
+ data: {
484
+ status: this.enums.status("RUNNING"),
485
+ startedAt: /* @__PURE__ */ new Date()
486
+ }
487
+ });
488
+ return result.count > 0;
489
+ }
490
+ async claimNextPendingRun() {
491
+ if (this.databaseType === "sqlite") {
492
+ return this.claimNextPendingRunSqlite();
493
+ }
494
+ return this.claimNextPendingRunPostgres();
495
+ }
496
+ /**
497
+ * PostgreSQL implementation using FOR UPDATE SKIP LOCKED for zero-contention claiming.
498
+ * This atomically:
499
+ * 1. Finds the highest priority PENDING run (FIFO within same priority)
500
+ * 2. Locks it exclusively (other workers skip locked rows)
501
+ * 3. Updates it to RUNNING
502
+ * 4. Returns the claimed run
503
+ */
504
+ async claimNextPendingRunPostgres() {
505
+ const results = await this.prisma.$queryRaw`
506
+ WITH claimed AS (
507
+ SELECT id
508
+ FROM "workflow_runs"
509
+ WHERE status = ${this.enums.status("PENDING")}
510
+ ORDER BY priority DESC, "createdAt" ASC
511
+ LIMIT 1
512
+ FOR UPDATE SKIP LOCKED
513
+ )
514
+ UPDATE "workflow_runs"
515
+ SET status = ${this.enums.status("RUNNING")},
516
+ "startedAt" = NOW(),
517
+ "updatedAt" = NOW()
518
+ FROM claimed
519
+ WHERE "workflow_runs".id = claimed.id
520
+ RETURNING "workflow_runs".*
521
+ `;
522
+ if (results.length === 0) {
523
+ return null;
524
+ }
525
+ return this.mapWorkflowRun(results[0]);
526
+ }
527
+ /**
528
+ * SQLite implementation using optimistic locking.
529
+ * SQLite doesn't support FOR UPDATE SKIP LOCKED, so we use a two-step approach:
530
+ * 1. Find a PENDING run
531
+ * 2. Atomically update it (only succeeds if still PENDING)
532
+ * 3. If another worker claimed it, retry
533
+ */
534
+ async claimNextPendingRunSqlite() {
535
+ const run = await this.prisma.workflowRun.findFirst({
536
+ where: { status: this.enums.status("PENDING") },
537
+ orderBy: [{ priority: "desc" }, { createdAt: "asc" }]
538
+ });
539
+ if (!run) {
540
+ return null;
541
+ }
542
+ const result = await this.prisma.workflowRun.updateMany({
543
+ where: {
544
+ id: run.id,
545
+ status: this.enums.status("PENDING")
546
+ // Optimistic lock
547
+ },
548
+ data: {
549
+ status: this.enums.status("RUNNING"),
550
+ startedAt: /* @__PURE__ */ new Date(),
551
+ updatedAt: /* @__PURE__ */ new Date()
552
+ }
553
+ });
554
+ if (result.count === 0) {
555
+ return this.claimNextPendingRunSqlite();
556
+ }
557
+ const claimedRun = await this.prisma.workflowRun.findUnique({
558
+ where: { id: run.id }
559
+ });
560
+ return claimedRun ? this.mapWorkflowRun(claimedRun) : null;
561
+ }
562
+ // ============================================================================
563
+ // WorkflowStage Operations
564
+ // ============================================================================
565
+ async createStage(data) {
566
+ const stage = await this.prisma.workflowStage.create({
567
+ data: {
568
+ workflowRunId: data.workflowRunId,
569
+ stageId: data.stageId,
570
+ stageName: data.stageName,
571
+ stageNumber: data.stageNumber,
572
+ executionGroup: data.executionGroup,
573
+ status: data.status ? this.enums.status(data.status) : this.enums.status("PENDING"),
574
+ startedAt: data.startedAt,
575
+ config: data.config,
576
+ inputData: data.inputData
577
+ }
578
+ });
579
+ return this.mapWorkflowStage(stage);
580
+ }
581
+ async upsertStage(data) {
582
+ const stage = await this.prisma.workflowStage.upsert({
583
+ where: {
584
+ workflowRunId_stageId: {
585
+ workflowRunId: data.workflowRunId,
586
+ stageId: data.stageId
587
+ }
588
+ },
589
+ create: {
590
+ workflowRunId: data.create.workflowRunId,
591
+ stageId: data.create.stageId,
592
+ stageName: data.create.stageName,
593
+ stageNumber: data.create.stageNumber,
594
+ executionGroup: data.create.executionGroup,
595
+ status: data.create.status ? this.enums.status(data.create.status) : this.enums.status("RUNNING"),
596
+ startedAt: data.create.startedAt ?? /* @__PURE__ */ new Date(),
597
+ config: data.create.config,
598
+ inputData: data.create.inputData
599
+ },
600
+ update: {
601
+ status: data.update.status ? this.enums.status(data.update.status) : void 0,
602
+ startedAt: data.update.startedAt
603
+ }
604
+ });
605
+ return this.mapWorkflowStage(stage);
606
+ }
607
+ async updateStage(id, data) {
608
+ await this.prisma.workflowStage.update({
609
+ where: { id },
610
+ data: this.buildStageUpdateData(data)
611
+ });
612
+ }
613
+ async updateStageByRunAndStageId(workflowRunId, stageId, data) {
614
+ await this.prisma.workflowStage.update({
615
+ where: {
616
+ workflowRunId_stageId: { workflowRunId, stageId }
617
+ },
618
+ data: this.buildStageUpdateData(data)
619
+ });
620
+ }
621
+ buildStageUpdateData(data) {
622
+ return {
623
+ status: data.status ? this.enums.status(data.status) : void 0,
624
+ startedAt: data.startedAt,
625
+ completedAt: data.completedAt,
626
+ duration: data.duration,
627
+ outputData: data.outputData,
628
+ config: data.config,
629
+ suspendedState: data.suspendedState,
630
+ resumeData: data.resumeData,
631
+ nextPollAt: data.nextPollAt,
632
+ pollInterval: data.pollInterval,
633
+ maxWaitUntil: data.maxWaitUntil,
634
+ metrics: data.metrics,
635
+ embeddingInfo: data.embeddingInfo,
636
+ errorMessage: data.errorMessage
637
+ };
638
+ }
639
+ async getStage(runId, stageId) {
640
+ const stage = await this.prisma.workflowStage.findUnique({
641
+ where: {
642
+ workflowRunId_stageId: { workflowRunId: runId, stageId }
643
+ }
644
+ });
645
+ return stage ? this.mapWorkflowStage(stage) : null;
646
+ }
647
+ async getStageById(id) {
648
+ const stage = await this.prisma.workflowStage.findUnique({ where: { id } });
649
+ return stage ? this.mapWorkflowStage(stage) : null;
650
+ }
651
+ async getStagesByRun(runId, options) {
652
+ const stages = await this.prisma.workflowStage.findMany({
653
+ where: {
654
+ workflowRunId: runId,
655
+ ...options?.status && { status: this.enums.status(options.status) }
656
+ },
657
+ orderBy: { executionGroup: options?.orderBy ?? "asc" }
658
+ });
659
+ return stages.map((s) => this.mapWorkflowStage(s));
660
+ }
661
+ async getSuspendedStages(beforeDate) {
662
+ const stages = await this.prisma.workflowStage.findMany({
663
+ where: {
664
+ status: this.enums.status("SUSPENDED"),
665
+ nextPollAt: { lte: beforeDate }
666
+ },
667
+ include: {
668
+ workflowRun: { select: { workflowType: true } }
669
+ }
670
+ });
671
+ return stages.map((s) => this.mapWorkflowStage(s));
672
+ }
673
+ async getFirstSuspendedStageReadyToResume(runId) {
674
+ const stage = await this.prisma.workflowStage.findFirst({
675
+ where: {
676
+ workflowRunId: runId,
677
+ status: this.enums.status("SUSPENDED"),
678
+ nextPollAt: null
679
+ // Ready to resume (poll cleared by orchestrator)
680
+ },
681
+ orderBy: { executionGroup: "asc" }
682
+ });
683
+ return stage ? this.mapWorkflowStage(stage) : null;
684
+ }
685
+ async getFirstFailedStage(runId) {
686
+ const stage = await this.prisma.workflowStage.findFirst({
687
+ where: {
688
+ workflowRunId: runId,
689
+ status: this.enums.status("FAILED")
690
+ },
691
+ orderBy: { executionGroup: "desc" }
692
+ });
693
+ return stage ? this.mapWorkflowStage(stage) : null;
694
+ }
695
+ async getLastCompletedStage(runId) {
696
+ const stage = await this.prisma.workflowStage.findFirst({
697
+ where: {
698
+ workflowRunId: runId,
699
+ status: this.enums.status("COMPLETED")
700
+ },
701
+ orderBy: { executionGroup: "desc" }
702
+ });
703
+ return stage ? this.mapWorkflowStage(stage) : null;
704
+ }
705
+ async getLastCompletedStageBefore(runId, executionGroup) {
706
+ const stage = await this.prisma.workflowStage.findFirst({
707
+ where: {
708
+ workflowRunId: runId,
709
+ status: this.enums.status("COMPLETED"),
710
+ executionGroup: { lt: executionGroup }
711
+ },
712
+ orderBy: { executionGroup: "desc" }
713
+ });
714
+ return stage ? this.mapWorkflowStage(stage) : null;
715
+ }
716
+ async deleteStage(id) {
717
+ await this.prisma.workflowStage.delete({ where: { id } });
718
+ }
719
+ // ============================================================================
720
+ // WorkflowLog Operations
721
+ // ============================================================================
722
+ async createLog(data) {
723
+ await this.prisma.workflowLog.create({
724
+ data: {
725
+ workflowRunId: data.workflowRunId,
726
+ workflowStageId: data.workflowStageId,
727
+ level: this.enums.logLevel(data.level),
728
+ message: data.message,
729
+ metadata: data.metadata
730
+ }
731
+ });
732
+ }
733
+ // ============================================================================
734
+ // WorkflowArtifact Operations
735
+ // ============================================================================
736
+ async saveArtifact(data) {
737
+ await this.prisma.workflowArtifact.upsert({
738
+ where: {
739
+ workflowRunId_key: {
740
+ workflowRunId: data.workflowRunId,
741
+ key: data.key
742
+ }
743
+ },
744
+ create: {
745
+ workflowRunId: data.workflowRunId,
746
+ workflowStageId: data.workflowStageId,
747
+ key: data.key,
748
+ type: this.enums.artifactType(data.type),
749
+ data: data.data,
750
+ size: data.size,
751
+ metadata: data.metadata
752
+ },
753
+ update: {
754
+ data: data.data,
755
+ size: data.size,
756
+ metadata: data.metadata
757
+ }
758
+ });
759
+ }
760
+ async loadArtifact(runId, key) {
761
+ const artifact = await this.prisma.workflowArtifact.findUnique({
762
+ where: {
763
+ workflowRunId_key: { workflowRunId: runId, key }
764
+ }
765
+ });
766
+ return artifact?.data;
767
+ }
768
+ async hasArtifact(runId, key) {
769
+ const artifact = await this.prisma.workflowArtifact.findUnique({
770
+ where: {
771
+ workflowRunId_key: { workflowRunId: runId, key }
772
+ },
773
+ select: { id: true }
774
+ });
775
+ return artifact !== null;
776
+ }
777
+ async deleteArtifact(runId, key) {
778
+ await this.prisma.workflowArtifact.delete({
779
+ where: {
780
+ workflowRunId_key: { workflowRunId: runId, key }
781
+ }
782
+ });
783
+ }
784
+ async listArtifacts(runId) {
785
+ const artifacts = await this.prisma.workflowArtifact.findMany({
786
+ where: { workflowRunId: runId }
787
+ });
788
+ return artifacts.map(
789
+ (a) => this.mapWorkflowArtifact(a)
790
+ );
791
+ }
792
+ async getStageIdForArtifact(runId, stageId) {
793
+ const stage = await this.prisma.workflowStage.findUnique({
794
+ where: {
795
+ workflowRunId_stageId: { workflowRunId: runId, stageId }
796
+ },
797
+ select: { id: true }
798
+ });
799
+ return stage?.id ?? null;
800
+ }
801
+ // ============================================================================
802
+ // Stage Output Convenience Methods
803
+ // ============================================================================
804
+ async saveStageOutput(runId, workflowType, stageId, output) {
805
+ const key = `workflow-v2/${workflowType}/${runId}/${stageId}/output.json`;
806
+ const json = JSON.stringify(output);
807
+ const size = Buffer.byteLength(json, "utf8");
808
+ const workflowStageId = await this.getStageIdForArtifact(runId, stageId);
809
+ await this.prisma.workflowArtifact.upsert({
810
+ where: {
811
+ workflowRunId_key: { workflowRunId: runId, key }
812
+ },
813
+ update: {
814
+ data: output,
815
+ size,
816
+ workflowStageId
817
+ },
818
+ create: {
819
+ workflowRunId: runId,
820
+ workflowStageId,
821
+ key,
822
+ type: this.enums.artifactType("STAGE_OUTPUT"),
823
+ data: output,
824
+ size
825
+ }
826
+ });
827
+ return key;
828
+ }
829
+ // ============================================================================
830
+ // Type Mappers
831
+ // ============================================================================
832
+ mapWorkflowRun(run) {
833
+ return {
834
+ id: run.id,
835
+ createdAt: run.createdAt,
836
+ updatedAt: run.updatedAt,
837
+ workflowId: run.workflowId,
838
+ workflowName: run.workflowName,
839
+ workflowType: run.workflowType,
840
+ status: run.status,
841
+ startedAt: run.startedAt,
842
+ completedAt: run.completedAt,
843
+ duration: run.duration,
844
+ input: run.input,
845
+ output: run.output,
846
+ config: run.config,
847
+ totalCost: run.totalCost,
848
+ totalTokens: run.totalTokens,
849
+ priority: run.priority
850
+ };
851
+ }
852
+ mapWorkflowStage(stage) {
853
+ return {
854
+ id: stage.id,
855
+ createdAt: stage.createdAt,
856
+ updatedAt: stage.updatedAt,
857
+ workflowRunId: stage.workflowRunId,
858
+ stageId: stage.stageId,
859
+ stageName: stage.stageName,
860
+ stageNumber: stage.stageNumber,
861
+ executionGroup: stage.executionGroup,
862
+ status: stage.status,
863
+ startedAt: stage.startedAt,
864
+ completedAt: stage.completedAt,
865
+ duration: stage.duration,
866
+ inputData: stage.inputData,
867
+ outputData: stage.outputData,
868
+ config: stage.config,
869
+ suspendedState: stage.suspendedState,
870
+ resumeData: stage.resumeData,
871
+ nextPollAt: stage.nextPollAt,
872
+ pollInterval: stage.pollInterval,
873
+ maxWaitUntil: stage.maxWaitUntil,
874
+ metrics: stage.metrics,
875
+ embeddingInfo: stage.embeddingInfo,
876
+ errorMessage: stage.errorMessage
877
+ };
878
+ }
879
+ mapWorkflowArtifact(artifact) {
880
+ return {
881
+ id: artifact.id,
882
+ createdAt: artifact.createdAt,
883
+ updatedAt: artifact.updatedAt,
884
+ workflowRunId: artifact.workflowRunId,
885
+ workflowStageId: artifact.workflowStageId,
886
+ key: artifact.key,
887
+ type: artifact.type,
888
+ data: artifact.data,
889
+ size: artifact.size,
890
+ metadata: artifact.metadata
891
+ };
892
+ }
893
+ };
894
+ function createPrismaWorkflowPersistence(prisma, options) {
895
+ return new PrismaWorkflowPersistence(prisma, options);
896
+ }
897
+
898
+ export { PrismaAICallLogger, PrismaJobQueue, PrismaWorkflowPersistence, createEnumHelper, createPrismaAICallLogger, createPrismaJobQueue, createPrismaWorkflowPersistence };
899
+ //# sourceMappingURL=chunk-7IITBLFY.js.map
900
+ //# sourceMappingURL=chunk-7IITBLFY.js.map