@dudousxd/nestjs-agent-store-drizzle 0.7.1 → 0.8.1

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
@@ -543,8 +543,8 @@ var DrizzleAgentStore = class {
543
543
  const start = /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
544
544
  const end = /* @__PURE__ */ new Date(`${day}T23:59:59.999Z`);
545
545
  const rows = await this.db.select().from(agentTokenUsage).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(agentTokenUsage.actorRef, actorRef), (0, import_drizzle_orm2.gte)(agentTokenUsage.createdAt, start), (0, import_drizzle_orm2.lte)(agentTokenUsage.createdAt, end)));
546
- const usedTokens = rows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0);
547
- const costUsd = rows.reduce((sum, row) => sum + (row.costUsd ?? 0), 0);
546
+ const usedTokens = rows.reduce((sum2, row) => sum2 + row.inputTokens + row.outputTokens, 0);
547
+ const costUsd = rows.reduce((sum2, row) => sum2 + (row.costUsd ?? 0), 0);
548
548
  return {
549
549
  usedTokens,
550
550
  costUsd
@@ -644,6 +644,35 @@ function emptyPage(query) {
644
644
  };
645
645
  }
646
646
  __name(emptyPage, "emptyPage");
647
+ function toRecentRunRow(run) {
648
+ return {
649
+ runId: run.id,
650
+ threadId: run.threadId,
651
+ actorRef: run.actorRef,
652
+ agentName: run.agentName ?? null,
653
+ status: run.status,
654
+ durationMs: run.durationMs ?? null,
655
+ errorCode: run.errorCode ?? null,
656
+ errorMessage: run.errorMessage ?? null,
657
+ retries: run.retries,
658
+ startedAt: run.startedAt.toISOString(),
659
+ promptHash: run.promptHash ?? null
660
+ };
661
+ }
662
+ __name(toRecentRunRow, "toRecentRunRow");
663
+ function toRunToolCallRow(toolCall) {
664
+ return {
665
+ toolCallId: toolCall.id,
666
+ toolName: toolCall.toolName,
667
+ toolType: toolCall.toolType,
668
+ status: toolCall.status,
669
+ executionMs: toolCall.executionMs ?? null,
670
+ executedByRef: toolCall.executedByRef ?? null,
671
+ error: toolCall.error ?? null,
672
+ createdAt: toolCall.createdAt.toISOString()
673
+ };
674
+ }
675
+ __name(toRunToolCallRow, "toRunToolCallRow");
647
676
  function toUsageInput(row) {
648
677
  return {
649
678
  modelId: row.modelId,
@@ -753,25 +782,63 @@ var DrizzleGovernanceQueries = class {
753
782
  runId: row.runId
754
783
  }));
755
784
  }
