@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.js CHANGED
@@ -178,7 +178,7 @@ var agentQueuedMessage = sqliteTable("agent_queued_message", {
178
178
  ]);
179
179
  var agentTokenUsage = sqliteTable("agent_token_usage", {
180
180
  id: text("id").primaryKey(),
181
- threadId: text("thread_id").notNull().references(() => agentThread.id, {
181
+ threadId: text("thread_id").references(() => agentThread.id, {
182
182
  onDelete: "cascade"
183
183
  }),
184
184
  actorRef: text("actor_ref").notNull(),
@@ -195,13 +195,32 @@ var agentTokenUsage = sqliteTable("agent_token_usage", {
195
195
  costUsd: real("cost_usd"),
196
196
  /** Where `cost_usd` came from: `'provider'` (reported) or `'estimate'` (tokens × the price row); null on rows from before it existed (reported). */
197
197
  costSource: text("cost_source").$type(),
198
+ /** The run the row belongs to. */
199
+ runId: text("run_id"),
200
+ /** What the spend was for: `agent:<name>` for an agent turn, the app's own otherwise; null → `agent`. */
201
+ feature: text("feature"),
202
+ provider: text("provider"),
203
+ tenantRef: text("tenant_ref"),
204
+ /** Units a row bills by when they are not tokens. */
205
+ audioSeconds: real("audio_seconds"),
206
+ characters: integer("characters"),
207
+ requests: integer("requests"),
208
+ /** The caller's own JSON, never read by the library. */
209
+ metadata: text("metadata", {
210
+ mode: "json"
211
+ }).$type(),
212
+ /** `false` keeps the row out of the agent's quota windows (`recordUsage({ countsTowardsQuota: false })`). */
213
+ countsTowardsQuota: integer("counts_towards_quota", {
214
+ mode: "boolean"
215
+ }).notNull().default(true),
198
216
  createdAt: integer("created_at", {
199
217
  mode: "timestamp_ms"
200
218
  }).notNull()
201
219
  }, (table) => [
202
220
  index("agent_token_usage_actor_created_idx").on(table.actorRef, table.createdAt),
203
221
  index("agent_token_usage_created_idx").on(table.createdAt),
204
- index("agent_token_usage_thread_idx").on(table.threadId)
222
+ index("agent_token_usage_thread_idx").on(table.threadId),
223
+ index("agent_token_usage_feature_created_idx").on(table.feature, table.createdAt)
205
224
  ]);
206
225
  var agentGenuiCall = sqliteTable("agent_genui_call", {
207
226
  id: text("id").primaryKey(),
@@ -283,7 +302,17 @@ var agentRun = sqliteTable("agent_run", {
283
302
  * for a detached child, which outlives its parent's turn and so is paired by nothing in the
284
303
  * transcript.
285
304
  */
286
- parentRunId: text("parent_run_id")
305
+ parentRunId: text("parent_run_id"),
306
+ /** Enqueue → body start (ms); null when not read. */
307
+ queueMs: integer("queue_ms"),
308
+ /** Where the completed run's time went: model and tool totals, counts, and the per-step list. */
309
+ modelMs: integer("model_ms"),
310
+ toolsMs: integer("tools_ms"),
311
+ modelCalls: integer("model_calls"),
312
+ toolCalls: integer("tool_calls"),
313
+ timings: text("timings", {
314
+ mode: "json"
315
+ }).$type()
287
316
  }, (table) => [
288
317
  index("agent_run_started_idx").on(table.startedAt)
289
318
  ]);
@@ -629,7 +658,7 @@ var agentQueuedMessage2 = mysqlTable("agent_queued_message", {
629
658
  ]);
630
659
  var agentTokenUsage2 = mysqlTable("agent_token_usage", {
631
660
  id: key("id").primaryKey(),
632
- threadId: key("thread_id").notNull().references(() => agentThread2.id, {
661
+ threadId: key("thread_id").references(() => agentThread2.id, {
633
662
  onDelete: "cascade"
634
663
  }),
635
664
  actorRef: key("actor_ref").notNull(),
@@ -643,11 +672,28 @@ var agentTokenUsage2 = mysqlTable("agent_token_usage", {
643
672
  costUsd: double("cost_usd"),
644
673
  /** Where `cost_usd` came from: `'provider'` (reported) or `'estimate'` (tokens × the price row); null on rows from before it existed (reported). */
645
674
  costSource: key("cost_source").$type(),
675
+ /** The run the row belongs to. */
676
+ runId: key("run_id"),
677
+ /** What the spend was for: `agent:<name>` for an agent turn, the app's own otherwise; null → `agent`. */
678
+ feature: key("feature"),
679
+ provider: key("provider"),
680
+ tenantRef: key("tenant_ref"),
681
+ /** Units a row bills by when they are not tokens. */
682
+ audioSeconds: double("audio_seconds"),
683
+ characters: bigint("characters", {
684
+ mode: "number"
685
+ }),
686
+ requests: int("requests"),
687
+ /** The caller's own JSON, never read by the library. */
688
+ metadata: json("metadata").$type(),
689
+ /** `false` keeps the row out of the agent's quota windows (`recordUsage({ countsTowardsQuota: false })`). */
690
+ countsTowardsQuota: boolean("counts_towards_quota").notNull().default(true),
646
691
  createdAt: ts("created_at").notNull()
647
692
  }, (table) => [
648
693
  index2("agent_token_usage_actor_created_idx").on(table.actorRef, table.createdAt),
649
694
  index2("agent_token_usage_created_idx").on(table.createdAt),
650
- index2("agent_token_usage_thread_idx").on(table.threadId)
695
+ index2("agent_token_usage_thread_idx").on(table.threadId),
696
+ index2("agent_token_usage_feature_created_idx").on(table.feature, table.createdAt)
651
697
  ]);
652
698
  var agentGenuiCall2 = mysqlTable("agent_genui_call", {
653
699
  id: key("id").primaryKey(),
@@ -719,7 +765,15 @@ var agentRun2 = mysqlTable("agent_run", {
719
765
  startedAt: ts("started_at").notNull(),
720
766
  settledAt: ts("settled_at"),
721
767
  promptHash: key("prompt_hash"),
722
- parentRunId: key("parent_run_id")
768
+ parentRunId: key("parent_run_id"),
769
+ /** Enqueue → body start (ms); null when not read. */
770
+ queueMs: int("queue_ms"),
771
+ /** Where the completed run's time went: model and tool totals, counts, and the per-step list. */
772
+ modelMs: int("model_ms"),
773
+ toolsMs: int("tools_ms"),
774
+ modelCalls: int("model_calls"),
775
+ toolCalls: int("tool_calls"),
776
+ timings: json("timings").$type()
723
777
  }, (table) => [
724
778
  index2("agent_run_started_idx").on(table.startedAt)
725
779
  ]);
@@ -1091,7 +1145,7 @@ var agentQueuedMessage3 = pgTable("agent_queued_message", {
1091
1145
  ]);
1092
1146
  var agentTokenUsage3 = pgTable("agent_token_usage", {
1093
1147
  id: text2("id").primaryKey(),
1094
- threadId: text2("thread_id").notNull().references(() => agentThread3.id, {
1148
+ threadId: text2("thread_id").references(() => agentThread3.id, {
1095
1149
  onDelete: "cascade"
1096
1150
  }),
1097
1151
  actorRef: text2("actor_ref").notNull(),
@@ -1105,11 +1159,28 @@ var agentTokenUsage3 = pgTable("agent_token_usage", {
1105
1159
  costUsd: doublePrecision("cost_usd"),
1106
1160
  /** Where `cost_usd` came from: `'provider'` (reported) or `'estimate'` (tokens × the price row); null on rows from before it existed (reported). */
1107
1161
  costSource: text2("cost_source").$type(),
1162
+ /** The run the row belongs to. */
1163
+ runId: text2("run_id"),
1164
+ /** What the spend was for: `agent:<name>` for an agent turn, the app's own otherwise; null → `agent`. */
1165
+ feature: text2("feature"),
1166
+ provider: text2("provider"),
1167
+ tenantRef: text2("tenant_ref"),
1168
+ /** Units a row bills by when they are not tokens. */
1169
+ audioSeconds: doublePrecision("audio_seconds"),
1170
+ characters: bigint2("characters", {
1171
+ mode: "number"
1172
+ }),
1173
+ requests: integer2("requests"),
1174
+ /** The caller's own JSON, never read by the library. */
1175
+ metadata: jsonb("metadata").$type(),
1176
+ /** `false` keeps the row out of the agent's quota windows (`recordUsage({ countsTowardsQuota: false })`). */
1177
+ countsTowardsQuota: boolean2("counts_towards_quota").notNull().default(true),
1108
1178
  createdAt: ts2("created_at").notNull()
1109
1179
  }, (table) => [
1110
1180
  index3("agent_token_usage_actor_created_idx").on(table.actorRef, table.createdAt),
1111
1181
  index3("agent_token_usage_created_idx").on(table.createdAt),
1112
- index3("agent_token_usage_thread_idx").on(table.threadId)
1182
+ index3("agent_token_usage_thread_idx").on(table.threadId),
1183
+ index3("agent_token_usage_feature_created_idx").on(table.feature, table.createdAt)
1113
1184
  ]);
1114
1185
  var agentGenuiCall3 = pgTable("agent_genui_call", {
1115
1186
  id: text2("id").primaryKey(),
@@ -1181,7 +1252,15 @@ var agentRun3 = pgTable("agent_run", {
1181
1252
  startedAt: ts2("started_at").notNull(),
1182
1253
  settledAt: ts2("settled_at"),
1183
1254
  promptHash: text2("prompt_hash"),
1184
- parentRunId: text2("parent_run_id")
1255
+ parentRunId: text2("parent_run_id"),
1256
+ /** Enqueue → body start (ms); null when not read. */
1257
+ queueMs: integer2("queue_ms"),
1258
+ /** Where the completed run's time went: model and tool totals, counts, and the per-step list. */
1259
+ modelMs: integer2("model_ms"),
1260
+ toolsMs: integer2("tools_ms"),
1261
+ modelCalls: integer2("model_calls"),
1262
+ toolCalls: integer2("tool_calls"),
1263
+ timings: jsonb("timings").$type()
1185
1264
  }, (table) => [
1186
1265
  index3("agent_run_started_idx").on(table.startedAt)
1187
1266
  ]);
@@ -1639,10 +1718,60 @@ function specs(dialect) {
1639
1718
  }
1640
1719
  __name(specs, "specs");
1641
1720
  async function existingColumns(db, dialect, table) {
1642
- const rows = dialect === "sqlite" ? await querySql(db, dialect, sql.raw(`PRAGMA table_info("${table}")`)) : await querySql(db, dialect, sql`select column_name as name from information_schema.columns where table_schema = ${sql.raw(dialect === "mysql" ? "database()" : "current_schema()")} and table_name = ${table}`);
1643
- return new Set(rows.map((row) => String(row.name)));
1721
+ return new Set((await columnNullability(db, dialect, table)).keys());
1644
1722
  }
1645
1723
  __name(existingColumns, "existingColumns");
1724
+ async function columnNullability(db, dialect, table) {
1725
+ if (dialect === "sqlite") {
1726
+ const rows2 = await querySql(db, dialect, sql.raw(`PRAGMA table_info("${table}")`));
1727
+ return new Map(rows2.map((row) => [
1728
+ String(row.name),
1729
+ Number(row.notnull) === 0
1730
+ ]));
1731
+ }
1732
+ const rows = await querySql(db, dialect, sql`select column_name as name, is_nullable as nullable from information_schema.columns where table_schema = ${sql.raw(dialect === "mysql" ? "database()" : "current_schema()")} and table_name = ${table}`);
1733
+ return new Map(rows.map((row) => [
1734
+ String(row.name ?? row.NAME),
1735
+ String(row.nullable ?? row.NULLABLE).toUpperCase() === "YES"
1736
+ ]));
1737
+ }
1738
+ __name(columnNullability, "columnNullability");
1739
+ async function relaxNotNull(db, dialect, spec, column, all) {
1740
+ const q = /* @__PURE__ */ __name((identifier) => quote(dialect, identifier), "q");
1741
+ if (dialect === "postgres") {
1742
+ await runSql(db, dialect, sql.raw(`ALTER TABLE ${q(spec.name)} ALTER COLUMN ${q(column.name)} DROP NOT NULL`));
1743
+ return;
1744
+ }
1745
+ if (dialect === "mysql") {
1746
+ await runSql(db, dialect, sql.raw(`ALTER TABLE ${q(spec.name)} MODIFY ${columnDefinition(dialect, column)}`));
1747
+ return;
1748
+ }
1749
+ if (all.some((other) => other.foreignKeys.some((key2) => key2.table === spec.name && other !== spec))) {
1750
+ throw new Error(`agent schema: cannot make ${spec.name}.${column.name} nullable on SQLite \u2014 other tables reference ${spec.name}`);
1751
+ }
1752
+ const scratch = `${spec.name}__rebuild`;
1753
+ const present = [
1754
+ ...(await columnNullability(db, dialect, spec.name)).keys()
1755
+ ];
1756
+ const list = present.map(q).join(", ");
1757
+ await runSql(db, dialect, sql.raw("SAVEPOINT agent_schema_rebuild"));
1758
+ try {
1759
+ await runSql(db, dialect, sql.raw(`DROP TABLE IF EXISTS ${q(scratch)}`));
1760
+ await runSql(db, dialect, sql.raw(createTableSql(dialect, {
1761
+ ...spec,
1762
+ name: scratch
1763
+ })));
1764
+ await runSql(db, dialect, sql.raw(`INSERT INTO ${q(scratch)} (${list}) SELECT ${list} FROM ${q(spec.name)}`));
1765
+ await runSql(db, dialect, sql.raw(`DROP TABLE ${q(spec.name)}`));
1766
+ await runSql(db, dialect, sql.raw(`ALTER TABLE ${q(scratch)} RENAME TO ${q(spec.name)}`));
1767
+ await runSql(db, dialect, sql.raw("RELEASE SAVEPOINT agent_schema_rebuild"));
1768
+ } catch (error) {
1769
+ await runSql(db, dialect, sql.raw("ROLLBACK TO SAVEPOINT agent_schema_rebuild")).catch(() => void 0);
1770
+ await runSql(db, dialect, sql.raw("RELEASE SAVEPOINT agent_schema_rebuild")).catch(() => void 0);
1771
+ throw error;
1772
+ }
1773
+ }
1774
+ __name(relaxNotNull, "relaxNotNull");
1646
1775
  async function indexExists(db, dialect, table, index4) {
1647
1776
  let query;
1648
1777
  if (dialect === "sqlite") {
@@ -1656,7 +1785,8 @@ async function indexExists(db, dialect, table, index4) {
1656
1785
  }
1657
1786
  __name(indexExists, "indexExists");
1658
1787
  async function heal(db, dialect) {
1659
- for (const spec of specs(dialect)) {
1788
+ const all = specs(dialect);
1789
+ for (const spec of all) {
1660
1790
  await runSql(db, dialect, sql.raw(createTableSql(dialect, spec)));
1661
1791
  const present = await existingColumns(db, dialect, spec.name);
1662
1792
  for (const column of spec.columns) {
@@ -1664,6 +1794,13 @@ async function heal(db, dialect) {
1664
1794
  await runSql(db, dialect, sql.raw(`ALTER TABLE ${quote(dialect, spec.name)} ADD COLUMN ${columnDefinition(dialect, column)}`));
1665
1795
  }
1666
1796
  }
1797
+ const nullable3 = await columnNullability(db, dialect, spec.name);
1798
+ for (const column of spec.columns) {
1799
+ if (column.notNull || spec.primaryKey.includes(column.name)) continue;
1800
+ if (nullable3.get(column.name) === false) {
1801
+ await relaxNotNull(db, dialect, spec, column, all);
1802
+ }
1803
+ }
1667
1804
  for (const index4 of spec.indexes) {
1668
1805
  if (!await indexExists(db, dialect, spec.name, index4.name)) {
1669
1806
  await runSql(db, dialect, sql.raw(createIndexSql(dialect, spec.name, index4)));
@@ -2854,7 +2991,13 @@ var DrizzleAgentStore = class {
2854
2991
  startedAt: /* @__PURE__ */ new Date(),
2855
2992
  settledAt: null,
2856
2993
  promptHash: run.promptHash ?? null,
2857
- parentRunId: run.parentRunId ?? null
2994
+ parentRunId: run.parentRunId ?? null,
2995
+ queueMs: run.queueMs !== void 0 ? Math.max(0, Math.round(run.queueMs)) : null,
2996
+ modelMs: null,
2997
+ toolsMs: null,
2998
+ modelCalls: null,
2999
+ toolCalls: null,
3000
+ timings: null
2858
3001
  };
2859
3002
  await this.db.insert(this.t.agentRun).values(runRow);
2860
3003
  }
@@ -2882,6 +3025,13 @@ var DrizzleAgentStore = class {
2882
3025
  if (end.errorMessage !== void 0) {
2883
3026
  updates.errorMessage = end.errorMessage;
2884
3027
  }
3028
+ if (end.timings !== void 0) {
3029
+ updates.modelMs = Math.round(end.timings.modelMs);
3030
+ updates.toolsMs = Math.round(end.timings.toolsMs);
3031
+ updates.modelCalls = end.timings.modelCalls;
3032
+ updates.toolCalls = end.timings.toolCalls;
3033
+ updates.timings = end.timings.steps;
3034
+ }
2885
3035
  await this.db.update(this.t.agentRun).set(updates).where(eq2(this.t.agentRun.id, end.runId));
2886
3036
  }
2887
3037
  /** Bump the run's llm-step retry counter. Atomic `retries = retries + 1`, no read-modify-write. */
@@ -3084,9 +3234,18 @@ var DrizzleAgentStore = class {
3084
3234
  async recordUsage(input) {
3085
3235
  await this.db.insert(this.t.agentTokenUsage).values({
3086
3236
  id: crypto.randomUUID(),
3087
- threadId: input.threadId,
3237
+ threadId: input.threadId ?? null,
3088
3238
  actorRef: input.actorRef,
3089
3239
  messageId: input.messageId ?? null,
3240
+ runId: input.runId ?? null,
3241
+ feature: input.feature ?? null,
3242
+ provider: input.provider ?? null,
3243
+ tenantRef: input.tenantRef ?? null,
3244
+ audioSeconds: input.audioSeconds ?? null,
3245
+ characters: input.characters ?? null,
3246
+ requests: input.requests ?? null,
3247
+ metadata: input.metadata ?? null,
3248
+ countsTowardsQuota: input.countsTowardsQuota !== false,
3090
3249
  modelId: input.modelId,
3091
3250
  purpose: input.purpose,
3092
3251
  inputTokens: input.usage.inputTokens,
@@ -3171,7 +3330,7 @@ var DrizzleAgentStore = class {
3171
3330
  });
3172
3331
  const from = new Date(Math.min(...bounds.map(({ start }) => start.getTime())));
3173
3332
  const to = new Date(Math.max(...bounds.map(({ end }) => end.getTime())));
3174
- const [row] = await this.db.select(columns).from(usage).where(and2(eq2(usage.actorRef, actorRef), within(from, to)));
3333
+ const [row] = await this.db.select(columns).from(usage).where(and2(eq2(usage.actorRef, actorRef), within(from, to), eq2(usage.countsTowardsQuota, true)));
3175
3334
  return bounds.map((_, index4) => ({
3176
3335
  usedTokens: Number(row?.[`tokens${index4}`] ?? 0),
3177
3336
  costUsd: Number(row?.[`cost${index4}`] ?? 0),
@@ -3282,8 +3441,8 @@ function toQueuedMessage(row) {
3282
3441
  __name(toQueuedMessage, "toQueuedMessage");
3283
3442
 
3284
3443
  // src/drizzle-governance-queries.ts
3285
- import { bucketByActor, bucketByModel, bucketByThread, bucketUsageTrend, dayBoundsUtc, rollupThreadUsage, summarizeGenuiCalls, truncateDetailContent } from "@dudousxd/nestjs-agent-core";
3286
- import { and as and3, asc as asc3, count, desc as desc2, eq as eq3, gte as gte2, inArray as inArray2, isNull as isNull3, like, lte as lte3, sql as sql4, sum } from "drizzle-orm";
3444
+ import { bucketByActor, bucketByFeature, bucketByModel, bucketByThread, bucketUsageBreakdown, bucketUsageTrend, dayBoundsUtc, rollupThreadUsage, summarizeGenuiCalls, truncateDetailContent, usageRangeBounds } from "@dudousxd/nestjs-agent-core";
3445
+ import { and as and3, asc as asc3, count, desc as desc2, eq as eq3, gte as gte2, inArray as inArray2, isNull as isNull3, like, lt, lte as lte3, sql as sql4, sum } from "drizzle-orm";
3287
3446
  var UNCLASSIFIED_ERROR_CODE = "unknown";
3288
3447
  var DEFAULT_AGENT_BUCKET = "(default)";
3289
3448
  var TOOL_CALL_STATUSES = [
@@ -3404,10 +3563,26 @@ function toRecentRunRow(run) {
3404
3563
  retries: run.retries,
3405
3564
  startedAt: run.startedAt.toISOString(),
3406
3565
  promptHash: run.promptHash ?? null,
3407
- parentRunId: run.parentRunId ?? null
3566
+ parentRunId: run.parentRunId ?? null,
3567
+ queueMs: run.queueMs ?? null,
3568
+ modelMs: run.modelMs ?? null,
3569
+ toolsMs: run.toolsMs ?? null,
3570
+ modelCalls: run.modelCalls ?? null,
3571
+ toolCalls: run.toolCalls ?? null
3408
3572
  };
3409
3573
  }
3410
3574
  __name(toRecentRunRow, "toRecentRunRow");
3575
+ function toRunTimings(run) {
3576
+ if (!Array.isArray(run.timings)) return null;
3577
+ return {
3578
+ modelMs: run.modelMs ?? 0,
3579
+ toolsMs: run.toolsMs ?? 0,
3580
+ modelCalls: run.modelCalls ?? 0,
3581
+ toolCalls: run.toolCalls ?? 0,
3582
+ steps: run.timings
3583
+ };
3584
+ }
3585
+ __name(toRunTimings, "toRunTimings");
3411
3586
  function toRunToolCallRow(toolCall) {
3412
3587
  return {
3413
3588
  toolCallId: toolCall.id,
@@ -3423,6 +3598,12 @@ function toRunToolCallRow(toolCall) {
3423
3598
  __name(toRunToolCallRow, "toRunToolCallRow");
3424
3599
  function toUsageInput(row) {
3425
3600
  return {
3601
+ feature: row.feature ?? null,
3602
+ purpose: row.purpose ?? null,
3603
+ costSource: row.costSource ?? null,
3604
+ audioSeconds: row.audioSeconds == null ? null : Number(row.audioSeconds),
3605
+ characters: row.characters == null ? null : Number(row.characters),
3606
+ requests: row.requests ?? null,
3426
3607
  modelId: row.modelId,
3427
3608
  actorRef: row.actorRef,
3428
3609
  threadId: row.threadId,
@@ -3470,8 +3651,13 @@ var DrizzleGovernanceQueries = class {
3470
3651
  }
3471
3652
  return pricing;
3472
3653
  }
3473
- async usageInRange(range) {
3474
- const { start, end } = dayBoundsUtc(range);
3654
+ /**
3655
+ * The usage rows in range, with only what the bucketers read: `requests` always (a recorded row
3656
+ * counts as its own requests in every rollup), and the feature/unit columns only for the
3657
+ * per-feature reads that group by them.
3658
+ */
3659
+ async usageInRange(range, withFeature = false) {
3660
+ const { start, end } = usageRangeBounds(range);
3475
3661
  const usage = this.t.agentTokenUsage;
3476
3662
  const rows = await this.db.select({
3477
3663
  modelId: usage.modelId,
@@ -3482,14 +3668,30 @@ var DrizzleGovernanceQueries = class {
3482
3668
  outputTokens: usage.outputTokens,
3483
3669
  cacheWriteTokens: usage.cacheWriteTokens,
3484
3670
  cacheReadTokens: usage.cacheReadTokens,
3485
- costUsd: usage.costUsd
3486
- }).from(usage).where(and3(gte2(this.t.agentTokenUsage.createdAt, start), lte3(this.t.agentTokenUsage.createdAt, end)));
3671
+ costUsd: usage.costUsd,
3672
+ requests: usage.requests,
3673
+ ...withFeature ? {
3674
+ feature: usage.feature,
3675
+ purpose: usage.purpose,
3676
+ costSource: usage.costSource,
3677
+ audioSeconds: usage.audioSeconds,
3678
+ characters: usage.characters
3679
+ } : {}
3680
+ }).from(usage).where(and3(gte2(this.t.agentTokenUsage.createdAt, start), lt(this.t.agentTokenUsage.createdAt, end)));
3487
3681
  return rows.map(toUsageInput);
3488
3682
  }
3489
3683
  async spendByModel(range) {
3490
3684
  const pricing = await this.loadPricing();
3491
3685
  return bucketByModel(await this.usageInRange(range), pricing);
3492
3686
  }
3687
+ async spendByFeature(range) {
3688
+ const pricing = await this.loadPricing();
3689
+ return bucketByFeature(await this.usageInRange(range, true), pricing);
3690
+ }
3691
+ async usageBreakdown(range, filter) {
3692
+ const pricing = await this.loadPricing();
3693
+ return bucketUsageBreakdown(await this.usageInRange(range, true), pricing, filter);
3694
+ }
3493
3695
  async spendByActor(range) {
3494
3696
  const pricing = await this.loadPricing();
3495
3697
  return bucketByActor(await this.usageInRange(range), pricing);
@@ -3503,7 +3705,9 @@ var DrizzleGovernanceQueries = class {
3503
3705
  const pricing = await this.loadPricing();
3504
3706
  const rows = await this.usageInRange(range);
3505
3707
  const threadIds = [
3506
- ...new Set(rows.map((row) => row.threadId))
3708
+ ...new Set(rows.flatMap((row) => row.threadId === null ? [] : [
3709
+ row.threadId
3710
+ ]))
3507
3711
  ];
3508
3712
  if (threadIds.length === 0) {
3509
3713
  return [];
@@ -3581,6 +3785,7 @@ var DrizzleGovernanceQueries = class {
3581
3785
  outputTokens: sum(this.t.agentTokenUsage.outputTokens)
3582
3786
  }).from(this.t.agentTokenUsage).where(inArray2(this.t.agentTokenUsage.threadId, threadIds)).groupBy(this.t.agentTokenUsage.threadId);
3583
3787
  for (const row of usageTotals) {
3788
+ if (row.threadId === null) continue;
3584
3789
  const bucket = rollups.get(row.threadId);
3585
3790
  if (bucket !== void 0) {
3586
3791
  bucket.totalTokens = Number(row.inputTokens ?? 0) + Number(row.outputTokens ?? 0);
@@ -3957,7 +4162,8 @@ var DrizzleGovernanceQueries = class {
3957
4162
  actorRef: row.thread.actorRef,
3958
4163
  deleted: row.thread.deletedAt != null
3959
4164
  },
3960
- toolCalls: toolCalls.map(toRunToolCallRow)
4165
+ toolCalls: toolCalls.map(toRunToolCallRow),
4166
+ timings: toRunTimings(row.run)
3961
4167
  };
3962
4168
  }
3963
4169
  /**
@@ -4241,7 +4447,7 @@ var DrizzlePricingStore = class {
4241
4447
 
4242
4448
  // src/drizzle-token-stream-sink.ts
4243
4449
  import { SqlTokenStreamSink } from "@dudousxd/nestjs-agent-core";
4244
- import { and as and6, asc as asc4, eq as eq6, gt as gt2, inArray as inArray4, lt, max as max3, sql as sql5 } from "drizzle-orm";
4450
+ import { and as and6, asc as asc4, eq as eq6, gt as gt2, inArray as inArray4, lt as lt2, max as max3, sql as sql5 } from "drizzle-orm";
4245
4451
  var DrizzleStreamFrameTable = class {
4246
4452
  static {
4247
4453
  __name(this, "DrizzleStreamFrameTable");
@@ -4281,7 +4487,7 @@ var DrizzleStreamFrameTable = class {
4281
4487
  async lapsedRuns(cutoff) {
4282
4488
  const rows = await this.db.select({
4283
4489
  runId: this.t.agentStreamFrame.runId
4284
- }).from(this.t.agentStreamFrame).groupBy(this.t.agentStreamFrame.runId).having(lt(max3(this.t.agentStreamFrame.createdAt), cutoff));
4490
+ }).from(this.t.agentStreamFrame).groupBy(this.t.agentStreamFrame.runId).having(lt2(max3(this.t.agentStreamFrame.createdAt), cutoff));
4285
4491
  return rows.map((row) => row.runId);
4286
4492
  }
4287
4493
  isUniqueViolation(error) {
@@ -4298,7 +4504,7 @@ var DrizzleTokenStreamSink = class extends SqlTokenStreamSink {
4298
4504
  };
4299
4505
 
4300
4506
  // src/drizzle-confirm-token-store.ts
4301
- import { eq as eq7, lt as lt2 } from "drizzle-orm";
4507
+ import { eq as eq7, lt as lt3 } from "drizzle-orm";
4302
4508
  var DrizzleConfirmTokenStore = class {
4303
4509
  static {
4304
4510
  __name(this, "DrizzleConfirmTokenStore");
@@ -4332,7 +4538,7 @@ var DrizzleConfirmTokenStore = class {
4332
4538
  }
4333
4539
  async purgeExpired(now = Date.now()) {
4334
4540
  const table = this.t.agentConfirmToken;
4335
- return affectedRows(this.dialect, this.db.delete(table).where(lt2(table.expiresAt, now)), table.hash);
4541
+ return affectedRows(this.dialect, this.db.delete(table).where(lt3(table.expiresAt, now)), table.hash);
4336
4542
  }
4337
4543
  };
4338
4544
 
@@ -4410,7 +4616,7 @@ var DrizzleChannelStore = class {
4410
4616
 
4411
4617
  // src/drizzle-poppy-store.ts
4412
4618
  import { newEventId } from "@dudousxd/nestjs-agent-core";
4413
- import { and as and8, desc as desc3, eq as eq9, gt as gt4, isNull as isNull4, lt as lt3, lte as lte5, or as or3 } from "drizzle-orm";
4619
+ import { and as and8, desc as desc3, eq as eq9, gt as gt4, isNull as isNull4, lt as lt4, lte as lte5, or as or3 } from "drizzle-orm";
4414
4620
  var MAX_APPEND_ATTEMPTS = 50;
4415
4621
  var sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
4416
4622
  var nullable = /* @__PURE__ */ __name((value) => value == null ? null : String(value), "nullable");
@@ -4595,13 +4801,13 @@ var DrizzlePoppyStore = class {
4595
4801
  if (!last) return;
4596
4802
  const [dropped] = await this.db.select({
4597
4803
  seq: table.seq
4598
- }).from(table).where(and8(eq9(table.conversationId, conversationId), lt3(table.createdAt, before), lt3(table.seq, toInt(last.seq)))).orderBy(desc3(table.seq)).limit(1);
4804
+ }).from(table).where(and8(eq9(table.conversationId, conversationId), lt4(table.createdAt, before), lt4(table.seq, toInt(last.seq)))).orderBy(desc3(table.seq)).limit(1);
4599
4805
  if (!dropped) return;
4600
4806
  const upTo = toInt(dropped.seq);
4601
4807
  const conversations = this.t.agentPoppyConversation;
4602
4808
  await this.db.update(conversations).set({
4603
4809
  prunedSeq: upTo
4604
- }).where(and8(eq9(conversations.id, conversationId), lt3(conversations.prunedSeq, upTo)));
4810
+ }).where(and8(eq9(conversations.id, conversationId), lt4(conversations.prunedSeq, upTo)));
4605
4811
  await this.db.delete(table).where(and8(eq9(table.conversationId, conversationId), lte5(table.seq, upTo)));
4606
4812
  }
4607
4813
  async claimMessage(owner, messageId, receipt) {
@@ -4789,7 +4995,7 @@ __name(toConnection, "toConnection");
4789
4995
  // src/drizzle-rag-ingestion-log.ts
4790
4996
  import { subscribe, unsubscribe } from "diagnostics_channel";
4791
4997
  import { Injectable, Logger } from "@nestjs/common";
4792
- import { and as and10, asc as asc6, count as count2, desc as desc4, eq as eq11, gt as gt5, lt as lt4, or as or4 } from "drizzle-orm";
4998
+ import { and as and10, asc as asc6, count as count2, desc as desc4, eq as eq11, gt as gt5, lt as lt5, or as or4 } from "drizzle-orm";
4793
4999
  function _ts_decorate(decorators, target, key2, desc5) {
4794
5000
  var c = arguments.length, r = c < 3 ? target : desc5 === null ? desc5 = Object.getOwnPropertyDescriptor(target, key2) : desc5, d;
4795
5001
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -5043,7 +5249,7 @@ var DrizzleRagIngestionLog = class _DrizzleRagIngestionLog {
5043
5249
  if (cursor === void 0) {
5044
5250
  return base;
5045
5251
  }
5046
- return and10(base, or4(lt4(this.t.ragIngestionLog.updatedAt, cursor.updatedAt), and10(eq11(this.t.ragIngestionLog.updatedAt, cursor.updatedAt), gt5(this.t.ragIngestionLog.documentId, cursor.documentId))));
5252
+ return and10(base, or4(lt5(this.t.ragIngestionLog.updatedAt, cursor.updatedAt), and10(eq11(this.t.ragIngestionLog.updatedAt, cursor.updatedAt), gt5(this.t.ragIngestionLog.documentId, cursor.documentId))));
5047
5253
  }
5048
5254
  /**
5049
5255
  * Upsert one outcome. Keyed by document id so the row always reflects the *current* state — a