@sixb/sqlite 0.1.6 → 0.1.8
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/dist/agents/rows.d.ts +7 -4
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts +5 -0
- package/dist/ai-cost-groups.d.ts.map +1 -0
- package/dist/ai-cost-storage.d.ts +2 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-limit-storage.d.ts +35 -0
- package/dist/ai-limit-storage.d.ts.map +1 -0
- package/dist/ai-usage-storage.d.ts +1 -0
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/auth-storage/access-tokens.d.ts +1 -1
- package/dist/auth-storage/access-tokens.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +4 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts +29 -0
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +19 -1
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +3 -2
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3443 -789
- package/dist/index.js.map +29 -20
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +3 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
- package/dist/objects/query-compiler.d.ts.map +1 -0
- package/dist/objects/read-scope.d.ts +17 -0
- package/dist/objects/read-scope.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +51 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +33 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +9 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/share-grant-storage.d.ts +20 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +19 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +1 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/transactions.d.ts +8 -0
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/rows.ts +36 -9
- package/src/agents/runs.ts +152 -13
- package/src/agents/threads.ts +2 -17
- package/src/ai-cost-groups.ts +100 -0
- package/src/ai-cost-storage.ts +67 -25
- package/src/ai-limit-storage.ts +981 -0
- package/src/ai-usage-storage.ts +24 -1
- package/src/auth-storage/access-tokens.ts +77 -68
- package/src/auth-storage/auth-storage.ts +80 -1
- package/src/auth-storage/device-authorizations.ts +153 -0
- package/src/auth-storage/rows.ts +41 -0
- package/src/auth-storage/shared.ts +1 -0
- package/src/execution-storage.ts +22 -3
- package/src/index.ts +24 -3
- package/src/migrations/029-model-accounting.sql +79 -0
- package/src/migrations/030-ai-usage-limits.sql +100 -0
- package/src/migrations/031-subagent-runs.sql +231 -0
- package/src/migrations/032-conversation-run-spec.sql +78 -0
- package/src/migrations/033-retire-agent-definitions.sql +89 -0
- package/src/migrations/034-device-authorizations.sql +22 -0
- package/src/migrations/035-share-grants.sql +66 -0
- package/src/migrations/036-share-sessions.sql +226 -0
- package/src/migrations.ts +22 -0
- package/src/objects/index.ts +2 -0
- package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
- package/src/objects/read-scope.ts +316 -0
- package/src/objects/reader.ts +526 -0
- package/src/objects/rows.ts +123 -0
- package/src/objects/selected-reader.ts +119 -0
- package/src/objects/storage.ts +296 -0
- package/src/share-grant-storage.ts +252 -0
- package/src/share-session-storage.ts +189 -0
- package/src/storage-errors.ts +4 -0
- package/src/transactions.ts +32 -0
- package/src/workflow-run-storage.ts +8 -8
- package/dist/object-query-compiler.d.ts.map +0 -1
- package/dist/object-storage.d.ts.map +0 -1
- package/src/object-storage.ts +0 -709
package/src/agents/runs.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertAgentRunExecution,
|
|
4
|
+
assertConversationAgentRunSpec,
|
|
5
|
+
assertCreateSubagentRunInput,
|
|
6
|
+
assertSubagentRunResult,
|
|
7
|
+
subagentRunMatchesCreateInput,
|
|
8
|
+
} from "@sixb/core/internal/agent-run-storage-provider"
|
|
3
9
|
import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
|
|
4
10
|
import { serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
5
11
|
import {
|
|
@@ -8,7 +14,9 @@ import {
|
|
|
8
14
|
type AgentRunStore,
|
|
9
15
|
AgentStorageError,
|
|
10
16
|
type ConfirmAgentRunExecutionOwnershipInput,
|
|
17
|
+
type ConversationAgentRunRecord,
|
|
11
18
|
type CreateAgentRunInput,
|
|
19
|
+
type CreateSubagentRunInput,
|
|
12
20
|
type ExecutionStorage,
|
|
13
21
|
type FinishAgentRunInput,
|
|
14
22
|
type FinishQueuedAgentRunInput,
|
|
@@ -16,6 +24,7 @@ import {
|
|
|
16
24
|
type ListAgentRunsResult,
|
|
17
25
|
type ReclaimAgentRunInput,
|
|
18
26
|
type StartAgentRunInput,
|
|
27
|
+
type SubagentRunRecord,
|
|
19
28
|
} from "@sixb/core/storage"
|
|
20
29
|
import {
|
|
21
30
|
appendRunListFilters,
|
|
@@ -34,14 +43,14 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
34
43
|
private readonly executions: ExecutionStorage
|
|
35
44
|
) {}
|
|
36
45
|
|
|
37
|
-
async create(input: CreateAgentRunInput): Promise<
|
|
46
|
+
async create(input: CreateAgentRunInput): Promise<ConversationAgentRunRecord> {
|
|
47
|
+
assertConversationAgentRunSpec(input.spec, "SixbSqlite")
|
|
38
48
|
const createdAt = input.createdAt ?? new Date()
|
|
39
49
|
await assertAgentRunExecution({
|
|
40
50
|
executions: this.executions,
|
|
41
51
|
projectId: input.projectId,
|
|
42
52
|
executionId: input.executionId,
|
|
43
53
|
runId: input.id,
|
|
44
|
-
agentId: input.agentId,
|
|
45
54
|
})
|
|
46
55
|
|
|
47
56
|
return this.db.transaction(() => {
|
|
@@ -55,7 +64,6 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
55
64
|
`[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
|
|
56
65
|
)
|
|
57
66
|
}
|
|
58
|
-
|
|
59
67
|
if (thread.active_run_id !== null) {
|
|
60
68
|
throw new AgentStorageError(
|
|
61
69
|
"active_run_exists",
|
|
@@ -71,14 +79,15 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
71
79
|
project_id,
|
|
72
80
|
id,
|
|
73
81
|
execution_id,
|
|
82
|
+
kind,
|
|
74
83
|
thread_id,
|
|
75
|
-
agent_id,
|
|
76
84
|
trigger_message_id,
|
|
85
|
+
spec,
|
|
77
86
|
requester_group_ids,
|
|
78
87
|
status,
|
|
79
88
|
attempt,
|
|
80
89
|
created_at
|
|
81
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?)
|
|
90
|
+
) VALUES (?, ?, ?, 'conversation', ?, ?, ?, ?, 'queued', 0, ?)
|
|
82
91
|
`
|
|
83
92
|
)
|
|
84
93
|
.run(
|
|
@@ -86,8 +95,8 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
86
95
|
input.id,
|
|
87
96
|
input.executionId,
|
|
88
97
|
input.threadId,
|
|
89
|
-
input.agentId,
|
|
90
98
|
input.triggerMessageId,
|
|
99
|
+
JSON.stringify(input.spec),
|
|
91
100
|
JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds)),
|
|
92
101
|
createdAt.toISOString()
|
|
93
102
|
)
|
|
@@ -108,7 +117,115 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
108
117
|
)
|
|
109
118
|
.run(input.id, createdAt.toISOString(), input.projectId, input.threadId)
|
|
110
119
|
|
|
111
|
-
|
|
120
|
+
const created = this.requireRun(input.projectId, input.id)
|
|
121
|
+
if (created.kind !== "conversation") {
|
|
122
|
+
throw new Error(`[SixbSqlite] Agent run insert '${input.id}' returned the wrong run kind.`)
|
|
123
|
+
}
|
|
124
|
+
return created
|
|
125
|
+
})()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async createSubagent(input: CreateSubagentRunInput): Promise<SubagentRunRecord> {
|
|
129
|
+
assertCreateSubagentRunInput(input, "SixbSqlite")
|
|
130
|
+
const execution = await assertAgentRunExecution({
|
|
131
|
+
executions: this.executions,
|
|
132
|
+
projectId: input.projectId,
|
|
133
|
+
executionId: input.executionId,
|
|
134
|
+
runId: input.id,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return this.db.transaction(() => {
|
|
138
|
+
const parentRow = this.db
|
|
139
|
+
.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?")
|
|
140
|
+
.get(input.projectId, input.parentRunId) as AgentRunRow | null
|
|
141
|
+
if (!parentRow || parentRow.kind !== "conversation") {
|
|
142
|
+
throw new AgentStorageError(
|
|
143
|
+
"run_not_found",
|
|
144
|
+
`[SixbSqlite] Parent Agent run '${input.parentRunId}' was not found.`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const existingRow = this.db
|
|
149
|
+
.query("SELECT * FROM agent_runs WHERE project_id = ? AND id = ?")
|
|
150
|
+
.get(input.projectId, input.id) as AgentRunRow | null
|
|
151
|
+
if (existingRow) {
|
|
152
|
+
const existing = rowToRunRecord(existingRow)
|
|
153
|
+
if (subagentRunMatchesCreateInput(existing, input)) return existing
|
|
154
|
+
throw new AgentStorageError(
|
|
155
|
+
"duplicate_id",
|
|
156
|
+
`[SixbSqlite] Subagent run '${input.id}' already exists with different immutable inputs.`
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
if (parentRow.status !== "running") {
|
|
160
|
+
throw new AgentStorageError(
|
|
161
|
+
"invalid_state",
|
|
162
|
+
`[SixbSqlite] Parent Agent run '${parentRow.id}' is not running (status '${parentRow.status}').`
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
execution.source.type !== "execution" ||
|
|
167
|
+
execution.source.executionId !== parentRow.execution_id
|
|
168
|
+
) {
|
|
169
|
+
throw new AgentStorageError(
|
|
170
|
+
"invalid_input",
|
|
171
|
+
`[SixbSqlite] Subagent execution '${execution.id}' is not a child of parent execution '${parentRow.execution_id}'.`
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
if (parentRow.execution_token !== input.parentExecutionToken) {
|
|
175
|
+
throw new AgentStorageError(
|
|
176
|
+
"execution_lost",
|
|
177
|
+
`[SixbSqlite] Parent Agent run '${parentRow.id}' is no longer owned by this execution.`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const count = this.db
|
|
182
|
+
.query(
|
|
183
|
+
`SELECT COUNT(*) AS count FROM agent_runs
|
|
184
|
+
WHERE project_id = ? AND parent_run_id = ? AND status IN ('queued', 'running')`
|
|
185
|
+
)
|
|
186
|
+
.get(input.projectId, input.parentRunId) as { count: number }
|
|
187
|
+
if (count.count >= input.maxActiveChildren) {
|
|
188
|
+
throw new AgentStorageError(
|
|
189
|
+
"active_child_limit",
|
|
190
|
+
`[SixbSqlite] Agent run '${parentRow.id}' already has ${count.count} active subagents.`
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
this.db
|
|
196
|
+
.query(
|
|
197
|
+
`
|
|
198
|
+
INSERT INTO agent_runs (
|
|
199
|
+
project_id, id, execution_id, kind, parent_run_id, spawn_key, spec,
|
|
200
|
+
requester_group_ids, status, attempt, created_at
|
|
201
|
+
) VALUES (?, ?, ?, 'subagent', ?, ?, ?, ?, 'queued', 0, ?)
|
|
202
|
+
`
|
|
203
|
+
)
|
|
204
|
+
.run(
|
|
205
|
+
input.projectId,
|
|
206
|
+
input.id,
|
|
207
|
+
input.executionId,
|
|
208
|
+
input.parentRunId,
|
|
209
|
+
input.spawnKey,
|
|
210
|
+
JSON.stringify(input.spec),
|
|
211
|
+
parentRow.requester_group_ids,
|
|
212
|
+
(input.createdAt ?? new Date()).toISOString()
|
|
213
|
+
)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (isUniqueConstraintError(error)) {
|
|
216
|
+
throw new AgentStorageError(
|
|
217
|
+
"duplicate_id",
|
|
218
|
+
`[SixbSqlite] Subagent run '${input.id}' conflicts with an existing durable spawn.`
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
throw error
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const created = this.requireRun(input.projectId, input.id)
|
|
225
|
+
if (created.kind !== "subagent") {
|
|
226
|
+
throw new Error(`[SixbSqlite] Subagent insert '${input.id}' returned the wrong run kind.`)
|
|
227
|
+
}
|
|
228
|
+
return created
|
|
112
229
|
})()
|
|
113
230
|
}
|
|
114
231
|
|
|
@@ -205,7 +322,6 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
205
322
|
`[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`
|
|
206
323
|
)
|
|
207
324
|
}
|
|
208
|
-
|
|
209
325
|
this.db
|
|
210
326
|
.query(
|
|
211
327
|
`
|
|
@@ -235,6 +351,19 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
235
351
|
`[SixbSqlite] Execution token is no longer current on agent run '${input.id}'.`
|
|
236
352
|
)
|
|
237
353
|
}
|
|
354
|
+
if (input.status === "succeeded" && run.kind === "subagent") {
|
|
355
|
+
assertSubagentRunResult(input.result, run.id, "SixbSqlite")
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
input.status === "succeeded" &&
|
|
359
|
+
run.kind === "conversation" &&
|
|
360
|
+
input.result !== undefined
|
|
361
|
+
) {
|
|
362
|
+
throw new AgentStorageError(
|
|
363
|
+
"invalid_input",
|
|
364
|
+
`[SixbSqlite] Conversational Agent run '${run.id}' cannot persist a subagent result.`
|
|
365
|
+
)
|
|
366
|
+
}
|
|
238
367
|
|
|
239
368
|
this.db
|
|
240
369
|
.query(
|
|
@@ -246,6 +375,7 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
246
375
|
finish_reason = ?,
|
|
247
376
|
error = ?,
|
|
248
377
|
diagnostics = ?,
|
|
378
|
+
result = ?,
|
|
249
379
|
execution_token = NULL,
|
|
250
380
|
execution_queue_lease_expires_at = NULL,
|
|
251
381
|
completed_at = ?
|
|
@@ -258,6 +388,9 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
258
388
|
input.finishReason ?? null,
|
|
259
389
|
errorValue,
|
|
260
390
|
input.diagnostics === undefined ? null : JSON.stringify(input.diagnostics),
|
|
391
|
+
input.status === "succeeded" && input.result !== undefined
|
|
392
|
+
? JSON.stringify(input.result)
|
|
393
|
+
: null,
|
|
261
394
|
completedAt.toISOString(),
|
|
262
395
|
input.projectId,
|
|
263
396
|
input.id
|
|
@@ -304,21 +437,26 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
304
437
|
}
|
|
305
438
|
|
|
306
439
|
async list(input: ListAgentRunsInput): Promise<ListAgentRunsResult> {
|
|
307
|
-
if (hasEmptyStatuses(input)) {
|
|
440
|
+
if (hasEmptyStatuses(input) || input.kinds?.length === 0) {
|
|
308
441
|
return { runs: [], hasMore: false, total: 0 }
|
|
309
442
|
}
|
|
310
443
|
|
|
311
444
|
const whereClauses = ["project_id = ?"]
|
|
312
445
|
const args: SqliteValue[] = [input.projectId]
|
|
313
446
|
|
|
447
|
+
if (input.kinds) {
|
|
448
|
+
whereClauses.push(`kind IN (${input.kinds.map(() => "?").join(", ")})`)
|
|
449
|
+
args.push(...input.kinds)
|
|
450
|
+
}
|
|
451
|
+
|
|
314
452
|
if (input.threadId) {
|
|
315
453
|
whereClauses.push("thread_id = ?")
|
|
316
454
|
args.push(input.threadId)
|
|
317
455
|
}
|
|
318
456
|
|
|
319
|
-
if (input.
|
|
320
|
-
whereClauses.push("
|
|
321
|
-
args.push(input.
|
|
457
|
+
if (input.parentRunId) {
|
|
458
|
+
whereClauses.push("parent_run_id = ?")
|
|
459
|
+
args.push(input.parentRunId)
|
|
322
460
|
}
|
|
323
461
|
|
|
324
462
|
appendRunListFilters(whereClauses, args, input, "COALESCE(started_at, created_at)")
|
|
@@ -367,6 +505,7 @@ export class SqliteAgentRunStore implements AgentRunStore {
|
|
|
367
505
|
}
|
|
368
506
|
|
|
369
507
|
private releaseThread(run: AgentRunRow, completedAt: Date): void {
|
|
508
|
+
if (run.kind !== "conversation") return
|
|
370
509
|
this.db
|
|
371
510
|
.query(
|
|
372
511
|
`
|
package/src/agents/threads.ts
CHANGED
|
@@ -25,7 +25,6 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
|
|
|
25
25
|
INSERT INTO agent_threads (
|
|
26
26
|
project_id,
|
|
27
27
|
id,
|
|
28
|
-
agent_id,
|
|
29
28
|
owner_principal_type,
|
|
30
29
|
owner_principal_id,
|
|
31
30
|
title,
|
|
@@ -35,13 +34,12 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
|
|
|
35
34
|
message_count,
|
|
36
35
|
created_at,
|
|
37
36
|
updated_at
|
|
38
|
-
) VALUES (?, ?, ?, ?, ?, ?,
|
|
37
|
+
) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, 0, ?, ?)
|
|
39
38
|
`
|
|
40
39
|
)
|
|
41
40
|
.run(
|
|
42
41
|
input.projectId,
|
|
43
42
|
input.id,
|
|
44
|
-
input.agentId,
|
|
45
43
|
input.ownerPrincipal.type,
|
|
46
44
|
input.ownerPrincipal.id,
|
|
47
45
|
input.title ?? null,
|
|
@@ -72,26 +70,13 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
async list(input: ListAgentThreadsInput): Promise<ListAgentThreadsResult> {
|
|
75
|
-
if (
|
|
76
|
-
(input.statuses !== undefined && input.statuses.length === 0) ||
|
|
77
|
-
(input.agentIds !== undefined && input.agentIds.length === 0)
|
|
78
|
-
) {
|
|
73
|
+
if (input.statuses !== undefined && input.statuses.length === 0) {
|
|
79
74
|
return { threads: [], hasMore: false, total: 0 }
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
const whereClauses = ["project_id = ?"]
|
|
83
78
|
const args: SqliteValue[] = [input.projectId]
|
|
84
79
|
|
|
85
|
-
if (input.agentId) {
|
|
86
|
-
whereClauses.push("agent_id = ?")
|
|
87
|
-
args.push(input.agentId)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (input.agentIds) {
|
|
91
|
-
whereClauses.push(`agent_id IN (${input.agentIds.map(() => "?").join(", ")})`)
|
|
92
|
-
args.push(...input.agentIds)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
80
|
if (input.statuses) {
|
|
96
81
|
whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
|
|
97
82
|
args.push(...input.statuses)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AiModelCallGroupRow,
|
|
3
|
+
aiModelCallGroupFragmentFromRow,
|
|
4
|
+
buildAiModelCallGroupsFromFragments,
|
|
5
|
+
normalizeAiModelCallAccountingQuery,
|
|
6
|
+
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
7
|
+
import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
|
|
8
|
+
import type { SqliteStoreConnection } from "./transactions"
|
|
9
|
+
|
|
10
|
+
/** Paginate initiating executions, then aggregate their matching calls without loading usage payloads. */
|
|
11
|
+
export async function listAiModelCallGroups(
|
|
12
|
+
connection: SqliteStoreConnection,
|
|
13
|
+
input: ListAiModelCallGroupsInput
|
|
14
|
+
): Promise<ListAiModelCallGroupsResult> {
|
|
15
|
+
const query = normalizeAiModelCallAccountingQuery(input)
|
|
16
|
+
const parameters = [
|
|
17
|
+
query.projectId,
|
|
18
|
+
query.from.toISOString(),
|
|
19
|
+
query.to.toISOString(),
|
|
20
|
+
query.providerId ?? null,
|
|
21
|
+
query.modelId ?? null,
|
|
22
|
+
query.valuationStatus ?? null,
|
|
23
|
+
]
|
|
24
|
+
const count = connection.db.query(COUNT_SQL).get(...parameters) as { total: string }
|
|
25
|
+
const rows = connection.db
|
|
26
|
+
.query(PAGE_SQL)
|
|
27
|
+
.all(...parameters, query.limit, query.offset) as AiModelCallGroupRow[]
|
|
28
|
+
const total = Number(count?.total)
|
|
29
|
+
if (!Number.isSafeInteger(total) || total < 0)
|
|
30
|
+
throw new RangeError("[Sixb] Invalid model-call group count.")
|
|
31
|
+
const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow))
|
|
32
|
+
return { items, total, hasMore: query.offset + items.length < total }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FILTERED_SQL = `
|
|
36
|
+
WITH filtered AS (
|
|
37
|
+
SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
|
|
38
|
+
usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
|
|
39
|
+
usage.total_tokens,
|
|
40
|
+
COALESCE(cost.status, 'unvalued') AS valuation_status,
|
|
41
|
+
cost.currency AS amount_currency, cost.amount_nanos,
|
|
42
|
+
root_agent.id AS root_agent_run_id,
|
|
43
|
+
root_agent.thread_id AS root_thread_id,
|
|
44
|
+
direct_agent.kind AS direct_kind,
|
|
45
|
+
direct_agent.id AS direct_run_id,
|
|
46
|
+
direct_agent.parent_run_id AS parent_run_id,
|
|
47
|
+
workflow_node.node_id AS workflow_agent_step_id,
|
|
48
|
+
workflow_agent.node_run_id AS workflow_node_run_id,
|
|
49
|
+
workflow_node.workflow_id AS workflow_id,
|
|
50
|
+
workflow_node.workflow_run_id AS workflow_run_id
|
|
51
|
+
FROM ai_model_call_usage AS usage
|
|
52
|
+
LEFT JOIN agent_runs AS direct_agent
|
|
53
|
+
ON direct_agent.project_id = usage.project_id AND direct_agent.execution_id = usage.execution_id
|
|
54
|
+
LEFT JOIN agent_runs AS root_agent
|
|
55
|
+
ON root_agent.project_id = usage.project_id
|
|
56
|
+
AND root_agent.id = COALESCE(direct_agent.parent_run_id, direct_agent.id)
|
|
57
|
+
LEFT JOIN workflow_agent_node_runs AS workflow_agent
|
|
58
|
+
ON workflow_agent.project_id = usage.project_id AND workflow_agent.execution_id = usage.execution_id
|
|
59
|
+
LEFT JOIN workflow_node_runs AS workflow_node
|
|
60
|
+
ON workflow_node.project_id = workflow_agent.project_id AND workflow_node.id = workflow_agent.node_run_id
|
|
61
|
+
LEFT JOIN ai_model_call_valuations AS cost
|
|
62
|
+
ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
|
|
63
|
+
WHERE usage.project_id = ?1
|
|
64
|
+
AND usage.occurred_at >= ?2
|
|
65
|
+
AND usage.occurred_at < ?3
|
|
66
|
+
AND (?4 IS NULL OR usage.provider_id = ?4)
|
|
67
|
+
AND (?5 IS NULL OR usage.requested_model_id = ?5)
|
|
68
|
+
AND (?6 IS NULL OR COALESCE(cost.status, 'unvalued') = ?6)
|
|
69
|
+
), roots AS (
|
|
70
|
+
SELECT root_execution_id, MAX(occurred_at) AS last_call_at
|
|
71
|
+
FROM filtered GROUP BY root_execution_id
|
|
72
|
+
)
|
|
73
|
+
`
|
|
74
|
+
const COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`
|
|
75
|
+
const PAGE_SQL = `${FILTERED_SQL}, page AS (
|
|
76
|
+
SELECT root_execution_id FROM roots
|
|
77
|
+
ORDER BY last_call_at DESC, root_execution_id ASC LIMIT ?7 OFFSET ?8
|
|
78
|
+
)
|
|
79
|
+
SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
80
|
+
MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
|
|
81
|
+
CAST(COUNT(*) AS TEXT) AS call_count,
|
|
82
|
+
CAST(COUNT(total_tokens) AS TEXT) AS token_count,
|
|
83
|
+
CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
|
|
84
|
+
valuation_status, amount_currency,
|
|
85
|
+
CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
|
|
86
|
+
CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
|
|
87
|
+
CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low,
|
|
88
|
+
MAX(root_agent_run_id) AS root_agent_run_id,
|
|
89
|
+
MAX(root_thread_id) AS root_thread_id,
|
|
90
|
+
MAX(direct_kind) AS direct_kind,
|
|
91
|
+
MAX(direct_run_id) AS direct_run_id,
|
|
92
|
+
MAX(parent_run_id) AS parent_run_id,
|
|
93
|
+
MAX(workflow_agent_step_id) AS workflow_agent_step_id,
|
|
94
|
+
MAX(workflow_node_run_id) AS workflow_node_run_id,
|
|
95
|
+
MAX(workflow_id) AS workflow_id,
|
|
96
|
+
MAX(workflow_run_id) AS workflow_run_id
|
|
97
|
+
FROM filtered JOIN page ON page.root_execution_id = filtered.root_execution_id
|
|
98
|
+
GROUP BY filtered.root_execution_id, execution_id, provider_id, requested_model_id,
|
|
99
|
+
valuation_status, amount_currency
|
|
100
|
+
`
|
package/src/ai-cost-storage.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseAiModelCallCostDetails,
|
|
11
11
|
toAccountingItem,
|
|
12
12
|
} from "@sixb/core/internal/ai-cost-storage-provider"
|
|
13
|
+
import { normalizeModelProviderIds } from "@sixb/core/models"
|
|
13
14
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
14
15
|
import {
|
|
15
16
|
type AiAccountingOverview,
|
|
@@ -20,9 +21,13 @@ import {
|
|
|
20
21
|
type AiModelCallUsageRecord,
|
|
21
22
|
type ListAiModelCallAccountingInput,
|
|
22
23
|
type ListAiModelCallAccountingResult,
|
|
24
|
+
type ListAiModelCallGroupsInput,
|
|
25
|
+
type ListAiModelCallGroupsResult,
|
|
23
26
|
type QueryAiAccountingOverviewInput,
|
|
24
27
|
type SummarizeAiCostExecutionsInput,
|
|
25
28
|
} from "@sixb/core/storage"
|
|
29
|
+
import { listAiModelCallGroups } from "./ai-cost-groups"
|
|
30
|
+
import { requestedReasoningFromRow } from "./ai-usage-storage"
|
|
26
31
|
import { isUniqueConstraintError } from "./storage-errors"
|
|
27
32
|
import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
|
|
28
33
|
|
|
@@ -30,6 +35,12 @@ import { runImmediateTransaction, type SqliteStoreConnection } from "./transacti
|
|
|
30
35
|
export class SqliteAiCostStorage implements AiCostStorage {
|
|
31
36
|
constructor(private readonly connection: SqliteStoreConnection) {}
|
|
32
37
|
|
|
38
|
+
async listModelCallGroups(
|
|
39
|
+
input: ListAiModelCallGroupsInput
|
|
40
|
+
): Promise<ListAiModelCallGroupsResult> {
|
|
41
|
+
return listAiModelCallGroups(this.connection, input)
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
async recordModelCallCost(input: AiModelCallCostRecord): Promise<void> {
|
|
34
45
|
const record = normalizeAiModelCallCostRecord(input)
|
|
35
46
|
runImmediateTransaction(this.connection.db, () => {
|
|
@@ -176,13 +187,15 @@ const SQLITE_ACCOUNTING_LIST_COUNT_SQL = `
|
|
|
176
187
|
const SQLITE_ACCOUNTING_LIST_PAGE_SQL = `
|
|
177
188
|
SELECT usage.*,
|
|
178
189
|
CASE
|
|
179
|
-
WHEN direct_agent.
|
|
190
|
+
WHEN direct_agent.kind = 'conversation' THEN 'agent'
|
|
191
|
+
WHEN direct_agent.kind = 'subagent' THEN 'subagent'
|
|
180
192
|
WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
|
|
181
193
|
ELSE NULL
|
|
182
194
|
END AS attribution_kind,
|
|
183
|
-
|
|
195
|
+
workflow_node.node_id AS attribution_agent_step_id,
|
|
184
196
|
direct_agent.id AS attribution_agent_run_id,
|
|
185
197
|
direct_agent.thread_id AS attribution_thread_id,
|
|
198
|
+
direct_agent.parent_run_id AS attribution_parent_run_id,
|
|
186
199
|
workflow_agent.node_run_id AS attribution_node_run_id,
|
|
187
200
|
workflow_node.workflow_id AS attribution_workflow_id,
|
|
188
201
|
workflow_node.workflow_run_id AS attribution_workflow_run_id,
|
|
@@ -231,7 +244,12 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
231
244
|
usage.text_output_tokens,
|
|
232
245
|
usage.reasoning_output_tokens,
|
|
233
246
|
usage.reporting_status,
|
|
234
|
-
|
|
247
|
+
CASE
|
|
248
|
+
WHEN direct_agent.kind = 'conversation' THEN 'agent'
|
|
249
|
+
WHEN workflow_agent.node_run_id IS NOT NULL THEN 'workflowAgent'
|
|
250
|
+
ELSE NULL
|
|
251
|
+
END AS attribution_agent_kind,
|
|
252
|
+
workflow_node.node_id AS attribution_agent_step_id,
|
|
235
253
|
workflow_node.workflow_id AS attribution_workflow_id,
|
|
236
254
|
CASE
|
|
237
255
|
WHEN cost.status = 'rated' THEN 'rated'
|
|
@@ -256,11 +274,11 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
256
274
|
AND (? IS NULL OR usage.provider_id = ?)
|
|
257
275
|
AND (? IS NULL OR usage.requested_model_id = ?)
|
|
258
276
|
), expanded AS (
|
|
259
|
-
SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS bucket_start,
|
|
277
|
+
SELECT 'totals' AS dimension, NULL AS key_1, NULL AS key_2, NULL AS key_3, NULL AS bucket_start,
|
|
260
278
|
classified.*
|
|
261
279
|
FROM classified
|
|
262
280
|
UNION ALL
|
|
263
|
-
SELECT 'series', NULL, NULL,
|
|
281
|
+
SELECT 'series', NULL, NULL, NULL,
|
|
264
282
|
CASE ?
|
|
265
283
|
WHEN 'hour' THEN strftime('%Y-%m-%dT%H:00:00.000Z', occurred_at)
|
|
266
284
|
WHEN 'day' THEN strftime('%Y-%m-%dT00:00:00.000Z', occurred_at)
|
|
@@ -273,17 +291,18 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
273
291
|
classified.*
|
|
274
292
|
FROM classified
|
|
275
293
|
UNION ALL
|
|
276
|
-
SELECT 'model', provider_id, requested_model_id, NULL, classified.* FROM classified
|
|
294
|
+
SELECT 'model', provider_id, requested_model_id, NULL, NULL, classified.* FROM classified
|
|
277
295
|
UNION ALL
|
|
278
|
-
SELECT 'agent',
|
|
279
|
-
FROM classified WHERE
|
|
296
|
+
SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, classified.*
|
|
297
|
+
FROM classified WHERE attribution_agent_kind IS NOT NULL
|
|
280
298
|
UNION ALL
|
|
281
|
-
SELECT 'workflow', attribution_workflow_id, NULL, NULL, classified.*
|
|
299
|
+
SELECT 'workflow', attribution_workflow_id, NULL, NULL, NULL, classified.*
|
|
282
300
|
FROM classified WHERE attribution_workflow_id IS NOT NULL
|
|
283
301
|
)
|
|
284
302
|
SELECT dimension,
|
|
285
303
|
key_1,
|
|
286
304
|
key_2,
|
|
305
|
+
key_3,
|
|
287
306
|
bucket_start,
|
|
288
307
|
amount_currency,
|
|
289
308
|
CAST(COUNT(*) AS TEXT) AS model_call_count,
|
|
@@ -313,7 +332,7 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
|
|
|
313
332
|
CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
|
|
314
333
|
CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low
|
|
315
334
|
FROM expanded
|
|
316
|
-
GROUP BY dimension, key_1, key_2, bucket_start, amount_currency
|
|
335
|
+
GROUP BY dimension, key_1, key_2, key_3, bucket_start, amount_currency
|
|
317
336
|
`
|
|
318
337
|
|
|
319
338
|
function accountingListFilterParameters(
|
|
@@ -355,6 +374,7 @@ interface UsageRow {
|
|
|
355
374
|
readonly call_id: string
|
|
356
375
|
readonly provider_id: string
|
|
357
376
|
readonly requested_model_id: string
|
|
377
|
+
readonly requested_reasoning: string | null
|
|
358
378
|
readonly response_model_id: string | null
|
|
359
379
|
readonly response_id: string
|
|
360
380
|
readonly input_tokens: number | null
|
|
@@ -367,15 +387,17 @@ interface UsageRow {
|
|
|
367
387
|
readonly reasoning_output_tokens: number | null
|
|
368
388
|
readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
|
|
369
389
|
readonly raw_usage: string | null
|
|
390
|
+
readonly provider_ids: string | null
|
|
370
391
|
readonly occurred_at: string
|
|
371
392
|
readonly recorded_at: string
|
|
372
393
|
}
|
|
373
394
|
|
|
374
395
|
interface JoinedRow extends UsageRow {
|
|
375
|
-
readonly attribution_kind: "agent" | "workflowAgent" | null
|
|
376
|
-
readonly
|
|
396
|
+
readonly attribution_kind: "agent" | "subagent" | "workflowAgent" | null
|
|
397
|
+
readonly attribution_agent_step_id: string | null
|
|
377
398
|
readonly attribution_agent_run_id: string | null
|
|
378
399
|
readonly attribution_thread_id: string | null
|
|
400
|
+
readonly attribution_parent_run_id: string | null
|
|
379
401
|
readonly attribution_node_run_id: string | null
|
|
380
402
|
readonly attribution_workflow_id: string | null
|
|
381
403
|
readonly attribution_workflow_run_id: string | null
|
|
@@ -394,6 +416,7 @@ interface AggregateRow {
|
|
|
394
416
|
readonly dimension: AiAccountingAggregateFragment["dimension"]
|
|
395
417
|
readonly key_1: string | null
|
|
396
418
|
readonly key_2: string | null
|
|
419
|
+
readonly key_3: string | null
|
|
397
420
|
readonly bucket_start: string | null
|
|
398
421
|
readonly amount_currency: string | null
|
|
399
422
|
readonly model_call_count: string
|
|
@@ -436,7 +459,12 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
|
|
|
436
459
|
row.dimension === "model"
|
|
437
460
|
? { providerId: required(row.key_1), modelId: required(row.key_2) }
|
|
438
461
|
: row.dimension === "agent"
|
|
439
|
-
? {
|
|
462
|
+
? {
|
|
463
|
+
agentKind: required(row.key_1) as "agent" | "workflowAgent",
|
|
464
|
+
...(row.key_1 === "workflowAgent"
|
|
465
|
+
? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
|
|
466
|
+
: {}),
|
|
467
|
+
}
|
|
440
468
|
: row.dimension === "workflow"
|
|
441
469
|
? { workflowId: required(row.key_1) }
|
|
442
470
|
: row.dimension === "series"
|
|
@@ -510,17 +538,22 @@ function accountingItemFromRow(row: JoinedRow): AiAccountingRecordSetItem {
|
|
|
510
538
|
: row.attribution_kind === "agent"
|
|
511
539
|
? {
|
|
512
540
|
kind: "agent" as const,
|
|
513
|
-
agentId: required(row.attribution_agent_id),
|
|
514
541
|
agentRunId: required(row.attribution_agent_run_id),
|
|
515
542
|
threadId: required(row.attribution_thread_id),
|
|
516
543
|
}
|
|
517
|
-
:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
544
|
+
: row.attribution_kind === "subagent"
|
|
545
|
+
? {
|
|
546
|
+
kind: "subagent" as const,
|
|
547
|
+
subagentRunId: required(row.attribution_agent_run_id),
|
|
548
|
+
parentRunId: required(row.attribution_parent_run_id),
|
|
549
|
+
}
|
|
550
|
+
: {
|
|
551
|
+
kind: "workflowAgent" as const,
|
|
552
|
+
agentStepId: required(row.attribution_agent_step_id),
|
|
553
|
+
nodeRunId: required(row.attribution_node_run_id),
|
|
554
|
+
workflowId: required(row.attribution_workflow_id),
|
|
555
|
+
workflowRunId: required(row.attribution_workflow_run_id),
|
|
556
|
+
}
|
|
524
557
|
return {
|
|
525
558
|
usage,
|
|
526
559
|
...(attribution ? { attribution } : {}),
|
|
@@ -537,7 +570,13 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
|
|
|
537
570
|
callId: row.call_id,
|
|
538
571
|
requesterGroupIds,
|
|
539
572
|
providerId: row.provider_id,
|
|
573
|
+
...(row.provider_ids === null
|
|
574
|
+
? {}
|
|
575
|
+
: { providerIds: normalizeModelProviderIds(JSON.parse(row.provider_ids)) }),
|
|
540
576
|
requestedModelId: row.requested_model_id,
|
|
577
|
+
...(row.requested_reasoning === null
|
|
578
|
+
? {}
|
|
579
|
+
: { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
|
|
541
580
|
...(row.response_model_id === null ? {} : { responseModelId: row.response_model_id }),
|
|
542
581
|
responseId: row.response_id,
|
|
543
582
|
usage: {
|
|
@@ -610,14 +649,15 @@ function costRecord(input: {
|
|
|
610
649
|
ratedAt: string
|
|
611
650
|
}): AiModelCallCostRecord {
|
|
612
651
|
const details = parseAiModelCallCostDetails(input.details)
|
|
652
|
+
const priceSource =
|
|
653
|
+
details.priceSource === undefined
|
|
654
|
+
? undefined
|
|
655
|
+
: { ...details.priceSource, observedAt: new Date(details.priceSource.observedAt) }
|
|
613
656
|
const base = {
|
|
657
|
+
...(details.estimate === undefined ? {} : { estimate: details.estimate }),
|
|
614
658
|
projectId: input.projectId,
|
|
615
659
|
usageRecordId: input.usageId,
|
|
616
660
|
pricingContext: details.pricingContext,
|
|
617
|
-
priceSource: {
|
|
618
|
-
...details.priceSource,
|
|
619
|
-
observedAt: new Date(details.priceSource.observedAt),
|
|
620
|
-
},
|
|
621
661
|
ratedAt: new Date(input.ratedAt),
|
|
622
662
|
}
|
|
623
663
|
if (input.status === "rated") {
|
|
@@ -625,6 +665,7 @@ function costRecord(input: {
|
|
|
625
665
|
...base,
|
|
626
666
|
status: "rated",
|
|
627
667
|
billingIdentity: { providerId: required(input.providerId), modelId: required(input.modelId) },
|
|
668
|
+
priceSource: required(priceSource),
|
|
628
669
|
money: { currency: required(input.currency), amountNanos: required(input.amountNanos) },
|
|
629
670
|
components: required(details.components),
|
|
630
671
|
})
|
|
@@ -635,6 +676,7 @@ function costRecord(input: {
|
|
|
635
676
|
...(input.providerId && input.modelId
|
|
636
677
|
? { billingIdentity: { providerId: input.providerId, modelId: input.modelId } }
|
|
637
678
|
: {}),
|
|
679
|
+
...(priceSource === undefined ? {} : { priceSource }),
|
|
638
680
|
reason: required(input.reason) as Extract<
|
|
639
681
|
AiModelCallCostRecord,
|
|
640
682
|
{ status: "unpriceable" }
|