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