@sixb/sqlite 0.1.21 → 0.1.23

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 (100) hide show
  1. package/dist/agents/agent-storage.d.ts +8 -2
  2. package/dist/agents/agent-storage.d.ts.map +1 -1
  3. package/dist/agents/approvals.d.ts +51 -0
  4. package/dist/agents/approvals.d.ts.map +1 -0
  5. package/dist/agents/checkpoints.d.ts +1 -0
  6. package/dist/agents/checkpoints.d.ts.map +1 -1
  7. package/dist/agents/index.d.ts +1 -0
  8. package/dist/agents/index.d.ts.map +1 -1
  9. package/dist/agents/memory.d.ts +21 -0
  10. package/dist/agents/memory.d.ts.map +1 -0
  11. package/dist/agents/messages.d.ts.map +1 -1
  12. package/dist/agents/rows.d.ts +15 -1
  13. package/dist/agents/rows.d.ts.map +1 -1
  14. package/dist/agents/runs.d.ts +14 -1
  15. package/dist/agents/runs.d.ts.map +1 -1
  16. package/dist/agents/settings.d.ts +29 -0
  17. package/dist/agents/settings.d.ts.map +1 -0
  18. package/dist/agents/threads.d.ts +12 -1
  19. package/dist/agents/threads.d.ts.map +1 -1
  20. package/dist/ai-cost-groups.d.ts.map +1 -1
  21. package/dist/ai-cost-storage.d.ts.map +1 -1
  22. package/dist/ai-usage-storage.d.ts +4 -1
  23. package/dist/ai-usage-storage.d.ts.map +1 -1
  24. package/dist/artifact-storage.d.ts +10 -1
  25. package/dist/artifact-storage.d.ts.map +1 -1
  26. package/dist/auth-storage/auth-storage.d.ts +3 -2
  27. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  28. package/dist/auth-storage/users.d.ts +7 -3
  29. package/dist/auth-storage/users.d.ts.map +1 -1
  30. package/dist/execution-storage.d.ts +2 -1
  31. package/dist/execution-storage.d.ts.map +1 -1
  32. package/dist/index.js +3348 -1515
  33. package/dist/index.js.map +32 -26
  34. package/dist/migrations.d.ts +0 -1
  35. package/dist/migrations.d.ts.map +1 -1
  36. package/dist/objects/conditional-reader.d.ts +8 -0
  37. package/dist/objects/conditional-reader.d.ts.map +1 -0
  38. package/dist/objects/query-compiler.d.ts +26 -1
  39. package/dist/objects/query-compiler.d.ts.map +1 -1
  40. package/dist/objects/read-scope.d.ts +10 -1
  41. package/dist/objects/read-scope.d.ts.map +1 -1
  42. package/dist/objects/reader.d.ts +2 -0
  43. package/dist/objects/reader.d.ts.map +1 -1
  44. package/dist/objects/scoped-reader.d.ts +15 -0
  45. package/dist/objects/scoped-reader.d.ts.map +1 -0
  46. package/dist/objects/selected-reader.d.ts.map +1 -1
  47. package/dist/objects/storage.d.ts +6 -1
  48. package/dist/objects/storage.d.ts.map +1 -1
  49. package/dist/ontology-storage/index.d.ts +2 -0
  50. package/dist/ontology-storage/index.d.ts.map +1 -1
  51. package/dist/ontology-storage/materialization-writer.d.ts +2 -0
  52. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
  53. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  54. package/dist/ontology-storage/unique-values.d.ts +42 -0
  55. package/dist/ontology-storage/unique-values.d.ts.map +1 -0
  56. package/package.json +3 -3
  57. package/src/agents/agent-storage.ts +11 -2
  58. package/src/agents/approvals.ts +456 -0
  59. package/src/agents/checkpoints.ts +9 -4
  60. package/src/agents/index.ts +1 -0
  61. package/src/agents/memory.ts +195 -0
  62. package/src/agents/messages.ts +21 -18
  63. package/src/agents/rows.ts +46 -1
  64. package/src/agents/runs.ts +202 -21
  65. package/src/agents/settings.ts +112 -0
  66. package/src/agents/threads.ts +385 -1
  67. package/src/ai-cost-groups.ts +11 -1
  68. package/src/ai-cost-storage.ts +54 -12
  69. package/src/ai-usage-storage.ts +79 -2
  70. package/src/artifact-storage.ts +206 -9
  71. package/src/auth-storage/auth-storage.ts +58 -26
  72. package/src/auth-storage/users.ts +106 -60
  73. package/src/execution-storage.ts +39 -0
  74. package/src/migrations/067-artifact-kinds.sql +32 -0
  75. package/src/migrations/068-artifact-state.sql +15 -0
  76. package/src/migrations/069-agent-thread-participants.sql +16 -0
  77. package/src/migrations/070-forked-context-checkpoints.sql +45 -0
  78. package/src/migrations/071-unique-values.sql +22 -0
  79. package/src/migrations/072-generated-media-usage.sql +32 -0
  80. package/src/migrations/073-model-call-files.sql +11 -0
  81. package/src/migrations/074-agent-memory.sql +32 -0
  82. package/src/migrations/075-artifact-threads.sql +21 -0
  83. package/src/migrations/076-agent-approvals.sql +59 -0
  84. package/src/migrations/077-agent-approval-resume.sql +20 -0
  85. package/src/migrations/078-subagent-threads.sql +241 -0
  86. package/src/migrations/079-subagent-questions.sql +6 -0
  87. package/src/migrations.ts +75 -1
  88. package/src/objects/README.md +7 -0
  89. package/src/objects/conditional-reader.ts +27 -0
  90. package/src/objects/query-compiler.ts +36 -6
  91. package/src/objects/read-scope.ts +140 -23
  92. package/src/objects/reader.ts +47 -14
  93. package/src/objects/scoped-reader.ts +103 -0
  94. package/src/objects/selected-reader.ts +12 -75
  95. package/src/objects/storage.ts +16 -0
  96. package/src/ontology-storage/index.ts +3 -0
  97. package/src/ontology-storage/materialization-writer.ts +14 -0
  98. package/src/ontology-storage/materializations.ts +16 -1
  99. package/src/ontology-storage/unique-values.ts +187 -0
  100. package/src/transactions.ts +1 -1
