@dudousxd/nestjs-agent-store-drizzle 0.8.2 → 0.9.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
@@ -12,6 +12,8 @@ var agentThread = sqliteTable("agent_thread", {
12
12
  mode: "boolean"
13
13
  }).notNull().default(false),
14
14
  activeStreamId: text("active_stream_id"),
15
+ /** The agent a new turn on this thread defaults to when the caller names none. */
16
+ defaultAgent: text("default_agent"),
15
17
  createdAt: integer("created_at", {
16
18
  mode: "timestamp_ms"
17
19
  }).notNull(),
@@ -37,6 +39,9 @@ var agentMessage = sqliteTable("agent_message", {
37
39
  toolResults: text("tool_results", {
38
40
  mode: "json"
39
41
  }).$type(),
42
+ attachments: text("attachments", {
43
+ mode: "json"
44
+ }).$type(),
40
45
  followUps: text("follow_ups", {
41
46
  mode: "json"
42
47
  }).$type(),
@@ -44,6 +49,7 @@ var agentMessage = sqliteTable("agent_message", {
44
49
  mode: "json"
45
50
  }).$type(),
46
51
  agentName: text("agent_name"),
52
+ runId: text("run_id"),
47
53
  createdAt: integer("created_at", {
48
54
  mode: "timestamp_ms"
49
55
  }).notNull()
@@ -75,7 +81,9 @@ var agentToolCall = sqliteTable("agent_tool_call", {
75
81
  }),
76
82
  /** The run (turn) this call belongs to, for a trace deep-link; null for a pre-rollout row. */
77
83
  runId: text("run_id")
78
- });
84
+ }, (table) => [
85
+ index("agent_tool_call_message_idx").on(table.messageId)
86
+ ]);
79
87
  var agentTokenUsage = sqliteTable("agent_token_usage", {
80
88
  id: text("id").primaryKey(),
81
89
  threadId: text("thread_id").notNull().references(() => agentThread.id, {
@@ -134,7 +142,15 @@ var agentRun = sqliteTable("agent_run", {
134
142
  mode: "timestamp_ms"
135
143
  }),
136
144
  /** sha256 hex of the run's resolved (pre-RAG) system prompt; null for a pre-existing run. */
137
- promptHash: text("prompt_hash")
145
+ promptHash: text("prompt_hash"),
146
+ /**
147
+ * The run that delegated this one; null for a turn nobody delegated. The durable runtime's
148
+ * journal holds the same edge, but only there — this column is what lets a reader of run ROWS
149
+ * build the delegation tree and roll a child's cost up to the turn that asked for it, including
150
+ * for a detached child, which outlives its parent's turn and so is paired by nothing in the
151
+ * transcript.
152
+ */
153
+ parentRunId: text("parent_run_id")
138
154
  }, (table) => [
139
155
  index("agent_run_started_idx").on(table.startedAt)
140
156
  ]);
@@ -157,6 +173,7 @@ var statements = [
157
173
  title TEXT NOT NULL,
158
174
  transient INTEGER NOT NULL DEFAULT 0,
159
175
  active_stream_id TEXT,
176
+ default_agent TEXT,
160
177
  created_at INTEGER NOT NULL,
161
178
  updated_at INTEGER NOT NULL,
162
179
  deleted_at INTEGER
@@ -170,9 +187,11 @@ var statements = [
170
187
  content TEXT NOT NULL,
171
188
  tool_calls TEXT,
172
189
  tool_results TEXT,
190
+ attachments TEXT,
173
191
  follow_ups TEXT,
174
192
  usage TEXT,
175
193
  agent_name TEXT,
194
+ run_id TEXT,
176
195
  created_at INTEGER NOT NULL
177
196
  )`,
178
197
  `CREATE INDEX IF NOT EXISTS agent_message_thread_created_idx
@@ -192,6 +211,8 @@ var statements = [
192
211
  executed_at INTEGER,
193
212
  run_id TEXT
194
213
  )`,
214
+ `CREATE INDEX IF NOT EXISTS agent_tool_call_message_idx
215
+ ON agent_tool_call (message_id)`,
195
216
  `CREATE TABLE IF NOT EXISTS agent_token_usage (
196
217
  id TEXT PRIMARY KEY NOT NULL,
197
218
  thread_id TEXT NOT NULL REFERENCES agent_thread(id) ON DELETE CASCADE,
@@ -230,20 +251,59 @@ var statements = [
230
251
  retries INTEGER NOT NULL DEFAULT 0,
231
252
  started_at INTEGER NOT NULL,
232
253
  settled_at INTEGER,
233
- prompt_hash TEXT
254
+ prompt_hash TEXT,
255
+ parent_run_id TEXT
234
256
  )`,
235
257
  `CREATE INDEX IF NOT EXISTS agent_run_started_idx
236
258
  ON agent_run (started_at)`
237
259
  ];
260
+ var additiveColumns = [
261
+ {
262
+ table: "agent_message",
263
+ column: "run_id",
264
+ ddl: "ALTER TABLE agent_message ADD COLUMN run_id TEXT"
265
+ },
266
+ {
267
+ table: "agent_message",
268
+ column: "attachments",
269
+ ddl: "ALTER TABLE agent_message ADD COLUMN attachments TEXT"
270
+ },
271
+ {
272
+ table: "agent_thread",
273
+ column: "default_agent",
274
+ ddl: "ALTER TABLE agent_thread ADD COLUMN default_agent TEXT"
275
+ },
276
+ {
277
+ table: "agent_run",
278
+ column: "parent_run_id",
279
+ ddl: "ALTER TABLE agent_run ADD COLUMN parent_run_id TEXT"
280
+ }
281
+ ];
238
282
  async function ensureAgentSchema(db) {
239
283
  for (const statement of statements) {
240
284
  await db.run(sql.raw(statement));
241
285
  }
286
+ for (const { table, column, ddl } of additiveColumns) {
287
+ const existing = await db.all(sql.raw(`PRAGMA table_info(${table})`));
288
+ if (!existing.some((row) => row.name === column)) {
289
+ await db.run(sql.raw(ddl));
290
+ }
291
+ }
242
292
  }
243
293
  __name(ensureAgentSchema, "ensureAgentSchema");
244
294
 
245
295
  // src/drizzle-agent-store.ts
246
- import { and, asc, desc, eq, gte, inArray, isNull, lte, sql as sql2 } from "drizzle-orm";
296
+ import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, lte, sql as sql2 } from "drizzle-orm";
297
+ var TURN_MESSAGE_COLUMNS = {
298
+ id: agentMessage.id,
299
+ role: agentMessage.role,
300
+ content: agentMessage.content,
301
+ agentName: agentMessage.agentName,
302
+ toolCalls: agentMessage.toolCalls,
303
+ toolResults: agentMessage.toolResults,
304
+ attachments: agentMessage.attachments,
305
+ createdAt: agentMessage.createdAt
306
+ };
247
307
  var DrizzleAgentStore = class {
248
308
  static {
249
309
  __name(this, "DrizzleAgentStore");
@@ -261,6 +321,7 @@ var DrizzleAgentStore = class {
261
321
  title: input.title ?? "New chat",
262
322
  transient: input.transient ?? false,
263
323
  activeStreamId: null,
324
+ defaultAgent: null,
264
325
  createdAt: now,
265
326
  updatedAt: now,
266
327
  deletedAt: null
@@ -283,6 +344,50 @@ var DrizzleAgentStore = class {
283
344
  } : {}
284
345
  };
285
346
  }
347
+ /**
348
+ * The window a turn reads off a thread: its newest `messageLimit` messages (oldest first), the
349
+ * title, the default agent, and whether the thread has EVER been answered.
350
+ *
351
+ * {@link getThread} is the wrong read for a turn. It materializes the transcript — every message,
352
+ * every attachment, every tool output the thread ever recorded — and the run then journals what it
353
+ * loaded, so a long thread pays for its whole history on every turn and again on every replay.
354
+ *
355
+ * `hasAssistantMessage` is answered over the WHOLE thread, not the returned page: it answers "has
356
+ * this conversation been answered before?", and a thread whose window happens to hold only the
357
+ * user's last questions has still been answered. `null` when the thread is unknown or
358
+ * soft-deleted, same as {@link getThread}.
359
+ */
360
+ async loadThreadForTurn(query) {
361
+ const [thread] = await this.db.select({
362
+ title: agentThread.title,
363
+ defaultAgent: agentThread.defaultAgent
364
+ }).from(agentThread).where(and(eq(agentThread.id, query.threadId), isNull(agentThread.deletedAt)));
365
+ if (thread === void 0) {
366
+ return null;
367
+ }
368
+ const messages = await this.loadTurnWindow(query.threadId, query.messageLimit);
369
+ const [answered] = await this.db.select({
370
+ id: agentMessage.id
371
+ }).from(agentMessage).where(and(eq(agentMessage.threadId, query.threadId), eq(agentMessage.role, "assistant"))).limit(1);
372
+ return {
373
+ title: thread.title,
374
+ defaultAgent: thread.defaultAgent,
375
+ hasAssistantMessage: answered !== void 0,
376
+ messages
377
+ };
378
+ }
379
+ /**
380
+ * The newest `limit` messages of a thread, oldest first, projected to the columns a model turn
381
+ * reads. Read newest-first so the LIMIT is the database's job, then reversed for the prompt.
382
+ */
383
+ async loadTurnWindow(threadId, limit) {
384
+ if (limit !== void 0 && limit <= 0) {
385
+ return [];
386
+ }
387
+ const window = this.db.select(TURN_MESSAGE_COLUMNS).from(agentMessage).where(eq(agentMessage.threadId, threadId)).orderBy(desc(agentMessage.createdAt), desc(agentMessage.id));
388
+ const rows = limit === void 0 ? await window : await window.limit(limit);
389
+ return rows.reverse().map((row) => this.toStoredMessage(row));
390
+ }
286
391
  async listThreads(actorRef, limit = 50) {
287
392
  const threads = await this.db.select().from(agentThread).where(and(eq(agentThread.actorRef, actorRef), eq(agentThread.transient, false), isNull(agentThread.deletedAt))).orderBy(desc(agentThread.updatedAt)).limit(limit);
288
393
  const summaries = [];
@@ -313,6 +418,7 @@ var DrizzleAgentStore = class {
313
418
  title: source.title,
314
419
  transient: false,
315
420
  activeStreamId: null,
421
+ defaultAgent: source.defaultAgent,
316
422
  createdAt: now,
317
423
  updatedAt: now,
318
424
  deletedAt: null
@@ -326,9 +432,14 @@ var DrizzleAgentStore = class {
326
432
  content: message.content,
327
433
  toolCalls: message.toolCalls,
328
434
  toolResults: message.toolResults,
435
+ attachments: message.attachments,
329
436
  followUps: message.followUps,
330
437
  usage: message.usage,
331
438
  agentName: message.agentName,
439
+ // The copy is the same message, so it keeps the run that wrote it. A reader asking which
440
+ // turn produced this text gets the truthful answer; the run's own thread is still the
441
+ // original, so a run-scoped read never picks the fork's rows up.
442
+ runId: message.runId,
332
443
  createdAt: message.createdAt
333
444
  })));
334
445
  }
@@ -348,9 +459,10 @@ var DrizzleAgentStore = class {
348
459
  }
349
460
  async runForToolCall(toolCallId) {
350
461
  const [row] = await this.db.select({
462
+ runId: agentToolCall.runId,
351
463
  activeStreamId: agentThread.activeStreamId
352
464
  }).from(agentToolCall).innerJoin(agentMessage, eq(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, eq(agentMessage.threadId, agentThread.id)).where(eq(agentToolCall.id, toolCallId));
353
- return row?.activeStreamId ?? null;
465
+ return row?.runId ?? row?.activeStreamId ?? null;
354
466
  }
355
467
  async ownerOfActiveStream(runId) {
356
468
  const [thread] = await this.db.select({
@@ -375,7 +487,43 @@ var DrizzleAgentStore = class {
375
487
  activeStreamId: runId
376
488
  }).where(eq(agentThread.id, threadId));
377
489
  }
378
- /** Persist the start of a run (turn). Replay-safe: called under a durable localStep. */
490
+ /**
491
+ * Patch a thread's `title` and/or `defaultAgent`. Each field is only touched when PRESENT in
492
+ * `patch` — `defaultAgent: null` clears it (falls back to the app default), while an omitted
493
+ * `defaultAgent` leaves whatever is stored untouched. A no-op (including `updatedAt`) when the
494
+ * patch is empty or the thread doesn't exist.
495
+ */
496
+ async updateThread(threadId, patch) {
497
+ const updates = {};
498
+ if (patch.title !== void 0) {
499
+ updates.title = patch.title;
500
+ }
501
+ if (patch.defaultAgent !== void 0) {
502
+ updates.defaultAgent = patch.defaultAgent;
503
+ }
504
+ if (Object.keys(updates).length === 0) {
505
+ return;
506
+ }
507
+ updates.updatedAt = /* @__PURE__ */ new Date();
508
+ await this.db.update(agentThread).set(updates).where(eq(agentThread.id, threadId));
509
+ }
510
+ /**
511
+ * The thread's default agent, projected — the caller wants one nullable scalar to decide which
512
+ * agent answers the next turn, and {@link getThread} would materialize the whole transcript to
513
+ * hand it over. `null` when the thread is unknown, soft-deleted, or has no default set.
514
+ */
515
+ async defaultAgentForThread(threadId) {
516
+ const [thread] = await this.db.select({
517
+ defaultAgent: agentThread.defaultAgent
518
+ }).from(agentThread).where(and(eq(agentThread.id, threadId), isNull(agentThread.deletedAt)));
519
+ return thread?.defaultAgent ?? null;
520
+ }
521
+ /**
522
+ * Persist the start of a run (turn). Replay-safe: called under a durable localStep.
523
+ *
524
+ * Takes the SPI's own {@link RecordRunStartInput} rather than a hand-copied shape: a field added
525
+ * to the input is otherwise accepted and dropped, silently, by every adapter that re-declares it.
526
+ */
379
527
  async recordRunStart(run) {
380
528
  const runRow = {
381
529
  id: run.runId,
@@ -389,7 +537,8 @@ var DrizzleAgentStore = class {
389
537
  retries: 0,
390
538
  startedAt: /* @__PURE__ */ new Date(),
391
539
  settledAt: null,
392
- promptHash: run.promptHash ?? null
540
+ promptHash: run.promptHash ?? null,
541
+ parentRunId: run.parentRunId ?? null
393
542
  };
394
543
  await this.db.insert(agentRun).values(runRow);
395
544
  }
@@ -429,9 +578,11 @@ var DrizzleAgentStore = class {
429
578
  content: input.content,
430
579
  toolCalls: input.toolCalls ?? null,
431
580
  toolResults: input.toolResults ?? null,
581
+ attachments: input.attachments ?? null,
432
582
  followUps: input.followUps ?? null,
433
583
  usage: input.usage ?? null,
434
584
  agentName: input.agentName ?? null,
585
+ runId: input.runId ?? null,
435
586
  createdAt: now
436
587
  };
437
588
  await this.db.insert(agentMessage).values(message);
@@ -440,6 +591,11 @@ var DrizzleAgentStore = class {
440
591
  }).where(eq(agentThread.id, input.threadId));
441
592
  return this.toStoredMessage(message);
442
593
  }
594
+ async setMessageToolResults(messageId, results) {
595
+ await this.db.update(agentMessage).set({
596
+ toolResults: results
597
+ }).where(eq(agentMessage.id, messageId));
598
+ }
443
599
  async truncateFrom(threadId, messageId) {
444
600
  const messages = await this.db.select().from(agentMessage).where(eq(agentMessage.threadId, threadId)).orderBy(asc(agentMessage.createdAt), asc(agentMessage.id));
445
601
  const cutoff = messages.findIndex((message) => message.id === messageId);
@@ -488,6 +644,39 @@ var DrizzleAgentStore = class {
488
644
  }
489
645
  await this.db.update(agentToolCall).set(updates).where(eq(agentToolCall.id, input.toolCallId));
490
646
  }
647
+ /**
648
+ * Of `mediaIds`, the ones a surviving message in one of this actor's threads still carries.
649
+ *
650
+ * Reads the `attachments` JSON back out and matches in memory rather than pushing the match into
651
+ * SQL: the column holds an array of objects, and every dialect this adapter's sibling targets
652
+ * spells that query differently (`jsonb` containment, `json_table`, `json_each`), while none of
653
+ * them can use an index for it anyway. The scan is bounded by ONE actor's attachment-bearing
654
+ * messages, which is a small set — attachments are rare, and a message without one is excluded
655
+ * by the `is not null` before any JSON is parsed.
656
+ *
657
+ * Soft-deleted threads are deliberately included: their message rows survive, so the bytes they
658
+ * point at are still reachable from stored state and are not garbage.
659
+ */
660
+ async referencedMediaIds(actorRef, mediaIds) {
661
+ if (mediaIds.length === 0) {
662
+ return [];
663
+ }
664
+ const rows = await this.db.select({
665
+ attachments: agentMessage.attachments
666
+ }).from(agentMessage).innerJoin(agentThread, eq(agentMessage.threadId, agentThread.id)).where(and(eq(agentThread.actorRef, actorRef), isNotNull(agentMessage.attachments)));
667
+ const wanted = new Set(mediaIds);
668
+ const found = /* @__PURE__ */ new Set();
669
+ for (const row of rows) {
670
+ for (const attachment of row.attachments ?? []) {
671
+ if (wanted.has(attachment.mediaId)) {
672
+ found.add(attachment.mediaId);
673
+ }
674
+ }
675
+ }
676
+ return [
677
+ ...wanted
678
+ ].filter((mediaId) => found.has(mediaId));
679
+ }
491
680
  async recordUsage(input) {
492
681
  await this.db.insert(agentTokenUsage).values({
493
682
  id: crypto.randomUUID(),
@@ -522,6 +711,7 @@ var DrizzleAgentStore = class {
522
711
  transient: thread.transient,
523
712
  createdAt: thread.createdAt.toISOString(),
524
713
  updatedAt: thread.updatedAt.toISOString(),
714
+ defaultAgent: thread.defaultAgent,
525
715
  ...lastContent !== void 0 ? {
526
716
  lastMessagePreview: lastContent.slice(0, 120)
527
717
  } : {}
@@ -539,6 +729,9 @@ var DrizzleAgentStore = class {
539
729
  ...message.toolResults != null ? {
540
730
  toolResults: message.toolResults
541
731
  } : {},
732
+ ...message.attachments != null ? {
733
+ attachments: message.attachments
734
+ } : {},
542
735
  ...message.followUps != null ? {
543
736
  followUps: message.followUps
544
737
  } : {},
@@ -547,6 +740,9 @@ var DrizzleAgentStore = class {
547
740
  } : {},
548
741
  ...message.agentName != null ? {
549
742
  agentName: message.agentName
743
+ } : {},
744
+ ...message.runId != null ? {
745
+ runId: message.runId
550
746
  } : {}
551
747
  };
552
748
  }
@@ -580,7 +776,8 @@ __name(isToolKind, "isToolKind");
580
776
  var RUN_STATUSES = [
581
777
  "running",
582
778
  "completed",
583
- "failed"
779
+ "failed",
780
+ "cancelled"
584
781
  ];
585
782
  function isRunStatus(value) {
586
783
  return RUN_STATUSES.includes(value);
@@ -621,7 +818,8 @@ function toRecentRunRow(run) {
621
818
  errorMessage: run.errorMessage ?? null,
622
819
  retries: run.retries,
623
820
  startedAt: run.startedAt.toISOString(),
624
- promptHash: run.promptHash ?? null
821
+ promptHash: run.promptHash ?? null,
822
+ parentRunId: run.parentRunId ?? null
625
823
  };
626
824
  }
627
825
  __name(toRecentRunRow, "toRecentRunRow");