@pikku/kysely 0.13.24 → 0.13.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-analytics-service.d.ts +30 -0
  5. package/dist/src/kysely-analytics-service.js +80 -0
  6. package/dist/src/kysely-feature-flag-store.d.ts +37 -0
  7. package/dist/src/kysely-feature-flag-store.js +239 -0
  8. package/dist/src/kysely-scope-service.d.ts +1 -0
  9. package/dist/src/kysely-scope-service.js +29 -0
  10. package/dist/src/kysely-tables.d.ts +45 -0
  11. package/dist/src/schema/analytics.schema.d.ts +17 -0
  12. package/dist/src/schema/analytics.schema.js +48 -0
  13. package/dist/src/schema/flag.schema.d.ts +16 -0
  14. package/dist/src/schema/flag.schema.js +62 -0
  15. package/dist/src/schema/index.d.ts +2 -0
  16. package/dist/src/schema/index.js +6 -0
  17. package/package.json +11 -6
  18. package/run-tests.sh +9 -1
  19. package/bin/pikku-kysely-pure.ts +0 -59
  20. package/dist/src/kysely-lock-vault.d.ts +0 -34
  21. package/dist/src/kysely-lock-vault.js +0 -77
  22. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  23. package/dist/src/schema/data-lock.schema.js +0 -27
  24. package/dist/tsconfig.tsbuildinfo +0 -1
  25. package/src/classification-crypto.test.ts +0 -143
  26. package/src/classification-crypto.ts +0 -163
  27. package/src/classification-plugin.pglite.test.ts +0 -226
  28. package/src/classification-plugin.test.ts +0 -265
  29. package/src/classification-plugin.ts +0 -249
  30. package/src/coercion-plugin.test.ts +0 -160
  31. package/src/coercion-plugin.ts +0 -187
  32. package/src/create-audited-kysely.test.ts +0 -560
  33. package/src/create-audited-kysely.ts +0 -254
  34. package/src/index.ts +0 -81
  35. package/src/kysely-agent-run-scores.ts +0 -47
  36. package/src/kysely-agent-run-service.ts +0 -244
  37. package/src/kysely-agent-run-state-service.ts +0 -200
  38. package/src/kysely-agent-storage-service.ts +0 -632
  39. package/src/kysely-audit-service.test.ts +0 -352
  40. package/src/kysely-audit-service.ts +0 -237
  41. package/src/kysely-channel-store.ts +0 -107
  42. package/src/kysely-credential-service.ts +0 -286
  43. package/src/kysely-deployment-service.ts +0 -163
  44. package/src/kysely-eventhub-store.ts +0 -53
  45. package/src/kysely-json.ts +0 -5
  46. package/src/kysely-scope-service.test.ts +0 -418
  47. package/src/kysely-scope-service.ts +0 -425
  48. package/src/kysely-secret-service.ts +0 -242
  49. package/src/kysely-services.test.ts +0 -405
  50. package/src/kysely-session-store.ts +0 -57
  51. package/src/kysely-tables.test.ts +0 -110
  52. package/src/kysely-tables.ts +0 -434
  53. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  54. package/src/kysely-virtual-user-run-store.ts +0 -235
  55. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  56. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  57. package/src/kysely-webhook-service.test.ts +0 -153
  58. package/src/kysely-webhook-service.ts +0 -139
  59. package/src/kysely-workflow-mirror.test.ts +0 -259
  60. package/src/kysely-workflow-mirror.ts +0 -374
  61. package/src/kysely-workflow-run-service.ts +0 -305
  62. package/src/kysely-workflow-service.test.ts +0 -544
  63. package/src/kysely-workflow-service.ts +0 -820
  64. package/src/schema/agent.schema.ts +0 -162
  65. package/src/schema/audit.schema.ts +0 -57
  66. package/src/schema/channel.schema.ts +0 -38
  67. package/src/schema/credential.schema.ts +0 -58
  68. package/src/schema/deployment.schema.ts +0 -47
  69. package/src/schema/index.ts +0 -441
  70. package/src/schema/pikku-schema.types.ts +0 -169
  71. package/src/schema/schema.test.ts +0 -591
  72. package/src/schema/scope.schema.ts +0 -110
  73. package/src/schema/secret.schema.ts +0 -49
  74. package/src/schema/session.schema.ts +0 -20
  75. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  76. package/src/schema/virtual-user.schema.ts +0 -106
  77. package/src/schema/webhook.schema.ts +0 -58
  78. package/src/schema/workflow.schema.ts +0 -150
  79. package/src/serialize-plugin.ts +0 -233
  80. package/tsconfig.json +0 -17