@@ -1,24 +1,40 @@
1
1
  import type { Database } from "bun:sqlite"
2
2
  import {
3
+ assertConversationThreadRow,
4
+ participantsToAdd,
5
+ planAgentThreadFork,
6
+ remapArtifactIds,
3
7
  snapshotAgentThreadSandboxParams,
4
8
  transitionAgentThreadSandbox,
5
9
  } from "@sixb/core/internal/agent-run-storage-provider"
6
10
  import {
11
+ type AddAgentThreadParticipantsInput,
12
+ type AgentContextCheckpointRecord,
7
13
  AgentStorageError,
14
+ type AgentThreadParticipantRecord,
8
15
  type AgentThreadRecord,
9
16
  type AgentThreadSandboxState,
10
17
  type AgentThreadStore,
11
18
  type CreateAgentThreadInput,
19
+ type ForkAgentThreadInput,
20
+ type ListAgentThreadChildrenInput,
21
+ type ListAgentThreadParticipantsInput,
12
22
  type ListAgentThreadsInput,
13
23
  type ListAgentThreadsResult,
24
+ type RemoveAgentThreadParticipantInput,
14
25
  type TransitionAgentThreadSandboxInput,
15
26
  } from "@sixb/core/storage"
16
27
  import type { SqliteValue } from "../run-list-query"
17
28
  import { isUniqueConstraintError } from "../storage-errors"
18
29
  import {
30
+ type AgentContextCheckpointRow,
31
+ type AgentMessageRow,
19
32
  type AgentRunRow,
33
+ type AgentThreadParticipantRow,
20
34
  type AgentThreadRow,
21
35
  queryAgentList,
36
+ rowToContextCheckpointRecord,
37
+ rowToParticipantRecord,
22
38
  rowToRunRecord,
23
39
  rowToThreadRecord,
24
40
  } from "./rows"
@@ -100,6 +116,130 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
100
116
  return this.requireThread(input.projectId, input.id)
101
117
  }
102
118
 