756
- async recentThreads(limit) {
757
- const threads = await this.db.select().from(agentThread).where((0, import_drizzle_orm3.isNull)(agentThread.deletedAt)).orderBy((0, import_drizzle_orm3.desc)(agentThread.updatedAt), (0, import_drizzle_orm3.desc)(agentThread.id)).limit(limit);
758
- const result = [];
759
- for (const thread of threads) {
760
- const [messageCountRow] = await this.db.select({
761
- value: (0, import_drizzle_orm3.count)()
762
- }).from(agentMessage).where((0, import_drizzle_orm3.eq)(agentMessage.threadId, thread.id));
763
- const usageRows = await this.db.select().from(agentTokenUsage).where((0, import_drizzle_orm3.eq)(agentTokenUsage.threadId, thread.id));
764
- const totalTokens = usageRows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0);
765
- result.push({
785
+ /**
786
+ * Message counts and token totals for a whole set of threads in TWO grouped queries, not two per
787
+ * thread — a 200-row page used to cost four hundred round trips. Mirrors the MikroORM adapter's
788
+ * `threadRollups`, which reaches the same numbers without a QueryBuilder.
789
+ */
790
+ async threadRollups(threadIds) {
791
+ const rollups = new Map(threadIds.map((threadId) => [
792
+ threadId,
793
+ {
794
+ messageCount: 0,
795
+ totalTokens: 0
796
+ }
797
+ ]));
798
+ if (threadIds.length === 0) {
799
+ return rollups;
800
+ }
801
+ const messageCounts = await this.db.select({
802
+ threadId: agentMessage.threadId,
803
+ value: (0, import_drizzle_orm3.count)()
804
+ }).from(agentMessage).where((0, import_drizzle_orm3.inArray)(agentMessage.threadId, threadIds)).groupBy(agentMessage.threadId);
805
+ for (const row of messageCounts) {
806
+ const bucket = rollups.get(row.threadId);
807
+ if (bucket !== void 0) {
808
+ bucket.messageCount = row.value;
809
+ }
810
+ }
811
+ const usageTotals = await this.db.select({
812
+ threadId: agentTokenUsage.threadId,
813
+ inputTokens: (0, import_drizzle_orm3.sum)(agentTokenUsage.inputTokens),
814
+ outputTokens: (0, import_drizzle_orm3.sum)(agentTokenUsage.outputTokens)
815
+ }).from(agentTokenUsage).where((0, import_drizzle_orm3.inArray)(agentTokenUsage.threadId, threadIds)).groupBy(agentTokenUsage.threadId);
816
+ for (const row of usageTotals) {
817
+ const bucket = rollups.get(row.threadId);
818
+ if (bucket !== void 0) {
819
+ bucket.totalTokens = Number(row.inputTokens ?? 0) + Number(row.outputTokens ?? 0);
820
+ }
821
+ }
822
+ return rollups;
823
+ }
824
+ /** Decorate an ordered thread list with its batched rollups, preserving the incoming order. */
825
+ async toThreadActivityRows(threads) {
826
+ const rollups = await this.threadRollups(threads.map((thread) => thread.id));
827
+ return threads.map((thread) => {
828
+ const rollup = rollups.get(thread.id);
829
+ return {
766
830
  threadId: thread.id,
767
831
  title: thread.title,
768
832
  actorRef: thread.actorRef,
769
- messageCount: messageCountRow?.value ?? 0,
770
- totalTokens,
833
+ messageCount: rollup?.messageCount ?? 0,
834
+ totalTokens: rollup?.totalTokens ?? 0,
771
835
  lastActivityAt: thread.updatedAt.toISOString()
772
- });
773
- }
774
- return result;
836
+ };
837
+ });
838
+ }
839
+ async recentThreads(limit) {
840
+ const threads = await this.db.select().from(agentThread).where((0, import_drizzle_orm3.isNull)(agentThread.deletedAt)).orderBy((0, import_drizzle_orm3.desc)(agentThread.updatedAt), (0, import_drizzle_orm3.desc)(agentThread.id)).limit(limit);
841
+ return this.toThreadActivityRows(threads);
775
842
  }
776
843
  async runsInRange(range) {
777
844
  const { start, end } = (0, import_nestjs_agent_core.dayBoundsUtc)(range);
@@ -873,19 +940,7 @@ var DrizzleGovernanceQueries = class {
873
940
  }
874
941
  async recentRuns(limit) {
875
942
  const runs = await this.db.select().from(agentRun).orderBy((0, import_drizzle_orm3.desc)(agentRun.startedAt), (0, import_drizzle_orm3.desc)(agentRun.id)).limit(limit);
876
- return runs.map((run) => ({
877
- runId: run.id,
878
- threadId: run.threadId,
879
- actorRef: run.actorRef,
880
- agentName: run.agentName ?? null,
881
- status: run.status,
882
- durationMs: run.durationMs ?? null,
883
- errorCode: run.errorCode ?? null,
884
- errorMessage: run.errorMessage ?? null,
885
- retries: run.retries,
886
- startedAt: run.startedAt.toISOString(),
887
- promptHash: run.promptHash ?? null
888
- }));
943
+ return runs.map(toRecentRunRow);
889
944
  }
890
945
  /**
891
946
  * Tool calls sitting `pending_approval`, oldest first (an inbox drains from the back), joined
@@ -918,8 +973,10 @@ var DrizzleGovernanceQueries = class {
918
973
  }
919
974
  /**
920
975
  * Per-tool call/failure/rejection/latency rollup over the range, highest call count first.
921
- * `p95ExecutionMs` is computed over calls that recorded a non-null `executionMs` (regardless of
922
- * their final status); `null` when none did.
976
+ * `p50ExecutionMs`/`p95ExecutionMs` are computed over calls that recorded a non-null `executionMs`
977
+ * (regardless of their final status); both `null` when none did. Percentiles are taken in-process
978
+ * off the sorted sample rather than in SQL, matching the MikroORM adapter — MySQL has no
979
+ * `PERCENTILE_CONT` and one portable implementation beats three dialect-specific ones.
923
980
  */
924
981
  async toolStats(range) {
925
982
  const { start, end } = (0, import_nestjs_agent_core.dayBoundsUtc)(range);
@@ -952,6 +1009,7 @@ var DrizzleGovernanceQueries = class {
952
1009
  calls: bucket.calls,
953
1010
  failed: bucket.failed,
954
1011
  rejected: bucket.rejected,
1012
+ p50ExecutionMs: percentileMs(bucket.executionMs, 0.5),
955
1013
  p95ExecutionMs: percentileMs(bucket.executionMs, 0.95)
956
1014
  });
957
1015
  }
