@dudousxd/nestjs-agent-store-drizzle 0.3.4 → 0.5.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
@@ -113,12 +113,36 @@ var agentModelPricing = sqliteTable("agent_model_pricing", {
113
113
  mode: "boolean"
114
114
  }).notNull()
115
115
  });
116
+ var agentRun = sqliteTable("agent_run", {
117
+ id: text("id").primaryKey(),
118
+ threadId: text("thread_id").notNull().references(() => agentThread.id, {
119
+ onDelete: "cascade"
120
+ }),
121
+ actorRef: text("actor_ref").notNull(),
122
+ agentName: text("agent_name"),
123
+ status: text("status").$type().notNull(),
124
+ durationMs: integer("duration_ms"),
125
+ errorCode: text("error_code"),
126
+ errorMessage: text("error_message"),
127
+ retries: integer("retries").notNull().default(0),
128
+ startedAt: integer("started_at", {
129
+ mode: "timestamp_ms"
130
+ }).notNull(),
131
+ settledAt: integer("settled_at", {
132
+ mode: "timestamp_ms"
133
+ }),
134
+ /** sha256 hex of the run's resolved (pre-RAG) system prompt; null for a pre-existing run. */
135
+ promptHash: text("prompt_hash")
136
+ }, (table) => [
137
+ index("agent_run_started_idx").on(table.startedAt)
138
+ ]);
116
139
  var agentSchema = {
117
140
  agentThread,
118
141
  agentMessage,
119
142
  agentToolCall,
120
143
  agentTokenUsage,
121
- agentModelPricing
144
+ agentModelPricing,
145
+ agentRun
122
146
  };
123
147
 
124
148
  // src/ensure-schema.ts
@@ -190,7 +214,23 @@ var statements = [
190
214
  cache_read_price_per_1m REAL,
191
215
  effective_from INTEGER NOT NULL,
192
216
  is_current INTEGER NOT NULL
193
- )`
217
+ )`,
218
+ `CREATE TABLE IF NOT EXISTS agent_run (
219
+ id TEXT PRIMARY KEY NOT NULL,
220
+ thread_id TEXT NOT NULL REFERENCES agent_thread(id) ON DELETE CASCADE,
221
+ actor_ref TEXT NOT NULL,
222
+ agent_name TEXT,
223
+ status TEXT NOT NULL,
224
+ duration_ms INTEGER,
225
+ error_code TEXT,
226
+ error_message TEXT,
227
+ retries INTEGER NOT NULL DEFAULT 0,
228
+ started_at INTEGER NOT NULL,
229
+ settled_at INTEGER,
230
+ prompt_hash TEXT
231
+ )`,
232
+ `CREATE INDEX IF NOT EXISTS agent_run_started_idx
233
+ ON agent_run (started_at)`
194
234
  ];