@@ -1,369 +0,0 @@
1
- import { describe, test, beforeEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import Database from 'better-sqlite3'
4
- import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
5
-
6
- import { KyselyVirtualUserRunStore } from './kysely-virtual-user-run-store.js'
7
- import { SerializePlugin } from './serialize-plugin.js'
8
- import type { KyselyPikkuDB } from './kysely-tables.js'
9
- import { applyPikkuSchemas, virtualUserSchema } from './schema/index.js'
10
-
11
- // SerializePlugin is here because most projects in this package run with it, and
12
- // it is the harder case: it deserialises JSON columns the store has already
13
- // stringified. The store has to survive that, and its absence.
14
- const newDb = () =>
15
- new Kysely<KyselyPikkuDB>({
16
- dialect: new SqliteDialect({ database: new Database(':memory:') }),
17
- plugins: [new CamelCasePlugin(), new SerializePlugin()],
18
- })
19
-
20
- const migratedDb = async () => {
21
- const db = newDb()
22
- await applyPikkuSchemas(db, [virtualUserSchema])
23
- return db
24
- }
25
-
26
- const TALLY = {
27
- steps: 12,
28
- calls: 30,
29
- mutations: 4,
30
- tokensIn: 900,
31
- tokensOut: 120,
32
- elapsed: 4200,
33
- } as any
34
-
35
- const FINDING = {
36
- kind: 'schema-mismatch',
37
- detail: 'listDocs returned a doc with no title',
38
- rpcName: 'listDocs',
39
- step: 7,
40
- } as any
41
-
42
- const INTENT = {
43
- id: 'intent_1',
44
- sourceId: 'exportADoc',
45
- title: 'Export a doc',
46
- status: 'done',
47
- steps: [0, 1],
48
- suspensions: 0,
49
- } as any
50
-
51
- const step = (index: number, over: Record<string, unknown> = {}) =>
52
- ({
53
- index,
54
- intentId: 'intent_1',
55
- action: { kind: 'call', rpcName: 'listDocs', input: {} },
56
- status: 200,
57
- ok: true,
58
- response: '{"docs":[]}',
59
- tokensIn: 40,
60
- tokensOut: 12,
61
- ...over,
62
- }) as any
63
-
64
- describe('KyselyVirtualUserRunStore', () => {
65
- let db: Kysely<KyselyPikkuDB>
66
- let store: KyselyVirtualUserRunStore
67
-
68
- beforeEach(async () => {
69
- db = await migratedDb()
70
- store = new KyselyVirtualUserRunStore(db)
71
- })
72
-
73
- // The runtime never creates this table, so a project that skipped the
74
- // migration has to be told which command writes it rather than quietly
75
- // getting one at first use.
76
- test('refuses to start against a database that never migrated it', async () => {
77
- const store = new KyselyVirtualUserRunStore(newDb())
78
- await assert.rejects(
79
- store.start({ persona: 'susan', disposition: 'realistic', seed: 42 }),
80
- /pikku db generate/
81
- )
82
- })
83
-
84
- test('writes against a migrated table', async () => {
85
- const runId = await store.start({
86
- persona: 'susan',
87
- disposition: 'realistic',
88
- seed: 42,
89
- })
90
- assert.ok(runId)
91
- })
92
-
93
- test('a run starts as running, with no findings and no finish time', async () => {
94
- const runId = await store.start({
95
- persona: 'susan',
96
- disposition: 'careless',
97
- seed: 7,
98
- goals: ['find the export button'],
99
- memory: { docId: 'doc_1' },
100
- startedBy: 'user_1',
101
- })
102
-
103
- const run = await store.get(runId)
104
- assert.equal(run?.status, 'running')
105
- assert.deepEqual(run?.findings, [])
106
- assert.equal(run?.finishedAt, null)
107
- assert.equal(run?.error, null)
108
- assert.deepEqual(run?.goals, ['find the export button'])
109
- assert.deepEqual(run?.memory, { docId: 'doc_1' })
110
- assert.equal(run?.startedBy, 'user_1')
111
- })
112
-
113
- // The seed is the whole reason a finding is reproducible, and a BIGINT comes
114
- // back as a string on some drivers — so it has to survive the round trip as a
115
- // number.
116
- test('the seed round-trips as a number', async () => {
117
- const runId = await store.start({
118
- persona: 'susan',
119
- disposition: 'realistic',
120
- seed: 2_147_483_646,
121
- })
122
- const run = await store.get(runId)
123
- assert.strictEqual(run?.seed, 2_147_483_646)
124
- })
125
-
126
- test('completing a run stores what it found', async () => {
127
- const runId = await store.start({
128
- persona: 'susan',
129
- disposition: 'auditor',
130
- seed: 1,
131
- memory: { docId: 'doc_1' },
132
- })
133
-
134
- await store.complete(runId, {
135
- findings: [FINDING],
136
- tally: TALLY,
137
- memory: { docId: 'doc_1', orgId: 'org_9' },
138
- stoppedBy: 'budget-steps',
139
- intents: [INTENT],
140
- steps: [step(0), step(1)],
141
- })
142
-
143
- const run = await store.get(runId)
144
- assert.equal(run?.status, 'completed')
145
- assert.equal(run?.findings.length, 1)
146
- assert.equal(run?.findings[0]?.detail, FINDING.detail)
147
- assert.deepEqual(run?.tally, TALLY)
148
- assert.equal(run?.stoppedBy, 'budget-steps')
149
- // Overwritten, not merged: the engine's memory already carries what it was
150
- // given plus what it learned.
151
- assert.deepEqual(run?.memory, { docId: 'doc_1', orgId: 'org_9' })
152
- assert.ok(run?.finishedAt instanceof Date)
153
- })
154
-
155
- // A crashed run and a run that found nothing are different answers, and this
156
- // record is the only place that distinction survives.
157
- test('a failed run is failed, not an empty success', async () => {
158
- const runId = await store.start({
159
- persona: 'susan',
160
- disposition: 'realistic',
161
- seed: 1,
162
- })
163
-
164
- await store.fail(runId, 'SCENARIO_ACTOR_SECRET is not set')
165
-
166
- const run = await store.get(runId)
167
- assert.equal(run?.status, 'failed')
168
- assert.equal(run?.error, 'SCENARIO_ACTOR_SECRET is not set')
169
- assert.deepEqual(run?.findings, [])
170
- assert.ok(run?.finishedAt instanceof Date)
171
- })
172
-
173
- test('an unknown run is null rather than a throw', async () => {
174
- await store.init()
175
- assert.equal(await store.get('nope'), null)
176
- })
177
-
178
- test('lists newest first', async () => {
179
- const first = await store.start({
180
- persona: 'susan',
181
- disposition: 'realistic',
182
- seed: 1,
183
- })
184
- const second = await store.start({
185
- persona: 'raj',
186
- disposition: 'realistic',
187
- seed: 2,
188
- })
189
-
190
- const runs = await store.list()
191
- assert.equal(runs.length, 2)
192
- assert.deepEqual(
193
- new Set(runs.map((run) => run.runId)),
194
- new Set([first, second])
195
- )
196
- })
197
-
198
- // Comparing what a persona finds this week against last week is the whole
199
- // reason runs are kept.
200
- test('narrows to one persona s history', async () => {
201
- await store.start({ persona: 'susan', disposition: 'realistic', seed: 1 })
202
- await store.start({ persona: 'raj', disposition: 'realistic', seed: 2 })
203
-
204
- const runs = await store.list({ persona: 'susan' })
205
- assert.equal(runs.length, 1)
206
- assert.equal(runs[0]?.persona, 'susan')
207
- })
208
-
209
- test('completing an unknown run does not throw, and leaves no steps behind', async () => {
210
- await store.init()
211
- await store.complete('nope', {
212
- findings: [],
213
- tally: TALLY,
214
- memory: {},
215
- stoppedBy: 'exhausted',
216
- intents: [INTENT],
217
- steps: [step(0)],
218
- })
219
- assert.deepEqual(await store.steps('nope'), [])
220
- })
221
-
222
- // Not every project installs SerializePlugin, and a bare SQLite driver cannot
223
- // bind a Date at all — so the whole round trip has to hold without it.
224
- test('round-trips without SerializePlugin', async () => {
225
- const bare = new Kysely<KyselyPikkuDB>({
226
- dialect: new SqliteDialect({ database: new Database(':memory:') }),
227
- plugins: [new CamelCasePlugin()],
228
- })
229
- await applyPikkuSchemas(bare, [virtualUserSchema])
230
- const bareStore = new KyselyVirtualUserRunStore(bare)
231
-
232
- const runId = await bareStore.start({
233
- persona: 'susan',
234
- disposition: 'realistic',
235
- seed: 99,
236
- goals: ['export a doc'],
237
- memory: { docId: 'doc_1' },
238
- })
239
- await bareStore.complete(runId, {
240
- findings: [FINDING],
241
- tally: TALLY,
242
- memory: { docId: 'doc_1' },
243
- stoppedBy: 'exhausted',
244
- intents: [INTENT],
245
- steps: [step(0)],
246
- })
247
-
248
- const run = await bareStore.get(runId)
249
- assert.equal(run?.status, 'completed')
250
- assert.strictEqual(run?.seed, 99)
251
- assert.deepEqual(run?.goals, ['export a doc'])
252
- assert.deepEqual(run?.tally, TALLY)
253
- assert.equal(run?.findings[0]?.detail, FINDING.detail)
254
- assert.ok(run?.createdAt instanceof Date)
255
- assert.ok(run?.finishedAt instanceof Date)
256
- assert.deepEqual(await bareStore.steps(runId), [step(0)])
257
- })
258
-
259
- test('the transcript comes back in the order it happened', async () => {
260
- const runId = await store.start({
261
- persona: 'susan',
262
- disposition: 'realistic',
263
- seed: 1,
264
- })
265
-
266
- await store.complete(runId, {
267
- findings: [],
268
- tally: TALLY,
269
- memory: {},
270
- stoppedBy: 'exhausted',
271
- intents: [INTENT],
272
- steps: [step(2), step(0), step(1)],
273
- })
274
-
275
- const steps = await store.steps(runId)
276
- assert.deepEqual(
277
- steps.map((one) => one.index),
278
- [0, 1, 2]
279
- )
280
- assert.deepEqual(steps[0], step(0))
281
- })
282
-
283
- // The turn the model got wrong is the one worth reading, and it carries none
284
- // of the fields a successful call does.
285
- test('a turn with no response and no status survives the round trip', async () => {
286
- const runId = await store.start({
287
- persona: 'susan',
288
- disposition: 'careless',
289
- seed: 1,
290
- })
291
-
292
- const invalid = step(0, {
293
- intentId: undefined,
294
- action: { kind: 'invalid', detail: 'no such rpc: listDoc' },
295
- status: undefined,
296
- ok: undefined,
297
- response: undefined,
298
- findingKinds: ['invalid-action'],
299
- })
300
-
301
- await store.complete(runId, {
302
- findings: [],
303
- tally: TALLY,
304
- memory: {},
305
- stoppedBy: 'exhausted',
306
- intents: [],
307
- steps: [invalid],
308
- })
309
-
310
- const [stored] = await store.steps(runId)
311
- assert.deepEqual(stored, {
312
- index: 0,
313
- action: { kind: 'invalid', detail: 'no such rpc: listDoc' },
314
- findingKinds: ['invalid-action'],
315
- tokensIn: 40,
316
- tokensOut: 12,
317
- })
318
- })
319
-
320
- // Ten columns times a 500-step budget is five thousand bound variables, and a
321
- // bare sqlite driver takes 999 — so the long run is exactly the one that must
322
- // not be lost.
323
- test('a run longer than one insert can hold is stored whole', async () => {
324
- const runId = await store.start({
325
- persona: 'susan',
326
- disposition: 'adversarial',
327
- seed: 1,
328
- })
329
-
330
- const steps = Array.from({ length: 240 }, (_, index) => step(index))
331
- await store.complete(runId, {
332
- findings: [],
333
- tally: TALLY,
334
- memory: {},
335
- stoppedBy: 'budget-steps',
336
- intents: [],
337
- steps,
338
- })
339
-
340
- assert.equal((await store.steps(runId, { limit: 500 })).length, 240)
341
- assert.deepEqual(
342
- (await store.steps(runId, { limit: 2, offset: 100 })).map(
343
- (one) => one.index
344
- ),
345
- [100, 101]
346
- )
347
- })
348
-
349
- test('intents ride on the run, not on the transcript', async () => {
350
- const runId = await store.start({
351
- persona: 'susan',
352
- disposition: 'realistic',
353
- seed: 1,
354
- })
355
-
356
- await store.complete(runId, {
357
- findings: [],
358
- tally: TALLY,
359
- memory: {},
360
- stoppedBy: 'exhausted',
361
- intents: [INTENT],
362
- steps: [],
363
- })
364
-
365
- const [listed] = await store.list({ persona: 'susan' })
366
- assert.deepEqual(listed?.intents, [INTENT])
367
- assert.deepEqual(await store.steps(runId), [])
368
- })
369
- })
@@ -1,235 +0,0 @@
1
- import type { Kysely, Selectable } from 'kysely'
2
- import { randomUUID } from 'node:crypto'
3
- import type {
4
- StepRecord,
5
- VirtualUserDisposition,
6
- } from '@pikku/core/virtual-user'
7
- import type {
8
- VirtualUserRunOutcome,
9
- VirtualUserRunRecord,
10
- VirtualUserRunStart,
11
- VirtualUserRunStore,
12
- } from '@pikku/core/virtual-user'
13
- import type {
14
- KyselyPikkuDB,
15
- VirtualUserRunStepTable,
16
- VirtualUserRunTable,
17
- } from './kysely-tables.js'
18
- import { parseJson } from './kysely-json.js'
19
- import { requirePikkuSchema } from './schema/index.js'
20
- import { virtualUserSchema } from './schema/virtual-user.schema.js'
21
-
22
- /**
23
- * Records virtual-user runs in a `virtualUserRun` table.
24
- *
25
- * The store is the whole record of a run: `runVirtualUser` returns as soon as
26
- * the row exists and the engine keeps going without it, so nothing else knows
27
- * the run happened. That is why `fail()` exists as its own call — a run that
28
- * crashed and a run that found nothing are different answers, and a row left at
29
- * `running` is neither.
30
- *
31
- * Works with or without `SerializePlugin`, which not every project installs: the
32
- * JSON columns are written as strings (which the plugin passes through) and read
33
- * back through `parseJson` (which passes through what the plugin already
34
- * parsed). Timestamps are written as ISO strings for the same reason — a bare
35
- * SQLite driver cannot bind a `Date` at all.
36
- *
37
- * SECURITY: an `adversarial` run's findings are working exploits carrying live
38
- * ids. This is a privileged store; the scaffolded reads are scope-gated, and
39
- * exposing these rows more widely publishes your own exploits.
40
- */
41
- /**
42
- * How many step rows go in one insert.
43
- *
44
- * Not a performance dial. A bare SQLite driver binds at most 999 variables per
45
- * statement, and ten columns times a 500-step budget is five thousand — so an
46
- * un-chunked insert does not fail on a long run, it fails on the *interesting*
47
- * long run, which is the one nobody wants to lose.
48
- */
49
- const STEP_INSERT_CHUNK = 50
50
-
51
- export class KyselyVirtualUserRunStore implements VirtualUserRunStore {
52
- private initialized = false
53
-
54
- constructor(private db: Kysely<KyselyPikkuDB>) {}
55
-
56
- /**
57
- * Creates the table on first use, like the audit sink — the runtime does not
58
- * need it, so it arrives with the feature that fills it rather than in every
59
- * database.
60
- */
61
- public async init(): Promise<void> {
62
- if (this.initialized) return
63
- await requirePikkuSchema(this.db, virtualUserSchema)
64
- this.initialized = true
65
- }
66
-
67
- async start(run: VirtualUserRunStart): Promise<string> {
68
- await this.init()
69
- const runId = randomUUID()
70
- await this.db
71
- .insertInto('virtualUserRun')
72
- .values({
73
- runId,
74
- persona: run.persona,
75
- disposition: run.disposition,
76
- // Text on every engine (see the schema), so the seed round-trips
77
- // through drivers that hand a BIGINT back as a string.
78
- seed: String(run.seed),
79
- status: 'running',
80
- goals: JSON.stringify(run.goals ?? []),
81
- memory: JSON.stringify(run.memory ?? {}),
82
- findings: '[]',
83
- startedBy: run.startedBy ?? null,
84
- })
85
- .execute()
86
- return runId
87
- }
88
-
89
- async complete(runId: string, outcome: VirtualUserRunOutcome): Promise<void> {
90
- await this.init()
91
- const updated = await this.db
92
- .updateTable('virtualUserRun')
93
- .set({
94
- status: 'completed',
95
- findings: JSON.stringify(outcome.findings),
96
- tally: JSON.stringify(outcome.tally),
97
- // Overwritten rather than merged: the engine's memory already carries
98
- // what it was given, plus what it learned on the way.
99
- memory: JSON.stringify(outcome.memory),
100
- intents: JSON.stringify(outcome.intents),
101
- stoppedBy: outcome.stoppedBy,
102
- finishedAt: new Date().toISOString(),
103
- })
104
- .where('runId', '=', runId)
105
- .executeTakeFirst()
106
- // Nothing owns steps written against a run that is not there, and nothing
107
- // would ever read or reap them — there is no foreign key to refuse them.
108
- if (updated.numUpdatedRows === 0n) return
109
- await this.writeSteps(runId, outcome.steps)
110
- }
111
-
112
- async fail(runId: string, error: string): Promise<void> {
113
- await this.init()
114
- await this.db
115
- .updateTable('virtualUserRun')
116
- .set({
117
- status: 'failed',
118
- error,
119
- finishedAt: new Date().toISOString(),
120
- })
121
- .where('runId', '=', runId)
122
- .execute()
123
- }
124
-
125
- async get(runId: string): Promise<VirtualUserRunRecord | null> {
126
- await this.init()
127
- const row = await this.db
128
- .selectFrom('virtualUserRun')
129
- .selectAll()
130
- .where('runId', '=', runId)
131
- .executeTakeFirst()
132
- return row ? this.toRecord(row) : null
133
- }
134
-
135
- async list(options?: {
136
- persona?: string
137
- limit?: number
138
- offset?: number
139
- }): Promise<VirtualUserRunRecord[]> {
140
- await this.init()
141
- let query = this.db.selectFrom('virtualUserRun').selectAll()
142
- if (options?.persona) {
143
- query = query.where('persona', '=', options.persona)
144
- }
145
- const rows = await query
146
- .orderBy('createdAt', 'desc')
147
- .limit(options?.limit ?? 50)
148
- .offset(options?.offset ?? 0)
149
- .execute()
150
- return rows.map((row) => this.toRecord(row))
151
- }
152
-
153
- async steps(
154
- runId: string,
155
- options?: { limit?: number; offset?: number }
156
- ): Promise<StepRecord[]> {
157
- await this.init()
158
- const rows = await this.db
159
- .selectFrom('virtualUserRunStep')
160
- .selectAll()
161
- .where('runId', '=', runId)
162
- .orderBy('stepIndex', 'asc')
163
- .limit(options?.limit ?? 1000)
164
- .offset(options?.offset ?? 0)
165
- .execute()
166
- return rows.map((row) => this.toStep(row))
167
- }
168
-
169
- private async writeSteps(
170
- runId: string,
171
- steps: readonly StepRecord[]
172
- ): Promise<void> {
173
- for (let at = 0; at < steps.length; at += STEP_INSERT_CHUNK) {
174
- const chunk = steps.slice(at, at + STEP_INSERT_CHUNK)
175
- await this.db
176
- .insertInto('virtualUserRunStep')
177
- .values(
178
- chunk.map((step) => ({
179
- runId,
180
- stepIndex: step.index,
181
- intentId: step.intentId ?? null,
182
- action: JSON.stringify(step.action),
183
- status: step.status ?? null,
184
- ok: step.ok === undefined ? null : step.ok ? 1 : 0,
185
- response:
186
- step.response === undefined
187
- ? null
188
- : JSON.stringify(step.response),
189
- findingKinds: step.findingKinds
190
- ? JSON.stringify(step.findingKinds)
191
- : null,
192
- tokensIn: step.tokensIn,
193
- tokensOut: step.tokensOut,
194
- }))
195
- )
196
- .execute()
197
- }
198
- }
199
-
200
- private toStep(row: Selectable<VirtualUserRunStepTable>): StepRecord {
201
- return {
202
- index: Number(row.stepIndex),
203
- ...(row.intentId ? { intentId: row.intentId } : {}),
204
- action: parseJson(row.action),
205
- ...(row.status === null ? {} : { status: Number(row.status) }),
206
- ...(row.ok === null ? {} : { ok: Boolean(row.ok) }),
207
- ...(row.response === null ? {} : { response: parseJson(row.response) }),
208
- ...(row.findingKinds
209
- ? { findingKinds: parseJson(row.findingKinds) }
210
- : {}),
211
- tokensIn: Number(row.tokensIn),
212
- tokensOut: Number(row.tokensOut),
213
- }
214
- }
215
-
216
- private toRecord(row: Selectable<VirtualUserRunTable>): VirtualUserRunRecord {
217
- return {
218
- runId: row.runId,
219
- persona: row.persona,
220
- disposition: row.disposition as VirtualUserDisposition,
221
- seed: Number(row.seed),
222
- status: row.status,
223
- goals: parseJson(row.goals) ?? [],
224
- memory: parseJson(row.memory) ?? {},
225
- findings: parseJson(row.findings) ?? [],
226
- intents: parseJson(row.intents) ?? [],
227
- tally: row.tally ? (parseJson(row.tally) ?? null) : null,
228
- stoppedBy: row.stoppedBy,
229
- error: row.error,
230
- startedBy: row.startedBy,
231
- createdAt: new Date(row.createdAt),
232
- finishedAt: row.finishedAt ? new Date(row.finishedAt) : null,
233
- }
234
- }
235
- }