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