@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 +225 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -3
- package/dist/index.d.ts +39 -3
- package/dist/index.js +227 -62
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
2
2
|
import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
|
|
3
|
-
import { MessageRole, ToolCallRequest, ToolResult, MessageUsage, ToolKind, ToolCallStatus, UsagePurpose, AgentStore, CreateThreadInput, ThreadSummary, ThreadDetail, AppendMessageInput, StoredMessage, RecordToolCallInput, UpdateToolCallInput, RecordUsageInput, AgentGovernanceQueries, AgentPricingStore, GovernanceRange, ModelSpendRow, ActorSpendRow, ThreadSpendRow, UsageTrendPoint, ToolCallActivityRow, ThreadActivityRow, RunMetrics, RunAgentBreakdownRow, RunErrorBreakdownRow, RunTrendPoint, RecentRunRow, PendingApprovalRow, ToolStatRow, GovernancePageQuery, ToolCallWhere, GovernancePage, ThreadWhere, RunWhere, ModelPriceInput, CurrentModelPrice } from '@dudousxd/nestjs-agent-core';
|
|
3
|
+
import { MessageRole, ToolCallRequest, ToolResult, MessageUsage, ToolKind, ToolCallStatus, UsagePurpose, AgentStore, CreateThreadInput, ThreadSummary, ThreadDetail, AppendMessageInput, StoredMessage, RecordToolCallInput, UpdateToolCallInput, RecordUsageInput, AgentGovernanceQueries, AgentPricingStore, GovernanceRange, ModelSpendRow, ActorSpendRow, ThreadSpendRow, UsageTrendPoint, ToolCallActivityRow, ThreadActivityRow, RunMetrics, RunAgentBreakdownRow, RunErrorBreakdownRow, RunTrendPoint, RecentRunRow, PendingApprovalRow, ToolStatRow, GovernancePageQuery, ToolCallWhere, GovernancePage, ThreadWhere, RunWhere, ApprovalWhere, GovernanceRunDetail, GovernanceThreadDetailQuery, GovernanceThreadDetail, ModelPriceInput, CurrentModelPrice } from '@dudousxd/nestjs-agent-core';
|
|
4
4
|
import { Type, DynamicModule } from '@nestjs/common';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -2550,6 +2550,14 @@ declare class DrizzleGovernanceQueries implements AgentGovernanceQueries {
|
|
|
2550
2550
|
spendByThread(range: GovernanceRange, limit: number): Promise<ThreadSpendRow[]>;
|
|
2551
2551
|
usageTrend(range: GovernanceRange): Promise<UsageTrendPoint[]>;
|
|
2552
2552
|
recentToolCalls(limit: number): Promise<ToolCallActivityRow[]>;
|
|
2553
|
+
/**
|
|
2554
|
+
* Message counts and token totals for a whole set of threads in TWO grouped queries, not two per
|
|
2555
|
+
* thread — a 200-row page used to cost four hundred round trips. Mirrors the MikroORM adapter's
|
|
2556
|
+
* `threadRollups`, which reaches the same numbers without a QueryBuilder.
|
|
2557
|
+
*/
|
|
2558
|
+
private threadRollups;
|
|
2559
|
+
/** Decorate an ordered thread list with its batched rollups, preserving the incoming order. */
|
|
2560
|
+
private toThreadActivityRows;
|
|
2553
2561
|
recentThreads(limit: number): Promise<ThreadActivityRow[]>;
|
|
2554
2562
|
private runsInRange;
|
|
2555
2563
|
runMetrics(range: GovernanceRange): Promise<RunMetrics>;
|
|
@@ -2565,8 +2573,10 @@ declare class DrizzleGovernanceQueries implements AgentGovernanceQueries {
|
|
|
2565
2573
|
pendingApprovals(limit: number): Promise<PendingApprovalRow[]>;
|
|
2566
2574
|
/**
|
|
2567
2575
|
* Per-tool call/failure/rejection/latency rollup over the range, highest call count first.
|
|
2568
|
-
* `p95ExecutionMs`
|
|
2569
|
-
* their final status); `null` when none did.
|
|
2576
|
+
* `p50ExecutionMs`/`p95ExecutionMs` are computed over calls that recorded a non-null `executionMs`
|
|
2577
|
+
* (regardless of their final status); both `null` when none did. Percentiles are taken in-process
|
|
2578
|
+
* off the sorted sample rather than in SQL, matching the MikroORM adapter — MySQL has no
|
|
2579
|
+
* `PERCENTILE_CONT` and one portable implementation beats three dialect-specific ones.
|
|
2570
2580
|
*/
|
|
2571
2581
|
toolStats(range: GovernanceRange): Promise<ToolStatRow[]>;
|
|
2572
2582
|
/**
|
|
@@ -2588,6 +2598,32 @@ declare class DrizzleGovernanceQueries implements AgentGovernanceQueries {
|
|
|
2588
2598
|
* values outside the known literals can never match a row — short-circuits to an empty page.
|
|
2589
2599
|
*/
|
|
2590
2600
|
runsPage(query: GovernancePageQuery<RunWhere>): Promise<GovernancePage<RecentRunRow>>;
|
|
2601
|
+
/**
|
|
2602
|
+
* Paged, filterable approvals inbox, oldest first — the same ordering as {@link pendingApprovals},
|
|
2603
|
+
* now with the `total` that method structurally cannot report. The join to `agent_message` /
|
|
2604
|
+
* `agent_thread` is unconditional (it already resolves title/actor/agentName), so the `threadId`/
|
|
2605
|
+
* `actorRef`/`agentName` filters cost nothing extra.
|
|
2606
|
+
*
|
|
2607
|
+
* The order is TOTAL (`created_at asc, id asc`, and `id` is the primary key), so no row can land on
|
|
2608
|
+
* two pages or on none for a given snapshot. Ascending order also means a newly requested approval
|
|
2609
|
+
* appends past the last page instead of shifting the page an operator is currently reading.
|
|
2610
|
+
*/
|
|
2611
|
+
approvalsPage(query: GovernancePageQuery<ApprovalWhere>): Promise<GovernancePage<PendingApprovalRow>>;
|
|
2612
|
+
/**
|
|
2613
|
+
* One run, its owning thread's headline and its tool calls — TWO queries regardless of how many
|
|
2614
|
+
* tools the run called. `toolCalls` comes off `agent_tool_call.run_id`, so a run recorded before
|
|
2615
|
+
* that column was written reports none.
|
|
2616
|
+
*/
|
|
2617
|
+
runDetail(runId: string): Promise<GovernanceRunDetail | null>;
|
|
2618
|
+
/**
|
|
2619
|
+
* One thread with its lifetime usage rollup, its newest runs and its newest messages. A fixed six
|
|
2620
|
+
* queries (thread, usage, message count, message page, that page's tool-call counts, runs+count) —
|
|
2621
|
+
* the per-message tool-call counts are ONE grouped read over the returned message ids, not one per
|
|
2622
|
+
* message. Soft-deleted threads are returned with `deleted: true`; an audit still needs them.
|
|
2623
|
+
*/
|
|
2624
|
+
threadDetail(query: GovernanceThreadDetailQuery): Promise<GovernanceThreadDetail | null>;
|
|
2625
|
+
/** Tool-call counts for a set of messages in ONE grouped query — the detail view's no-N+1 guarantee. */
|
|
2626
|
+
private toolCallCountsByMessage;
|
|
2591
2627
|
}
|
|
2592
2628
|
|
|
2593
2629
|
/**
|
package/dist/index.js
CHANGED
|
@@ -508,8 +508,8 @@ var DrizzleAgentStore = class {
|
|
|
508
508
|
const start = /* @__PURE__ */ new Date(`${day}T00:00:00.000Z`);
|
|
509
509
|
const end = /* @__PURE__ */ new Date(`${day}T23:59:59.999Z`);
|
|
510
510
|
const rows = await this.db.select().from(agentTokenUsage).where(and(eq(agentTokenUsage.actorRef, actorRef), gte(agentTokenUsage.createdAt, start), lte(agentTokenUsage.createdAt, end)));
|
|
511
|
-
const usedTokens = rows.reduce((
|
|
512
|
-
const costUsd = rows.reduce((
|
|
511
|
+
const usedTokens = rows.reduce((sum2, row) => sum2 + row.inputTokens + row.outputTokens, 0);
|
|
512
|
+
const costUsd = rows.reduce((sum2, row) => sum2 + (row.costUsd ?? 0), 0);
|
|
513
513
|
return {
|
|
514
514
|
usedTokens,
|
|
515
515
|
costUsd
|
|
@@ -553,8 +553,8 @@ var DrizzleAgentStore = class {
|
|
|
553
553
|
};
|
|
554
554
|
|
|
555
555
|
// src/drizzle-governance-queries.ts
|
|
556
|
-
import { bucketByActor, bucketByModel, bucketByThread, bucketUsageTrend, dayBoundsUtc } from "@dudousxd/nestjs-agent-core";
|
|
557
|
-
import { and as and2, count, desc as desc2, eq as eq2, gte as gte2, inArray as inArray2, isNull as isNull2, like, lte as lte2, sql as sql3 } from "drizzle-orm";
|
|
556
|
+
import { bucketByActor, bucketByModel, bucketByThread, bucketUsageTrend, dayBoundsUtc, rollupThreadUsage, truncateDetailContent } from "@dudousxd/nestjs-agent-core";
|
|
557
|
+
import { and as and2, asc as asc2, count, desc as desc2, eq as eq2, gte as gte2, inArray as inArray2, isNull as isNull2, like, lte as lte2, sql as sql3, sum } from "drizzle-orm";
|
|
558
558
|
var UNCLASSIFIED_ERROR_CODE = "unknown";
|
|
559
559
|
var DEFAULT_AGENT_BUCKET = "(default)";
|
|
560
560
|
var TOOL_CALL_STATUSES = [
|
|
@@ -609,6 +609,35 @@ function emptyPage(query) {
|
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
611
|
__name(emptyPage, "emptyPage");
|
|
612
|
+
function toRecentRunRow(run) {
|
|
613
|
+
return {
|
|
614
|
+
runId: run.id,
|
|
615
|
+
threadId: run.threadId,
|
|
616
|
+
actorRef: run.actorRef,
|
|
617
|
+
agentName: run.agentName ?? null,
|
|
618
|
+
status: run.status,
|
|
619
|
+
durationMs: run.durationMs ?? null,
|
|
620
|
+
errorCode: run.errorCode ?? null,
|
|
621
|
+
errorMessage: run.errorMessage ?? null,
|
|
622
|
+
retries: run.retries,
|
|
623
|
+
startedAt: run.startedAt.toISOString(),
|
|
624
|
+
promptHash: run.promptHash ?? null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
__name(toRecentRunRow, "toRecentRunRow");
|
|
628
|
+
function toRunToolCallRow(toolCall) {
|
|
629
|
+
return {
|
|
630
|
+
toolCallId: toolCall.id,
|
|
631
|
+
toolName: toolCall.toolName,
|
|
632
|
+
toolType: toolCall.toolType,
|
|
633
|
+
status: toolCall.status,
|
|
634
|
+
executionMs: toolCall.executionMs ?? null,
|
|
635
|
+
executedByRef: toolCall.executedByRef ?? null,
|
|
636
|
+
error: toolCall.error ?? null,
|
|
637
|
+
createdAt: toolCall.createdAt.toISOString()
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
__name(toRunToolCallRow, "toRunToolCallRow");
|
|
612
641
|
function toUsageInput(row) {
|
|
613
642
|
return {
|
|
614
643
|
modelId: row.modelId,
|
|
@@ -718,25 +747,63 @@ var DrizzleGovernanceQueries = class {
|
|
|
718
747
|
runId: row.runId
|
|
719
748
|
}));
|
|
720
749
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
750
|
+
/**
|
|
751
|
+
* Message counts and token totals for a whole set of threads in TWO grouped queries, not two per
|
|
752
|
+
* thread — a 200-row page used to cost four hundred round trips. Mirrors the MikroORM adapter's
|
|
753
|
+
* `threadRollups`, which reaches the same numbers without a QueryBuilder.
|
|
754
|
+
*/
|
|
755
|
+
async threadRollups(threadIds) {
|
|
756
|
+
const rollups = new Map(threadIds.map((threadId) => [
|
|
757
|
+
threadId,
|
|
758
|
+
{
|
|
759
|
+
messageCount: 0,
|
|
760
|
+
totalTokens: 0
|
|
761
|
+
}
|
|
762
|
+
]));
|
|
763
|
+
if (threadIds.length === 0) {
|
|
764
|
+
return rollups;
|
|
765
|
+
}
|
|
766
|
+
const messageCounts = await this.db.select({
|
|
767
|
+
threadId: agentMessage.threadId,
|
|
768
|
+
value: count()
|
|
769
|
+
}).from(agentMessage).where(inArray2(agentMessage.threadId, threadIds)).groupBy(agentMessage.threadId);
|
|
770
|
+
for (const row of messageCounts) {
|
|
771
|
+
const bucket = rollups.get(row.threadId);
|
|
772
|
+
if (bucket !== void 0) {
|
|
773
|
+
bucket.messageCount = row.value;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const usageTotals = await this.db.select({
|
|
777
|
+
threadId: agentTokenUsage.threadId,
|
|
778
|
+
inputTokens: sum(agentTokenUsage.inputTokens),
|
|
779
|
+
outputTokens: sum(agentTokenUsage.outputTokens)
|
|
780
|
+
}).from(agentTokenUsage).where(inArray2(agentTokenUsage.threadId, threadIds)).groupBy(agentTokenUsage.threadId);
|
|
781
|
+
for (const row of usageTotals) {
|
|
782
|
+
const bucket = rollups.get(row.threadId);
|
|
783
|
+
if (bucket !== void 0) {
|
|
784
|
+
bucket.totalTokens = Number(row.inputTokens ?? 0) + Number(row.outputTokens ?? 0);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return rollups;
|
|
788
|
+
}
|
|
789
|
+
/** Decorate an ordered thread list with its batched rollups, preserving the incoming order. */
|
|
790
|
+
async toThreadActivityRows(threads) {
|
|
791
|
+
const rollups = await this.threadRollups(threads.map((thread) => thread.id));
|
|
792
|
+
return threads.map((thread) => {
|
|
793
|
+
const rollup = rollups.get(thread.id);
|
|
794
|
+
return {
|
|
731
795
|
threadId: thread.id,
|
|
732
796
|
title: thread.title,
|
|
733
797
|
actorRef: thread.actorRef,
|
|
734
|
-
messageCount:
|
|
735
|
-
totalTokens,
|
|
798
|
+
messageCount: rollup?.messageCount ?? 0,
|
|
799
|
+
totalTokens: rollup?.totalTokens ?? 0,
|
|
736
800
|
lastActivityAt: thread.updatedAt.toISOString()
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
async recentThreads(limit) {
|
|
805
|
+
const threads = await this.db.select().from(agentThread).where(isNull2(agentThread.deletedAt)).orderBy(desc2(agentThread.updatedAt), desc2(agentThread.id)).limit(limit);
|
|
806
|
+
return this.toThreadActivityRows(threads);
|
|
740
807
|
}
|
|
741
808
|
async runsInRange(range) {
|
|
742
809
|
const { start, end } = dayBoundsUtc(range);
|
|
@@ -838,19 +905,7 @@ var DrizzleGovernanceQueries = class {
|
|
|
838
905
|
}
|
|
839
906
|
async recentRuns(limit) {
|
|
840
907
|
const runs = await this.db.select().from(agentRun).orderBy(desc2(agentRun.startedAt), desc2(agentRun.id)).limit(limit);
|
|
841
|
-
return runs.map(
|
|
842
|
-
runId: run.id,
|
|
843
|
-
threadId: run.threadId,
|
|
844
|
-
actorRef: run.actorRef,
|
|
845
|
-
agentName: run.agentName ?? null,
|
|
846
|
-
status: run.status,
|
|
847
|
-
durationMs: run.durationMs ?? null,
|
|
848
|
-
errorCode: run.errorCode ?? null,
|
|
849
|
-
errorMessage: run.errorMessage ?? null,
|
|
850
|
-
retries: run.retries,
|
|
851
|
-
startedAt: run.startedAt.toISOString(),
|
|
852
|
-
promptHash: run.promptHash ?? null
|
|
853
|
-
}));
|
|
908
|
+
return runs.map(toRecentRunRow);
|
|
854
909
|
}
|
|
855
910
|
/**
|
|
856
911
|
* Tool calls sitting `pending_approval`, oldest first (an inbox drains from the back), joined
|
|
@@ -883,8 +938,10 @@ var DrizzleGovernanceQueries = class {
|
|
|
883
938
|
}
|
|
884
939
|
/**
|
|
885
940
|
* Per-tool call/failure/rejection/latency rollup over the range, highest call count first.
|
|
886
|
-
* `p95ExecutionMs`
|
|
887
|
-
* their final status); `null` when none did.
|
|
941
|
+
* `p50ExecutionMs`/`p95ExecutionMs` are computed over calls that recorded a non-null `executionMs`
|
|
942
|
+
* (regardless of their final status); both `null` when none did. Percentiles are taken in-process
|
|
943
|
+
* off the sorted sample rather than in SQL, matching the MikroORM adapter — MySQL has no
|
|
944
|
+
* `PERCENTILE_CONT` and one portable implementation beats three dialect-specific ones.
|
|
888
945
|
*/
|
|
889
946
|
async toolStats(range) {
|
|
890
947
|
const { start, end } = dayBoundsUtc(range);
|
|
@@ -917,6 +974,7 @@ var DrizzleGovernanceQueries = class {
|
|
|
917
974
|
calls: bucket.calls,
|
|
918
975
|
failed: bucket.failed,
|
|
919
976
|
rejected: bucket.rejected,
|
|
977
|
+
p50ExecutionMs: percentileMs(bucket.executionMs, 0.5),
|
|
920
978
|
p95ExecutionMs: percentileMs(bucket.executionMs, 0.95)
|
|
921
979
|
});
|
|
922
980
|
}
|
|
@@ -978,22 +1036,7 @@ var DrizzleGovernanceQueries = class {
|
|
|
978
1036
|
value: count()
|
|
979
1037
|
}).from(agentThread).where(whereClause);
|
|
980
1038
|
const threads = await this.db.select().from(agentThread).where(whereClause).orderBy(desc2(agentThread.updatedAt), desc2(agentThread.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
981
|
-
const rows =
|
|
982
|
-
for (const thread of threads) {
|
|
983
|
-
const [messageCountRow] = await this.db.select({
|
|
984
|
-
value: count()
|
|
985
|
-
}).from(agentMessage).where(eq2(agentMessage.threadId, thread.id));
|
|
986
|
-
const usageRows = await this.db.select().from(agentTokenUsage).where(eq2(agentTokenUsage.threadId, thread.id));
|
|
987
|
-
const totalTokens = usageRows.reduce((sum, row) => sum + row.inputTokens + row.outputTokens, 0);
|
|
988
|
-
rows.push({
|
|
989
|
-
threadId: thread.id,
|
|
990
|
-
title: thread.title,
|
|
991
|
-
actorRef: thread.actorRef,
|
|
992
|
-
messageCount: messageCountRow?.value ?? 0,
|
|
993
|
-
totalTokens,
|
|
994
|
-
lastActivityAt: thread.updatedAt.toISOString()
|
|
995
|
-
});
|
|
996
|
-
}
|
|
1039
|
+
const rows = await this.toThreadActivityRows(threads);
|
|
997
1040
|
return {
|
|
998
1041
|
rows,
|
|
999
1042
|
total: totalRow?.value ?? 0,
|
|
@@ -1016,24 +1059,146 @@ var DrizzleGovernanceQueries = class {
|
|
|
1016
1059
|
}).from(agentRun).where(whereClause);
|
|
1017
1060
|
const runs = await this.db.select().from(agentRun).where(whereClause).orderBy(desc2(agentRun.startedAt), desc2(agentRun.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
1018
1061
|
return {
|
|
1019
|
-
rows: runs.map(
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1062
|
+
rows: runs.map(toRecentRunRow),
|
|
1063
|
+
total: totalRow?.value ?? 0,
|
|
1064
|
+
page: query.page,
|
|
1065
|
+
pageSize: query.pageSize
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Paged, filterable approvals inbox, oldest first — the same ordering as {@link pendingApprovals},
|
|
1070
|
+
* now with the `total` that method structurally cannot report. The join to `agent_message` /
|
|
1071
|
+
* `agent_thread` is unconditional (it already resolves title/actor/agentName), so the `threadId`/
|
|
1072
|
+
* `actorRef`/`agentName` filters cost nothing extra.
|
|
1073
|
+
*
|
|
1074
|
+
* The order is TOTAL (`created_at asc, id asc`, and `id` is the primary key), so no row can land on
|
|
1075
|
+
* two pages or on none for a given snapshot. Ascending order also means a newly requested approval
|
|
1076
|
+
* appends past the last page instead of shifting the page an operator is currently reading.
|
|
1077
|
+
*/
|
|
1078
|
+
async approvalsPage(query) {
|
|
1079
|
+
const filters = query.where;
|
|
1080
|
+
const whereClause = and2(eq2(agentToolCall.status, "pending_approval"), filters?.toolName !== void 0 ? eq2(agentToolCall.toolName, filters.toolName) : void 0, filters?.threadId !== void 0 ? eq2(agentThread.id, filters.threadId) : void 0, filters?.actorRef !== void 0 ? eq2(agentThread.actorRef, filters.actorRef) : void 0, filters?.agentName !== void 0 ? eq2(agentMessage.agentName, filters.agentName) : void 0, filters?.fromDay !== void 0 ? gte2(agentToolCall.createdAt, dayStartUtc(filters.fromDay)) : void 0, filters?.toDay !== void 0 ? lte2(agentToolCall.createdAt, dayEndUtc(filters.toDay)) : void 0);
|
|
1081
|
+
const [totalRow] = await this.db.select({
|
|
1082
|
+
value: count()
|
|
1083
|
+
}).from(agentToolCall).innerJoin(agentMessage, eq2(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, eq2(agentMessage.threadId, agentThread.id)).where(whereClause);
|
|
1084
|
+
const rows = await this.db.select({
|
|
1085
|
+
toolCallId: agentToolCall.id,
|
|
1086
|
+
toolName: agentToolCall.toolName,
|
|
1087
|
+
input: agentToolCall.input,
|
|
1088
|
+
threadId: agentThread.id,
|
|
1089
|
+
threadTitle: agentThread.title,
|
|
1090
|
+
actorRef: agentThread.actorRef,
|
|
1091
|
+
agentName: agentMessage.agentName,
|
|
1092
|
+
requestedAt: agentToolCall.createdAt,
|
|
1093
|
+
runId: agentToolCall.runId
|
|
1094
|
+
}).from(agentToolCall).innerJoin(agentMessage, eq2(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, eq2(agentMessage.threadId, agentThread.id)).where(whereClause).orderBy(asc2(agentToolCall.createdAt), asc2(agentToolCall.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
|
|
1095
|
+
return {
|
|
1096
|
+
rows: rows.map((row) => ({
|
|
1097
|
+
toolCallId: row.toolCallId,
|
|
1098
|
+
toolName: row.toolName,
|
|
1099
|
+
input: row.input,
|
|
1100
|
+
threadId: row.threadId,
|
|
1101
|
+
threadTitle: row.threadTitle,
|
|
1102
|
+
actorRef: row.actorRef,
|
|
1103
|
+
agentName: row.agentName ?? null,
|
|
1104
|
+
requestedAt: row.requestedAt.toISOString(),
|
|
1105
|
+
runId: row.runId
|
|
1031
1106
|
})),
|
|
1032
1107
|
total: totalRow?.value ?? 0,
|
|
1033
1108
|
page: query.page,
|
|
1034
1109
|
pageSize: query.pageSize
|
|
1035
1110
|
};
|
|
1036
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* One run, its owning thread's headline and its tool calls — TWO queries regardless of how many
|
|
1114
|
+
* tools the run called. `toolCalls` comes off `agent_tool_call.run_id`, so a run recorded before
|
|
1115
|
+
* that column was written reports none.
|
|
1116
|
+
*/
|
|
1117
|
+
async runDetail(runId) {
|
|
1118
|
+
const [row] = await this.db.select({
|
|
1119
|
+
run: agentRun,
|
|
1120
|
+
thread: agentThread
|
|
1121
|
+
}).from(agentRun).innerJoin(agentThread, eq2(agentRun.threadId, agentThread.id)).where(eq2(agentRun.id, runId)).limit(1);
|
|
1122
|
+
if (row === void 0) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
const toolCalls = await this.db.select().from(agentToolCall).where(eq2(agentToolCall.runId, runId)).orderBy(asc2(agentToolCall.createdAt), asc2(agentToolCall.id));
|
|
1126
|
+
return {
|
|
1127
|
+
run: toRecentRunRow(row.run),
|
|
1128
|
+
thread: {
|
|
1129
|
+
threadId: row.thread.id,
|
|
1130
|
+
title: row.thread.title,
|
|
1131
|
+
actorRef: row.thread.actorRef,
|
|
1132
|
+
deleted: row.thread.deletedAt != null
|
|
1133
|
+
},
|
|
1134
|
+
toolCalls: toolCalls.map(toRunToolCallRow)
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* One thread with its lifetime usage rollup, its newest runs and its newest messages. A fixed six
|
|
1139
|
+
* queries (thread, usage, message count, message page, that page's tool-call counts, runs+count) —
|
|
1140
|
+
* the per-message tool-call counts are ONE grouped read over the returned message ids, not one per
|
|
1141
|
+
* message. Soft-deleted threads are returned with `deleted: true`; an audit still needs them.
|
|
1142
|
+
*/
|
|
1143
|
+
async threadDetail(query) {
|
|
1144
|
+
const [thread] = await this.db.select().from(agentThread).where(eq2(agentThread.id, query.threadId)).limit(1);
|
|
1145
|
+
if (thread === void 0) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
const pricing = await this.loadPricing();
|
|
1149
|
+
const usageRows = await this.db.select().from(agentTokenUsage).where(eq2(agentTokenUsage.threadId, thread.id));
|
|
1150
|
+
const usage = rollupThreadUsage(usageRows.map(toUsageInput), pricing);
|
|
1151
|
+
const [messageCountRow] = await this.db.select({
|
|
1152
|
+
value: count()
|
|
1153
|
+
}).from(agentMessage).where(eq2(agentMessage.threadId, thread.id));
|
|
1154
|
+
const messages = await this.db.select().from(agentMessage).where(eq2(agentMessage.threadId, thread.id)).orderBy(desc2(agentMessage.createdAt), desc2(agentMessage.id)).limit(query.messageLimit);
|
|
1155
|
+
const toolCallCounts = await this.toolCallCountsByMessage(messages.map((message) => message.id));
|
|
1156
|
+
const [runTotalRow] = await this.db.select({
|
|
1157
|
+
value: count()
|
|
1158
|
+
}).from(agentRun).where(eq2(agentRun.threadId, thread.id));
|
|
1159
|
+
const runs = await this.db.select().from(agentRun).where(eq2(agentRun.threadId, thread.id)).orderBy(desc2(agentRun.startedAt), desc2(agentRun.id)).limit(query.runLimit);
|
|
1160
|
+
return {
|
|
1161
|
+
thread: {
|
|
1162
|
+
threadId: thread.id,
|
|
1163
|
+
title: thread.title,
|
|
1164
|
+
actorRef: thread.actorRef,
|
|
1165
|
+
messageCount: messageCountRow?.value ?? 0,
|
|
1166
|
+
totalTokens: usage.totalTokens,
|
|
1167
|
+
lastActivityAt: thread.updatedAt.toISOString()
|
|
1168
|
+
},
|
|
1169
|
+
deleted: thread.deletedAt != null,
|
|
1170
|
+
usage,
|
|
1171
|
+
runs: runs.map(toRecentRunRow),
|
|
1172
|
+
runTotal: runTotalRow?.value ?? 0,
|
|
1173
|
+
messages: messages.map((message) => {
|
|
1174
|
+
const { content, truncated } = truncateDetailContent(message.content);
|
|
1175
|
+
return {
|
|
1176
|
+
messageId: message.id,
|
|
1177
|
+
role: message.role,
|
|
1178
|
+
content,
|
|
1179
|
+
truncated,
|
|
1180
|
+
agentName: message.agentName ?? null,
|
|
1181
|
+
toolCallCount: toolCallCounts.get(message.id) ?? 0,
|
|
1182
|
+
createdAt: message.createdAt.toISOString()
|
|
1183
|
+
};
|
|
1184
|
+
})
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/** Tool-call counts for a set of messages in ONE grouped query — the detail view's no-N+1 guarantee. */
|
|
1188
|
+
async toolCallCountsByMessage(messageIds) {
|
|
1189
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1190
|
+
if (messageIds.length === 0) {
|
|
1191
|
+
return counts;
|
|
1192
|
+
}
|
|
1193
|
+
const rows = await this.db.select({
|
|
1194
|
+
messageId: agentToolCall.messageId,
|
|
1195
|
+
value: count()
|
|
1196
|
+
}).from(agentToolCall).where(inArray2(agentToolCall.messageId, messageIds)).groupBy(agentToolCall.messageId);
|
|
1197
|
+
for (const row of rows) {
|
|
1198
|
+
counts.set(row.messageId, row.value);
|
|
1199
|
+
}
|
|
1200
|
+
return counts;
|
|
1201
|
+
}
|
|
1037
1202
|
};
|
|
1038
1203
|
|
|
1039
1204
|
// src/drizzle-pricing-store.ts
|