119
+ async fork(input: ForkAgentThreadInput): Promise<AgentThreadRecord> {
120
+ return this.db
121
+ .transaction(() => {
122
+ const source = this.db
123
+ .query(
124
+ "SELECT id, kind, active_run_id FROM agent_threads WHERE project_id = ? AND id = ?"
125
+ )
126
+ .get(input.projectId, input.sourceThreadId) as Pick<
127
+ AgentThreadRow,
128
+ "id" | "kind" | "active_run_id"
129
+ > | null
130
+
131
+ if (!source) {
132
+ throw new AgentStorageError(
133
+ "thread_not_found",
134
+ `[SixbSqlite] Agent thread '${input.sourceThreadId}' not found for project '${input.projectId}'.`
135
+ )
136
+ }
137
+ assertConversationThreadRow(source, "can be forked", "SixbSqlite")
138
+
139
+ const activeRun = source.active_run_id === null ? null : this.findActiveRun(input, source)
140
+
141
+ // Only what the plan needs: the parts are copied by the database, not through here. A
142
+ // message of approval outcomes only is not counted (`countsAsThreadMessage`).
143
+ const messages = (
144
+ this.db
145
+ .query(
146
+ `
147
+ SELECT id, seq,
148
+ NOT (
149
+ json_array_length(parts) > 0 AND NOT EXISTS (
150
+ SELECT 1 FROM json_each(agent_messages.parts)
151
+ WHERE json_extract(value, '$.type') <> 'approval-outcome'
152
+ )
153
+ ) AS counted
154
+ FROM agent_messages WHERE project_id = ? AND thread_id = ? ORDER BY seq ASC
155
+ `
156
+ )
157
+ .all(input.projectId, source.id) as (Pick<AgentMessageRow, "id" | "seq"> & {
158
+ counted: number
159
+ })[]
160
+ ).map((row) => ({ id: row.id, seq: row.seq, counted: row.counted === 1 }))
161
+
162
+ const checkpoints = this.db
163
+ .query(
164
+ `
165
+ SELECT * FROM agent_context_checkpoints
166
+ WHERE project_id = ? AND thread_id = ?
167
+ ORDER BY summarized_through_seq DESC, created_at DESC, id DESC
168
+ `
169
+ )
170
+ .all(input.projectId, source.id) as AgentContextCheckpointRow[]
171
+
172
+ const plan = planAgentThreadFork(
173
+ input,
174
+ {
175
+ ...(activeRun?.trigger_message_id
176
+ ? { triggerMessageId: activeRun.trigger_message_id }
177
+ : {}),
178
+ messages,
179
+ checkpoints: checkpoints.map(rowToContextCheckpointRecord),
180
+ },
181
+ "SixbSqlite"
182
+ )
183
+
184
+ this.insertForkedThread(plan.thread)
185
+
186
+ this.db
187
+ .query(
188
+ `
189
+ INSERT INTO agent_messages (
190
+ project_id,
191
+ id,
192
+ thread_id,
193
+ run_id,
194
+ role,
195
+ author_principal_type,
196
+ author_principal_id,
197
+ seq,
198
+ parts,
199
+ metadata,
200
+ content_version,
201
+ created_at,
202
+ completed_at
203
+ )
204
+ SELECT
205
+ message.project_id,
206
+ json_extract(copy.value, '$[1]'),
207
+ ?,
208
+ message.run_id,
209
+ message.role,
210
+ message.author_principal_type,
211
+ message.author_principal_id,
212
+ message.seq,
213
+ message.parts,
214
+ message.metadata,
215
+ message.content_version,
216
+ message.created_at,
217
+ message.completed_at
218
+ FROM json_each(?) AS copy
219
+ JOIN agent_messages AS message
220
+ ON message.project_id = ? AND message.id = json_extract(copy.value, '$[0]')
221
+ `
222
+ )
223
+ .run(
224
+ plan.thread.id,
225
+ JSON.stringify(plan.messages.map((copy) => [copy.sourceId, copy.id])),
226
+ input.projectId
227
+ )
228
+ this.remapCopiedParts(
229
+ input.projectId,
230
+ plan.messages.map((copy) => copy.id),
231
+ input.artifactCopies
232
+ )
233
+
234
+ if (plan.checkpoint) {
235
+ this.insertCopiedCheckpoint(plan.checkpoint)
236
+ }
237
+
238
+ return this.requireThread(input.projectId, input.id)
239
+ })
240
+ .immediate()
241
+ }
242
+
103
243
  async getById(params: { projectId: string; id: string }): Promise<AgentThreadRecord | null> {
104
244
  const row = this.db
105
245
  .query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?")
@@ -135,12 +275,36 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
135
275
  })
