@sixb/pg 0.0.1

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,1065 @@
1
+ import type { Principal, WorkflowRunSource } from "@sixb/core"
2
+ import type { WorkflowIOSnapshot } from "@sixb/core/internal/workflows"
3
+ import type {
4
+ CancelWorkflowAgentNodeRunInput,
5
+ ConfirmWorkflowAgentNodeRunExecutionOwnershipInput,
6
+ ConfirmWorkflowRunExecutionOwnershipInput,
7
+ CreateWorkflowAgentNodeRunInput,
8
+ FinishWorkflowAgentNodeRunInput,
9
+ FinishWorkflowNodeRunInput,
10
+ FinishWorkflowRunInput,
11
+ ListLatestWorkflowRunsInput,
12
+ ListLatestWorkflowRunsResult,
13
+ ListWorkflowAgentNodeRunsInput,
14
+ ListWorkflowAgentNodeRunsResult,
15
+ ListWorkflowNodeRunsInput,
16
+ ListWorkflowNodeRunsResult,
17
+ ListWorkflowRunsInput,
18
+ ListWorkflowRunsResult,
19
+ QueueWorkflowRunInput,
20
+ ReclaimWorkflowAgentNodeRunInput,
21
+ ReclaimWorkflowRunInput,
22
+ ResumeWorkflowRunInput,
23
+ StartWorkflowAgentNodeRunInput,
24
+ StartWorkflowNodeRunInput,
25
+ StartWorkflowRunInput,
26
+ WaitWorkflowNodeRunInput,
27
+ WaitWorkflowRunInput,
28
+ WorkflowAgentNodeRunRecord,
29
+ WorkflowAgentNodeRunStorage,
30
+ WorkflowNodeRunRecord,
31
+ WorkflowNodeRunStorage,
32
+ WorkflowRunRecord,
33
+ WorkflowRunStorage,
34
+ } from "@sixb/core/storage"
35
+ import { WorkflowRunError } from "@sixb/core/storage"
36
+ import { queryLatestRunsByOwnerId } from "./latest-run-query"
37
+ import type { SqlParameter } from "./pg-client"
38
+ import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "./run-list-query"
39
+ import { isUniqueViolation } from "./storage-errors"
40
+ import { type PgStoreClient, runPgTransaction } from "./transactions"
41
+
42
+ export class PgWorkflowRunStorage implements WorkflowRunStorage {
43
+ readonly nodes: PgWorkflowNodeRunStorage
44
+ readonly agentNodes: PgWorkflowAgentNodeRunStorage
45
+
46
+ constructor(private readonly sql: PgStoreClient) {
47
+ this.nodes = new PgWorkflowNodeRunStorage(sql)
48
+ this.agentNodes = new PgWorkflowAgentNodeRunStorage(sql)
49
+ }
50
+
51
+ async queue(input: QueueWorkflowRunInput): Promise<WorkflowRunRecord> {
52
+ const queuedAt = input.queuedAt ?? new Date()
53
+
54
+ try {
55
+ const [row] = await this.sql<WorkflowRunDatabaseRow[]>`
56
+ INSERT INTO workflow_runs (
57
+ project_id,
58
+ id,
59
+ workflow_id,
60
+ status,
61
+ input,
62
+ queued_at,
63
+ started_at,
64
+ source,
65
+ requested_by_principal_type,
66
+ requested_by_principal_id,
67
+ attempt
68
+ ) VALUES (
69
+ ${input.projectId},
70
+ ${input.id},
71
+ ${input.workflowId},
72
+ ${"queued"},
73
+ ${serializeRecord(input.input)}::text::jsonb,
74
+ ${queuedAt},
75
+ ${queuedAt},
76
+ ${input.source ? JSON.stringify(input.source) : null}::text::jsonb,
77
+ ${input.requestedByPrincipal?.type ?? "system"},
78
+ ${input.requestedByPrincipal?.id ?? "system"},
79
+ ${0}
80
+ )
81
+ RETURNING *
82
+ `
83
+
84
+ return rowToWorkflowRunRecord(row)
85
+ } catch (error) {
86
+ if (isUniqueViolation(error)) {
87
+ throw new WorkflowRunError(
88
+ `[SixbPg] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
89
+ )
90
+ }
91
+
92
+ throw error
93
+ }
94
+ }
95
+
96
+ async start(input: StartWorkflowRunInput): Promise<WorkflowRunRecord> {
97
+ return runPgTransaction(this.sql, async (tx) => {
98
+ const startedAt = input.startedAt ?? new Date()
99
+ const [existing] = await tx<WorkflowRunDatabaseRow[]>`
100
+ SELECT * FROM workflow_runs
101
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
102
+ FOR UPDATE
103
+ `
104
+
105
+ if (existing) {
106
+ if (existing.status !== "queued") {
107
+ throw new WorkflowRunError(
108
+ `[SixbPg] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
109
+ )
110
+ }
111
+
112
+ if (existing.workflow_id !== input.workflowId) {
113
+ throw new WorkflowRunError(
114
+ `[SixbPg] Workflow run '${input.id}' workflow '${input.workflowId}' does not match existing workflow '${existing.workflow_id}'.`
115
+ )
116
+ }
117
+
118
+ const [updated] = await tx<WorkflowRunDatabaseRow[]>`
119
+ UPDATE workflow_runs
120
+ SET
121
+ status = ${"running"},
122
+ input = ${serializeRecord(input.input)}::text::jsonb,
123
+ started_at = ${startedAt},
124
+ finished_at = ${null},
125
+ error = ${null},
126
+ source = COALESCE(
127
+ source,
128
+ ${input.source ? JSON.stringify(input.source) : null}::text::jsonb
129
+ ),
130
+ attempt = attempt + 1,
131
+ execution_token = ${input.execution?.token ?? null},
132
+ execution_queue_lease_expires_at = ${input.execution?.queueLeaseExpiresAt ?? null}
133
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
134
+ RETURNING *
135
+ `
136
+
137
+ return rowToWorkflowRunRecord(updated)
138
+ }
139
+
140
+ try {
141
+ const [row] = await tx<WorkflowRunDatabaseRow[]>`
142
+ INSERT INTO workflow_runs (
143
+ project_id,
144
+ id,
145
+ workflow_id,
146
+ status,
147
+ input,
148
+ started_at,
149
+ source,
150
+ requested_by_principal_type,
151
+ requested_by_principal_id,
152
+ attempt,
153
+ execution_token,
154
+ execution_queue_lease_expires_at
155
+ ) VALUES (
156
+ ${input.projectId},
157
+ ${input.id},
158
+ ${input.workflowId},
159
+ ${"running"},
160
+ ${serializeRecord(input.input)}::text::jsonb,
161
+ ${startedAt},
162
+ ${input.source ? JSON.stringify(input.source) : null}::text::jsonb,
163
+ ${input.requestedByPrincipal?.type ?? "system"},
164
+ ${input.requestedByPrincipal?.id ?? "system"},
165
+ ${1},
166
+ ${input.execution?.token ?? null},
167
+ ${input.execution?.queueLeaseExpiresAt ?? null}
168
+ )
169
+ RETURNING *
170
+ `
171
+
172
+ return rowToWorkflowRunRecord(row)
173
+ } catch (error) {
174
+ if (isUniqueViolation(error)) {
175
+ throw new WorkflowRunError(
176
+ `[SixbPg] Workflow run '${input.id}' already exists for project '${input.projectId}'.`
177
+ )
178
+ }
179
+
180
+ throw error
181
+ }
182
+ })
183
+ }
184
+
185
+ async reclaim(input: ReclaimWorkflowRunInput): Promise<WorkflowRunRecord> {
186
+ return runPgTransaction(this.sql, async (tx) => {
187
+ const existing = await lockWorkflowRun(tx, input.projectId, input.id, "running")
188
+ const [updated] = await tx<WorkflowRunDatabaseRow[]>`
189
+ UPDATE workflow_runs SET
190
+ attempt = ${Number(existing.attempt) + 1},
191
+ execution_token = ${input.execution.token},
192
+ execution_queue_lease_expires_at = ${input.execution.queueLeaseExpiresAt}
193
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
194
+ RETURNING *
195
+ `
196
+ return rowToWorkflowRunRecord(updated)
197
+ })
198
+ }
199
+
200
+ async confirmExecutionOwnership(
201
+ input: ConfirmWorkflowRunExecutionOwnershipInput
202
+ ): Promise<WorkflowRunRecord> {
203
+ return runPgTransaction(this.sql, async (tx) => {
204
+ const existing = await lockWorkflowRun(tx, input.projectId, input.id, "running")
205
+ assertWorkflowRunExecutionOwnership(existing, input.executionToken)
206
+ const current = existing.execution_queue_lease_expires_at
207
+ ? new Date(existing.execution_queue_lease_expires_at)
208
+ : input.queueLeaseExpiresAt
209
+ const [updated] = await tx<WorkflowRunDatabaseRow[]>`
210
+ UPDATE workflow_runs SET execution_queue_lease_expires_at = ${new Date(
211
+ Math.max(current.getTime(), input.queueLeaseExpiresAt.getTime())
212
+ )}
213
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
214
+ RETURNING *
215
+ `
216
+ return rowToWorkflowRunRecord(updated)
217
+ })
218
+ }
219
+
220
+ async finish(input: FinishWorkflowRunInput): Promise<WorkflowRunRecord> {
221
+ return runPgTransaction(this.sql, async (tx) => {
222
+ const [existing] = await tx<WorkflowRunDatabaseRow[]>`
223
+ SELECT * FROM workflow_runs
224
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
225
+ FOR UPDATE
226
+ `
227
+
228
+ if (!existing) {
229
+ throw new WorkflowRunError(
230
+ `[SixbPg] Workflow run '${input.id}' not found for project '${input.projectId}'.`
231
+ )
232
+ }
233
+
234
+ if (!canFinishWorkflowRun(existing.status, input.status)) {
235
+ throw new WorkflowRunError(
236
+ `[SixbPg] Workflow run '${input.id}' for project '${input.projectId}' cannot be finished from status '${existing.status}'.`
237
+ )
238
+ }
239
+ assertWorkflowRunExecutionOwnership(existing, input.executionToken)
240
+
241
+ const [updated] =
242
+ input.status === "succeeded"
243
+ ? await tx<WorkflowRunDatabaseRow[]>`
244
+ UPDATE workflow_runs
245
+ SET
246
+ status = ${input.status},
247
+ finished_at = ${input.finishedAt ?? new Date()},
248
+ error = ${null},
249
+ execution_token = ${null},
250
+ execution_queue_lease_expires_at = ${null}
251
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
252
+ RETURNING *
253
+ `
254
+ : await tx<WorkflowRunDatabaseRow[]>`
255
+ UPDATE workflow_runs
256
+ SET
257
+ status = ${input.status},
258
+ finished_at = ${input.finishedAt ?? new Date()},
259
+ error = ${input.error ?? null},
260
+ execution_token = ${null},
261
+ execution_queue_lease_expires_at = ${null}
262
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
263
+ RETURNING *
264
+ `
265
+
266
+ return rowToWorkflowRunRecord(updated)
267
+ })
268
+ }
269
+
270
+ async wait(input: WaitWorkflowRunInput): Promise<WorkflowRunRecord> {
271
+ return runPgTransaction(this.sql, async (tx) => {
272
+ const [existing] = await tx<WorkflowRunDatabaseRow[]>`
273
+ SELECT * FROM workflow_runs
274
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
275
+ FOR UPDATE
276
+ `
277
+
278
+ if (!existing) {
279
+ throw new WorkflowRunError(
280
+ `[SixbPg] Workflow run '${input.id}' not found for project '${input.projectId}'.`
281
+ )
282
+ }
283
+
284
+ if (existing.status !== "running") {
285
+ throw new WorkflowRunError(
286
+ `[SixbPg] Workflow run '${input.id}' for project '${input.projectId}' must be running.`
287
+ )
288
+ }
289
+ assertWorkflowRunExecutionOwnership(existing, input.executionToken)
290
+
291
+ const [updated] = await tx<WorkflowRunDatabaseRow[]>`
292
+ UPDATE workflow_runs
293
+ SET
294
+ status = ${"waiting"},
295
+ finished_at = ${null},
296
+ error = ${null},
297
+ execution_token = ${null},
298
+ execution_queue_lease_expires_at = ${null}
299
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
300
+ RETURNING *
301
+ `
302
+
303
+ return rowToWorkflowRunRecord(updated)
304
+ })
305
+ }
306
+
307
+ async resume(input: ResumeWorkflowRunInput): Promise<WorkflowRunRecord> {
308
+ return runPgTransaction(this.sql, async (tx) => {
309
+ const [existing] = await tx<WorkflowRunDatabaseRow[]>`
310
+ SELECT * FROM workflow_runs
311
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
312
+ FOR UPDATE
313
+ `
314
+
315
+ if (!existing) {
316
+ throw new WorkflowRunError(
317
+ `[SixbPg] Workflow run '${input.id}' not found for project '${input.projectId}'.`
318
+ )
319
+ }
320
+
321
+ if (existing.status !== "waiting") {
322
+ throw new WorkflowRunError(
323
+ `[SixbPg] Workflow run '${input.id}' for project '${input.projectId}' must be waiting.`
324
+ )
325
+ }
326
+
327
+ const [updated] = await tx<WorkflowRunDatabaseRow[]>`
328
+ UPDATE workflow_runs
329
+ SET
330
+ status = ${"running"},
331
+ finished_at = ${null},
332
+ error = ${null},
333
+ attempt = attempt + 1,
334
+ execution_token = ${input.execution?.token ?? null},
335
+ execution_queue_lease_expires_at = ${input.execution?.queueLeaseExpiresAt ?? null}
336
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
337
+ RETURNING *
338
+ `
339
+
340
+ return rowToWorkflowRunRecord(updated)
341
+ })
342
+ }
343
+
344
+ async getById(params: { projectId: string; id: string }): Promise<WorkflowRunRecord | null> {
345
+ const [row] = await this.sql<WorkflowRunDatabaseRow[]>`
346
+ SELECT * FROM workflow_runs
347
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
348
+ `
349
+
350
+ return row ? rowToWorkflowRunRecord(row) : null
351
+ }
352
+
353
+ async list(input: ListWorkflowRunsInput): Promise<ListWorkflowRunsResult> {
354
+ if (hasEmptyStatuses(input) || input.workflowIds?.length === 0) {
355
+ return {
356
+ runs: [],
357
+ hasMore: false,
358
+ total: 0,
359
+ }
360
+ }
361
+
362
+ const whereClauses = ["project_id = $1"]
363
+ const params: SqlParameter[] = [input.projectId]
364
+ let index = 2
365
+
366
+ if (input.workflowId) {
367
+ whereClauses.push(`workflow_id = $${index++}`)
368
+ params.push(input.workflowId)
369
+ }
370
+
371
+ if (input.workflowIds) {
372
+ const placeholders = input.workflowIds.map(() => `$${index++}`)
373
+ whereClauses.push(`workflow_id IN (${placeholders.join(", ")})`)
374
+ params.push(...input.workflowIds)
375
+ }
376
+
377
+ index = appendRunListFilters(whereClauses, params, index, input)
378
+
379
+ const { rows, total, hasMore } = await queryRunList<WorkflowRunDatabaseRow>({
380
+ sql: this.sql,
381
+ tableName: "workflow_runs",
382
+ whereClauses,
383
+ params,
384
+ nextIndex: index,
385
+ order: input.order,
386
+ limit: input.limit,
387
+ offset: input.offset,
388
+ })
389
+ const runs = rows.map(rowToWorkflowRunRecord)
390
+
391
+ return {
392
+ runs,
393
+ hasMore,
394
+ total,
395
+ }
396
+ }
397
+
398
+ async listLatestByWorkflowIds(
399
+ input: ListLatestWorkflowRunsInput
400
+ ): Promise<ListLatestWorkflowRunsResult> {
401
+ const rows = await queryLatestRunsByOwnerId<WorkflowRunDatabaseRow>({
402
+ sql: this.sql,
403
+ tableName: "workflow_runs",
404
+ ownerColumn: "workflow_id",
405
+ ownerIds: input.workflowIds,
406
+ projectId: input.projectId,
407
+ ownerIdFor: (row) => row.workflow_id,
408
+ })
409
+
410
+ return {
411
+ runs: rows.map(rowToWorkflowRunRecord),
412
+ }
413
+ }
414
+ }
415
+
416
+ export class PgWorkflowAgentNodeRunStorage implements WorkflowAgentNodeRunStorage {
417
+ constructor(private readonly sql: PgStoreClient) {}
418
+
419
+ async create(input: CreateWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
420
+ try {
421
+ const [row] = await this.sql<WorkflowAgentNodeRunDatabaseRow[]>`
422
+ INSERT INTO workflow_agent_node_runs (
423
+ project_id, node_run_id, agent_id, status, prompt, attempt, created_at
424
+ ) VALUES (
425
+ ${input.projectId}, ${input.nodeRunId}, ${input.agentId}, ${"queued"},
426
+ ${input.prompt}, ${0}, ${input.createdAt ?? new Date()}
427
+ ) RETURNING *
428
+ `
429
+ return rowToWorkflowAgentNodeRunRecord(row)
430
+ } catch (error) {
431
+ if (isUniqueViolation(error)) {
432
+ throw new WorkflowRunError(
433
+ `[SixbPg] Agent execution already exists for workflow node run '${input.nodeRunId}'.`
434
+ )
435
+ }
436
+ throw error
437
+ }
438
+ }
439
+
440
+ async start(input: StartWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
441
+ return runPgTransaction(this.sql, async (tx) => {
442
+ const row = await lockWorkflowAgentNodeRun(tx, input.projectId, input.nodeRunId, "queued")
443
+ const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
444
+ UPDATE workflow_agent_node_runs SET
445
+ status = ${"running"},
446
+ execution_principal_type = ${input.executionPrincipal?.type ?? null},
447
+ execution_principal_id = ${input.executionPrincipal?.id ?? null},
448
+ model_id = ${input.modelId ?? null},
449
+ attempt = ${1},
450
+ execution_token = ${input.execution.token},
451
+ execution_queue_lease_expires_at = ${input.execution.queueLeaseExpiresAt},
452
+ started_at = ${input.startedAt ?? new Date()}
453
+ WHERE project_id = ${row.project_id} AND node_run_id = ${row.node_run_id}
454
+ RETURNING *
455
+ `
456
+ return rowToWorkflowAgentNodeRunRecord(updated)
457
+ })
458
+ }
459
+
460
+ async reclaim(input: ReclaimWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
461
+ return runPgTransaction(this.sql, async (tx) => {
462
+ const row = await lockWorkflowAgentNodeRun(tx, input.projectId, input.nodeRunId, "running")
463
+ const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
464
+ UPDATE workflow_agent_node_runs SET
465
+ attempt = ${Number(row.attempt) + 1},
466
+ execution_token = ${input.execution.token},
467
+ execution_queue_lease_expires_at = ${input.execution.queueLeaseExpiresAt}
468
+ WHERE project_id = ${input.projectId} AND node_run_id = ${input.nodeRunId}
469
+ RETURNING *
470
+ `
471
+ return rowToWorkflowAgentNodeRunRecord(updated)
472
+ })
473
+ }
474
+
475
+ async confirmExecutionOwnership(
476
+ input: ConfirmWorkflowAgentNodeRunExecutionOwnershipInput
477
+ ): Promise<WorkflowAgentNodeRunRecord> {
478
+ return runPgTransaction(this.sql, async (tx) => {
479
+ const row = await lockWorkflowAgentNodeRun(tx, input.projectId, input.nodeRunId, "running")
480
+ assertWorkflowAgentNodeOwnership(row, input.executionToken)
481
+ const currentLease = row.execution_queue_lease_expires_at
482
+ ? new Date(row.execution_queue_lease_expires_at)
483
+ : input.queueLeaseExpiresAt
484
+ const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
485
+ UPDATE workflow_agent_node_runs SET
486
+ execution_queue_lease_expires_at = ${new Date(
487
+ Math.max(currentLease.getTime(), input.queueLeaseExpiresAt.getTime())
488
+ )}
489
+ WHERE project_id = ${input.projectId} AND node_run_id = ${input.nodeRunId}
490
+ RETURNING *
491
+ `
492
+ return rowToWorkflowAgentNodeRunRecord(updated)
493
+ })
494
+ }
495
+
496
+ async finish(input: FinishWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
497
+ return runPgTransaction(this.sql, async (tx) => {
498
+ const row = await lockWorkflowAgentNodeRun(tx, input.projectId, input.nodeRunId, "running")
499
+ assertWorkflowAgentNodeOwnership(row, input.executionToken)
500
+ const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
501
+ UPDATE workflow_agent_node_runs SET
502
+ status = ${input.status},
503
+ model_id = COALESCE(${input.modelId ?? null}, model_id),
504
+ finish_reason = ${input.finishReason ?? null},
505
+ usage = ${input.usage ? JSON.stringify(input.usage) : null}::text::jsonb,
506
+ trace = ${input.trace ? JSON.stringify(input.trace) : null}::text::jsonb,
507
+ diagnostics = ${input.diagnostics ? JSON.stringify(input.diagnostics) : null}::text::jsonb,
508
+ error = ${input.status === "succeeded" ? null : (input.error ?? null)},
509
+ execution_token = ${null},
510
+ execution_queue_lease_expires_at = ${null},
511
+ completed_at = ${input.completedAt ?? new Date()}
512
+ WHERE project_id = ${input.projectId} AND node_run_id = ${input.nodeRunId}
513
+ RETURNING *
514
+ `
515
+ return rowToWorkflowAgentNodeRunRecord(updated)
516
+ })
517
+ }
518
+
519
+ async cancel(input: CancelWorkflowAgentNodeRunInput): Promise<WorkflowAgentNodeRunRecord> {
520
+ return runPgTransaction(this.sql, async (tx) => {
521
+ const [row] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
522
+ SELECT * FROM workflow_agent_node_runs
523
+ WHERE project_id = ${input.projectId} AND node_run_id = ${input.nodeRunId}
524
+ FOR UPDATE
525
+ `
526
+ if (!row) {
527
+ throw new WorkflowRunError(
528
+ `[SixbPg] Agent workflow node run '${input.nodeRunId}' not found.`
529
+ )
530
+ }
531
+ if (row.status !== "queued" && row.status !== "running") {
532
+ throw new WorkflowRunError(
533
+ `[SixbPg] Agent workflow node run '${input.nodeRunId}' cannot be cancelled from status '${row.status}'.`
534
+ )
535
+ }
536
+ const [updated] = await tx<WorkflowAgentNodeRunDatabaseRow[]>`
537
+ UPDATE workflow_agent_node_runs SET
538
+ status = ${"cancelled"},
539
+ error = ${input.error ?? null},
540
+ execution_token = ${null},
541
+ execution_queue_lease_expires_at = ${null},
542
+ completed_at = ${input.completedAt ?? new Date()}
543
+ WHERE project_id = ${input.projectId} AND node_run_id = ${input.nodeRunId}
544
+ RETURNING *
545
+ `
546
+ return rowToWorkflowAgentNodeRunRecord(updated)
547
+ })
548
+ }
549
+
550
+ async getByNodeRunId(params: {
551
+ projectId: string
552
+ nodeRunId: string
553
+ }): Promise<WorkflowAgentNodeRunRecord | null> {
554
+ const [row] = await this.sql<WorkflowAgentNodeRunDatabaseRow[]>`
555
+ SELECT * FROM workflow_agent_node_runs
556
+ WHERE project_id = ${params.projectId} AND node_run_id = ${params.nodeRunId}
557
+ `
558
+ return row ? rowToWorkflowAgentNodeRunRecord(row) : null
559
+ }
560
+
561
+ async list(input: ListWorkflowAgentNodeRunsInput): Promise<ListWorkflowAgentNodeRunsResult> {
562
+ if (hasEmptyStatuses(input)) return { runs: [], total: 0, hasMore: false }
563
+ const whereClauses = ["project_id = $1"]
564
+ const params: SqlParameter[] = [input.projectId]
565
+ let index = 2
566
+ if (input.agentId) {
567
+ whereClauses.push(`agent_id = $${index++}`)
568
+ params.push(input.agentId)
569
+ }
570
+ index = appendRunListFilters(whereClauses, params, index, input)
571
+ const result = await queryRunList<WorkflowAgentNodeRunDatabaseRow>({
572
+ sql: this.sql,
573
+ tableName: "workflow_agent_node_runs",
574
+ whereClauses,
575
+ params,
576
+ nextIndex: index,
577
+ order: input.order,
578
+ limit: input.limit,
579
+ offset: input.offset,
580
+ })
581
+ return { ...result, runs: result.rows.map(rowToWorkflowAgentNodeRunRecord) }
582
+ }
583
+ }
584
+
585
+ export class PgWorkflowNodeRunStorage implements WorkflowNodeRunStorage {
586
+ constructor(private readonly sql: PgStoreClient) {}
587
+
588
+ async start(input: StartWorkflowNodeRunInput): Promise<WorkflowNodeRunRecord> {
589
+ assertNonNegativeInteger(input.nodeIndex, "nodeIndex")
590
+
591
+ return runPgTransaction(this.sql, async (tx) => {
592
+ const [workflowRun] = await tx<WorkflowRunDatabaseRow[]>`
593
+ SELECT * FROM workflow_runs
594
+ WHERE project_id = ${input.projectId} AND id = ${input.workflowRunId}
595
+ FOR UPDATE
596
+ `
597
+
598
+ if (!workflowRun) {
599
+ throw new WorkflowRunError(
600
+ `[SixbPg] Workflow run '${input.workflowRunId}' not found for project '${input.projectId}'.`
601
+ )
602
+ }
603
+
604
+ if (workflowRun.status !== "running") {
605
+ throw new WorkflowRunError(
606
+ `[SixbPg] Workflow run '${input.workflowRunId}' for project '${input.projectId}' must be running.`
607
+ )
608
+ }
609
+ assertWorkflowRunExecutionOwnership(workflowRun, input.executionToken)
610
+
611
+ if (workflowRun.workflow_id !== input.workflowId) {
612
+ throw new WorkflowRunError(
613
+ `[SixbPg] Workflow node run '${input.id}' workflow '${input.workflowId}' does not match workflow run '${input.workflowRunId}' workflow '${workflowRun.workflow_id}'.`
614
+ )
615
+ }
616
+
617
+ try {
618
+ const [row] = await tx<WorkflowNodeRunDatabaseRow[]>`
619
+ INSERT INTO workflow_node_runs (
620
+ project_id,
621
+ id,
622
+ workflow_run_id,
623
+ workflow_id,
624
+ node_index,
625
+ node_type,
626
+ node_id,
627
+ node_key,
628
+ status,
629
+ input,
630
+ started_at
631
+ ) VALUES (
632
+ ${input.projectId},
633
+ ${input.id},
634
+ ${input.workflowRunId},
635
+ ${input.workflowId},
636
+ ${input.nodeIndex},
637
+ ${input.nodeType},
638
+ ${input.nodeId},
639
+ ${input.nodeKey},
640
+ ${"running"},
641
+ ${serializeRecord(input.input)}::text::jsonb,
642
+ ${input.startedAt ?? new Date()}
643
+ )
644
+ RETURNING *
645
+ `
646
+
647
+ return rowToWorkflowNodeRunRecord(row)
648
+ } catch (error) {
649
+ if (isUniqueViolation(error)) {
650
+ throw new WorkflowRunError(
651
+ `[SixbPg] Workflow node run '${input.id}' already exists for project '${input.projectId}'.`
652
+ )
653
+ }
654
+
655
+ throw error
656
+ }
657
+ })
658
+ }
659
+
660
+ async finish(input: FinishWorkflowNodeRunInput): Promise<WorkflowNodeRunRecord> {
661
+ return runPgTransaction(this.sql, async (tx) => {
662
+ const [existing] = await tx<WorkflowNodeRunDatabaseRow[]>`
663
+ SELECT * FROM workflow_node_runs
664
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
665
+ FOR UPDATE
666
+ `
667
+
668
+ if (!existing) {
669
+ throw new WorkflowRunError(
670
+ `[SixbPg] Workflow node run '${input.id}' not found for project '${input.projectId}'.`
671
+ )
672
+ }
673
+ await assertWorkflowNodeParentExecutionOwnership(tx, existing, input.executionToken)
674
+
675
+ if (existing.status !== "running" && existing.status !== "waiting") {
676
+ throw new WorkflowRunError(
677
+ `[SixbPg] Workflow node run '${input.id}' for project '${input.projectId}' is already terminal.`
678
+ )
679
+ }
680
+
681
+ const [updated] =
682
+ input.status === "succeeded"
683
+ ? await tx<WorkflowNodeRunDatabaseRow[]>`
684
+ UPDATE workflow_node_runs
685
+ SET
686
+ status = ${input.status},
687
+ finished_at = ${input.finishedAt ?? new Date()},
688
+ output = ${input.output ? serializeRecord(input.output) : null}::text::jsonb,
689
+ error = ${null}
690
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
691
+ RETURNING *
692
+ `
693
+ : await tx<WorkflowNodeRunDatabaseRow[]>`
694
+ UPDATE workflow_node_runs
695
+ SET
696
+ status = ${input.status},
697
+ finished_at = ${input.finishedAt ?? new Date()},
698
+ output = ${null},
699
+ error = ${input.error ?? null}
700
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
701
+ RETURNING *
702
+ `
703
+
704
+ return rowToWorkflowNodeRunRecord(updated)
705
+ })
706
+ }
707
+
708
+ async wait(input: WaitWorkflowNodeRunInput): Promise<WorkflowNodeRunRecord> {
709
+ return runPgTransaction(this.sql, async (tx) => {
710
+ const [existing] = await tx<WorkflowNodeRunDatabaseRow[]>`
711
+ SELECT * FROM workflow_node_runs
712
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
713
+ FOR UPDATE
714
+ `
715
+
716
+ if (!existing) {
717
+ throw new WorkflowRunError(
718
+ `[SixbPg] Workflow node run '${input.id}' not found for project '${input.projectId}'.`
719
+ )
720
+ }
721
+ await assertWorkflowNodeParentExecutionOwnership(tx, existing, input.executionToken)
722
+
723
+ if (existing.status !== "running") {
724
+ throw new WorkflowRunError(
725
+ `[SixbPg] Workflow node run '${input.id}' for project '${input.projectId}' must be running.`
726
+ )
727
+ }
728
+
729
+ const [updated] = await tx<WorkflowNodeRunDatabaseRow[]>`
730
+ UPDATE workflow_node_runs
731
+ SET
732
+ status = ${"waiting"},
733
+ finished_at = ${null},
734
+ output = ${null},
735
+ error = ${null}
736
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
737
+ RETURNING *
738
+ `
739
+
740
+ return rowToWorkflowNodeRunRecord(updated)
741
+ })
742
+ }
743
+
744
+ async getById(params: { projectId: string; id: string }): Promise<WorkflowNodeRunRecord | null> {
745
+ const [row] = await this.sql<WorkflowNodeRunDatabaseRow[]>`
746
+ SELECT * FROM workflow_node_runs
747
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
748
+ `
749
+
750
+ return row ? rowToWorkflowNodeRunRecord(row) : null
751
+ }
752
+
753
+ async list(input: ListWorkflowNodeRunsInput): Promise<ListWorkflowNodeRunsResult> {
754
+ if (hasEmptyStatuses(input)) {
755
+ return {
756
+ nodes: [],
757
+ hasMore: false,
758
+ total: 0,
759
+ }
760
+ }
761
+
762
+ const whereClauses = ["project_id = $1"]
763
+ const params: SqlParameter[] = [input.projectId]
764
+ let index = 2
765
+
766
+ if (input.workflowRunId) {
767
+ whereClauses.push(`workflow_run_id = $${index++}`)
768
+ params.push(input.workflowRunId)
769
+ }
770
+
771
+ if (input.workflowId) {
772
+ whereClauses.push(`workflow_id = $${index++}`)
773
+ params.push(input.workflowId)
774
+ }
775
+
776
+ if (input.nodeType) {
777
+ whereClauses.push(`node_type = $${index++}`)
778
+ params.push(input.nodeType)
779
+ }
780
+
781
+ if (input.nodeId) {
782
+ whereClauses.push(`node_id = $${index++}`)
783
+ params.push(input.nodeId)
784
+ }
785
+
786
+ if (input.nodeKey) {
787
+ whereClauses.push(`node_key = $${index++}`)
788
+ params.push(input.nodeKey)
789
+ }
790
+
791
+ index = appendRunListFilters(whereClauses, params, index, input)
792
+
793
+ const { rows, total, hasMore } = await queryRunList<WorkflowNodeRunDatabaseRow>({
794
+ sql: this.sql,
795
+ tableName: "workflow_node_runs",
796
+ whereClauses,
797
+ params,
798
+ nextIndex: index,
799
+ order: input.order,
800
+ limit: input.limit,
801
+ offset: input.offset,
802
+ })
803
+ const nodes = rows.map(rowToWorkflowNodeRunRecord)
804
+
805
+ return {
806
+ nodes,
807
+ hasMore,
808
+ total,
809
+ }
810
+ }
811
+ }
812
+
813
+ function serializeRecord(value: WorkflowIOSnapshot): string {
814
+ return JSON.stringify(value)
815
+ }
816
+
817
+ function parseRecord(value: WorkflowIOSnapshot | string): WorkflowIOSnapshot {
818
+ return typeof value === "string" ? (JSON.parse(value) as WorkflowIOSnapshot) : value
819
+ }
820
+
821
+ function rowToWorkflowRunRecord(row: WorkflowRunDatabaseRow): WorkflowRunRecord {
822
+ return {
823
+ id: row.id,
824
+ projectId: row.project_id,
825
+ workflowId: row.workflow_id,
826
+ status: row.status,
827
+ input: parseRecord(row.input),
828
+ queuedAt: row.queued_at ? new Date(row.queued_at) : undefined,
829
+ startedAt: new Date(row.started_at),
830
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
831
+ error: row.error ?? undefined,
832
+ source: parseSource(row.source),
833
+ requestedByPrincipal: {
834
+ type: row.requested_by_principal_type,
835
+ id: row.requested_by_principal_id,
836
+ },
837
+ attempt: Number(row.attempt),
838
+ ...(row.execution_token && row.execution_queue_lease_expires_at
839
+ ? {
840
+ execution: {
841
+ token: row.execution_token,
842
+ queueLeaseExpiresAt: new Date(row.execution_queue_lease_expires_at),
843
+ },
844
+ }
845
+ : {}),
846
+ }
847
+ }
848
+
849
+ function parseSource(value: WorkflowRunSource | string | null): WorkflowRunSource | undefined {
850
+ if (value === null || value === undefined) {
851
+ return undefined
852
+ }
853
+
854
+ return typeof value === "string" ? (JSON.parse(value) as WorkflowRunSource) : value
855
+ }
856
+
857
+ function rowToWorkflowNodeRunRecord(row: WorkflowNodeRunDatabaseRow): WorkflowNodeRunRecord {
858
+ return {
859
+ id: row.id,
860
+ projectId: row.project_id,
861
+ workflowRunId: row.workflow_run_id,
862
+ workflowId: row.workflow_id,
863
+ nodeIndex: Number(row.node_index),
864
+ nodeType: row.node_type,
865
+ nodeId: row.node_id,
866
+ nodeKey: row.node_key,
867
+ status: row.status,
868
+ input: parseRecord(row.input),
869
+ startedAt: new Date(row.started_at),
870
+ finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
871
+ output: row.output ? parseRecord(row.output) : undefined,
872
+ error: row.error ?? undefined,
873
+ }
874
+ }
875
+
876
+ function assertNonNegativeInteger(value: number, fieldName: string): void {
877
+ if (!Number.isInteger(value) || value < 0) {
878
+ throw new WorkflowRunError(`[SixbPg] Workflow run ${fieldName} must be a non-negative integer.`)
879
+ }
880
+ }
881
+
882
+ function canFinishWorkflowRun(
883
+ current: WorkflowRunRecord["status"],
884
+ next: FinishWorkflowRunInput["status"]
885
+ ): boolean {
886
+ return (
887
+ current === "running" ||
888
+ (current === "waiting" && next !== "succeeded") ||
889
+ (current === "queued" && next !== "succeeded")
890
+ )
891
+ }
892
+
893
+ interface WorkflowRunDatabaseRow {
894
+ project_id: string
895
+ id: string
896
+ workflow_id: string
897
+ status: WorkflowRunRecord["status"]
898
+ input: WorkflowIOSnapshot | string
899
+ queued_at: Date | string | null
900
+ started_at: Date | string
901
+ finished_at: Date | string | null
902
+ error: string | null
903
+ source: WorkflowRunSource | string | null
904
+ requested_by_principal_type: Principal["type"]
905
+ requested_by_principal_id: string
906
+ attempt: number | string
907
+ execution_token: string | null
908
+ execution_queue_lease_expires_at: Date | string | null
909
+ }
910
+
911
+ interface WorkflowNodeRunDatabaseRow {
912
+ project_id: string
913
+ id: string
914
+ workflow_run_id: string
915
+ workflow_id: string
916
+ node_index: number | string
917
+ node_type: WorkflowNodeRunRecord["nodeType"]
918
+ node_id: string
919
+ node_key: string
920
+ status: WorkflowNodeRunRecord["status"]
921
+ input: WorkflowIOSnapshot | string
922
+ started_at: Date | string
923
+ finished_at: Date | string | null
924
+ output: WorkflowIOSnapshot | string | null
925
+ error: string | null
926
+ }
927
+
928
+ interface WorkflowAgentNodeRunDatabaseRow {
929
+ project_id: string
930
+ node_run_id: string
931
+ agent_id: string
932
+ status: WorkflowAgentNodeRunRecord["status"]
933
+ prompt: string
934
+ execution_principal_type: "serviceAccount" | null
935
+ execution_principal_id: string | null
936
+ model_id: string | null
937
+ finish_reason: WorkflowAgentNodeRunRecord["finishReason"] | null
938
+ usage: WorkflowAgentNodeRunRecord["usage"] | string | null
939
+ trace: WorkflowAgentNodeRunRecord["trace"] | string | null
940
+ diagnostics: WorkflowAgentNodeRunRecord["diagnostics"] | string | null
941
+ error: string | null
942
+ attempt: number | string
943
+ execution_token: string | null
944
+ execution_queue_lease_expires_at: Date | string | null
945
+ created_at: Date | string
946
+ started_at: Date | string | null
947
+ completed_at: Date | string | null
948
+ }
949
+
950
+ async function lockWorkflowAgentNodeRun(
951
+ sql: PgStoreClient,
952
+ projectId: string,
953
+ nodeRunId: string,
954
+ status: WorkflowAgentNodeRunRecord["status"]
955
+ ): Promise<WorkflowAgentNodeRunDatabaseRow> {
956
+ const [row] = await sql<WorkflowAgentNodeRunDatabaseRow[]>`
957
+ SELECT * FROM workflow_agent_node_runs
958
+ WHERE project_id = ${projectId} AND node_run_id = ${nodeRunId}
959
+ FOR UPDATE
960
+ `
961
+ if (!row) {
962
+ throw new WorkflowRunError(
963
+ `[SixbPg] Agent workflow node run '${nodeRunId}' not found for project '${projectId}'.`
964
+ )
965
+ }
966
+ if (row.status !== status) {
967
+ throw new WorkflowRunError(
968
+ `[SixbPg] Agent workflow node run '${nodeRunId}' must be ${status} (status '${row.status}').`
969
+ )
970
+ }
971
+ return row
972
+ }
973
+
974
+ function assertWorkflowAgentNodeOwnership(
975
+ row: WorkflowAgentNodeRunDatabaseRow,
976
+ token: string
977
+ ): void {
978
+ if (row.execution_token !== token) {
979
+ throw new WorkflowRunError(
980
+ `[SixbPg] Execution token is no longer current on agent workflow node run '${row.node_run_id}'.`
981
+ )
982
+ }
983
+ }
984
+
985
+ function rowToWorkflowAgentNodeRunRecord(
986
+ row: WorkflowAgentNodeRunDatabaseRow
987
+ ): WorkflowAgentNodeRunRecord {
988
+ return {
989
+ projectId: row.project_id,
990
+ nodeRunId: row.node_run_id,
991
+ agentId: row.agent_id,
992
+ status: row.status,
993
+ prompt: row.prompt,
994
+ ...(row.execution_principal_type && row.execution_principal_id
995
+ ? {
996
+ executionPrincipal: {
997
+ type: row.execution_principal_type,
998
+ id: row.execution_principal_id,
999
+ },
1000
+ }
1001
+ : {}),
1002
+ ...(row.model_id ? { modelId: row.model_id } : {}),
1003
+ ...(row.finish_reason ? { finishReason: row.finish_reason } : {}),
1004
+ ...(row.usage ? { usage: parseJson(row.usage) } : {}),
1005
+ ...(row.trace ? { trace: parseJson(row.trace) } : {}),
1006
+ ...(row.diagnostics ? { diagnostics: parseJson(row.diagnostics) } : {}),
1007
+ ...(row.error ? { error: row.error } : {}),
1008
+ attempt: Number(row.attempt),
1009
+ ...(row.execution_token && row.execution_queue_lease_expires_at
1010
+ ? {
1011
+ execution: {
1012
+ token: row.execution_token,
1013
+ queueLeaseExpiresAt: new Date(row.execution_queue_lease_expires_at),
1014
+ },
1015
+ }
1016
+ : {}),
1017
+ createdAt: new Date(row.created_at),
1018
+ ...(row.started_at ? { startedAt: new Date(row.started_at) } : {}),
1019
+ ...(row.completed_at ? { completedAt: new Date(row.completed_at) } : {}),
1020
+ }
1021
+ }
1022
+
1023
+ function parseJson<T>(value: T | string): T {
1024
+ return typeof value === "string" ? (JSON.parse(value) as T) : value
1025
+ }
1026
+
1027
+ async function lockWorkflowRun(
1028
+ sql: PgStoreClient,
1029
+ projectId: string,
1030
+ id: string,
1031
+ status: WorkflowRunRecord["status"]
1032
+ ): Promise<WorkflowRunDatabaseRow> {
1033
+ const [row] = await sql<WorkflowRunDatabaseRow[]>`
1034
+ SELECT * FROM workflow_runs WHERE project_id = ${projectId} AND id = ${id} FOR UPDATE
1035
+ `
1036
+ if (!row) throw new WorkflowRunError(`[SixbPg] Workflow run '${id}' not found.`)
1037
+ if (row.status !== status) {
1038
+ throw new WorkflowRunError(
1039
+ `[SixbPg] Workflow run '${id}' must be ${status} (status '${row.status}').`
1040
+ )
1041
+ }
1042
+ return row
1043
+ }
1044
+
1045
+ function assertWorkflowRunExecutionOwnership(row: WorkflowRunDatabaseRow, token?: string): void {
1046
+ if (row.execution_token !== (token ?? null)) {
1047
+ throw new WorkflowRunError(
1048
+ `[SixbPg] Execution token is no longer current on workflow run '${row.id}'.`
1049
+ )
1050
+ }
1051
+ }
1052
+
1053
+ async function assertWorkflowNodeParentExecutionOwnership(
1054
+ sql: PgStoreClient,
1055
+ node: WorkflowNodeRunDatabaseRow,
1056
+ token?: string
1057
+ ): Promise<void> {
1058
+ const [run] = await sql<WorkflowRunDatabaseRow[]>`
1059
+ SELECT * FROM workflow_runs
1060
+ WHERE project_id = ${node.project_id} AND id = ${node.workflow_run_id}
1061
+ FOR UPDATE
1062
+ `
1063
+ if (!run) throw new WorkflowRunError(`[SixbPg] Parent workflow run was not found.`)
1064
+ assertWorkflowRunExecutionOwnership(run, token)
1065
+ }