@dudousxd/nestjs-agent-store-drizzle 0.34.0 → 0.36.0

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/index.cjs CHANGED
@@ -240,7 +240,7 @@ var agentQueuedMessage = (0, import_sqlite_core.sqliteTable)("agent_queued_messa
240
240
  ]);
241
241
  var agentTokenUsage = (0, import_sqlite_core.sqliteTable)("agent_token_usage", {
242
242
  id: (0, import_sqlite_core.text)("id").primaryKey(),
243
- threadId: (0, import_sqlite_core.text)("thread_id").notNull().references(() => agentThread.id, {
243
+ threadId: (0, import_sqlite_core.text)("thread_id").references(() => agentThread.id, {
244
244
  onDelete: "cascade"
245
245
  }),
246
246
  actorRef: (0, import_sqlite_core.text)("actor_ref").notNull(),
@@ -257,13 +257,32 @@ var agentTokenUsage = (0, import_sqlite_core.sqliteTable)("agent_token_usage", {
257
257
  costUsd: (0, import_sqlite_core.real)("cost_usd"),
258
258
  /** Where `cost_usd` came from: `'provider'` (reported) or `'estimate'` (tokens × the price row); null on rows from before it existed (reported). */
259
259
  costSource: (0, import_sqlite_core.text)("cost_source").$type(),
260
+ /** The run the row belongs to. */
261
+ runId: (0, import_sqlite_core.text)("run_id"),
262
+ /** What the spend was for: `agent:<name>` for an agent turn, the app's own otherwise; null → `agent`. */
263
+ feature: (0, import_sqlite_core.text)("feature"),
264
+ provider: (0, import_sqlite_core.text)("provider"),
265
+ tenantRef: (0, import_sqlite_core.text)("tenant_ref"),
266
+ /** Units a row bills by when they are not tokens. */
267
+ audioSeconds: (0, import_sqlite_core.real)("audio_seconds"),
268
+ characters: (0, import_sqlite_core.integer)("characters"),
269
+ requests: (0, import_sqlite_core.integer)("requests"),
270
+ /** The caller's own JSON, never read by the library. */
271
+ metadata: (0, import_sqlite_core.text)("metadata", {
272
+ mode: "json"
273
+ }).$type(),
274
+ /** `false` keeps the row out of the agent's quota windows (`recordUsage({ countsTowardsQuota: false })`). */
275
+ countsTowardsQuota: (0, import_sqlite_core.integer)("counts_towards_quota", {
276
+ mode: "boolean"
277
+ }).notNull().default(true),
260
278
  createdAt: (0, import_sqlite_core.integer)("created_at", {
261
279
  mode: "timestamp_ms"
262
280
  }).notNull()
263
281
  }, (table) => [
264
282
  (0, import_sqlite_core.index)("agent_token_usage_actor_created_idx").on(table.actorRef, table.createdAt),
265
283
  (0, import_sqlite_core.index)("agent_token_usage_created_idx").on(table.createdAt),
266
- (0, import_sqlite_core.index)("agent_token_usage_thread_idx").on(table.threadId)
284
+ (0, import_sqlite_core.index)("agent_token_usage_thread_idx").on(table.threadId),
285
+ (0, import_sqlite_core.index)("agent_token_usage_feature_created_idx").on(table.feature, table.createdAt)
267
286
  ]);
268
287
  var agentGenuiCall = (0, import_sqlite_core.sqliteTable)("agent_genui_call", {
269
288
  id: (0, import_sqlite_core.text)("id").primaryKey(),
@@ -345,7 +364,17 @@ var agentRun = (0, import_sqlite_core.sqliteTable)("agent_run", {
345
364
  * for a detached child, which outlives its parent's turn and so is paired by nothing in the
346
365
  * transcript.
347
366
  */
348
- parentRunId: (0, import_sqlite_core.text)("parent_run_id")
367
+ parentRunId: (0, import_sqlite_core.text)("parent_run_id"),
368
+ /** Enqueue → body start (ms); null when not read. */
369
+ queueMs: (0, import_sqlite_core.integer)("queue_ms"),
370
+ /** Where the completed run's time went: model and tool totals, counts, and the per-step list. */
371
+ modelMs: (0, import_sqlite_core.integer)("model_ms"),
372
+ toolsMs: (0, import_sqlite_core.integer)("tools_ms"),
373
+ modelCalls: (0, import_sqlite_core.integer)("model_calls"),
374
+ toolCalls: (0, import_sqlite_core.integer)("tool_calls"),
375
+ timings: (0, import_sqlite_core.text)("timings", {
376
+ mode: "json"
377
+ }).$type()
349
378
  }, (table) => [
350
379
  (0, import_sqlite_core.index)("agent_run_started_idx").on(table.startedAt)
351
380
  ]);
@@ -691,7 +720,7 @@ var agentQueuedMessage2 = (0, import_mysql_core.mysqlTable)("agent_queued_messag
691
720
  ]);
692
721
  var agentTokenUsage2 = (0, import_mysql_core.mysqlTable)("agent_token_usage", {
693
722
  id: key("id").primaryKey(),
694
- threadId: key("thread_id").notNull().references(() => agentThread2.id, {
723
+ threadId: key("thread_id").references(() => agentThread2.id, {
695
724
  onDelete: "cascade"
696
725
  }),
697
726
  actorRef: key("actor_ref").notNull(),
@@ -705,11 +734,28 @@ var agentTokenUsage2 = (0, import_mysql_core.mysqlTable)("agent_token_usage", {
705
734
  costUsd: (0, import_mysql_core.double)("cost_usd"),
706
735
  /** Where `cost_usd` came from: `'provider'` (reported) or `'estimate'` (tokens × the price row); null on rows from before it existed (reported). */
707
736
  costSource: key("cost_source").$type(),
737
+ /** The run the row belongs to. */
738
+ runId: key("run_id"),
739
+ /** What the spend was for: `agent:<name>` for an agent turn, the app's own otherwise; null → `agent`. */
740
+ feature: key("feature"),
741
+ provider: key("provider"),
742
+ tenantRef: key("tenant_ref"),
743
+ /** Units a row bills by when they are not tokens. */
744
+ audioSeconds: (0, import_mysql_core.double)("audio_seconds"),
745
+ characters: (0, import_mysql_core.bigint)("characters", {
746
+ mode: "number"
747
+ }),
748
+ requests: (0, import_mysql_core.int)("requests"),
749
+ /** The caller's own JSON, never read by the library. */
750
+ metadata: (0, import_mysql_core.json)("metadata").$type(),
751
+ /** `false` keeps the row out of the agent's quota windows (`recordUsage({ countsTowardsQuota: false })`). */
752
+ countsTowardsQuota: (0, import_mysql_core.boolean)("counts_towards_quota").notNull().default(true),
708
753
  createdAt: ts("created_at").notNull()
709
754
  }, (table) => [
710
755
  (0, import_mysql_core.index)("agent_token_usage_actor_created_idx").on(table.actorRef, table.createdAt),
711
756
  (0, import_mysql_core.index)("agent_token_usage_created_idx").on(table.createdAt),
712
- (0, import_mysql_core.index)("agent_token_usage_thread_idx").on(table.threadId)
757
+ (0, import_mysql_core.index)("agent_token_usage_thread_idx").on(table.threadId),
758
+ (0, import_mysql_core.index)("agent_token_usage_feature_created_idx").on(table.feature, table.createdAt)
713
759
  ]);
714
760
  var agentGenuiCall2 = (0, import_mysql_core.mysqlTable)("agent_genui_call", {
715
761
  id: key("id").primaryKey(),
@@ -781,7 +827,15 @@ var agentRun2 = (0, import_mysql_core.mysqlTable)("agent_run", {
781
827
  startedAt: ts("started_at").notNull(),
782
828
  settledAt: ts("settled_at"),
783
829
  promptHash: key("prompt_hash"),
784
- parentRunId: key("parent_run_id")
830
+ parentRunId: key("parent_run_id"),
831
+ /** Enqueue → body start (ms); null when not read. */
832
+ queueMs: (0, import_mysql_core.int)("queue_ms"),
833
+ /** Where the completed run's time went: model and tool totals, counts, and the per-step list. */
834
+ modelMs: (0, import_mysql_core.int)("model_ms"),
835
+ toolsMs: (0, import_mysql_core.int)("tools_ms"),
836
+ modelCalls: (0, import_mysql_core.int)("model_calls"),
837
+ toolCalls: (0, import_mysql_core.int)("tool_calls"),
838
+ timings: (0, import_mysql_core.json)("timings").$type()
785
839
  }, (table) => [
786
840
  (0, import_mysql_core.index)("agent_run_started_idx").on(table.startedAt)
787
841
  ]);
@@ -1153,7 +1207,7 @@ var agentQueuedMessage3 = (0, import_pg_core.pgTable)("agent_queued_message", {
1153
1207
  ]);
1154
1208
  var agentTokenUsage3 = (0, import_pg_core.pgTable)("agent_token_usage", {
1155
1209
  id: (0, import_pg_core.text)("id").primaryKey(),
1156
- threadId: (0, import_pg_core.text)("thread_id").notNull().references(() => agentThread3.id, {
1210
+ threadId: (0, import_pg_core.text)("thread_id").references(() => agentThread3.id, {
1157
1211
  onDelete: "cascade"
1158
1212
  }),
1159
1213
  actorRef: (0, import_pg_core.text)("actor_ref").notNull(),
@@ -1167,11 +1221,28 @@ var agentTokenUsage3 = (0, import_pg_core.pgTable)("agent_token_usage", {
1167
1221
  costUsd: (0, import_pg_core.doublePrecision)("cost_usd"),
1168
1222
  /** Where `cost_usd` came from: `'provider'` (reported) or `'estimate'` (tokens × the price row); null on rows from before it existed (reported). */
1169
1223
  costSource: (0, import_pg_core.text)("cost_source").$type(),
1224
+ /** The run the row belongs to. */
1225
+ runId: (0, import_pg_core.text)("run_id"),
1226
+ /** What the spend was for: `agent:<name>` for an agent turn, the app's own otherwise; null → `agent`. */
1227
+ feature: (0, import_pg_core.text)("feature"),
1228
+ provider: (0, import_pg_core.text)("provider"),
1229
+ tenantRef: (0, import_pg_core.text)("tenant_ref"),
1230
+ /** Units a row bills by when they are not tokens. */
1231
+ audioSeconds: (0, import_pg_core.doublePrecision)("audio_seconds"),
1232
+ characters: (0, import_pg_core.bigint)("characters", {
1233
+ mode: "number"
1234
+ }),
1235
+ requests: (0, import_pg_core.integer)("requests"),
1236
+ /** The caller's own JSON, never read by the library. */
1237
+ metadata: (0, import_pg_core.jsonb)("metadata").$type(),
1238
+ /** `false` keeps the row out of the agent's quota windows (`recordUsage({ countsTowardsQuota: false })`). */
1239
+ countsTowardsQuota: (0, import_pg_core.boolean)("counts_towards_quota").notNull().default(true),
1170
1240
  createdAt: ts2("created_at").notNull()
1171
1241
  }, (table) => [
1172
1242
  (0, import_pg_core.index)("agent_token_usage_actor_created_idx").on(table.actorRef, table.createdAt),
1173
1243
  (0, import_pg_core.index)("agent_token_usage_created_idx").on(table.createdAt),
1174
- (0, import_pg_core.index)("agent_token_usage_thread_idx").on(table.threadId)
1244
+ (0, import_pg_core.index)("agent_token_usage_thread_idx").on(table.threadId),
1245
+ (0, import_pg_core.index)("agent_token_usage_feature_created_idx").on(table.feature, table.createdAt)
1175
1246
  ]);
1176
1247
  var agentGenuiCall3 = (0, import_pg_core.pgTable)("agent_genui_call", {
1177
1248
  id: (0, import_pg_core.text)("id").primaryKey(),
@@ -1243,7 +1314,15 @@ var agentRun3 = (0, import_pg_core.pgTable)("agent_run", {
1243
1314
  startedAt: ts2("started_at").notNull(),
1244
1315
  settledAt: ts2("settled_at"),
1245
1316
  promptHash: (0, import_pg_core.text)("prompt_hash"),
1246
- parentRunId: (0, import_pg_core.text)("parent_run_id")
1317
+ parentRunId: (0, import_pg_core.text)("parent_run_id"),
1318
+ /** Enqueue → body start (ms); null when not read. */
1319
+ queueMs: (0, import_pg_core.integer)("queue_ms"),
1320
+ /** Where the completed run's time went: model and tool totals, counts, and the per-step list. */
1321
+ modelMs: (0, import_pg_core.integer)("model_ms"),
1322
+ toolsMs: (0, import_pg_core.integer)("tools_ms"),
1323
+ modelCalls: (0, import_pg_core.integer)("model_calls"),
1324
+ toolCalls: (0, import_pg_core.integer)("tool_calls"),
1325
+ timings: (0, import_pg_core.jsonb)("timings").$type()
1247
1326
  }, (table) => [
1248
1327
  (0, import_pg_core.index)("agent_run_started_idx").on(table.startedAt)
1249
1328
  ]);
@@ -1701,10 +1780,60 @@ function specs(dialect) {
1701
1780
  }
1702
1781
  __name(specs, "specs");
1703
1782
  async function existingColumns(db, dialect, table) {
1704
- const rows = dialect === "sqlite" ? await querySql(db, dialect, import_drizzle_orm2.sql.raw(`PRAGMA table_info("${table}")`)) : await querySql(db, dialect, import_drizzle_orm2.sql`select column_name as name from information_schema.columns where table_schema = ${import_drizzle_orm2.sql.raw(dialect === "mysql" ? "database()" : "current_schema()")} and table_name = ${table}`);
1705
- return new Set(rows.map((row) => String(row.name)));
1783
+ return new Set((await columnNullability(db, dialect, table)).keys());
1706
1784
  }
1707
1785
  __name(existingColumns, "existingColumns");
1786
+ async function columnNullability(db, dialect, table) {
1787
+ if (dialect === "sqlite") {
1788
+ const rows2 = await querySql(db, dialect, import_drizzle_orm2.sql.raw(`PRAGMA table_info("${table}")`));
1789
+ return new Map(rows2.map((row) => [
1790
+ String(row.name),
1791
+ Number(row.notnull) === 0
1792
+ ]));
1793
+ }
1794
+ const rows = await querySql(db, dialect, import_drizzle_orm2.sql`select column_name as name, is_nullable as nullable from information_schema.columns where table_schema = ${import_drizzle_orm2.sql.raw(dialect === "mysql" ? "database()" : "current_schema()")} and table_name = ${table}`);
1795
+ return new Map(rows.map((row) => [
1796
+ String(row.name ?? row.NAME),
1797
+ String(row.nullable ?? row.NULLABLE).toUpperCase() === "YES"
1798
+ ]));
1799
+ }
1800
+ __name(columnNullability, "columnNullability");
1801
+ async function relaxNotNull(db, dialect, spec, column, all) {
1802
+ const q = /* @__PURE__ */ __name((identifier) => quote(dialect, identifier), "q");
1803
+ if (dialect === "postgres") {
1804
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(`ALTER TABLE ${q(spec.name)} ALTER COLUMN ${q(column.name)} DROP NOT NULL`));
1805
+ return;
1806
+ }
1807
+ if (dialect === "mysql") {
1808
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(`ALTER TABLE ${q(spec.name)} MODIFY ${columnDefinition(dialect, column)}`));
1809
+ return;
1810
+ }
1811
+ if (all.some((other) => other.foreignKeys.some((key2) => key2.table === spec.name && other !== spec))) {
1812
+ throw new Error(`agent schema: cannot make ${spec.name}.${column.name} nullable on SQLite \u2014 other tables reference ${spec.name}`);
1813
+ }
1814
+ const scratch = `${spec.name}__rebuild`;
1815
+ const present = [
1816
+ ...(await columnNullability(db, dialect, spec.name)).keys()
1817
+ ];
1818
+ const list = present.map(q).join(", ");
1819
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw("SAVEPOINT agent_schema_rebuild"));
1820
+ try {
1821
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(`DROP TABLE IF EXISTS ${q(scratch)}`));
1822
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(createTableSql(dialect, {
1823
+ ...spec,
1824
+ name: scratch
1825
+ })));
1826
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(`INSERT INTO ${q(scratch)} (${list}) SELECT ${list} FROM ${q(spec.name)}`));
1827
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(`DROP TABLE ${q(spec.name)}`));
1828
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw(`ALTER TABLE ${q(scratch)} RENAME TO ${q(spec.name)}`));
1829
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw("RELEASE SAVEPOINT agent_schema_rebuild"));
1830
+ } catch (error) {
1831
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw("ROLLBACK TO SAVEPOINT agent_schema_rebuild")).catch(() => void 0);
1832
+ await runSql(db, dialect, import_drizzle_orm2.sql.raw("RELEASE SAVEPOINT agent_schema_rebuild")).catch(() => void 0);
1833
+ throw error;
1834
+ }
1835
+ }
1836
+ __name(relaxNotNull, "relaxNotNull");
1708
1837
  async function indexExists(db, dialect, table, index4) {
1709
1838
  let query;
1710
1839
  if (dialect === "sqlite") {
@@ -1718,7 +1847,8 @@ async function indexExists(db, dialect, table, index4) {
1718
1847
  }
1719
1848
  __name(indexExists, "indexExists");
1720
1849
  async function heal(db, dialect) {
1721
- for (const spec of specs(dialect)) {
1850
+ const all = specs(dialect);
1851
+ for (const spec of all) {
1722
1852
  await runSql(db, dialect, import_drizzle_orm2.sql.raw(createTableSql(dialect, spec)));
1723
1853
  const present = await existingColumns(db, dialect, spec.name);
1724
1854
  for (const column of spec.columns) {
@@ -1726,6 +1856,13 @@ async function heal(db, dialect) {
1726
1856
  await runSql(db, dialect, import_drizzle_orm2.sql.raw(`ALTER TABLE ${quote(dialect, spec.name)} ADD COLUMN ${columnDefinition(dialect, column)}`));
1727
1857
  }
1728
1858
  }
1859
+ const nullable3 = await columnNullability(db, dialect, spec.name);
1860
+ for (const column of spec.columns) {
1861
+ if (column.notNull || spec.primaryKey.includes(column.name)) continue;
1862
+ if (nullable3.get(column.name) === false) {
1863
+ await relaxNotNull(db, dialect, spec, column, all);
1864
+ }
1865
+ }
1729
1866
  for (const index4 of spec.indexes) {
1730
1867
  if (!await indexExists(db, dialect, spec.name, index4.name)) {
1731
1868
  await runSql(db, dialect, import_drizzle_orm2.sql.raw(createIndexSql(dialect, spec.name, index4)));
@@ -2916,7 +3053,13 @@ var DrizzleAgentStore = class {
2916
3053
  startedAt: /* @__PURE__ */ new Date(),
2917
3054
  settledAt: null,
2918
3055
  promptHash: run.promptHash ?? null,
2919
- parentRunId: run.parentRunId ?? null
3056
+ parentRunId: run.parentRunId ?? null,
3057
+ queueMs: run.queueMs !== void 0 ? Math.max(0, Math.round(run.queueMs)) : null,
3058
+ modelMs: null,
3059
+ toolsMs: null,
3060
+ modelCalls: null,
3061
+ toolCalls: null,
3062
+ timings: null
2920
3063
  };
2921
3064
  await this.db.insert(this.t.agentRun).values(runRow);
2922
3065
  }
@@ -2944,6 +3087,13 @@ var DrizzleAgentStore = class {
2944
3087
  if (end.errorMessage !== void 0) {
2945
3088
  updates.errorMessage = end.errorMessage;
2946
3089
  }
3090
+ if (end.timings !== void 0) {
3091
+ updates.modelMs = Math.round(end.timings.modelMs);
3092
+ updates.toolsMs = Math.round(end.timings.toolsMs);
3093
+ updates.modelCalls = end.timings.modelCalls;
3094
+ updates.toolCalls = end.timings.toolCalls;
3095
+ updates.timings = end.timings.steps;
3096
+ }
2947
3097
  await this.db.update(this.t.agentRun).set(updates).where((0, import_drizzle_orm4.eq)(this.t.agentRun.id, end.runId));
2948
3098
  }
2949
3099
  /** Bump the run's llm-step retry counter. Atomic `retries = retries + 1`, no read-modify-write. */
@@ -3146,9 +3296,18 @@ var DrizzleAgentStore = class {
3146
3296
  async recordUsage(input) {
3147
3297
  await this.db.insert(this.t.agentTokenUsage).values({
3148
3298
  id: crypto.randomUUID(),
3149
- threadId: input.threadId,
3299
+ threadId: input.threadId ?? null,
3150
3300
  actorRef: input.actorRef,
3151
3301
  messageId: input.messageId ?? null,
3302
+ runId: input.runId ?? null,
3303
+ feature: input.feature ?? null,
3304
+ provider: input.provider ?? null,
3305
+ tenantRef: input.tenantRef ?? null,
3306
+ audioSeconds: input.audioSeconds ?? null,
3307
+ characters: input.characters ?? null,
3308
+ requests: input.requests ?? null,
3309
+ metadata: input.metadata ?? null,
3310
+ countsTowardsQuota: input.countsTowardsQuota !== false,
3152
3311
  modelId: input.modelId,
3153
3312
  purpose: input.purpose,
3154
3313
  inputTokens: input.usage.inputTokens,
@@ -3233,7 +3392,7 @@ var DrizzleAgentStore = class {
3233
3392
  });
3234
3393
  const from = new Date(Math.min(...bounds.map(({ start }) => start.getTime())));
3235
3394
  const to = new Date(Math.max(...bounds.map(({ end }) => end.getTime())));
3236
- const [row] = await this.db.select(columns).from(usage).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(usage.actorRef, actorRef), within(from, to)));
3395
+ const [row] = await this.db.select(columns).from(usage).where((0, import_drizzle_orm4.and)((0, import_drizzle_orm4.eq)(usage.actorRef, actorRef), within(from, to), (0, import_drizzle_orm4.eq)(usage.countsTowardsQuota, true)));
3237
3396
  return bounds.map((_, index4) => ({
3238
3397
  usedTokens: Number(row?.[`tokens${index4}`] ?? 0),
3239
3398
  costUsd: Number(row?.[`cost${index4}`] ?? 0),
@@ -3466,10 +3625,26 @@ function toRecentRunRow(run) {
3466
3625
  retries: run.retries,
3467
3626
  startedAt: run.startedAt.toISOString(),
3468
3627
  promptHash: run.promptHash ?? null,
3469
- parentRunId: run.parentRunId ?? null
3628
+ parentRunId: run.parentRunId ?? null,
3629
+ queueMs: run.queueMs ?? null,
3630
+ modelMs: run.modelMs ?? null,
3631
+ toolsMs: run.toolsMs ?? null,
3632
+ modelCalls: run.modelCalls ?? null,
3633
+ toolCalls: run.toolCalls ?? null
3470
3634
  };
3471
3635
  }
3472
3636
  __name(toRecentRunRow, "toRecentRunRow");
3637
+ function toRunTimings(run) {
3638
+ if (!Array.isArray(run.timings)) return null;
3639
+ return {
3640
+ modelMs: run.modelMs ?? 0,
3641
+ toolsMs: run.toolsMs ?? 0,
3642
+ modelCalls: run.modelCalls ?? 0,
3643
+ toolCalls: run.toolCalls ?? 0,
3644
+ steps: run.timings
3645
+ };
3646
+ }
3647
+ __name(toRunTimings, "toRunTimings");
3473
3648
  function toRunToolCallRow(toolCall) {
3474
3649
  return {
3475
3650
  toolCallId: toolCall.id,
@@ -3485,6 +3660,12 @@ function toRunToolCallRow(toolCall) {
3485
3660
  __name(toRunToolCallRow, "toRunToolCallRow");
3486
3661
  function toUsageInput(row) {
3487
3662
  return {
3663
+ feature: row.feature ?? null,
3664
+ purpose: row.purpose ?? null,
3665
+ costSource: row.costSource ?? null,
3666
+ audioSeconds: row.audioSeconds == null ? null : Number(row.audioSeconds),
3667
+ characters: row.characters == null ? null : Number(row.characters),
3668
+ requests: row.requests ?? null,
3488
3669
  modelId: row.modelId,
3489
3670
  actorRef: row.actorRef,
3490
3671
  threadId: row.threadId,
@@ -3532,8 +3713,13 @@ var DrizzleGovernanceQueries = class {
3532
3713
  }
3533
3714
  return pricing;
3534
3715
  }
3535
- async usageInRange(range) {
3536
- const { start, end } = (0, import_nestjs_agent_core5.dayBoundsUtc)(range);
3716
+ /**
3717
+ * The usage rows in range, with only what the bucketers read: `requests` always (a recorded row
3718
+ * counts as its own requests in every rollup), and the feature/unit columns only for the
3719
+ * per-feature reads that group by them.
3720
+ */
3721
+ async usageInRange(range, withFeature = false) {
3722
+ const { start, end } = (0, import_nestjs_agent_core5.usageRangeBounds)(range);
3537
3723
  const usage = this.t.agentTokenUsage;
3538
3724
  const rows = await this.db.select({
3539
3725
  modelId: usage.modelId,
@@ -3544,14 +3730,30 @@ var DrizzleGovernanceQueries = class {
3544
3730
  outputTokens: usage.outputTokens,
3545
3731
  cacheWriteTokens: usage.cacheWriteTokens,
3546
3732
  cacheReadTokens: usage.cacheReadTokens,
3547
- costUsd: usage.costUsd
3548
- }).from(usage).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.gte)(this.t.agentTokenUsage.createdAt, start), (0, import_drizzle_orm5.lte)(this.t.agentTokenUsage.createdAt, end)));
3733
+ costUsd: usage.costUsd,
3734
+ requests: usage.requests,
3735
+ ...withFeature ? {
3736
+ feature: usage.feature,
3737
+ purpose: usage.purpose,
3738
+ costSource: usage.costSource,
3739
+ audioSeconds: usage.audioSeconds,
3740
+ characters: usage.characters
3741
+ } : {}
3742
+ }).from(usage).where((0, import_drizzle_orm5.and)((0, import_drizzle_orm5.gte)(this.t.agentTokenUsage.createdAt, start), (0, import_drizzle_orm5.lt)(this.t.agentTokenUsage.createdAt, end)));
3549
3743
  return rows.map(toUsageInput);
3550
3744
  }
3551
3745
  async spendByModel(range) {
3552
3746
  const pricing = await this.loadPricing();
3553
3747
  return (0, import_nestjs_agent_core5.bucketByModel)(await this.usageInRange(range), pricing);
3554
3748
  }
3749
+ async spendByFeature(range) {
3750
+ const pricing = await this.loadPricing();
3751
+ return (0, import_nestjs_agent_core5.bucketByFeature)(await this.usageInRange(range, true), pricing);
3752
+ }
3753
+ async usageBreakdown(range, filter) {
3754
+ const pricing = await this.loadPricing();
3755
+ return (0, import_nestjs_agent_core5.bucketUsageBreakdown)(await this.usageInRange(range, true), pricing, filter);
3756
+ }
3555
3757
  async spendByActor(range) {
3556
3758
  const pricing = await this.loadPricing();
3557
3759
  return (0, import_nestjs_agent_core5.bucketByActor)(await this.usageInRange(range), pricing);
@@ -3565,7 +3767,9 @@ var DrizzleGovernanceQueries = class {
3565
3767
  const pricing = await this.loadPricing();
3566
3768
  const rows = await this.usageInRange(range);
3567
3769
  const threadIds = [
3568
- ...new Set(rows.map((row) => row.threadId))
3770
+ ...new Set(rows.flatMap((row) => row.threadId === null ? [] : [
3771
+ row.threadId
3772
+ ]))
3569
3773
  ];
3570
3774
  if (threadIds.length === 0) {
3571
3775
  return [];
@@ -3643,6 +3847,7 @@ var DrizzleGovernanceQueries = class {
3643
3847
  outputTokens: (0, import_drizzle_orm5.sum)(this.t.agentTokenUsage.outputTokens)
3644
3848
  }).from(this.t.agentTokenUsage).where((0, import_drizzle_orm5.inArray)(this.t.agentTokenUsage.threadId, threadIds)).groupBy(this.t.agentTokenUsage.threadId);
3645
3849
  for (const row of usageTotals) {
3850
+ if (row.threadId === null) continue;
3646
3851
  const bucket = rollups.get(row.threadId);
3647
3852
  if (bucket !== void 0) {
3648
3853
  bucket.totalTokens = Number(row.inputTokens ?? 0) + Number(row.outputTokens ?? 0);
@@ -4019,7 +4224,8 @@ var DrizzleGovernanceQueries = class {
4019
4224
  actorRef: row.thread.actorRef,
4020
4225
  deleted: row.thread.deletedAt != null
4021
4226
  },
4022
- toolCalls: toolCalls.map(toRunToolCallRow)
4227
+ toolCalls: toolCalls.map(toRunToolCallRow),
4228
+ timings: toRunTimings(row.run)
4023
4229
  };
4024
4230
  }
4025
4231
  /**