@pikku/kysely 0.13.24 → 0.13.27
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 +235 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +3 -1
- package/dist/src/kysely-analytics-service.d.ts +30 -0
- package/dist/src/kysely-analytics-service.js +80 -0
- package/dist/src/kysely-feature-flag-store.d.ts +37 -0
- package/dist/src/kysely-feature-flag-store.js +239 -0
- package/dist/src/kysely-tables.d.ts +45 -0
- package/dist/src/schema/analytics.schema.d.ts +17 -0
- package/dist/src/schema/analytics.schema.js +48 -0
- package/dist/src/schema/flag.schema.d.ts +16 -0
- package/dist/src/schema/flag.schema.js +62 -0
- package/dist/src/schema/index.d.ts +2 -0
- package/dist/src/schema/index.js +6 -0
- package/package.json +11 -6
- package/run-tests.sh +9 -1
- package/bin/pikku-kysely-pure.ts +0 -59
- package/dist/src/kysely-lock-vault.d.ts +0 -34
- package/dist/src/kysely-lock-vault.js +0 -77
- package/dist/src/schema/data-lock.schema.d.ts +0 -15
- package/dist/src/schema/data-lock.schema.js +0 -27
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/classification-crypto.test.ts +0 -143
- package/src/classification-crypto.ts +0 -163
- package/src/classification-plugin.pglite.test.ts +0 -226
- package/src/classification-plugin.test.ts +0 -265
- package/src/classification-plugin.ts +0 -249
- package/src/coercion-plugin.test.ts +0 -160
- package/src/coercion-plugin.ts +0 -187
- package/src/create-audited-kysely.test.ts +0 -560
- package/src/create-audited-kysely.ts +0 -254
- package/src/index.ts +0 -81
- package/src/kysely-agent-run-scores.ts +0 -47
- package/src/kysely-agent-run-service.ts +0 -244
- package/src/kysely-agent-run-state-service.ts +0 -200
- package/src/kysely-agent-storage-service.ts +0 -632
- package/src/kysely-audit-service.test.ts +0 -352
- package/src/kysely-audit-service.ts +0 -237
- package/src/kysely-channel-store.ts +0 -107
- package/src/kysely-credential-service.ts +0 -286
- package/src/kysely-deployment-service.ts +0 -163
- package/src/kysely-eventhub-store.ts +0 -53
- package/src/kysely-json.ts +0 -5
- package/src/kysely-scope-service.test.ts +0 -418
- package/src/kysely-scope-service.ts +0 -425
- package/src/kysely-secret-service.ts +0 -242
- package/src/kysely-services.test.ts +0 -405
- package/src/kysely-session-store.ts +0 -57
- package/src/kysely-tables.test.ts +0 -110
- package/src/kysely-tables.ts +0 -434
- package/src/kysely-virtual-user-run-store.test.ts +0 -369
- package/src/kysely-virtual-user-run-store.ts +0 -235
- package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
- package/src/kysely-virtual-user-schedule-store.ts +0 -187
- package/src/kysely-webhook-service.test.ts +0 -153
- package/src/kysely-webhook-service.ts +0 -139
- package/src/kysely-workflow-mirror.test.ts +0 -259
- package/src/kysely-workflow-mirror.ts +0 -374
- package/src/kysely-workflow-run-service.ts +0 -305
- package/src/kysely-workflow-service.test.ts +0 -544
- package/src/kysely-workflow-service.ts +0 -820
- package/src/schema/agent.schema.ts +0 -162
- package/src/schema/audit.schema.ts +0 -57
- package/src/schema/channel.schema.ts +0 -38
- package/src/schema/credential.schema.ts +0 -58
- package/src/schema/deployment.schema.ts +0 -47
- package/src/schema/index.ts +0 -441
- package/src/schema/pikku-schema.types.ts +0 -169
- package/src/schema/schema.test.ts +0 -591
- package/src/schema/scope.schema.ts +0 -110
- package/src/schema/secret.schema.ts +0 -49
- package/src/schema/session.schema.ts +0 -20
- package/src/schema/virtual-user-schedule.schema.ts +0 -48
- package/src/schema/virtual-user.schema.ts +0 -106
- package/src/schema/webhook.schema.ts +0 -58
- package/src/schema/workflow.schema.ts +0 -150
- package/src/serialize-plugin.ts +0 -233
- package/tsconfig.json +0 -17
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import type { AuditLog } from '@pikku/core/services'
|
|
2
|
-
import type {
|
|
3
|
-
Kysely,
|
|
4
|
-
KyselyPlugin,
|
|
5
|
-
PluginTransformQueryArgs,
|
|
6
|
-
PluginTransformResultArgs,
|
|
7
|
-
QueryResult,
|
|
8
|
-
RootOperationNode,
|
|
9
|
-
UnknownRow,
|
|
10
|
-
} from 'kysely'
|
|
11
|
-
|
|
12
|
-
export interface CreateAuditedKyselyOptions {
|
|
13
|
-
audit: AuditLog
|
|
14
|
-
auditReads?: boolean
|
|
15
|
-
eventType?: string
|
|
16
|
-
transactionId?: string | null
|
|
17
|
-
queryIdPrefix?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type AuditedQuery = {
|
|
21
|
-
queryKind: 'select' | 'insert' | 'update' | 'delete'
|
|
22
|
-
queryId: string
|
|
23
|
-
tables: string[]
|
|
24
|
-
changedColumns?: string[]
|
|
25
|
-
changes?: unknown
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const QUERY_KIND_BY_NODE: Record<
|
|
29
|
-
string,
|
|
30
|
-
AuditedQuery['queryKind'] | undefined
|
|
31
|
-
> = {
|
|
32
|
-
SelectQueryNode: 'select',
|
|
33
|
-
InsertQueryNode: 'insert',
|
|
34
|
-
UpdateQueryNode: 'update',
|
|
35
|
-
DeleteQueryNode: 'delete',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function getIdentifierName(node: unknown): string | undefined {
|
|
39
|
-
if (!node || typeof node !== 'object') return undefined
|
|
40
|
-
const candidate = node as {
|
|
41
|
-
name?: string
|
|
42
|
-
column?: { name?: string }
|
|
43
|
-
identifier?: { name?: string }
|
|
44
|
-
table?: { identifier?: { name?: string } }
|
|
45
|
-
}
|
|
46
|
-
return (
|
|
47
|
-
candidate.name ??
|
|
48
|
-
candidate.column?.name ??
|
|
49
|
-
candidate.identifier?.name ??
|
|
50
|
-
candidate.table?.identifier?.name
|
|
51
|
-
)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function collectTableNames(node: unknown, out: Set<string>): void {
|
|
55
|
-
if (!node || typeof node !== 'object') return
|
|
56
|
-
|
|
57
|
-
const candidate = node as {
|
|
58
|
-
kind?: string
|
|
59
|
-
table?: { identifier?: { name?: string } }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
candidate.kind === 'TableNode' &&
|
|
64
|
-
typeof candidate.table?.identifier?.name === 'string'
|
|
65
|
-
) {
|
|
66
|
-
out.add(candidate.table.identifier.name)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const value of Object.values(node as Record<string, unknown>)) {
|
|
70
|
-
if (Array.isArray(value)) {
|
|
71
|
-
for (const item of value) collectTableNames(item, out)
|
|
72
|
-
continue
|
|
73
|
-
}
|
|
74
|
-
collectTableNames(value, out)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function extractValue(node: unknown): unknown {
|
|
79
|
-
if (!node || typeof node !== 'object') return node
|
|
80
|
-
|
|
81
|
-
const candidate = node as {
|
|
82
|
-
kind?: string
|
|
83
|
-
value?: unknown
|
|
84
|
-
values?: unknown[]
|
|
85
|
-
left?: unknown
|
|
86
|
-
right?: unknown
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
switch (candidate.kind) {
|
|
90
|
-
case 'ValueNode':
|
|
91
|
-
return candidate.value
|
|
92
|
-
case 'PrimitiveValueListNode':
|
|
93
|
-
case 'ValueListNode':
|
|
94
|
-
return (candidate.values ?? []).map(extractValue)
|
|
95
|
-
case 'DefaultInsertValueNode':
|
|
96
|
-
return '[default]'
|
|
97
|
-
case 'ColumnNode':
|
|
98
|
-
case 'ReferenceNode':
|
|
99
|
-
return `[${candidate.kind}]`
|
|
100
|
-
case 'RawNode':
|
|
101
|
-
return '[raw]'
|
|
102
|
-
case 'SelectQueryNode':
|
|
103
|
-
return '[subquery]'
|
|
104
|
-
case 'ParensNode':
|
|
105
|
-
return extractValue((candidate as { node?: unknown }).node)
|
|
106
|
-
case 'BinaryOperationNode':
|
|
107
|
-
return {
|
|
108
|
-
left: extractValue(candidate.left),
|
|
109
|
-
right: extractValue(candidate.right),
|
|
110
|
-
}
|
|
111
|
-
default:
|
|
112
|
-
return `[${candidate.kind ?? 'expression'}]`
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function extractInsertChanges(node: any): {
|
|
117
|
-
changedColumns?: string[]
|
|
118
|
-
changes?: unknown
|
|
119
|
-
} {
|
|
120
|
-
const columns = Array.isArray(node.columns)
|
|
121
|
-
? node.columns
|
|
122
|
-
.map((column: unknown) => getIdentifierName(column))
|
|
123
|
-
.filter(Boolean)
|
|
124
|
-
: []
|
|
125
|
-
const values = Array.isArray(node.values?.values) ? node.values.values : []
|
|
126
|
-
if (columns.length === 0 || values.length === 0) {
|
|
127
|
-
return {
|
|
128
|
-
changedColumns: columns.length > 0 ? columns : undefined,
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const rows = values.map((row: any) => {
|
|
133
|
-
const rowValues = Array.isArray(row?.values)
|
|
134
|
-
? row.values.map(extractValue)
|
|
135
|
-
: []
|
|
136
|
-
return Object.fromEntries(
|
|
137
|
-
columns.map((column, index) => [column, rowValues[index]])
|
|
138
|
-
)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
changedColumns: columns,
|
|
143
|
-
changes: rows.length === 1 ? rows[0] : rows,
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function extractUpdateChanges(node: any): {
|
|
148
|
-
changedColumns?: string[]
|
|
149
|
-
changes?: unknown
|
|
150
|
-
} {
|
|
151
|
-
const updates = Array.isArray(node.updates) ? node.updates : []
|
|
152
|
-
const entries = updates
|
|
153
|
-
.map((update: any) => {
|
|
154
|
-
const column = getIdentifierName(update.column)
|
|
155
|
-
if (!column) return null
|
|
156
|
-
return [column, extractValue(update.value)] as const
|
|
157
|
-
})
|
|
158
|
-
.filter(Boolean) as Array<readonly [string, unknown]>
|
|
159
|
-
|
|
160
|
-
if (entries.length === 0) {
|
|
161
|
-
return {}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
changedColumns: entries.map(([column]) => column),
|
|
166
|
-
changes: Object.fromEntries(entries),
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function extractChangeSet(
|
|
171
|
-
node: RootOperationNode,
|
|
172
|
-
queryKind: AuditedQuery['queryKind']
|
|
173
|
-
) {
|
|
174
|
-
if (queryKind === 'insert') {
|
|
175
|
-
return extractInsertChanges(node as any)
|
|
176
|
-
}
|
|
177
|
-
if (queryKind === 'update') {
|
|
178
|
-
return extractUpdateChanges(node as any)
|
|
179
|
-
}
|
|
180
|
-
return {}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
class AuditedKyselyPlugin implements KyselyPlugin {
|
|
184
|
-
private readonly pending = new Map<unknown, AuditedQuery>()
|
|
185
|
-
private queryCounter = 0
|
|
186
|
-
|
|
187
|
-
constructor(private readonly options: CreateAuditedKyselyOptions) {}
|
|
188
|
-
|
|
189
|
-
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
|
|
190
|
-
const queryKind =
|
|
191
|
-
QUERY_KIND_BY_NODE[(args.node as { kind?: string }).kind ?? '']
|
|
192
|
-
if (!queryKind) {
|
|
193
|
-
return args.node
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (queryKind === 'select' && this.options.auditReads === false) {
|
|
197
|
-
return args.node
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const tables = new Set<string>()
|
|
201
|
-
collectTableNames(args.node, tables)
|
|
202
|
-
const queryId = `${this.options.queryIdPrefix ?? 'q'}-${++this.queryCounter}`
|
|
203
|
-
this.pending.set(args.queryId, {
|
|
204
|
-
queryKind,
|
|
205
|
-
queryId,
|
|
206
|
-
tables: [...tables].sort(),
|
|
207
|
-
...extractChangeSet(args.node, queryKind),
|
|
208
|
-
})
|
|
209
|
-
return args.node
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async transformResult(
|
|
213
|
-
args: PluginTransformResultArgs
|
|
214
|
-
): Promise<QueryResult<UnknownRow>> {
|
|
215
|
-
const pending = this.pending.get(args.queryId)
|
|
216
|
-
if (!pending) {
|
|
217
|
-
return args.result
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
this.pending.delete(args.queryId)
|
|
221
|
-
const rawResult = args.result as QueryResult<UnknownRow> & {
|
|
222
|
-
numAffectedRows?: bigint
|
|
223
|
-
numUpdatedOrDeletedRows?: bigint
|
|
224
|
-
}
|
|
225
|
-
const rowCount =
|
|
226
|
-
rawResult.rows.length > 0
|
|
227
|
-
? rawResult.rows.length
|
|
228
|
-
: Number(
|
|
229
|
-
rawResult.numUpdatedOrDeletedRows ?? rawResult.numAffectedRows ?? 0n
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
await this.options.audit.write({
|
|
233
|
-
type: this.options.eventType ?? 'db.query',
|
|
234
|
-
source: 'auto',
|
|
235
|
-
transactionId: this.options.transactionId ?? null,
|
|
236
|
-
queryId: pending.queryId,
|
|
237
|
-
metadata: {
|
|
238
|
-
queryKind: pending.queryKind,
|
|
239
|
-
tables: pending.tables,
|
|
240
|
-
changedColumns: pending.changedColumns,
|
|
241
|
-
changes: pending.changes,
|
|
242
|
-
rowCount,
|
|
243
|
-
},
|
|
244
|
-
})
|
|
245
|
-
return rawResult
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function createAuditedKysely<DB>(
|
|
250
|
-
db: Kysely<DB>,
|
|
251
|
-
options: CreateAuditedKyselyOptions
|
|
252
|
-
): Kysely<DB> {
|
|
253
|
-
return db.withPlugin(new AuditedKyselyPlugin(options))
|
|
254
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
export { KyselyChannelStore } from './kysely-channel-store.js'
|
|
2
|
-
export { KyselyEventHubStore } from './kysely-eventhub-store.js'
|
|
3
|
-
export { KyselyWorkflowService } from './kysely-workflow-service.js'
|
|
4
|
-
export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
|
|
5
|
-
export { KyselyWorkflowMirror } from './kysely-workflow-mirror.js'
|
|
6
|
-
export { KyselyDeploymentService } from './kysely-deployment-service.js'
|
|
7
|
-
export { KyselyAgentStorageService } from './kysely-agent-storage-service.js'
|
|
8
|
-
export { KyselyAgentRunService } from './kysely-agent-run-service.js'
|
|
9
|
-
export { KyselyAgentRunStateService } from './kysely-agent-run-state-service.js'
|
|
10
|
-
export { KyselySecretService } from './kysely-secret-service.js'
|
|
11
|
-
export { KyselyCredentialService } from './kysely-credential-service.js'
|
|
12
|
-
export { KyselySessionStore } from './kysely-session-store.js'
|
|
13
|
-
export { KyselyScopeService } from './kysely-scope-service.js'
|
|
14
|
-
export { KyselyWebhookService } from './kysely-webhook-service.js'
|
|
15
|
-
export {
|
|
16
|
-
createAuditedKysely,
|
|
17
|
-
type CreateAuditedKyselyOptions,
|
|
18
|
-
} from './create-audited-kysely.js'
|
|
19
|
-
export { KyselyAuditService } from './kysely-audit-service.js'
|
|
20
|
-
export { KyselyVirtualUserRunStore } from './kysely-virtual-user-run-store.js'
|
|
21
|
-
export { KyselyVirtualUserScheduleStore } from './kysely-virtual-user-schedule-store.js'
|
|
22
|
-
|
|
23
|
-
export {
|
|
24
|
-
SerializePlugin,
|
|
25
|
-
BaseSerializePlugin,
|
|
26
|
-
type Serializer,
|
|
27
|
-
type Deserializer,
|
|
28
|
-
} from './serialize-plugin.js'
|
|
29
|
-
export {
|
|
30
|
-
createCoercionPlugin,
|
|
31
|
-
type ColumnKind,
|
|
32
|
-
type CoercionMap,
|
|
33
|
-
type CreateCoercionPluginOptions,
|
|
34
|
-
} from './coercion-plugin.js'
|
|
35
|
-
export {
|
|
36
|
-
createClassificationPlugin,
|
|
37
|
-
type CreateClassificationPluginOptions,
|
|
38
|
-
} from './classification-plugin.js'
|
|
39
|
-
export {
|
|
40
|
-
ClassificationCrypto,
|
|
41
|
-
DEFAULT_KEY_ID,
|
|
42
|
-
isColumnEnvelope,
|
|
43
|
-
parseColumnEnvelope,
|
|
44
|
-
type ClassificationCryptoOptions,
|
|
45
|
-
type ColumnEnvelope,
|
|
46
|
-
type KEKResolver,
|
|
47
|
-
type ResolvedKEK,
|
|
48
|
-
} from './classification-crypto.js'
|
|
49
|
-
export {
|
|
50
|
-
agentSchema,
|
|
51
|
-
auditSchema,
|
|
52
|
-
channelSchema,
|
|
53
|
-
credentialSchema,
|
|
54
|
-
deploymentSchema,
|
|
55
|
-
scopeSchema,
|
|
56
|
-
secretSchema,
|
|
57
|
-
sessionSchema,
|
|
58
|
-
virtualUserSchema,
|
|
59
|
-
webhookSchema,
|
|
60
|
-
workflowSchema,
|
|
61
|
-
pikkuSchemas,
|
|
62
|
-
requiredPikkuSchemas,
|
|
63
|
-
applyPikkuSchemas,
|
|
64
|
-
compilePikkuSchemas,
|
|
65
|
-
requirePikkuSchema,
|
|
66
|
-
resolveRequirements,
|
|
67
|
-
type PikkuSchema,
|
|
68
|
-
type RequiredTypes,
|
|
69
|
-
type UnmetRequirement,
|
|
70
|
-
type SchemaStatementFactory,
|
|
71
|
-
} from './schema/index.js'
|
|
72
|
-
|
|
73
|
-
export type { KyselyPikkuDB } from './kysely-tables.js'
|
|
74
|
-
export type { WorkflowRunService } from '@pikku/core/workflow'
|
|
75
|
-
export type { AgentRunService, AgentRunRow } from '@pikku/core/agent'
|
|
76
|
-
export type {
|
|
77
|
-
VirtualUserRunStore,
|
|
78
|
-
VirtualUserRunRecord,
|
|
79
|
-
VirtualUserScheduleStore,
|
|
80
|
-
VirtualUserScheduleRecord,
|
|
81
|
-
} from '@pikku/core/virtual-user'
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from 'kysely'
|
|
2
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
3
|
-
import type { AgentRunScore } from '@pikku/core/agent-scorer'
|
|
4
|
-
import type { SaveScoreInput } from '@pikku/core/services'
|
|
5
|
-
import { parseJson } from './kysely-json.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Shared by the two Kysely services that implement `AgentRunStateService` — they
|
|
9
|
-
* differ in how they store approvals, not in how they store grades.
|
|
10
|
-
*/
|
|
11
|
-
export const saveRunScore = async (
|
|
12
|
-
db: Kysely<KyselyPikkuDB>,
|
|
13
|
-
score: SaveScoreInput
|
|
14
|
-
): Promise<void> => {
|
|
15
|
-
await db
|
|
16
|
-
.insertInto('agentRunScore')
|
|
17
|
-
.values({
|
|
18
|
-
id: crypto.randomUUID(),
|
|
19
|
-
runId: score.runId,
|
|
20
|
-
scorerName: score.scorerName,
|
|
21
|
-
score: score.score,
|
|
22
|
-
reason: score.reason ?? null,
|
|
23
|
-
metadata: score.metadata ? JSON.stringify(score.metadata) : null,
|
|
24
|
-
})
|
|
25
|
-
.execute()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const getRunScores = async (
|
|
29
|
-
db: Kysely<KyselyPikkuDB>,
|
|
30
|
-
runId: string
|
|
31
|
-
): Promise<AgentRunScore[]> => {
|
|
32
|
-
const rows = await db
|
|
33
|
-
.selectFrom('agentRunScore')
|
|
34
|
-
.selectAll()
|
|
35
|
-
.where('runId', '=', runId)
|
|
36
|
-
.orderBy('createdAt', 'asc')
|
|
37
|
-
.execute()
|
|
38
|
-
|
|
39
|
-
return rows.map((row) => ({
|
|
40
|
-
runId: row.runId,
|
|
41
|
-
scorerName: row.scorerName,
|
|
42
|
-
score: Number(row.score),
|
|
43
|
-
reason: row.reason ?? undefined,
|
|
44
|
-
metadata: parseJson(row.metadata),
|
|
45
|
-
createdAt: new Date(row.createdAt),
|
|
46
|
-
}))
|
|
47
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentThread,
|
|
3
|
-
AgentMessage,
|
|
4
|
-
AgentRunRow,
|
|
5
|
-
AgentRunService,
|
|
6
|
-
} from '@pikku/core/agent'
|
|
7
|
-
import type { Kysely } from 'kysely'
|
|
8
|
-
import { sql } from 'kysely'
|
|
9
|
-
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
10
|
-
import { parseJson } from './kysely-json.js'
|
|
11
|
-
|
|
12
|
-
// Owner ids routinely contain `_` (e.g. `user_123`), which LIKE treats as a
|
|
13
|
-
// single-character wildcard — so a raw prefix would match a foreign owner.
|
|
14
|
-
const escapeLikePattern = (value: string) =>
|
|
15
|
-
value.replace(/[\\%_]/g, (char) => `\\${char}`)
|
|
16
|
-
|
|
17
|
-
export class KyselyAgentRunService implements AgentRunService {
|
|
18
|
-
constructor(private db: Kysely<KyselyPikkuDB>) {}
|
|
19
|
-
|
|
20
|
-
async listThreads(options?: {
|
|
21
|
-
agentName?: string
|
|
22
|
-
resourceId?: string
|
|
23
|
-
owners?: string[]
|
|
24
|
-
limit?: number
|
|
25
|
-
offset?: number
|
|
26
|
-
}): Promise<AgentThread[]> {
|
|
27
|
-
const {
|
|
28
|
-
agentName,
|
|
29
|
-
resourceId,
|
|
30
|
-
owners,
|
|
31
|
-
limit = 50,
|
|
32
|
-
offset = 0,
|
|
33
|
-
} = options ?? {}
|
|
34
|
-
|
|
35
|
-
// An owners constraint is an authorization boundary, so an empty list must
|
|
36
|
-
// match nothing rather than degrade to "no filter".
|
|
37
|
-
if (owners && owners.length === 0) return []
|
|
38
|
-
|
|
39
|
-
let query = this.db
|
|
40
|
-
.selectFrom('agentThreads as t')
|
|
41
|
-
.select([
|
|
42
|
-
't.id',
|
|
43
|
-
't.resourceId',
|
|
44
|
-
't.title',
|
|
45
|
-
't.metadata',
|
|
46
|
-
't.createdAt',
|
|
47
|
-
't.updatedAt',
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
if (agentName) {
|
|
51
|
-
query = query.where(
|
|
52
|
-
't.id',
|
|
53
|
-
'in',
|
|
54
|
-
this.db
|
|
55
|
-
.selectFrom('agentRun')
|
|
56
|
-
.select('threadId')
|
|
57
|
-
.where('agentName', '=', agentName)
|
|
58
|
-
.distinct()
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (resourceId) {
|
|
63
|
-
query = query.where('t.resourceId', '=', resourceId)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (owners) {
|
|
67
|
-
query = query.where((eb) =>
|
|
68
|
-
eb.or(
|
|
69
|
-
owners.flatMap((owner) => [
|
|
70
|
-
eb('t.resourceId', '=', owner),
|
|
71
|
-
eb(
|
|
72
|
-
't.resourceId',
|
|
73
|
-
'like',
|
|
74
|
-
sql<string>`${`${escapeLikePattern(owner)}:%`} escape '\\'`
|
|
75
|
-
),
|
|
76
|
-
])
|
|
77
|
-
)
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const result = await query
|
|
82
|
-
.orderBy('t.updatedAt', 'desc')
|
|
83
|
-
.limit(limit)
|
|
84
|
-
.offset(offset)
|
|
85
|
-
.execute()
|
|
86
|
-
|
|
87
|
-
return result.map((row) => this.mapThreadRow(row))
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async getThread(threadId: string): Promise<AgentThread | null> {
|
|
91
|
-
const row = await this.db
|
|
92
|
-
.selectFrom('agentThreads')
|
|
93
|
-
.select([
|
|
94
|
-
'id',
|
|
95
|
-
'resourceId',
|
|
96
|
-
'title',
|
|
97
|
-
'metadata',
|
|
98
|
-
'createdAt',
|
|
99
|
-
'updatedAt',
|
|
100
|
-
])
|
|
101
|
-
.where('id', '=', threadId)
|
|
102
|
-
.executeTakeFirst()
|
|
103
|
-
|
|
104
|
-
if (!row) return null
|
|
105
|
-
return this.mapThreadRow(row)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async getThreadMessages(threadId: string): Promise<AgentMessage[]> {
|
|
109
|
-
const [msgResult, tcResult] = await Promise.all([
|
|
110
|
-
this.db
|
|
111
|
-
.selectFrom('agentMessage')
|
|
112
|
-
.select(['id', 'role', 'content', 'createdAt'])
|
|
113
|
-
.where('threadId', '=', threadId)
|
|
114
|
-
.orderBy('createdAt', 'asc')
|
|
115
|
-
.execute(),
|
|
116
|
-
this.db
|
|
117
|
-
.selectFrom('agentToolCall')
|
|
118
|
-
.select(['id', 'messageId', 'toolName', 'args', 'result'])
|
|
119
|
-
.where('threadId', '=', threadId)
|
|
120
|
-
.orderBy('createdAt', 'asc')
|
|
121
|
-
.execute(),
|
|
122
|
-
])
|
|
123
|
-
|
|
124
|
-
const tcByMessage = new Map<string, (typeof tcResult)[number][]>()
|
|
125
|
-
for (const tc of tcResult) {
|
|
126
|
-
const msgId = tc.messageId
|
|
127
|
-
if (!tcByMessage.has(msgId)) tcByMessage.set(msgId, [])
|
|
128
|
-
tcByMessage.get(msgId)!.push(tc)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const messages: AgentMessage[] = []
|
|
132
|
-
for (const row of msgResult) {
|
|
133
|
-
const msg: AgentMessage = {
|
|
134
|
-
id: row.id,
|
|
135
|
-
role: row.role as AgentMessage['role'],
|
|
136
|
-
content: row.content ?? undefined,
|
|
137
|
-
createdAt: new Date(row.createdAt as unknown as string),
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const tcs = tcByMessage.get(msg.id)
|
|
141
|
-
if (tcs?.length) {
|
|
142
|
-
msg.toolCalls = tcs.map((tc) => ({
|
|
143
|
-
id: tc.id,
|
|
144
|
-
name: tc.toolName,
|
|
145
|
-
args: parseJson(tc.args) as Record<string, unknown>,
|
|
146
|
-
}))
|
|
147
|
-
|
|
148
|
-
const completed = tcs.filter((tc) => tc.result != null)
|
|
149
|
-
if (completed.length) {
|
|
150
|
-
messages.push(msg)
|
|
151
|
-
messages.push({
|
|
152
|
-
id: `tool-results-${msg.id}`,
|
|
153
|
-
role: 'tool',
|
|
154
|
-
toolResults: completed.map((tc) => ({
|
|
155
|
-
id: tc.id,
|
|
156
|
-
name: tc.toolName,
|
|
157
|
-
result: tc.result!,
|
|
158
|
-
})),
|
|
159
|
-
createdAt: msg.createdAt,
|
|
160
|
-
})
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
messages.push(msg)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return messages
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async getThreadRuns(threadId: string): Promise<AgentRunRow[]> {
|
|
172
|
-
const result = await this.db
|
|
173
|
-
.selectFrom('agentRun')
|
|
174
|
-
.select([
|
|
175
|
-
'runId',
|
|
176
|
-
'agentName',
|
|
177
|
-
'threadId',
|
|
178
|
-
'resourceId',
|
|
179
|
-
'status',
|
|
180
|
-
'errorMessage',
|
|
181
|
-
'suspendReason',
|
|
182
|
-
'missingRpcs',
|
|
183
|
-
'usageInputTokens',
|
|
184
|
-
'usageOutputTokens',
|
|
185
|
-
'usageModel',
|
|
186
|
-
'createdAt',
|
|
187
|
-
'updatedAt',
|
|
188
|
-
])
|
|
189
|
-
.where('threadId', '=', threadId)
|
|
190
|
-
.orderBy('createdAt', 'desc')
|
|
191
|
-
.execute()
|
|
192
|
-
|
|
193
|
-
return result.map((row) => this.mapRunRow(row))
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async deleteThread(threadId: string): Promise<boolean> {
|
|
197
|
-
const result = await this.db
|
|
198
|
-
.deleteFrom('agentThreads')
|
|
199
|
-
.where('id', '=', threadId)
|
|
200
|
-
.executeTakeFirst()
|
|
201
|
-
|
|
202
|
-
return BigInt(result.numDeletedRows) > 0n
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async getDistinctAgentNames(): Promise<string[]> {
|
|
206
|
-
const result = await this.db
|
|
207
|
-
.selectFrom('agentRun')
|
|
208
|
-
.select('agentName')
|
|
209
|
-
.distinct()
|
|
210
|
-
.orderBy('agentName')
|
|
211
|
-
.execute()
|
|
212
|
-
|
|
213
|
-
return result.map((row) => row.agentName)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private mapThreadRow(row: any): AgentThread {
|
|
217
|
-
return {
|
|
218
|
-
id: row.id as string,
|
|
219
|
-
resourceId: row.resourceId as string,
|
|
220
|
-
title: (row.title as string) ?? undefined,
|
|
221
|
-
metadata: parseJson(row.metadata),
|
|
222
|
-
createdAt: new Date(row.createdAt as string),
|
|
223
|
-
updatedAt: new Date(row.updatedAt as string),
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private mapRunRow(row: any): AgentRunRow {
|
|
228
|
-
return {
|
|
229
|
-
runId: row.runId as string,
|
|
230
|
-
agentName: row.agentName as string,
|
|
231
|
-
threadId: row.threadId as string,
|
|
232
|
-
resourceId: row.resourceId as string,
|
|
233
|
-
status: row.status as string,
|
|
234
|
-
errorMessage: (row.errorMessage as string) ?? undefined,
|
|
235
|
-
suspendReason: (row.suspendReason as string) ?? undefined,
|
|
236
|
-
missingRpcs: parseJson(row.missingRpcs),
|
|
237
|
-
usageInputTokens: Number(row.usageInputTokens),
|
|
238
|
-
usageOutputTokens: Number(row.usageOutputTokens),
|
|
239
|
-
usageModel: row.usageModel as string,
|
|
240
|
-
createdAt: new Date(row.createdAt as string),
|
|
241
|
-
updatedAt: new Date(row.updatedAt as string),
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|