195
235
  async function ensureAgentSchema(db) {
196
236
  for (const statement of statements) {
@@ -200,7 +240,7 @@ async function ensureAgentSchema(db) {
200
240
  __name(ensureAgentSchema, "ensureAgentSchema");
201
241
 
202
242
  // src/drizzle-agent-store.ts
203
- import { and, asc, desc, eq, gte, inArray, isNull, lte } from "drizzle-orm";
243
+ import { and, asc, desc, eq, gte, inArray, isNull, lte, sql as sql2 } from "drizzle-orm";
204
244
  var DrizzleAgentStore = class {
205
245
  static {
206
246
  __name(this, "DrizzleAgentStore");
@@ -332,6 +372,47 @@ var DrizzleAgentStore = class {
332
372
  activeStreamId: runId
333
373
  }).where(eq(agentThread.id, threadId));
334
374
  }
375
+ /** Persist the start of a run (turn). Replay-safe: called under a durable localStep. */
376
+ async recordRunStart(run) {
377
+ const runRow = {
378
+ id: run.runId,
379
+ threadId: run.threadId,
380
+ actorRef: run.actorRef,
381
+ agentName: run.agentName ?? null,
382
+ status: "running",
383
+ durationMs: null,
384
+ errorCode: null,
385
+ errorMessage: null,
386
+ retries: 0,
387
+ startedAt: /* @__PURE__ */ new Date(),
388
+ settledAt: null,
389
+ promptHash: run.promptHash ?? null
390
+ };
391
+ await this.db.insert(agentRun).values(runRow);
392
+ }
393
+ /** Settle a run's outcome. A no-op when the run is unknown (mirrors `setTitle`/`updateThread`). */
394
+ async recordRunEnd(end) {
395
+ const updates = {
396
+ status: end.status,
397
+ settledAt: /* @__PURE__ */ new Date()
398
+ };
399
+ if (end.durationMs !== void 0) {
400
+ updates.durationMs = end.durationMs;
401
+ }
402
+ if (end.errorCode !== void 0) {
403
+ updates.errorCode = end.errorCode;
404
+ }
405
+ if (end.errorMessage !== void 0) {
406
+ updates.errorMessage = end.errorMessage;
407
+ }
408
+ await this.db.update(agentRun).set(updates).where(eq(agentRun.id, end.runId));
409
+ }
410
+ /** Bump the run's llm-step retry counter. Atomic `retries = retries + 1`, no read-modify-write. */
411
+ async bumpRunRetries(runId) {
412
+ await this.db.update(agentRun).set({
413
+ retries: sql2`${agentRun.retries} + 1`
414
+ }).where(eq(agentRun.id, runId));
415
+ }
335
416
  async appendMessage(input) {
336
417
  const [thread] = await this.db.select().from(agentThread).where(eq(agentThread.id, input.threadId));
337
418
  if (thread === void 0) {
@@ -470,6 +551,8 @@ var DrizzleAgentStore = class {
470
551
  // src/drizzle-governance-queries.ts
471
552
  import { bucketByActor, bucketByModel, bucketByThread, bucketUsageTrend, dayBoundsUtc } from "@dudousxd/nestjs-agent-core";
472
553
  import { and as and2, count, desc as desc2, eq as eq2, gte as gte2, inArray as inArray2, isNull as isNull2, lte as lte2 } from "drizzle-orm";
554
+ var UNCLASSIFIED_ERROR_CODE = "unknown";
555
+ var DEFAULT_AGENT_BUCKET = "(default)";
473
556
  function toUsageInput(row) {
474
557
  return {
475
558
  modelId: row.modelId,
@@ -484,6 +567,14 @@ function toUsageInput(row) {
484
567
  };
485
568
  }
486
569
  __name(toUsageInput, "toUsageInput");
570
+ function percentileMs(sortedDurationsMs, p) {
571
+ if (sortedDurationsMs.length === 0) {
572
+ return null;
573
+ }
574
+ const offset = Math.min(sortedDurationsMs.length - 1, Math.floor(p * sortedDurationsMs.length));
575
+ return sortedDurationsMs[offset] ?? null;
576
+ }
577
+ __name(percentileMs, "percentileMs");
487
578
  var DrizzleGovernanceQueries = class {
488
579
  static {
489
580
  __name(this, "DrizzleGovernanceQueries");
@@ -589,6 +680,189 @@ var DrizzleGovernanceQueries = class {
589
680
  }
590
681
  return result;
591
682
  }
683
+ async runsInRange(range) {
684
+ const { start, end } = dayBoundsUtc(range);
685
+ return this.db.select().from(agentRun).where(and2(gte2(agentRun.startedAt, start), lte2(agentRun.startedAt, end)));
686
+ }
687
+ async runMetrics(range) {
688
+ const runs = await this.runsInRange(range);
689
+ let completed = 0;
690
+ let failed = 0;
691
+ let retries = 0;
692
+ const settledDurationsMs = [];
693
+ for (const run of runs) {
694
+ if (run.status === "completed") {
695
+ completed += 1;
696
+ } else if (run.status === "failed") {
697
+ failed += 1;
698
+ }
699
+ retries += run.retries;
700
+ if ((run.status === "completed" || run.status === "failed") && run.durationMs != null) {
701
+ settledDurationsMs.push(run.durationMs);
702
+ }
703
+ }
704
+ settledDurationsMs.sort((left, right) => left - right);
705
+ return {
706
+ runs: runs.length,
707
+ completed,
708
+ failed,
709
+ successRate: runs.length === 0 ? 0 : completed / runs.length,
710
+ retries,
711
+ durationP50Ms: percentileMs(settledDurationsMs, 0.5),
712
+ durationP95Ms: percentileMs(settledDurationsMs, 0.95)
713
+ };
714
+ }
715
+ async runsByAgent(range) {
716
+ const runs = await this.runsInRange(range);
717
+ const byAgent = /* @__PURE__ */ new Map();
718
+ for (const run of runs) {
719
+ const agentName = run.agentName ?? DEFAULT_AGENT_BUCKET;
720
+ const bucket = byAgent.get(agentName) ?? {
721
+ runs: 0,
722
+ failed: 0,
723
+ retries: 0
724
+ };
725
+ bucket.runs += 1;
726
+ bucket.failed += run.status === "failed" ? 1 : 0;
727
+ bucket.retries += run.retries;
728
+ byAgent.set(agentName, bucket);
729
+ }
730
+ const result = [];
731
+ for (const [agentName, bucket] of byAgent) {
732
+ result.push({
733
+ agentName,
734
+ ...bucket
735
+ });
736
+ }
737
+ result.sort((left, right) => right.runs - left.runs || left.agentName.localeCompare(right.agentName));
738
+ return result;
739
+ }
740
+ async runErrors(range) {
741
+ const { start, end } = dayBoundsUtc(range);
742
+ const runs = await this.db.select().from(agentRun).where(and2(gte2(agentRun.startedAt, start), lte2(agentRun.startedAt, end), eq2(agentRun.status, "failed")));
743
+ const byError = /* @__PURE__ */ new Map();
744
+ for (const run of runs) {
745
+ const errorCode = run.errorCode ?? UNCLASSIFIED_ERROR_CODE;
746
+ byError.set(errorCode, (byError.get(errorCode) ?? 0) + 1);
747
+ }
748
+ const result = [];
749
+ for (const [errorCode, errorCount] of byError) {
750
+ result.push({
751
+ errorCode,
752
+ count: errorCount
753
+ });
754
+ }
755
+ result.sort((left, right) => right.count - left.count || left.errorCode.localeCompare(right.errorCode));
756
+ return result;
757
+ }
758
+ async runTrend(range) {
759
+ const runs = await this.runsInRange(range);
760
+ const byDay = /* @__PURE__ */ new Map();
761
+ for (const run of runs) {
762
+ const day = run.startedAt.toISOString().slice(0, 10);
763
+ const bucket = byDay.get(day) ?? {
764
+ runs: 0,
765
+ failed: 0
766
+ };
767
+ bucket.runs += 1;
768
+ bucket.failed += run.status === "failed" ? 1 : 0;
769
+ byDay.set(day, bucket);
770
+ }
771
+ const result = [];
772
+ for (const [day, bucket] of byDay) {
773
+ result.push({
774
+ day,
775
+ ...bucket
776
+ });
777
+ }
778
+ result.sort((left, right) => left.day.localeCompare(right.day));
779
+ return result;
780
+ }
781
+ async recentRuns(limit) {
782
+ const runs = await this.db.select().from(agentRun).orderBy(desc2(agentRun.startedAt), desc2(agentRun.id)).limit(limit);
783
+ return runs.map((run) => ({
784
+ runId: run.id,
785
+ threadId: run.threadId,
786
+ actorRef: run.actorRef,
787
+ agentName: run.agentName ?? null,
788
+ status: run.status,
789
+ durationMs: run.durationMs ?? null,
790
+ errorCode: run.errorCode ?? null,
791
+ errorMessage: run.errorMessage ?? null,
792
+ retries: run.retries,
793
+ startedAt: run.startedAt.toISOString(),
794
+ promptHash: run.promptHash ?? null
795
+ }));
796
+ }
797
+ /**
798
+ * Tool calls sitting `pending_approval`, oldest first (an inbox drains from the back), joined
799
+ * through their message to the owning thread for title/actorRef and to the message itself for
800
+ * `agentName` (null when the message carries none).
801
+ */
802
+ async pendingApprovals(limit) {
803
+ const rows = await this.db.select({
804
+ toolCallId: agentToolCall.id,
805
+ toolName: agentToolCall.toolName,
806
+ input: agentToolCall.input,
807
+ threadId: agentThread.id,
808
+ threadTitle: agentThread.title,
809
+ actorRef: agentThread.actorRef,
810
+ agentName: agentMessage.agentName,
811
+ requestedAt: agentToolCall.createdAt
812
+ }).from(agentToolCall).innerJoin(agentMessage, eq2(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, eq2(agentMessage.threadId, agentThread.id)).where(eq2(agentToolCall.status, "pending_approval")).orderBy(agentToolCall.createdAt, agentToolCall.id).limit(limit);
813
+ return rows.map((row) => ({
814
+ toolCallId: row.toolCallId,
815
+ toolName: row.toolName,
816
+ input: row.input,
817
+ threadId: row.threadId,
818
+ threadTitle: row.threadTitle,
819
+ actorRef: row.actorRef,
820
+ agentName: row.agentName ?? null,
821
+ requestedAt: row.requestedAt.toISOString()
822
+ }));
823
+ }
824
+ /**
825
+ * Per-tool call/failure/rejection/latency rollup over the range, highest call count first.
826
+ * `p95ExecutionMs` is computed over calls that recorded a non-null `executionMs` (regardless of
827
+ * their final status); `null` when none did.
828
+ */
829
+ async toolStats(range) {
830
+ const { start, end } = dayBoundsUtc(range);
831
+ const calls = await this.db.select().from(agentToolCall).where(and2(gte2(agentToolCall.createdAt, start), lte2(agentToolCall.createdAt, end)));
832
+ const byTool = /* @__PURE__ */ new Map();
833
+ for (const call of calls) {
834
+ const key = `${call.toolName} ${call.toolType}`;
835
+ const bucket = byTool.get(key) ?? {
836
+ toolName: call.toolName,
837
+ toolType: call.toolType,
838
+ calls: 0,
839
+ failed: 0,
840
+ rejected: 0,
841
+ executionMs: []
842
+ };
843
+ bucket.calls += 1;
844
+ bucket.failed += call.status === "failed" ? 1 : 0;
845
+ bucket.rejected += call.status === "rejected" ? 1 : 0;
846
+ if (call.executionMs != null) {
847
+ bucket.executionMs.push(call.executionMs);
848
+ }
849
+ byTool.set(key, bucket);
850
+ }
851
+ const result = [];
852
+ for (const bucket of byTool.values()) {
853
+ bucket.executionMs.sort((left, right) => left - right);
854
+ result.push({
855
+ toolName: bucket.toolName,
856
+ toolType: bucket.toolType,
857
+ calls: bucket.calls,
858
+ failed: bucket.failed,
859
+ rejected: bucket.rejected,
860
+ p95ExecutionMs: percentileMs(bucket.executionMs, 0.95)
861
+ });
862
+ }
863
+ result.sort((left, right) => right.calls - left.calls || left.toolName.localeCompare(right.toolName));
864
+ return result;
865
+ }
592
866
  };
593
867
 
594
868
  // src/drizzle-pricing-store.ts
@@ -706,6 +980,7 @@ export {
706
980
  DrizzlePricingStore,
707
981
  agentMessage,
708
982
  agentModelPricing,
983
+ agentRun,
709
984
  agentSchema,
710
985
  agentThread,
711
986
  agentTokenUsage,