@@ -1013,22 +1071,7 @@ var DrizzleGovernanceQueries = class {
1013
1071
  value: (0, import_drizzle_orm3.count)()
1014
1072
  }).from(agentThread).where(whereClause);
1015
1073
  const threads = await this.db.select().from(agentThread).where(whereClause).orderBy((0, import_drizzle_orm3.desc)(agentThread.updatedAt), (0, import_drizzle_orm3.desc)(agentThread.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
1016
- const rows = [];
1017
- for (const thread of threads) {
1018
- const [messageCountRow] = await this.db.select({
1019
- value: (0, import_drizzle_orm3.count)()
1020
- }).from(agentMessage).where((0, import_drizzle_orm3.eq)(agentMessage.threadId, thread.id));
1021
- const usageRows = await this.db.select().from(agentTokenUsage).where((0, import_drizzle_orm3.eq)(agentTokenUsage.threadId, thread.id));
1022
- const totalTokens = usageRows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0);
1023
- rows.push({
1024
- threadId: thread.id,
1025
- title: thread.title,
1026
- actorRef: thread.actorRef,
1027
- messageCount: messageCountRow?.value ?? 0,
1028
- totalTokens,
1029
- lastActivityAt: thread.updatedAt.toISOString()
1030
- });
1031
- }
1074
+ const rows = await this.toThreadActivityRows(threads);
1032
1075
  return {
1033
1076
  rows,
1034
1077
  total: totalRow?.value ?? 0,
@@ -1051,24 +1094,146 @@ var DrizzleGovernanceQueries = class {
1051
1094
  }).from(agentRun).where(whereClause);
1052
1095
  const runs = await this.db.select().from(agentRun).where(whereClause).orderBy((0, import_drizzle_orm3.desc)(agentRun.startedAt), (0, import_drizzle_orm3.desc)(agentRun.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
1053
1096
  return {
1054
- rows: runs.map((run) => ({
1055
- runId: run.id,
1056
- threadId: run.threadId,
1057
- actorRef: run.actorRef,
1058
- agentName: run.agentName ?? null,
1059
- status: run.status,
1060
- durationMs: run.durationMs ?? null,
1061
- errorCode: run.errorCode ?? null,
1062
- errorMessage: run.errorMessage ?? null,
1063
- retries: run.retries,
1064
- startedAt: run.startedAt.toISOString(),
1065
- promptHash: run.promptHash ?? null
1097
+ rows: runs.map(toRecentRunRow),
1098
+ total: totalRow?.value ?? 0,
1099
+ page: query.page,
1100
+ pageSize: query.pageSize
1101
+ };
1102
+ }
1103
+ /**
1104
+ * Paged, filterable approvals inbox, oldest first — the same ordering as {@link pendingApprovals},
1105
+ * now with the `total` that method structurally cannot report. The join to `agent_message` /
1106
+ * `agent_thread` is unconditional (it already resolves title/actor/agentName), so the `threadId`/
1107
+ * `actorRef`/`agentName` filters cost nothing extra.
1108
+ *
1109
+ * The order is TOTAL (`created_at asc, id asc`, and `id` is the primary key), so no row can land on
1110
+ * two pages or on none for a given snapshot. Ascending order also means a newly requested approval
1111
+ * appends past the last page instead of shifting the page an operator is currently reading.
1112
+ */
1113
+ async approvalsPage(query) {
1114
+ const filters = query.where;
1115
+ const whereClause = (0, import_drizzle_orm3.and)((0, import_drizzle_orm3.eq)(agentToolCall.status, "pending_approval"), filters?.toolName !== void 0 ? (0, import_drizzle_orm3.eq)(agentToolCall.toolName, filters.toolName) : void 0, filters?.threadId !== void 0 ? (0, import_drizzle_orm3.eq)(agentThread.id, filters.threadId) : void 0, filters?.actorRef !== void 0 ? (0, import_drizzle_orm3.eq)(agentThread.actorRef, filters.actorRef) : void 0, filters?.agentName !== void 0 ? (0, import_drizzle_orm3.eq)(agentMessage.agentName, filters.agentName) : void 0, filters?.fromDay !== void 0 ? (0, import_drizzle_orm3.gte)(agentToolCall.createdAt, dayStartUtc(filters.fromDay)) : void 0, filters?.toDay !== void 0 ? (0, import_drizzle_orm3.lte)(agentToolCall.createdAt, dayEndUtc(filters.toDay)) : void 0);
1116
+ const [totalRow] = await this.db.select({
1117
+ value: (0, import_drizzle_orm3.count)()
1118
+ }).from(agentToolCall).innerJoin(agentMessage, (0, import_drizzle_orm3.eq)(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, (0, import_drizzle_orm3.eq)(agentMessage.threadId, agentThread.id)).where(whereClause);
1119
+ const rows = await this.db.select({
1120
+ toolCallId: agentToolCall.id,
1121
+ toolName: agentToolCall.toolName,
1122
+ input: agentToolCall.input,
1123
+ threadId: agentThread.id,
1124
+ threadTitle: agentThread.title,
1125
+ actorRef: agentThread.actorRef,
1126
+ agentName: agentMessage.agentName,
1127
+ requestedAt: agentToolCall.createdAt,
1128
+ runId: agentToolCall.runId
1129
+ }).from(agentToolCall).innerJoin(agentMessage, (0, import_drizzle_orm3.eq)(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, (0, import_drizzle_orm3.eq)(agentMessage.threadId, agentThread.id)).where(whereClause).orderBy((0, import_drizzle_orm3.asc)(agentToolCall.createdAt), (0, import_drizzle_orm3.asc)(agentToolCall.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
1130
+ return {
1131
+ rows: rows.map((row) => ({
1132
+ toolCallId: row.toolCallId,
1133
+ toolName: row.toolName,
1134
+ input: row.input,
1135
+ threadId: row.threadId,
1136
+ threadTitle: row.threadTitle,
1137
+ actorRef: row.actorRef,
1138
+ agentName: row.agentName ?? null,
1139
+ requestedAt: row.requestedAt.toISOString(),
1140
+ runId: row.runId
1066
1141
  })),
1067
1142
  total: totalRow?.value ?? 0,
1068
1143
  page: query.page,
1069
1144
  pageSize: query.pageSize
1070
1145
  };
1071
1146
  }
1147
+ /**
1148
+ * One run, its owning thread's headline and its tool calls — TWO queries regardless of how many
1149
+ * tools the run called. `toolCalls` comes off `agent_tool_call.run_id`, so a run recorded before
1150
+ * that column was written reports none.
1151
+ */
1152
+ async runDetail(runId) {
1153
+ const [row] = await this.db.select({
1154
+ run: agentRun,
1155
+ thread: agentThread
1156
+ }).from(agentRun).innerJoin(agentThread, (0, import_drizzle_orm3.eq)(agentRun.threadId, agentThread.id)).where((0, import_drizzle_orm3.eq)(agentRun.id, runId)).limit(1);
1157
+ if (row === void 0) {
1158
+ return null;
1159
+ }
1160
+ const toolCalls = await this.db.select().from(agentToolCall).where((0, import_drizzle_orm3.eq)(agentToolCall.runId, runId)).orderBy((0, import_drizzle_orm3.asc)(agentToolCall.createdAt), (0, import_drizzle_orm3.asc)(agentToolCall.id));
1161
+ return {
1162
+ run: toRecentRunRow(row.run),
1163
+ thread: {
1164
+ threadId: row.thread.id,
1165
+ title: row.thread.title,
1166
+ actorRef: row.thread.actorRef,
1167
+ deleted: row.thread.deletedAt != null
1168
+ },
1169
+ toolCalls: toolCalls.map(toRunToolCallRow)
1170
+ };
1171
+ }
1172
+ /**
1173
+ * One thread with its lifetime usage rollup, its newest runs and its newest messages. A fixed six
1174
+ * queries (thread, usage, message count, message page, that page's tool-call counts, runs+count) —
1175
+ * the per-message tool-call counts are ONE grouped read over the returned message ids, not one per
1176
+ * message. Soft-deleted threads are returned with `deleted: true`; an audit still needs them.
1177
+ */
1178
+ async threadDetail(query) {
1179
+ const [thread] = await this.db.select().from(agentThread).where((0, import_drizzle_orm3.eq)(agentThread.id, query.threadId)).limit(1);
1180
+ if (thread === void 0) {
1181
+ return null;
1182
+ }
1183
+ const pricing = await this.loadPricing();
1184
+ const usageRows = await this.db.select().from(agentTokenUsage).where((0, import_drizzle_orm3.eq)(agentTokenUsage.threadId, thread.id));
1185
+ const usage = (0, import_nestjs_agent_core.rollupThreadUsage)(usageRows.map(toUsageInput), pricing);
1186
+ const [messageCountRow] = await this.db.select({
1187
+ value: (0, import_drizzle_orm3.count)()
1188
+ }).from(agentMessage).where((0, import_drizzle_orm3.eq)(agentMessage.threadId, thread.id));
1189
+ const messages = await this.db.select().from(agentMessage).where((0, import_drizzle_orm3.eq)(agentMessage.threadId, thread.id)).orderBy((0, import_drizzle_orm3.desc)(agentMessage.createdAt), (0, import_drizzle_orm3.desc)(agentMessage.id)).limit(query.messageLimit);
1190
+ const toolCallCounts = await this.toolCallCountsByMessage(messages.map((message) => message.id));
1191
+ const [runTotalRow] = await this.db.select({
1192
+ value: (0, import_drizzle_orm3.count)()
1193
+ }).from(agentRun).where((0, import_drizzle_orm3.eq)(agentRun.threadId, thread.id));
1194
+ const runs = await this.db.select().from(agentRun).where((0, import_drizzle_orm3.eq)(agentRun.threadId, thread.id)).orderBy((0, import_drizzle_orm3.desc)(agentRun.startedAt), (0, import_drizzle_orm3.desc)(agentRun.id)).limit(query.runLimit);
1195
+ return {
1196
+ thread: {
1197
+ threadId: thread.id,
1198
+ title: thread.title,
1199
+ actorRef: thread.actorRef,
1200
+ messageCount: messageCountRow?.value ?? 0,
1201
+ totalTokens: usage.totalTokens,
1202
+ lastActivityAt: thread.updatedAt.toISOString()
1203
+ },
1204
+ deleted: thread.deletedAt != null,
1205
+ usage,
1206
+ runs: runs.map(toRecentRunRow),
1207
+ runTotal: runTotalRow?.value ?? 0,
1208
+ messages: messages.map((message) => {
1209
+ const { content, truncated } = (0, import_nestjs_agent_core.truncateDetailContent)(message.content);
1210
+ return {
1211
+ messageId: message.id,
1212
+ role: message.role,
1213
+ content,
1214
+ truncated,
1215
+ agentName: message.agentName ?? null,
1216
+ toolCallCount: toolCallCounts.get(message.id) ?? 0,
1217
+ createdAt: message.createdAt.toISOString()
1218
+ };
1219
+ })
1220
+ };
1221
+ }
1222
+ /** Tool-call counts for a set of messages in ONE grouped query — the detail view's no-N+1 guarantee. */
1223
+ async toolCallCountsByMessage(messageIds) {
1224
+ const counts = /* @__PURE__ */ new Map();
1225
+ if (messageIds.length === 0) {
1226
+ return counts;
1227
+ }
1228
+ const rows = await this.db.select({
1229
+ messageId: agentToolCall.messageId,
1230
+ value: (0, import_drizzle_orm3.count)()
1231
+ }).from(agentToolCall).where((0, import_drizzle_orm3.inArray)(agentToolCall.messageId, messageIds)).groupBy(agentToolCall.messageId);
1232
+ for (const row of rows) {
1233
+ counts.set(row.messageId, row.value);
1234
+ }
1235
+ return counts;
1236
+ }
1072
1237
  };
1073
1238
 
1074
1239
  // src/drizzle-pricing-store.ts