@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.
Files changed (91) hide show
  1. package/dist/agents/rows.d.ts +7 -4
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/ai-cost-storage.d.ts +2 -1
  9. package/dist/ai-cost-storage.d.ts.map +1 -1
  10. package/dist/ai-limit-storage.d.ts +35 -0
  11. package/dist/ai-limit-storage.d.ts.map +1 -0
  12. package/dist/ai-usage-storage.d.ts +1 -0
  13. package/dist/ai-usage-storage.d.ts.map +1 -1
  14. package/dist/auth-storage/access-tokens.d.ts +1 -1
  15. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  16. package/dist/auth-storage/auth-storage.d.ts +4 -1
  17. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  18. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  19. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  20. package/dist/auth-storage/rows.d.ts +19 -1
  21. package/dist/auth-storage/rows.d.ts.map +1 -1
  22. package/dist/auth-storage/shared.d.ts +1 -1
  23. package/dist/auth-storage/shared.d.ts.map +1 -1
  24. package/dist/execution-storage.d.ts +3 -2
  25. package/dist/execution-storage.d.ts.map +1 -1
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3443 -789
  29. package/dist/index.js.map +29 -20
  30. package/dist/migrations.d.ts.map +1 -1
  31. package/dist/objects/index.d.ts +3 -0
  32. package/dist/objects/index.d.ts.map +1 -0
  33. package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
  34. package/dist/objects/query-compiler.d.ts.map +1 -0
  35. package/dist/objects/read-scope.d.ts +17 -0
  36. package/dist/objects/read-scope.d.ts.map +1 -0
  37. package/dist/objects/reader.d.ts +51 -0
  38. package/dist/objects/reader.d.ts.map +1 -0
  39. package/dist/objects/rows.d.ts +33 -0
  40. package/dist/objects/rows.d.ts.map +1 -0
  41. package/dist/objects/selected-reader.d.ts +9 -0
  42. package/dist/objects/selected-reader.d.ts.map +1 -0
  43. package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
  44. package/dist/objects/storage.d.ts.map +1 -0
  45. package/dist/share-grant-storage.d.ts +20 -0
  46. package/dist/share-grant-storage.d.ts.map +1 -0
  47. package/dist/share-session-storage.d.ts +19 -0
  48. package/dist/share-session-storage.d.ts.map +1 -0
  49. package/dist/storage-errors.d.ts +1 -0
  50. package/dist/storage-errors.d.ts.map +1 -1
  51. package/dist/transactions.d.ts +8 -0
  52. package/dist/transactions.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/agents/rows.ts +36 -9
  55. package/src/agents/runs.ts +152 -13
  56. package/src/agents/threads.ts +2 -17
  57. package/src/ai-cost-groups.ts +100 -0
  58. package/src/ai-cost-storage.ts +67 -25
  59. package/src/ai-limit-storage.ts +981 -0
  60. package/src/ai-usage-storage.ts +24 -1
  61. package/src/auth-storage/access-tokens.ts +77 -68
  62. package/src/auth-storage/auth-storage.ts +80 -1
  63. package/src/auth-storage/device-authorizations.ts +153 -0
  64. package/src/auth-storage/rows.ts +41 -0
  65. package/src/auth-storage/shared.ts +1 -0
  66. package/src/execution-storage.ts +22 -3
  67. package/src/index.ts +24 -3
  68. package/src/migrations/029-model-accounting.sql +79 -0
  69. package/src/migrations/030-ai-usage-limits.sql +100 -0
  70. package/src/migrations/031-subagent-runs.sql +231 -0
  71. package/src/migrations/032-conversation-run-spec.sql +78 -0
  72. package/src/migrations/033-retire-agent-definitions.sql +89 -0
  73. package/src/migrations/034-device-authorizations.sql +22 -0
  74. package/src/migrations/035-share-grants.sql +66 -0
  75. package/src/migrations/036-share-sessions.sql +226 -0
  76. package/src/migrations.ts +22 -0
  77. package/src/objects/index.ts +2 -0
  78. package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
  79. package/src/objects/read-scope.ts +316 -0
  80. package/src/objects/reader.ts +526 -0
  81. package/src/objects/rows.ts +123 -0
  82. package/src/objects/selected-reader.ts +119 -0
  83. package/src/objects/storage.ts +296 -0
  84. package/src/share-grant-storage.ts +252 -0
  85. package/src/share-session-storage.ts +189 -0
  86. package/src/storage-errors.ts +4 -0
  87. package/src/transactions.ts +32 -0
  88. package/src/workflow-run-storage.ts +8 -8
  89. package/dist/object-query-compiler.d.ts.map +0 -1
  90. package/dist/object-storage.d.ts.map +0 -1
  91. package/src/object-storage.ts +0 -709
@@ -1,5 +1,11 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import { assertAgentRunExecution } from "@sixb/core/internal/agent-run-storage-provider"
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<AgentRunRecord> {
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
- return this.requireRun(input.projectId, input.id)
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.agentId) {
320
- whereClauses.push("agent_id = ?")
321
- args.push(input.agentId)
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
  `
@@ -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 (?, ?, ?, ?, ?, ?, ?, NULL, NULL, 0, ?, ?)
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
+ `
@@ -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.id IS NOT NULL THEN '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
- COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
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
- COALESCE(direct_agent.agent_id, workflow_agent.agent_id) AS attribution_agent_id,
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', attribution_agent_id, NULL, NULL, classified.*
279
- FROM classified WHERE attribution_agent_id IS NOT NULL
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 attribution_agent_id: string | null
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
- ? { agentId: required(row.key_1) }
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
- kind: "workflowAgent" as const,
519
- agentId: required(row.attribution_agent_id),
520
- nodeRunId: required(row.attribution_node_run_id),
521
- workflowId: required(row.attribution_workflow_id),
522
- workflowRunId: required(row.attribution_workflow_run_id),
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" }