136
276
  }
137
277
 
278
+ async listChildren(input: ListAgentThreadChildrenInput): Promise<readonly AgentThreadRecord[]> {
279
+ // By `idx_agent_threads_children`, which holds sub-agents' threads only.
280
+ const turn = input.parentRunId === undefined ? "" : "AND parent_run_id = ?"
281
+ const rows = this.db
282
+ .query(
283
+ `
284
+ SELECT *
285
+ FROM agent_threads
286
+ WHERE project_id = ? AND parent_thread_id = ? AND kind = 'subagent' ${turn}
287
+ ORDER BY created_at ASC, id ASC
288
+ LIMIT ? OFFSET ?
289
+ `
290
+ )
291
+ .all(
292
+ input.projectId,
293
+ input.parentThreadId,
294
+ ...(input.parentRunId === undefined ? [] : [input.parentRunId]),
295
+ input.limit ?? 100,
296
+ input.offset ?? 0
297
+ ) as AgentThreadRow[]
298
+ return rows.map(rowToThreadRecord)
299
+ }
300
+
138
301
  async list(input: ListAgentThreadsInput): Promise<ListAgentThreadsResult> {
139
302
  if (input.statuses !== undefined && input.statuses.length === 0) {
140
303
  return { threads: [], hasMore: false, total: 0 }
141
304
  }
142
305
 
143
- const whereClauses = ["project_id = ?"]
306
+ // A sub-agent's thread is read through its conversation; the list indexes leave it out too.
307
+ const whereClauses = ["project_id = ?", "kind = 'conversation'"]
144
308
  const args: SqliteValue[] = [input.projectId]
145
309
 
146
310
  if (input.statuses) {
@@ -153,6 +317,17 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
153
317
  args.push(input.ownerPrincipal.type, input.ownerPrincipal.id)
154
318
  }
155
319
 
320
+ if (input.participantPrincipal) {
321
+ whereClauses.push(`EXISTS (
322
+ SELECT 1 FROM agent_thread_participants participant
323
+ WHERE participant.project_id = agent_threads.project_id
324
+ AND participant.thread_id = agent_threads.id
325
+ AND participant.principal_type = ?
326
+ AND participant.principal_id = ?
327
+ )`)
328
+ args.push(input.participantPrincipal.type, input.participantPrincipal.id)
329
+ }
330
+
156
331
  const dir = input.order === "asc" ? "ASC" : "DESC"
157
332
  const { rows, total, hasMore } = queryAgentList<AgentThreadRow>({
158
333
  db: this.db,
@@ -167,6 +342,215 @@ export class SqliteAgentThreadStore implements AgentThreadStore {
167
342
  return { threads: rows.map(rowToThreadRecord), hasMore, total }
168
343
  }
169
344
 
345
+ async addParticipants(
346
+ input: AddAgentThreadParticipantsInput
347
+ ): Promise<readonly AgentThreadParticipantRecord[]> {
348
+ return this.db
349
+ .transaction(() => {
350
+ const thread = this.db
351
+ .query("SELECT id, kind FROM agent_threads WHERE project_id = ? AND id = ?")
352
+ .get(input.projectId, input.threadId) as Pick<AgentThreadRow, "id" | "kind"> | null
353
+ if (!thread) {
354
+ throw new AgentStorageError(
355
+ "thread_not_found",
356
+ `[SixbSqlite] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
357
+ )
358
+ }
359
+ assertConversationThreadRow(thread, "has participants", "SixbSqlite")
360
+
361
+ const existing = this.selectParticipants(input.projectId, [input.threadId])
362
+ const added = participantsToAdd(input, existing, "SixbSqlite")
363
+ const addedAt = (input.addedAt ?? new Date()).toISOString()
364
+
365
+ const insert = this.db.query(`
366
+ INSERT INTO agent_thread_participants (
367
+ project_id,
368
+ thread_id,
369
+ principal_type,
370
+ principal_id,
371
+ added_at,
372
+ added_by_type,
373
+ added_by_id
374
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
375
+ `)
376
+ for (const principal of added) {
377
+ insert.run(
378
+ input.projectId,
379
+ input.threadId,
380
+ principal.type,
381
+ principal.id,
382
+ addedAt,
383
+ input.addedBy.type,
384
+ input.addedBy.id
385
+ )
386
+ }
387
+
388
+ return added.length === 0
389
+ ? existing
390
+ : this.selectParticipants(input.projectId, [input.threadId])
391
+ })
392
+ .immediate()
393
+ }
394
+
395
+ async removeParticipant(input: RemoveAgentThreadParticipantInput): Promise<boolean> {
396
+ const result = this.db
397
+ .query(`
398
+ DELETE FROM agent_thread_participants
399
+ WHERE project_id = ? AND thread_id = ? AND principal_type = ? AND principal_id = ?
400
+ `)
401
+ .run(input.projectId, input.threadId, input.principal.type, input.principal.id)
402
+
403
+ return result.changes > 0
404
+ }
405
+
406
+ async listParticipants(
407
+ input: ListAgentThreadParticipantsInput
408
+ ): Promise<readonly AgentThreadParticipantRecord[]> {
409
+ return this.selectParticipants(input.projectId, [...new Set(input.threadIds)])
410
+ }
411
+
412
+ private selectParticipants(
413
+ projectId: string,
414
+ threadIds: readonly string[]
415
+ ): AgentThreadParticipantRecord[] {
416
+ if (threadIds.length === 0) {
417
+ return []
418
+ }
419
+
420
+ const rows = this.db
421
+ .query(`
422
+ SELECT *
423
+ FROM agent_thread_participants
424
+ WHERE project_id = ?
425
+ AND thread_id IN (SELECT value FROM json_each(?))
426
+ ORDER BY added_at, principal_id, thread_id
427
+ `)
428
+ .all(projectId, JSON.stringify(threadIds)) as AgentThreadParticipantRow[]
429
+
430
+ return rows.map(rowToParticipantRecord)
431
+ }
432
+
433
+ private findActiveRun(
434
+ input: ForkAgentThreadInput,
435
+ source: Pick<AgentThreadRow, "active_run_id">
436
+ ): Pick<AgentRunRow, "trigger_message_id"> | null {
437
+ return this.db
438
+ .query("SELECT trigger_message_id FROM agent_runs WHERE project_id = ? AND id = ?")
439
+ .get(input.projectId, source.active_run_id) as Pick<AgentRunRow, "trigger_message_id"> | null
440
+ }
441
+
442
+ private insertForkedThread(thread: AgentThreadRecord): void {
443
+ try {
444
+ this.db
445
+ .query(
446
+ `
447
+ INSERT INTO agent_threads (
448
+ project_id,
449
+ id,
450
+ owner_principal_type,
451
+ owner_principal_id,
452
+ title,
453
+ sandbox_params,
454
+ sandbox_state,
455
+ status,
456
+ active_run_id,
457
+ last_message_at,
458
+ message_count,
459
+ created_at,
460
+ updated_at
461
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?)
462
+ `
463
+ )
464
+ .run(
465
+ thread.projectId,
466
+ thread.id,
467
+ thread.ownerPrincipal.type,
468
+ thread.ownerPrincipal.id,
469
+ thread.title ?? null,
470
+ thread.sandboxParams === undefined ? null : JSON.stringify(thread.sandboxParams),
471
+ thread.sandboxState === undefined ? null : JSON.stringify(thread.sandboxState),
472
+ thread.status,
473
+ thread.lastMessageAt?.toISOString() ?? null,
474
+ thread.messageCount,
475
+ thread.createdAt.toISOString(),
476
+ thread.updatedAt.toISOString()
477
+ )
478
+ } catch (error) {
479
+ if (isUniqueConstraintError(error)) {
480
+ throw new AgentStorageError(
481
+ "duplicate_id",
482
+ `[SixbSqlite] Agent thread '${thread.id}' already exists for project '${thread.projectId}'.`
483
+ )
484
+ }
485
+
486
+ throw error
487
+ }
488
+ }
489
+
490
+ private insertCopiedCheckpoint(checkpoint: AgentContextCheckpointRecord): void {
491
+ this.db
492
+ .query(
493
+ `
494
+ INSERT INTO agent_context_checkpoints (
495
+ project_id,
496
+ id,
497
+ thread_id,
498
+ created_by_run_id,
499
+ reason,
500
+ summary,
501
+ summary_format_version,
502
+ summarized_through_seq,
503
+ observed_head_seq,
504
+ estimated_input_tokens_before,
505
+ estimated_input_tokens_after,
506
+ summary_model_id,
507
+ created_at
508
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
509
+ `
510
+ )
511
+ .run(
512
+ checkpoint.projectId,
513
+ checkpoint.id,
514
+ checkpoint.threadId,
515
+ checkpoint.createdByRunId,
516
+ checkpoint.reason,
517
+ checkpoint.summary,
518
+ checkpoint.summaryFormatVersion,
519
+ checkpoint.summarizedThroughSeq,
520
+ checkpoint.observedHeadSeq,
521
+ checkpoint.estimatedInputTokensBefore,
522
+ checkpoint.estimatedInputTokensAfter,
523
+ checkpoint.summaryModelId,
524
+ checkpoint.createdAt.toISOString()
525
+ )
526
+ }
527
+
528
+ /** The copied messages name the fork's copies of the source's artifacts. */
529
+ private remapCopiedParts(
530
+ projectId: string,
531
+ messageIds: readonly string[],
532
+ copies: Readonly<Record<string, string>> | undefined
533
+ ): void {
534
+ if (!copies || Object.keys(copies).length === 0 || messageIds.length === 0) return
535
+
536
+ const rows = this.db
537
+ .query(
538
+ `SELECT id, parts FROM agent_messages
539
+ WHERE project_id = ? AND id IN (SELECT value FROM json_each(?))`
540
+ )
541
+ .all(projectId, JSON.stringify(messageIds)) as {
542
+ readonly id: string
543
+ readonly parts: string
544
+ }[]
545
+ const update = this.db.query(
546
+ "UPDATE agent_messages SET parts = ? WHERE project_id = ? AND id = ?"
547
+ )
548
+ for (const row of rows) {
549
+ const parts = remapArtifactIds(row.parts, copies)
550
+ if (parts !== row.parts) update.run(parts, projectId, row.id)
551
+ }
552
+ }
553
+
170
554
  private requireThread(projectId: string, id: string): AgentThreadRecord {
171
555
  const row = this.db
172
556
  .query("SELECT * FROM agent_threads WHERE project_id = ? AND id = ?")
@@ -3,6 +3,7 @@ import {
3
3
  aiModelCallGroupFragmentFromRow,
4
4
  buildAiModelCallGroupsFromFragments,
5
5
  normalizeAiModelCallAccountingQuery,
6
+ TOKENLESS_MODEL_KINDS,
6
7
  } from "@sixb/core/internal/ai-cost-storage-provider"
7
8
  import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
8
9
  import type { SqliteStoreConnection } from "./transactions"
@@ -32,11 +33,15 @@ export async function listAiModelCallGroups(
32
33
  return { items, total, hasMore: query.offset + items.length < total }
33
34
  }
34
35
 
36
+ // Fixed kind names, never user input.
37
+ const TOKENLESS_KINDS = TOKENLESS_MODEL_KINDS.map((kind) => `'${kind}'`).join(", ")
38
+
35
39
  const FILTERED_SQL = `
36
40
  WITH filtered AS (
37
41
  SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
38
42
  usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
39
- usage.total_tokens,
43
+ usage.total_tokens, usage.model_kind, usage.output_images, usage.input_characters,
44
+ usage.audio_duration_ms,
40
45
  execution.executor_kind, execution.executor_id, execution.authority_primitive_id AS primitive_id,
41
46
  COALESCE(cost.status, 'unvalued') AS valuation_status,
42
47
  cost.currency AS amount_currency, cost.amount_nanos,
@@ -82,7 +87,12 @@ SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id
82
87
  MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
83
88
  CAST(COUNT(*) AS TEXT) AS call_count,
84
89
  CAST(COUNT(total_tokens) AS TEXT) AS token_count,
90
+ CAST(COUNT(CASE WHEN total_tokens IS NULL AND model_kind IN (${TOKENLESS_KINDS}) THEN 1 END) AS TEXT)
91
+ AS tokenless_count,
85
92
  CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
93
+ CAST(SUM(output_images) AS TEXT) AS output_images,
94
+ CAST(SUM(input_characters) AS TEXT) AS input_characters,
95
+ CAST(SUM(audio_duration_ms) AS TEXT) AS audio_duration_ms,
86
96
  valuation_status, amount_currency,
87
97
  CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
88
98
  CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type AiAccountingAggregateFragment,
3
3
  type AiAccountingRecordSetItem,
4
+ aiAccountingModelKind,
4
5
  aiModelCallCostDetails,
5
6
  aiModelCallCostMatchesUsage,
6
7
  buildAiAccountingOverviewFromFragments,
@@ -238,6 +239,7 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
238
239
  WITH classified AS (
239
240
  SELECT usage.provider_id,
240
241
  usage.requested_model_id,
242
+ usage.model_kind,
241
243
  usage.occurred_at,
242
244
  usage.input_tokens,
243
245
  usage.output_tokens,
@@ -247,6 +249,10 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
247
249
  usage.text_output_tokens,
248
250
  usage.reasoning_output_tokens,
249
251
  usage.audio_duration_ms,
252
+ usage.image_output_tokens,
253
+ usage.audio_output_tokens,
254
+ usage.output_images,
255
+ usage.input_characters,
250
256
  usage.reporting_status,
251
257
  CASE
252
258
  WHEN direct_agent.kind = 'conversation' THEN 'agent'
@@ -300,6 +306,8 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
300
306
  UNION ALL
301
307
  SELECT 'model', provider_id, requested_model_id, NULL, NULL, classified.* FROM classified
302
308
  UNION ALL
309
+ SELECT 'kind', model_kind, NULL, NULL, NULL, classified.* FROM classified
310
+ UNION ALL
303
311
  SELECT 'agent', attribution_agent_kind, attribution_workflow_id, attribution_agent_step_id, NULL, classified.*
304
312
  FROM classified WHERE attribution_agent_kind IS NOT NULL
305
313
  UNION ALL
@@ -332,7 +340,15 @@ const SQLITE_ACCOUNTING_OVERVIEW_SQL = `
332
340
  CAST(COUNT(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_count,
333
341
  CAST(SUM(reasoning_output_tokens) AS TEXT) AS reasoning_output_tokens_sum,
334
342
  CAST(COUNT(audio_duration_ms) AS TEXT) AS audio_duration_ms_count,
343
+ CAST(COUNT(image_output_tokens) AS TEXT) AS image_output_tokens_count,
344
+ CAST(COUNT(audio_output_tokens) AS TEXT) AS audio_output_tokens_count,
345
+ CAST(COUNT(output_images) AS TEXT) AS output_images_count,
346
+ CAST(COUNT(input_characters) AS TEXT) AS input_characters_count,
335
347
  CAST(SUM(audio_duration_ms) AS TEXT) AS audio_duration_ms_sum,
348
+ CAST(SUM(image_output_tokens) AS TEXT) AS image_output_tokens_sum,
349
+ CAST(SUM(audio_output_tokens) AS TEXT) AS audio_output_tokens_sum,
350
+ CAST(SUM(output_images) AS TEXT) AS output_images_sum,
351
+ CAST(SUM(input_characters) AS TEXT) AS input_characters_sum,
336
352
  CAST(SUM(CASE WHEN valuation_status = 'rated' THEN 1 ELSE 0 END) AS TEXT)
337
353
  AS rated_call_count,
338
354
  CAST(SUM(CASE WHEN valuation_status = 'unpriceable' THEN 1 ELSE 0 END) AS TEXT)
@@ -398,6 +414,10 @@ interface UsageRow {
398
414
  readonly text_output_tokens: number | null
399
415
  readonly reasoning_output_tokens: number | null
400
416
  readonly audio_duration_ms: number | null
417
+ readonly image_output_tokens: number | null
418
+ readonly audio_output_tokens: number | null
419
+ readonly output_images: number | null
420
+ readonly input_characters: number | null
401
421
  readonly reporting_status: AiModelCallUsageRecord["usage"]["reportingStatus"]
402
422
  readonly raw_usage: string | null
403
423
  readonly provider_ids: string | null
@@ -453,7 +473,15 @@ interface AggregateRow {
453
473
  readonly reasoning_output_tokens_count: string
454
474
  readonly reasoning_output_tokens_sum: string | null
455
475
  readonly audio_duration_ms_count: string
476
+ readonly image_output_tokens_count: string
477
+ readonly audio_output_tokens_count: string
478
+ readonly output_images_count: string
479
+ readonly input_characters_count: string
456
480
  readonly audio_duration_ms_sum: string | null
481
+ readonly image_output_tokens_sum: string | null
482
+ readonly audio_output_tokens_sum: string | null
483
+ readonly output_images_sum: string | null
484
+ readonly input_characters_sum: string | null
457
485
  readonly rated_call_count: string
458
486
  readonly unpriceable_call_count: string
459
487
  readonly unvalued_call_count: string
@@ -477,18 +505,20 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
477
505
  const dimensionKeys =
478
506
  row.dimension === "model"
479
507
  ? { providerId: required(row.key_1), modelId: required(row.key_2) }
480
- : row.dimension === "agent"
481
- ? {
482
- agentKind: required(row.key_1) as "agent" | "workflowAgent",
483
- ...(row.key_1 === "workflowAgent"
484
- ? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
485
- : {}),
486
- }
487
- : row.dimension === "workflow"
488
- ? { workflowId: required(row.key_1) }
489
- : row.dimension === "series"
490
- ? { start: new Date(required(row.bucket_start)) }
491
- : {}
508
+ : row.dimension === "kind"
509
+ ? withModelKind(aiAccountingModelKind(row.key_1))
510
+ : row.dimension === "agent"
511
+ ? {
512
+ agentKind: required(row.key_1) as "agent" | "workflowAgent",
513
+ ...(row.key_1 === "workflowAgent"
514
+ ? { workflowId: required(row.key_2), agentStepId: required(row.key_3) }
515
+ : {}),
516
+ }
517
+ : row.dimension === "workflow"
518
+ ? { workflowId: required(row.key_1) }
519
+ : row.dimension === "series"
520
+ ? { start: new Date(required(row.bucket_start)) }
521
+ : {}
492
522
  return {
493
523
  dimension: row.dimension,
494
524
  ...dimensionKeys,
@@ -516,6 +546,10 @@ function aggregateFragmentFromRow(row: AggregateRow): AiAccountingAggregateFragm
516
546
  row.reasoning_output_tokens_sum
517
547
  ),
518
548
  audioDurationMs: meterFragment(row.audio_duration_ms_count, row.audio_duration_ms_sum),
549
+ imageOutputTokens: meterFragment(row.image_output_tokens_count, row.image_output_tokens_sum),
550
+ audioOutputTokens: meterFragment(row.audio_output_tokens_count, row.audio_output_tokens_sum),
551
+ outputImages: meterFragment(row.output_images_count, row.output_images_sum),
552
+ inputCharacters: meterFragment(row.input_characters_count, row.input_characters_sum),
519
553
  },
520
554
  costs: {
521
555
  ...(row.amount_currency === null ||
@@ -619,6 +653,10 @@ function usageFromRow(row: UsageRow, requesterGroupIds: readonly string[]): AiMo
619
653
  ? {}
620
654
  : { reasoningOutputTokens: row.reasoning_output_tokens }),
621
655
  ...(row.audio_duration_ms === null ? {} : { audioDurationMs: row.audio_duration_ms }),
656
+ ...(row.image_output_tokens === null ? {} : { imageOutputTokens: row.image_output_tokens }),
657
+ ...(row.audio_output_tokens === null ? {} : { audioOutputTokens: row.audio_output_tokens }),
658
+ ...(row.output_images === null ? {} : { outputImages: row.output_images }),
659
+ ...(row.input_characters === null ? {} : { inputCharacters: row.input_characters }),
622
660
  reportingStatus: row.reporting_status,
623
661
  },
624
662
  ...(row.raw_usage === null
@@ -807,3 +845,7 @@ function required<T>(value: T | null | undefined): T {
807
845
  }
808
846
  return value
809
847
  }
848
+
849
+ function withModelKind(modelKind: AiAccountingAggregateFragment["modelKind"]) {
850
+ return modelKind === undefined ? {} : { modelKind }
851
+ }