@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.
- package/README.md +270 -513
- package/dist/chunk-D7RVRRM2.js +3 -0
- package/dist/chunk-D7RVRRM2.js.map +1 -0
- package/dist/chunk-HL3OJG7W.js +1033 -0
- package/dist/chunk-HL3OJG7W.js.map +1 -0
- package/dist/chunk-MUWP5SF2.js +33 -0
- package/dist/chunk-MUWP5SF2.js.map +1 -0
- package/dist/chunk-NYKMT46J.js +1143 -0
- package/dist/chunk-NYKMT46J.js.map +1 -0
- package/dist/chunk-P4KMGCT3.js +2292 -0
- package/dist/chunk-P4KMGCT3.js.map +1 -0
- package/dist/chunk-SPXBCZLB.js +17 -0
- package/dist/chunk-SPXBCZLB.js.map +1 -0
- package/dist/cli/sync-models.d.ts +1 -0
- package/dist/cli/sync-models.js +210 -0
- package/dist/cli/sync-models.js.map +1 -0
- package/dist/client-D4PoxADF.d.ts +798 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -0
- package/dist/index-DAzCfO1R.d.ts +217 -0
- package/dist/index.d.ts +569 -0
- package/dist/index.js +399 -0
- package/dist/index.js.map +1 -0
- package/dist/interface-MMqhfQQK.d.ts +411 -0
- package/dist/kernel/index.d.ts +26 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/index.js.map +1 -0
- package/dist/kernel/testing/index.d.ts +44 -0
- package/dist/kernel/testing/index.js +85 -0
- package/dist/kernel/testing/index.js.map +1 -0
- package/dist/persistence/index.d.ts +2 -0
- package/dist/persistence/index.js +6 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/prisma/index.d.ts +37 -0
- package/dist/persistence/prisma/index.js +5 -0
- package/dist/persistence/prisma/index.js.map +1 -0
- package/dist/plugins-BCnDUwIc.d.ts +415 -0
- package/dist/ports-tU3rzPXJ.d.ts +245 -0
- package/dist/stage-BPw7m9Wx.d.ts +144 -0
- package/dist/testing/index.d.ts +264 -0
- package/dist/testing/index.js +920 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +11 -1
- package/skills/workflow-engine/SKILL.md +234 -348
- package/skills/workflow-engine/references/03-runtime-setup.md +111 -426
- package/skills/workflow-engine/references/05-persistence-setup.md +32 -0
- package/skills/workflow-engine/references/07-testing-patterns.md +141 -474
- 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
|