@pikku/kysely 0.12.10 → 0.12.12
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/CHANGELOG.md +22 -4
- package/dist/src/create-audited-kysely.d.ts +10 -0
- package/dist/src/create-audited-kysely.js +167 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/kysely-ai-agent-run-service.d.ts +1 -0
- package/dist/src/kysely-ai-agent-run-service.js +4 -1
- package/dist/src/kysely-channel-store.d.ts +3 -0
- package/dist/src/kysely-channel-store.js +25 -0
- package/dist/src/kysely-tables.d.ts +4 -0
- package/dist/src/kysely-workflow-mirror.d.ts +60 -0
- package/dist/src/kysely-workflow-mirror.js +329 -0
- package/dist/src/kysely-workflow-run-service.js +6 -0
- package/dist/src/kysely-workflow-service.d.ts +17 -14
- package/dist/src/kysely-workflow-service.js +19 -13
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/create-audited-kysely.test.ts +560 -0
- package/src/create-audited-kysely.ts +254 -0
- package/src/index.ts +5 -0
- package/src/kysely-ai-agent-run-service.ts +6 -1
- package/src/kysely-ai-run-state-service.ts +6 -2
- package/src/kysely-channel-store.ts +27 -0
- package/src/kysely-session-store.ts +1 -3
- package/src/kysely-tables.ts +4 -0
- package/src/kysely-workflow-mirror.test.ts +246 -0
- package/src/kysely-workflow-mirror.ts +438 -0
- package/src/kysely-workflow-run-service.ts +6 -0
- package/src/kysely-workflow-service.ts +37 -14
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import type { SerializedError } from '@pikku/core'
|
|
2
|
+
import type {
|
|
3
|
+
StepState,
|
|
4
|
+
WorkflowRunMirror,
|
|
5
|
+
WorkflowRunWire,
|
|
6
|
+
WorkflowStatus,
|
|
7
|
+
WorkflowPlannedStep,
|
|
8
|
+
WorkflowVersionStatus,
|
|
9
|
+
} from '@pikku/core/workflow'
|
|
10
|
+
import type { Kysely } from 'kysely'
|
|
11
|
+
import { sql } from 'kysely'
|
|
12
|
+
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Kysely-backed `WorkflowRunMirror`.
|
|
16
|
+
*
|
|
17
|
+
* Forwards executor writes to the same `workflow_runs` / `workflow_step` /
|
|
18
|
+
* `workflow_step_history` / `workflow_versions` tables that
|
|
19
|
+
* `KyselyWorkflowService` uses, so a `KyselyWorkflowRunService` reading
|
|
20
|
+
* those tables will see runs driven by any executor (Cloudflare Durable
|
|
21
|
+
* Object, Redis, MongoDB, in-memory) — not just kysely-driven runs.
|
|
22
|
+
*
|
|
23
|
+
* Errors thrown here are caught by the executor's `safeMirror` wrapper
|
|
24
|
+
* and logged, so a mirror outage cannot break a running workflow.
|
|
25
|
+
*/
|
|
26
|
+
export class KyselyWorkflowMirror implements WorkflowRunMirror {
|
|
27
|
+
private initialized = false
|
|
28
|
+
|
|
29
|
+
constructor(protected db: Kysely<KyselyPikkuDB>) {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create the underlying tables if they don't exist. Safe to call from
|
|
33
|
+
* either the mirror or `KyselyWorkflowService.init()` — schema is the
|
|
34
|
+
* same and uses `ifNotExists`.
|
|
35
|
+
*/
|
|
36
|
+
public async init(): Promise<void> {
|
|
37
|
+
if (this.initialized) return
|
|
38
|
+
|
|
39
|
+
await this.db.schema
|
|
40
|
+
.createTable('workflow_runs')
|
|
41
|
+
.ifNotExists()
|
|
42
|
+
.addColumn('workflow_run_id', 'text', (col) => col.primaryKey())
|
|
43
|
+
.addColumn('workflow', 'text', (col) => col.notNull())
|
|
44
|
+
.addColumn('status', 'text', (col) => col.notNull())
|
|
45
|
+
.addColumn('input', 'text', (col) => col.notNull())
|
|
46
|
+
.addColumn('output', 'text')
|
|
47
|
+
.addColumn('error', 'text')
|
|
48
|
+
.addColumn('state', 'text', (col) => col.defaultTo('{}'))
|
|
49
|
+
.addColumn('inline', 'boolean', (col) => col.defaultTo(false))
|
|
50
|
+
.addColumn('graph_hash', 'text')
|
|
51
|
+
.addColumn('deterministic', 'boolean', (col) => col.defaultTo(false))
|
|
52
|
+
.addColumn('planned_steps', 'text')
|
|
53
|
+
.addColumn('wire', 'text')
|
|
54
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
55
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
56
|
+
)
|
|
57
|
+
.addColumn('updated_at', 'timestamp', (col) =>
|
|
58
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
59
|
+
)
|
|
60
|
+
.execute()
|
|
61
|
+
|
|
62
|
+
await this.db.schema
|
|
63
|
+
.createTable('workflow_step')
|
|
64
|
+
.ifNotExists()
|
|
65
|
+
.addColumn('workflow_step_id', 'text', (col) => col.primaryKey())
|
|
66
|
+
.addColumn('workflow_run_id', 'text', (col) =>
|
|
67
|
+
col
|
|
68
|
+
.notNull()
|
|
69
|
+
.references('workflow_runs.workflow_run_id')
|
|
70
|
+
.onDelete('cascade')
|
|
71
|
+
)
|
|
72
|
+
.addColumn('step_name', 'text', (col) => col.notNull())
|
|
73
|
+
.addColumn('rpc_name', 'text')
|
|
74
|
+
.addColumn('data', 'text')
|
|
75
|
+
.addColumn('status', 'text', (col) => col.notNull().defaultTo('pending'))
|
|
76
|
+
.addColumn('result', 'text')
|
|
77
|
+
.addColumn('error', 'text')
|
|
78
|
+
.addColumn('child_run_id', 'text')
|
|
79
|
+
.addColumn('branch_taken', 'text')
|
|
80
|
+
.addColumn('retries', 'integer')
|
|
81
|
+
.addColumn('retry_delay', 'text')
|
|
82
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
83
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
84
|
+
)
|
|
85
|
+
.addColumn('updated_at', 'timestamp', (col) =>
|
|
86
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
87
|
+
)
|
|
88
|
+
.addUniqueConstraint('workflow_step_run_name_unique', [
|
|
89
|
+
'workflow_run_id',
|
|
90
|
+
'step_name',
|
|
91
|
+
])
|
|
92
|
+
.execute()
|
|
93
|
+
|
|
94
|
+
await this.db.schema
|
|
95
|
+
.createTable('workflow_step_history')
|
|
96
|
+
.ifNotExists()
|
|
97
|
+
.addColumn('history_id', 'text', (col) => col.primaryKey())
|
|
98
|
+
.addColumn('workflow_step_id', 'text', (col) =>
|
|
99
|
+
col
|
|
100
|
+
.notNull()
|
|
101
|
+
.references('workflow_step.workflow_step_id')
|
|
102
|
+
.onDelete('cascade')
|
|
103
|
+
)
|
|
104
|
+
.addColumn('status', 'text', (col) => col.notNull())
|
|
105
|
+
.addColumn('result', 'text')
|
|
106
|
+
.addColumn('error', 'text')
|
|
107
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
108
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
109
|
+
)
|
|
110
|
+
.addColumn('running_at', 'timestamp')
|
|
111
|
+
.addColumn('scheduled_at', 'timestamp')
|
|
112
|
+
.addColumn('succeeded_at', 'timestamp')
|
|
113
|
+
.addColumn('failed_at', 'timestamp')
|
|
114
|
+
.execute()
|
|
115
|
+
|
|
116
|
+
await this.db.schema
|
|
117
|
+
.createTable('workflow_versions')
|
|
118
|
+
.ifNotExists()
|
|
119
|
+
.addColumn('workflow_name', 'text', (col) => col.notNull())
|
|
120
|
+
.addColumn('graph_hash', 'text', (col) => col.notNull())
|
|
121
|
+
.addColumn('graph', 'text', (col) => col.notNull())
|
|
122
|
+
.addColumn('source', 'text', (col) => col.notNull())
|
|
123
|
+
.addColumn('status', 'text', (col) => col.notNull().defaultTo('active'))
|
|
124
|
+
.addColumn('created_at', 'timestamp', (col) =>
|
|
125
|
+
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
126
|
+
)
|
|
127
|
+
.addPrimaryKeyConstraint('workflow_versions_pk', [
|
|
128
|
+
'workflow_name',
|
|
129
|
+
'graph_hash',
|
|
130
|
+
])
|
|
131
|
+
.execute()
|
|
132
|
+
|
|
133
|
+
this.initialized = true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async createRun(
|
|
137
|
+
runId: string,
|
|
138
|
+
workflowName: string,
|
|
139
|
+
input: any,
|
|
140
|
+
inline: boolean,
|
|
141
|
+
graphHash: string,
|
|
142
|
+
wire: WorkflowRunWire,
|
|
143
|
+
options?: {
|
|
144
|
+
deterministic?: boolean
|
|
145
|
+
plannedSteps?: WorkflowPlannedStep[]
|
|
146
|
+
}
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
await this.db
|
|
149
|
+
.insertInto('workflowRuns')
|
|
150
|
+
.values({
|
|
151
|
+
workflowRunId: runId,
|
|
152
|
+
workflow: workflowName,
|
|
153
|
+
status: 'running',
|
|
154
|
+
input: JSON.stringify(input),
|
|
155
|
+
inline,
|
|
156
|
+
graphHash: graphHash ?? null,
|
|
157
|
+
deterministic: options?.deterministic ?? false,
|
|
158
|
+
plannedSteps: options?.plannedSteps
|
|
159
|
+
? JSON.stringify(options.plannedSteps)
|
|
160
|
+
: null,
|
|
161
|
+
wire: wire ? JSON.stringify(wire) : null,
|
|
162
|
+
})
|
|
163
|
+
.execute()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async updateRunStatus(
|
|
167
|
+
id: string,
|
|
168
|
+
status: WorkflowStatus,
|
|
169
|
+
output?: any,
|
|
170
|
+
error?: SerializedError
|
|
171
|
+
): Promise<void> {
|
|
172
|
+
await this.db
|
|
173
|
+
.updateTable('workflowRuns')
|
|
174
|
+
.set({
|
|
175
|
+
status,
|
|
176
|
+
output: output !== undefined ? JSON.stringify(output) : null,
|
|
177
|
+
error: error !== undefined ? JSON.stringify(error) : null,
|
|
178
|
+
updatedAt: new Date(),
|
|
179
|
+
})
|
|
180
|
+
.where('workflowRunId', '=', id)
|
|
181
|
+
.execute()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async insertStepState(
|
|
185
|
+
runId: string,
|
|
186
|
+
step: StepState & {
|
|
187
|
+
stepName: string
|
|
188
|
+
rpcName: string | null
|
|
189
|
+
data: any
|
|
190
|
+
}
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const now = step.createdAt ?? new Date()
|
|
193
|
+
|
|
194
|
+
await this.db
|
|
195
|
+
.insertInto('workflowStep')
|
|
196
|
+
.values({
|
|
197
|
+
workflowStepId: step.stepId,
|
|
198
|
+
workflowRunId: runId,
|
|
199
|
+
stepName: step.stepName,
|
|
200
|
+
rpcName: step.rpcName,
|
|
201
|
+
data: step.data != null ? JSON.stringify(step.data) : null,
|
|
202
|
+
status: step.status,
|
|
203
|
+
retries: step.retries ?? null,
|
|
204
|
+
retryDelay: step.retryDelay?.toString() ?? null,
|
|
205
|
+
createdAt: now,
|
|
206
|
+
updatedAt: now,
|
|
207
|
+
})
|
|
208
|
+
.execute()
|
|
209
|
+
|
|
210
|
+
await this.insertHistoryRecord(step.stepId, step.status)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async setStepRunning(stepId: string): Promise<void> {
|
|
214
|
+
await this.updateStepStatus(stepId, 'running')
|
|
215
|
+
await this.appendOrUpdateLatestHistory(stepId, 'running')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async setStepScheduled(stepId: string): Promise<void> {
|
|
219
|
+
await this.updateStepStatus(stepId, 'scheduled')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async setStepResult(stepId: string, result: any): Promise<void> {
|
|
223
|
+
const resultJson = JSON.stringify(result)
|
|
224
|
+
await this.db
|
|
225
|
+
.updateTable('workflowStep')
|
|
226
|
+
.set({
|
|
227
|
+
status: 'succeeded',
|
|
228
|
+
result: resultJson,
|
|
229
|
+
error: null,
|
|
230
|
+
updatedAt: new Date(),
|
|
231
|
+
})
|
|
232
|
+
.where('workflowStepId', '=', stepId)
|
|
233
|
+
.execute()
|
|
234
|
+
await this.appendOrUpdateLatestHistory(stepId, 'succeeded', resultJson)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async setStepChildRunId(stepId: string, childRunId: string): Promise<void> {
|
|
238
|
+
await this.db
|
|
239
|
+
.updateTable('workflowStep')
|
|
240
|
+
.set({ childRunId, updatedAt: new Date() })
|
|
241
|
+
.where('workflowStepId', '=', stepId)
|
|
242
|
+
.execute()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async setStepError(stepId: string, error: SerializedError): Promise<void> {
|
|
246
|
+
const errorJson = JSON.stringify(error)
|
|
247
|
+
await this.db
|
|
248
|
+
.updateTable('workflowStep')
|
|
249
|
+
.set({
|
|
250
|
+
status: 'failed',
|
|
251
|
+
error: errorJson,
|
|
252
|
+
result: null,
|
|
253
|
+
updatedAt: new Date(),
|
|
254
|
+
})
|
|
255
|
+
.where('workflowStepId', '=', stepId)
|
|
256
|
+
.execute()
|
|
257
|
+
await this.appendOrUpdateLatestHistory(
|
|
258
|
+
stepId,
|
|
259
|
+
'failed',
|
|
260
|
+
undefined,
|
|
261
|
+
errorJson
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async createRetryAttempt(
|
|
266
|
+
failedStepId: string,
|
|
267
|
+
newStep: StepState & { stepName: string }
|
|
268
|
+
): Promise<void> {
|
|
269
|
+
await this.db
|
|
270
|
+
.updateTable('workflowStep')
|
|
271
|
+
.set({
|
|
272
|
+
status: newStep.status,
|
|
273
|
+
result: null,
|
|
274
|
+
error: null,
|
|
275
|
+
updatedAt: new Date(),
|
|
276
|
+
})
|
|
277
|
+
.where('workflowStepId', '=', failedStepId)
|
|
278
|
+
.execute()
|
|
279
|
+
await this.insertHistoryRecord(failedStepId, newStep.status)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async setBranchTaken(stepId: string, branchKey: string): Promise<void> {
|
|
283
|
+
await this.db
|
|
284
|
+
.updateTable('workflowStep')
|
|
285
|
+
.set({ branchTaken: branchKey, updatedAt: new Date() })
|
|
286
|
+
.where('workflowStepId', '=', stepId)
|
|
287
|
+
.execute()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async updateRunState(
|
|
291
|
+
runId: string,
|
|
292
|
+
name: string,
|
|
293
|
+
value: unknown
|
|
294
|
+
): Promise<void> {
|
|
295
|
+
const row = await this.db
|
|
296
|
+
.selectFrom('workflowRuns')
|
|
297
|
+
.select('state')
|
|
298
|
+
.where('workflowRunId', '=', runId)
|
|
299
|
+
.executeTakeFirst()
|
|
300
|
+
|
|
301
|
+
const state: Record<string, unknown> =
|
|
302
|
+
row?.state != null
|
|
303
|
+
? typeof row.state === 'string'
|
|
304
|
+
? JSON.parse(row.state)
|
|
305
|
+
: row.state
|
|
306
|
+
: {}
|
|
307
|
+
state[name] = value
|
|
308
|
+
|
|
309
|
+
await this.db
|
|
310
|
+
.updateTable('workflowRuns')
|
|
311
|
+
.set({ state: JSON.stringify(state), updatedAt: new Date() })
|
|
312
|
+
.where('workflowRunId', '=', runId)
|
|
313
|
+
.execute()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async upsertWorkflowVersion(
|
|
317
|
+
name: string,
|
|
318
|
+
graphHash: string,
|
|
319
|
+
graph: any,
|
|
320
|
+
source: string,
|
|
321
|
+
status: WorkflowVersionStatus = 'active'
|
|
322
|
+
): Promise<void> {
|
|
323
|
+
await this.db
|
|
324
|
+
.insertInto('workflowVersions')
|
|
325
|
+
.values({
|
|
326
|
+
workflowName: name,
|
|
327
|
+
graphHash,
|
|
328
|
+
graph: JSON.stringify(graph),
|
|
329
|
+
source,
|
|
330
|
+
status,
|
|
331
|
+
})
|
|
332
|
+
.onConflict((oc) => oc.columns(['workflowName', 'graphHash']).doNothing())
|
|
333
|
+
.execute()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async updateWorkflowVersionStatus(
|
|
337
|
+
name: string,
|
|
338
|
+
graphHash: string,
|
|
339
|
+
status: WorkflowVersionStatus
|
|
340
|
+
): Promise<void> {
|
|
341
|
+
await this.db
|
|
342
|
+
.updateTable('workflowVersions')
|
|
343
|
+
.set({ status })
|
|
344
|
+
.where('workflowName', '=', name)
|
|
345
|
+
.where('graphHash', '=', graphHash)
|
|
346
|
+
.execute()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ─── internals ────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
private async updateStepStatus(
|
|
352
|
+
stepId: string,
|
|
353
|
+
status: StepState['status']
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
await this.db
|
|
356
|
+
.updateTable('workflowStep')
|
|
357
|
+
.set({ status, updatedAt: new Date() })
|
|
358
|
+
.where('workflowStepId', '=', stepId)
|
|
359
|
+
.execute()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async insertHistoryRecord(
|
|
363
|
+
stepId: string,
|
|
364
|
+
status: string,
|
|
365
|
+
resultJson?: string | null,
|
|
366
|
+
errorJson?: string | null
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
const now = new Date()
|
|
369
|
+
const values: Record<string, any> = {
|
|
370
|
+
historyId: crypto.randomUUID(),
|
|
371
|
+
workflowStepId: stepId,
|
|
372
|
+
status,
|
|
373
|
+
result: resultJson ?? null,
|
|
374
|
+
error: errorJson ?? null,
|
|
375
|
+
createdAt: now,
|
|
376
|
+
}
|
|
377
|
+
const tsField = timestampFieldFor(status)
|
|
378
|
+
if (tsField) values[tsField] = now
|
|
379
|
+
|
|
380
|
+
await this.db
|
|
381
|
+
.insertInto('workflowStepHistory')
|
|
382
|
+
.values(values as any)
|
|
383
|
+
.execute()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The executor pattern is: `insertStepState` writes a 'pending' history
|
|
388
|
+
* row, then later transitions UPDATE that latest row in place rather
|
|
389
|
+
* than appending a new one — except for retry attempts which append.
|
|
390
|
+
*
|
|
391
|
+
* We mirror the same shape so `KyselyWorkflowRunService.getRunHistory`
|
|
392
|
+
* returns the same sequence the executor would have produced.
|
|
393
|
+
*/
|
|
394
|
+
private async appendOrUpdateLatestHistory(
|
|
395
|
+
stepId: string,
|
|
396
|
+
status: string,
|
|
397
|
+
resultJson?: string,
|
|
398
|
+
errorJson?: string
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
const latest = await this.db
|
|
401
|
+
.selectFrom('workflowStepHistory')
|
|
402
|
+
.select('historyId')
|
|
403
|
+
.where('workflowStepId', '=', stepId)
|
|
404
|
+
.orderBy('createdAt', 'desc')
|
|
405
|
+
.limit(1)
|
|
406
|
+
.executeTakeFirst()
|
|
407
|
+
|
|
408
|
+
if (latest) {
|
|
409
|
+
const update: Record<string, any> = { status }
|
|
410
|
+
if (resultJson !== undefined) update.result = resultJson
|
|
411
|
+
if (errorJson !== undefined) update.error = errorJson
|
|
412
|
+
const tsField = timestampFieldFor(status)
|
|
413
|
+
if (tsField) update[tsField] = new Date()
|
|
414
|
+
await this.db
|
|
415
|
+
.updateTable('workflowStepHistory')
|
|
416
|
+
.set(update)
|
|
417
|
+
.where('historyId', '=', latest.historyId)
|
|
418
|
+
.execute()
|
|
419
|
+
} else {
|
|
420
|
+
await this.insertHistoryRecord(stepId, status, resultJson, errorJson)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function timestampFieldFor(status: string): string | null {
|
|
426
|
+
switch (status) {
|
|
427
|
+
case 'running':
|
|
428
|
+
return 'runningAt'
|
|
429
|
+
case 'scheduled':
|
|
430
|
+
return 'scheduledAt'
|
|
431
|
+
case 'succeeded':
|
|
432
|
+
return 'succeededAt'
|
|
433
|
+
case 'failed':
|
|
434
|
+
return 'failedAt'
|
|
435
|
+
default:
|
|
436
|
+
return null
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -30,6 +30,8 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
30
30
|
'error',
|
|
31
31
|
'inline',
|
|
32
32
|
'graphHash',
|
|
33
|
+
'deterministic',
|
|
34
|
+
'plannedSteps',
|
|
33
35
|
'wire',
|
|
34
36
|
'createdAt',
|
|
35
37
|
'updatedAt',
|
|
@@ -64,6 +66,8 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
64
66
|
'error',
|
|
65
67
|
'inline',
|
|
66
68
|
'graphHash',
|
|
69
|
+
'deterministic',
|
|
70
|
+
'plannedSteps',
|
|
67
71
|
'wire',
|
|
68
72
|
'createdAt',
|
|
69
73
|
'updatedAt',
|
|
@@ -255,6 +259,8 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
255
259
|
error: parseJson(row.error),
|
|
256
260
|
inline: row.inline as boolean | undefined,
|
|
257
261
|
graphHash: row.graphHash as string | undefined,
|
|
262
|
+
deterministic: row.deterministic as boolean | undefined,
|
|
263
|
+
plannedSteps: parseJson(row.plannedSteps),
|
|
258
264
|
wire: parseJson(row.wire) ?? { type: 'unknown' },
|
|
259
265
|
createdAt: new Date(row.createdAt as string),
|
|
260
266
|
updatedAt: new Date(row.updatedAt as string),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SerializedError } from '@pikku/core'
|
|
2
2
|
import {
|
|
3
3
|
PikkuWorkflowService,
|
|
4
|
+
type WorkflowPlannedStep,
|
|
4
5
|
type WorkflowRun,
|
|
5
6
|
type WorkflowRunWire,
|
|
6
7
|
type StepState,
|
|
@@ -43,6 +44,8 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
43
44
|
.addColumn('state', 'text', (col) => col.defaultTo('{}'))
|
|
44
45
|
.addColumn('inline', 'boolean', (col) => col.defaultTo(false))
|
|
45
46
|
.addColumn('graph_hash', 'text')
|
|
47
|
+
.addColumn('deterministic', 'boolean', (col) => col.defaultTo(false))
|
|
48
|
+
.addColumn('planned_steps', 'text')
|
|
46
49
|
.addColumn('wire', 'text')
|
|
47
50
|
.addColumn('created_at', 'timestamp', (col) =>
|
|
48
51
|
col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
|
|
@@ -134,12 +137,16 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
134
137
|
this.initialized = true
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
async
|
|
140
|
+
protected async createRunImpl(
|
|
138
141
|
workflowName: string,
|
|
139
142
|
input: any,
|
|
140
143
|
inline: boolean,
|
|
141
144
|
graphHash: string,
|
|
142
|
-
wire: WorkflowRunWire
|
|
145
|
+
wire: WorkflowRunWire,
|
|
146
|
+
options?: {
|
|
147
|
+
deterministic?: boolean
|
|
148
|
+
plannedSteps?: WorkflowPlannedStep[]
|
|
149
|
+
}
|
|
143
150
|
): Promise<string> {
|
|
144
151
|
const id = crypto.randomUUID()
|
|
145
152
|
await this.db
|
|
@@ -151,6 +158,10 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
151
158
|
input: JSON.stringify(input),
|
|
152
159
|
inline,
|
|
153
160
|
graphHash: graphHash ?? null,
|
|
161
|
+
deterministic: options?.deterministic ?? false,
|
|
162
|
+
plannedSteps: options?.plannedSteps
|
|
163
|
+
? JSON.stringify(options.plannedSteps)
|
|
164
|
+
: null,
|
|
154
165
|
wire: wire ? JSON.stringify(wire) : null,
|
|
155
166
|
})
|
|
156
167
|
.execute()
|
|
@@ -162,7 +173,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
162
173
|
return this.runService.getRun(id)
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
async
|
|
176
|
+
protected async updateRunStatusImpl(
|
|
166
177
|
id: string,
|
|
167
178
|
status: WorkflowStatus,
|
|
168
179
|
output?: any,
|
|
@@ -180,7 +191,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
180
191
|
.execute()
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
async
|
|
194
|
+
protected async insertStepStateImpl(
|
|
184
195
|
runId: string,
|
|
185
196
|
stepName: string,
|
|
186
197
|
rpcName: string | null,
|
|
@@ -274,7 +285,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
274
285
|
return this.runService.getRunHistory(runId)
|
|
275
286
|
}
|
|
276
287
|
|
|
277
|
-
async
|
|
288
|
+
protected async setStepRunningImpl(stepId: string): Promise<void> {
|
|
278
289
|
await this.db
|
|
279
290
|
.updateTable('workflowStep')
|
|
280
291
|
.set({ status: 'running', updatedAt: new Date() })
|
|
@@ -298,7 +309,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
|
|
301
|
-
async
|
|
312
|
+
protected async setStepScheduledImpl(stepId: string): Promise<void> {
|
|
302
313
|
await this.db
|
|
303
314
|
.updateTable('workflowStep')
|
|
304
315
|
.set({ status: 'scheduled', updatedAt: new Date() })
|
|
@@ -348,7 +359,10 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
348
359
|
}
|
|
349
360
|
}
|
|
350
361
|
|
|
351
|
-
async
|
|
362
|
+
protected async setStepChildRunIdImpl(
|
|
363
|
+
stepId: string,
|
|
364
|
+
childRunId: string
|
|
365
|
+
): Promise<void> {
|
|
352
366
|
await this.db
|
|
353
367
|
.updateTable('workflowStep')
|
|
354
368
|
.set({
|
|
@@ -359,7 +373,10 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
359
373
|
.execute()
|
|
360
374
|
}
|
|
361
375
|
|
|
362
|
-
async
|
|
376
|
+
protected async setStepResultImpl(
|
|
377
|
+
stepId: string,
|
|
378
|
+
result: any
|
|
379
|
+
): Promise<void> {
|
|
363
380
|
const resultJson = JSON.stringify(result)
|
|
364
381
|
|
|
365
382
|
await this.db
|
|
@@ -390,7 +407,10 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
390
407
|
}
|
|
391
408
|
}
|
|
392
409
|
|
|
393
|
-
async
|
|
410
|
+
protected async setStepErrorImpl(
|
|
411
|
+
stepId: string,
|
|
412
|
+
error: Error
|
|
413
|
+
): Promise<void> {
|
|
394
414
|
const serializedError: SerializedError = {
|
|
395
415
|
message: error.message,
|
|
396
416
|
stack: error.stack,
|
|
@@ -426,7 +446,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
426
446
|
}
|
|
427
447
|
}
|
|
428
448
|
|
|
429
|
-
async
|
|
449
|
+
protected async createRetryAttemptImpl(
|
|
430
450
|
stepId: string,
|
|
431
451
|
status: 'pending' | 'running'
|
|
432
452
|
): Promise<StepState> {
|
|
@@ -569,7 +589,10 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
569
589
|
return results
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
async
|
|
592
|
+
protected async setBranchTakenImpl(
|
|
593
|
+
stepId: string,
|
|
594
|
+
branchKey: string
|
|
595
|
+
): Promise<void> {
|
|
573
596
|
await this.db
|
|
574
597
|
.updateTable('workflowStep')
|
|
575
598
|
.set({ branchTaken: branchKey, updatedAt: new Date() })
|
|
@@ -577,7 +600,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
577
600
|
.execute()
|
|
578
601
|
}
|
|
579
602
|
|
|
580
|
-
async
|
|
603
|
+
protected async updateRunStateImpl(
|
|
581
604
|
runId: string,
|
|
582
605
|
name: string,
|
|
583
606
|
value: unknown
|
|
@@ -609,7 +632,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
609
632
|
return parseJson(row.state) ?? {}
|
|
610
633
|
}
|
|
611
634
|
|
|
612
|
-
async
|
|
635
|
+
protected async upsertWorkflowVersionImpl(
|
|
613
636
|
name: string,
|
|
614
637
|
graphHash: string,
|
|
615
638
|
graph: any,
|
|
@@ -629,7 +652,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
629
652
|
.execute()
|
|
630
653
|
}
|
|
631
654
|
|
|
632
|
-
async
|
|
655
|
+
protected async updateWorkflowVersionStatusImpl(
|
|
633
656
|
name: string,
|
|
634
657
|
graphHash: string,
|
|
635
658
|
status: WorkflowVersionStatus
|