@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +13 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +41 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +29 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +138 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +159 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- 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
|
+
}
|