@dudousxd/nestjs-agent-store-drizzle 0.8.1 → 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/README.md +52 -3
- package/dist/index.cjs +206 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -16
- package/dist/index.d.ts +231 -16
- package/dist/index.js +207 -9
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -36,9 +36,58 @@ export class AppModule {}
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
The package ships the `agentSchema` (Drizzle tables), `ensureAgentSchema` (a non-destructive
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
DDL helper for a quick start), `DrizzleAgentStore`, and `DrizzleGovernanceQueries`. For production,
|
|
40
|
+
prefer your normal drizzle-kit migrations over the `ensureAgentSchema` helper.
|
|
41
|
+
|
|
42
|
+
## The read a turn makes
|
|
43
|
+
|
|
44
|
+
The agent loop does not call `getThread` to build a prompt. This store implements the core SPI's
|
|
45
|
+
`ThreadTurnReader`, so the loop asks it for `loadThreadForTurn({ threadId, messageLimit })` instead:
|
|
46
|
+
the thread's newest `messageLimit` messages oldest-first, bounded by the database rather than in
|
|
47
|
+
memory, projected to the columns a model turn reads (`usage`, `follow_ups` and `run_id` stay in the
|
|
48
|
+
table), plus the title, the default agent, and whether the thread has EVER been answered.
|
|
49
|
+
|
|
50
|
+
`messageLimit` is the configured `HistoryPolicy.maxMessages`. A policy that summarizes, or whose
|
|
51
|
+
ceiling is only a token budget, names no row bound and the whole thread is read — see the core
|
|
52
|
+
README. Nothing to wire: the loop probes for the method, and `getThread` remains the right read for
|
|
53
|
+
a client rendering a transcript.
|
|
54
|
+
|
|
55
|
+
## Upgrading a database that already exists
|
|
56
|
+
|
|
57
|
+
`ensureAgentSchema` is additive only — `CREATE TABLE IF NOT EXISTS` plus `CREATE INDEX IF NOT
|
|
58
|
+
EXISTS`, and an explicit add-column pass for columns this package introduced after a table shipped,
|
|
59
|
+
since the `CREATE TABLE` guard is inert against a table that already exists. Calling it on boot is
|
|
60
|
+
enough; it never drops a column or changes a type.
|
|
61
|
+
|
|
62
|
+
On your own drizzle-kit migrations, those additive statements are yours to write:
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
ALTER TABLE agent_thread ADD COLUMN default_agent TEXT;
|
|
66
|
+
ALTER TABLE agent_message ADD COLUMN run_id TEXT;
|
|
67
|
+
ALTER TABLE agent_message ADD COLUMN attachments TEXT;
|
|
68
|
+
ALTER TABLE agent_run ADD COLUMN parent_run_id TEXT;
|
|
69
|
+
CREATE INDEX agent_tool_call_message_idx ON agent_tool_call (message_id);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The index is not optional on Postgres: `agent_tool_call.message_id` carries a foreign key, and
|
|
73
|
+
Postgres — unlike MySQL — does not index one for you, while both message-scoped reads (the thread
|
|
74
|
+
reader's `IN (…)` and `truncateFrom`'s delete) filter on it.
|
|
75
|
+
|
|
76
|
+
## Attachment housekeeping
|
|
77
|
+
|
|
78
|
+
`referencedMediaIds(actorRef, mediaIds)` answers which of a set of media ids a message that still
|
|
79
|
+
exists carries, for one actor — the inverse of the host's own staged-media inventory, and the half a
|
|
80
|
+
sweep cannot work out for itself. It needs **no schema change**: it reads the `attachments` JSON
|
|
81
|
+
column messages have carried since attachments shipped, so an existing database answers correctly
|
|
82
|
+
the moment you upgrade, with nothing to backfill.
|
|
83
|
+
|
|
84
|
+
The match happens in memory rather than in SQL. The column holds an array of objects, every dialect
|
|
85
|
+
spells that query differently, and none of them can use an index for it; the scan is bounded to one
|
|
86
|
+
actor's attachment-bearing messages, and a message with no attachments never leaves the database.
|
|
87
|
+
|
|
88
|
+
A thread that was soft-deleted still counts as holding its references: the message rows survive, so
|
|
89
|
+
the bytes are still reachable from stored state. Delete the thread for real and the cascade makes
|
|
90
|
+
them collectable on the next sweep.
|
|
42
91
|
|
|
43
92
|
## Cost accounting
|
|
44
93
|
|
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,8 @@ var agentThread = (0, import_sqlite_core.sqliteTable)("agent_thread", {
|
|
|
47
47
|
mode: "boolean"
|
|
48
48
|
}).notNull().default(false),
|
|
49
49
|
activeStreamId: (0, import_sqlite_core.text)("active_stream_id"),
|
|
50
|
+
/** The agent a new turn on this thread defaults to when the caller names none. */
|
|
51
|
+
defaultAgent: (0, import_sqlite_core.text)("default_agent"),
|
|
50
52
|
createdAt: (0, import_sqlite_core.integer)("created_at", {
|
|
51
53
|
mode: "timestamp_ms"
|
|
52
54
|
}).notNull(),
|
|
@@ -72,6 +74,9 @@ var agentMessage = (0, import_sqlite_core.sqliteTable)("agent_message", {
|
|
|
72
74
|
toolResults: (0, import_sqlite_core.text)("tool_results", {
|
|
73
75
|
mode: "json"
|
|
74
76
|
}).$type(),
|
|
77
|
+
attachments: (0, import_sqlite_core.text)("attachments", {
|
|
78
|
+
mode: "json"
|
|
79
|
+
}).$type(),
|
|
75
80
|
followUps: (0, import_sqlite_core.text)("follow_ups", {
|
|
76
81
|
mode: "json"
|
|
77
82
|
}).$type(),
|
|
@@ -79,6 +84,7 @@ var agentMessage = (0, import_sqlite_core.sqliteTable)("agent_message", {
|
|
|
79
84
|
mode: "json"
|
|
80
85
|
}).$type(),
|
|
81
86
|
agentName: (0, import_sqlite_core.text)("agent_name"),
|
|
87
|
+
runId: (0, import_sqlite_core.text)("run_id"),
|
|
82
88
|
createdAt: (0, import_sqlite_core.integer)("created_at", {
|
|
83
89
|
mode: "timestamp_ms"
|
|
84
90
|
}).notNull()
|
|
@@ -110,7 +116,9 @@ var agentToolCall = (0, import_sqlite_core.sqliteTable)("agent_tool_call", {
|
|
|
110
116
|
}),
|
|
111
117
|
/** The run (turn) this call belongs to, for a trace deep-link; null for a pre-rollout row. */
|
|
112
118
|
runId: (0, import_sqlite_core.text)("run_id")
|
|
113
|
-
})
|
|
119
|
+
}, (table) => [
|
|
120
|
+
(0, import_sqlite_core.index)("agent_tool_call_message_idx").on(table.messageId)
|
|
121
|
+
]);
|
|
114
122
|
var agentTokenUsage = (0, import_sqlite_core.sqliteTable)("agent_token_usage", {
|
|
115
123
|
id: (0, import_sqlite_core.text)("id").primaryKey(),
|
|
116
124
|
threadId: (0, import_sqlite_core.text)("thread_id").notNull().references(() => agentThread.id, {
|
|
@@ -169,7 +177,15 @@ var agentRun = (0, import_sqlite_core.sqliteTable)("agent_run", {
|
|
|
169
177
|
mode: "timestamp_ms"
|
|
170
178
|
}),
|
|
171
179
|
/** sha256 hex of the run's resolved (pre-RAG) system prompt; null for a pre-existing run. */
|
|
172
|
-
promptHash: (0, import_sqlite_core.text)("prompt_hash")
|
|
180
|
+
promptHash: (0, import_sqlite_core.text)("prompt_hash"),
|
|
181
|
+
/**
|
|
182
|
+
* The run that delegated this one; null for a turn nobody delegated. The durable runtime's
|
|
183
|
+
* journal holds the same edge, but only there — this column is what lets a reader of run ROWS
|
|
184
|
+
* build the delegation tree and roll a child's cost up to the turn that asked for it, including
|
|
185
|
+
* for a detached child, which outlives its parent's turn and so is paired by nothing in the
|
|
186
|
+
* transcript.
|
|
187
|
+
*/
|
|
188
|
+
parentRunId: (0, import_sqlite_core.text)("parent_run_id")
|
|
173
189
|
}, (table) => [
|
|
174
190
|
(0, import_sqlite_core.index)("agent_run_started_idx").on(table.startedAt)
|
|
175
191
|
]);
|
|
@@ -192,6 +208,7 @@ var statements = [
|
|
|
192
208
|
title TEXT NOT NULL,
|
|
193
209
|
transient INTEGER NOT NULL DEFAULT 0,
|
|
194
210
|
active_stream_id TEXT,
|
|
211
|
+
default_agent TEXT,
|
|
195
212
|
created_at INTEGER NOT NULL,
|
|
196
213
|
updated_at INTEGER NOT NULL,
|
|
197
214
|
deleted_at INTEGER
|
|
@@ -205,9 +222,11 @@ var statements = [
|
|
|
205
222
|
content TEXT NOT NULL,
|
|
206
223
|
tool_calls TEXT,
|
|
207
224
|
tool_results TEXT,
|
|
225
|
+
attachments TEXT,
|
|
208
226
|
follow_ups TEXT,
|
|
209
227
|
usage TEXT,
|
|
210
228
|
agent_name TEXT,
|
|
229
|
+
run_id TEXT,
|
|
211
230
|
created_at INTEGER NOT NULL
|
|
212
231
|
)`,
|
|
213
232
|
`CREATE INDEX IF NOT EXISTS agent_message_thread_created_idx
|
|
@@ -227,6 +246,8 @@ var statements = [
|
|
|
227
246
|
executed_at INTEGER,
|
|
228
247
|
run_id TEXT
|
|
229
248
|
)`,
|
|
249
|
+
`CREATE INDEX IF NOT EXISTS agent_tool_call_message_idx
|
|
250
|
+
ON agent_tool_call (message_id)`,
|
|
230
251
|
`CREATE TABLE IF NOT EXISTS agent_token_usage (
|
|
231
252
|
id TEXT PRIMARY KEY NOT NULL,
|
|
232
253
|
thread_id TEXT NOT NULL REFERENCES agent_thread(id) ON DELETE CASCADE,
|
|
@@ -265,20 +286,59 @@ var statements = [
|
|
|
265
286
|
retries INTEGER NOT NULL DEFAULT 0,
|
|
266
287
|
started_at INTEGER NOT NULL,
|
|
267
288
|
settled_at INTEGER,
|
|
268
|
-
prompt_hash TEXT
|
|
289
|
+
prompt_hash TEXT,
|
|
290
|
+
parent_run_id TEXT
|
|
269
291
|
)`,
|
|
270
292
|
`CREATE INDEX IF NOT EXISTS agent_run_started_idx
|
|
271
293
|
ON agent_run (started_at)`
|
|
272
294
|
];
|
|
295
|
+
var additiveColumns = [
|
|
296
|
+
{
|
|
297
|
+
table: "agent_message",
|
|
298
|
+
column: "run_id",
|
|
299
|
+
ddl: "ALTER TABLE agent_message ADD COLUMN run_id TEXT"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
table: "agent_message",
|
|
303
|
+
column: "attachments",
|
|
304
|
+
ddl: "ALTER TABLE agent_message ADD COLUMN attachments TEXT"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
table: "agent_thread",
|
|
308
|
+
column: "default_agent",
|
|
309
|
+
ddl: "ALTER TABLE agent_thread ADD COLUMN default_agent TEXT"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
table: "agent_run",
|
|
313
|
+
column: "parent_run_id",
|
|
314
|
+
ddl: "ALTER TABLE agent_run ADD COLUMN parent_run_id TEXT"
|
|
315
|
+
}
|
|
316
|
+
];
|
|
273
317
|
async function ensureAgentSchema(db) {
|
|
274
318
|
for (const statement of statements) {
|
|
275
319
|
await db.run(import_drizzle_orm.sql.raw(statement));
|
|
276
320
|
}
|
|
321
|
+
for (const { table, column, ddl } of additiveColumns) {
|
|
322
|
+
const existing = await db.all(import_drizzle_orm.sql.raw(`PRAGMA table_info(${table})`));
|
|
323
|
+
if (!existing.some((row) => row.name === column)) {
|
|
324
|
+
await db.run(import_drizzle_orm.sql.raw(ddl));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
277
327
|
}
|
|
278
328
|
__name(ensureAgentSchema, "ensureAgentSchema");
|
|
279
329
|
|
|
280
330
|
// src/drizzle-agent-store.ts
|
|
281
331
|
var import_drizzle_orm2 = require("drizzle-orm");
|
|
332
|
+
var TURN_MESSAGE_COLUMNS = {
|
|
333
|
+
id: agentMessage.id,
|
|
334
|
+
role: agentMessage.role,
|
|
335
|
+
content: agentMessage.content,
|
|
336
|
+
agentName: agentMessage.agentName,
|
|
337
|
+
toolCalls: agentMessage.toolCalls,
|
|
338
|
+
toolResults: agentMessage.toolResults,
|
|
339
|
+
attachments: agentMessage.attachments,
|
|
340
|
+
createdAt: agentMessage.createdAt
|
|
341
|
+
};
|
|
282
342
|
var DrizzleAgentStore = class {
|
|
283
343
|
static {
|
|
284
344
|
__name(this, "DrizzleAgentStore");
|
|
@@ -296,6 +356,7 @@ var DrizzleAgentStore = class {
|
|
|
296
356
|
title: input.title ?? "New chat",
|
|
297
357
|
transient: input.transient ?? false,
|
|
298
358
|
activeStreamId: null,
|
|
359
|
+
defaultAgent: null,
|
|
299
360
|
createdAt: now,
|
|
300
361
|
updatedAt: now,
|
|
301
362
|
deletedAt: null
|
|
@@ -318,6 +379,50 @@ var DrizzleAgentStore = class {
|
|
|
318
379
|
} : {}
|
|
319
380
|
};
|
|
320
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* The window a turn reads off a thread: its newest `messageLimit` messages (oldest first), the
|
|
384
|
+
* title, the default agent, and whether the thread has EVER been answered.
|
|
385
|
+
*
|
|
386
|
+
* {@link getThread} is the wrong read for a turn. It materializes the transcript — every message,
|
|
387
|
+
* every attachment, every tool output the thread ever recorded — and the run then journals what it
|
|
388
|
+
* loaded, so a long thread pays for its whole history on every turn and again on every replay.
|
|
389
|
+
*
|
|
390
|
+
* `hasAssistantMessage` is answered over the WHOLE thread, not the returned page: it answers "has
|
|
391
|
+
* this conversation been answered before?", and a thread whose window happens to hold only the
|
|
392
|
+
* user's last questions has still been answered. `null` when the thread is unknown or
|
|
393
|
+
* soft-deleted, same as {@link getThread}.
|
|
394
|
+
*/
|
|
395
|
+
async loadThreadForTurn(query) {
|
|
396
|
+
const [thread] = await this.db.select({
|
|
397
|
+
title: agentThread.title,
|
|
398
|
+
defaultAgent: agentThread.defaultAgent
|
|
399
|
+
}).from(agentThread).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(agentThread.id, query.threadId), (0, import_drizzle_orm2.isNull)(agentThread.deletedAt)));
|
|
400
|
+
if (thread === void 0) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const messages = await this.loadTurnWindow(query.threadId, query.messageLimit);
|
|
404
|
+
const [answered] = await this.db.select({
|
|
405
|
+
id: agentMessage.id
|
|
406
|
+
}).from(agentMessage).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(agentMessage.threadId, query.threadId), (0, import_drizzle_orm2.eq)(agentMessage.role, "assistant"))).limit(1);
|
|
407
|
+
return {
|
|
408
|
+
title: thread.title,
|
|
409
|
+
defaultAgent: thread.defaultAgent,
|
|
410
|
+
hasAssistantMessage: answered !== void 0,
|
|
411
|
+
messages
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The newest `limit` messages of a thread, oldest first, projected to the columns a model turn
|
|
416
|
+
* reads. Read newest-first so the LIMIT is the database's job, then reversed for the prompt.
|
|
417
|
+
*/
|
|
418
|
+
async loadTurnWindow(threadId, limit) {
|
|
419
|
+
if (limit !== void 0 && limit <= 0) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const window = this.db.select(TURN_MESSAGE_COLUMNS).from(agentMessage).where((0, import_drizzle_orm2.eq)(agentMessage.threadId, threadId)).orderBy((0, import_drizzle_orm2.desc)(agentMessage.createdAt), (0, import_drizzle_orm2.desc)(agentMessage.id));
|
|
423
|
+
const rows = limit === void 0 ? await window : await window.limit(limit);
|
|
424
|
+
return rows.reverse().map((row) => this.toStoredMessage(row));
|
|
425
|
+
}
|
|
321
426
|
async listThreads(actorRef, limit = 50) {
|
|
322
427
|
const threads = await this.db.select().from(agentThread).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(agentThread.actorRef, actorRef), (0, import_drizzle_orm2.eq)(agentThread.transient, false), (0, import_drizzle_orm2.isNull)(agentThread.deletedAt))).orderBy((0, import_drizzle_orm2.desc)(agentThread.updatedAt)).limit(limit);
|
|
323
428
|
const summaries = [];
|
|
@@ -348,6 +453,7 @@ var DrizzleAgentStore = class {
|
|
|
348
453
|
title: source.title,
|
|
349
454
|
transient: false,
|
|
350
455
|
activeStreamId: null,
|
|
456
|
+
defaultAgent: source.defaultAgent,
|
|
351
457
|
createdAt: now,
|
|
352
458
|
updatedAt: now,
|
|
353
459
|
deletedAt: null
|
|
@@ -361,9 +467,14 @@ var DrizzleAgentStore = class {
|
|
|
361
467
|
content: message.content,
|
|
362
468
|
toolCalls: message.toolCalls,
|
|
363
469
|
toolResults: message.toolResults,
|
|
470
|
+
attachments: message.attachments,
|
|
364
471
|
followUps: message.followUps,
|
|
365
472
|
usage: message.usage,
|
|
366
473
|
agentName: message.agentName,
|
|
474
|
+
// The copy is the same message, so it keeps the run that wrote it. A reader asking which
|
|
475
|
+
// turn produced this text gets the truthful answer; the run's own thread is still the
|
|
476
|
+
// original, so a run-scoped read never picks the fork's rows up.
|
|
477
|
+
runId: message.runId,
|
|
367
478
|
createdAt: message.createdAt
|
|
368
479
|
})));
|
|
369
480
|
}
|
|
@@ -383,9 +494,10 @@ var DrizzleAgentStore = class {
|
|
|
383
494
|
}
|
|
384
495
|
async runForToolCall(toolCallId) {
|
|
385
496
|
const [row] = await this.db.select({
|
|
497
|
+
runId: agentToolCall.runId,
|
|
386
498
|
activeStreamId: agentThread.activeStreamId
|
|
387
499
|
}).from(agentToolCall).innerJoin(agentMessage, (0, import_drizzle_orm2.eq)(agentToolCall.messageId, agentMessage.id)).innerJoin(agentThread, (0, import_drizzle_orm2.eq)(agentMessage.threadId, agentThread.id)).where((0, import_drizzle_orm2.eq)(agentToolCall.id, toolCallId));
|
|
388
|
-
return row?.activeStreamId ?? null;
|
|
500
|
+
return row?.runId ?? row?.activeStreamId ?? null;
|
|
389
501
|
}
|
|
390
502
|
async ownerOfActiveStream(runId) {
|
|
391
503
|
const [thread] = await this.db.select({
|
|
@@ -410,7 +522,43 @@ var DrizzleAgentStore = class {
|
|
|
410
522
|
activeStreamId: runId
|
|
411
523
|
}).where((0, import_drizzle_orm2.eq)(agentThread.id, threadId));
|
|
412
524
|
}
|
|
413
|
-
/**
|
|
525
|
+
/**
|
|
526
|
+
* Patch a thread's `title` and/or `defaultAgent`. Each field is only touched when PRESENT in
|
|
527
|
+
* `patch` — `defaultAgent: null` clears it (falls back to the app default), while an omitted
|
|
528
|
+
* `defaultAgent` leaves whatever is stored untouched. A no-op (including `updatedAt`) when the
|
|
529
|
+
* patch is empty or the thread doesn't exist.
|
|
530
|
+
*/
|
|
531
|
+
async updateThread(threadId, patch) {
|
|
532
|
+
const updates = {};
|
|
533
|
+
if (patch.title !== void 0) {
|
|
534
|
+
updates.title = patch.title;
|
|
535
|
+
}
|
|
536
|
+
if (patch.defaultAgent !== void 0) {
|
|
537
|
+
updates.defaultAgent = patch.defaultAgent;
|
|
538
|
+
}
|
|
539
|
+
if (Object.keys(updates).length === 0) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
updates.updatedAt = /* @__PURE__ */ new Date();
|
|
543
|
+
await this.db.update(agentThread).set(updates).where((0, import_drizzle_orm2.eq)(agentThread.id, threadId));
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The thread's default agent, projected — the caller wants one nullable scalar to decide which
|
|
547
|
+
* agent answers the next turn, and {@link getThread} would materialize the whole transcript to
|
|
548
|
+
* hand it over. `null` when the thread is unknown, soft-deleted, or has no default set.
|
|
549
|
+
*/
|
|
550
|
+
async defaultAgentForThread(threadId) {
|
|
551
|
+
const [thread] = await this.db.select({
|
|
552
|
+
defaultAgent: agentThread.defaultAgent
|
|
553
|
+
}).from(agentThread).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(agentThread.id, threadId), (0, import_drizzle_orm2.isNull)(agentThread.deletedAt)));
|
|
554
|
+
return thread?.defaultAgent ?? null;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Persist the start of a run (turn). Replay-safe: called under a durable localStep.
|
|
558
|
+
*
|
|
559
|
+
* Takes the SPI's own {@link RecordRunStartInput} rather than a hand-copied shape: a field added
|
|
560
|
+
* to the input is otherwise accepted and dropped, silently, by every adapter that re-declares it.
|
|
561
|
+
*/
|
|
414
562
|
async recordRunStart(run) {
|
|
415
563
|
const runRow = {
|
|
416
564
|
id: run.runId,
|
|
@@ -424,7 +572,8 @@ var DrizzleAgentStore = class {
|
|
|
424
572
|
retries: 0,
|
|
425
573
|
startedAt: /* @__PURE__ */ new Date(),
|
|
426
574
|
settledAt: null,
|
|
427
|
-
promptHash: run.promptHash ?? null
|
|
575
|
+
promptHash: run.promptHash ?? null,
|
|
576
|
+
parentRunId: run.parentRunId ?? null
|
|
428
577
|
};
|
|
429
578
|
await this.db.insert(agentRun).values(runRow);
|
|
430
579
|
}
|
|
@@ -464,9 +613,11 @@ var DrizzleAgentStore = class {
|
|
|
464
613
|
content: input.content,
|
|
465
614
|
toolCalls: input.toolCalls ?? null,
|
|
466
615
|
toolResults: input.toolResults ?? null,
|
|
616
|
+
attachments: input.attachments ?? null,
|
|
467
617
|
followUps: input.followUps ?? null,
|
|
468
618
|
usage: input.usage ?? null,
|
|
469
619
|
agentName: input.agentName ?? null,
|
|
620
|
+
runId: input.runId ?? null,
|
|
470
621
|
createdAt: now
|
|
471
622
|
};
|
|
472
623
|
await this.db.insert(agentMessage).values(message);
|
|
@@ -475,6 +626,11 @@ var DrizzleAgentStore = class {
|
|
|
475
626
|
}).where((0, import_drizzle_orm2.eq)(agentThread.id, input.threadId));
|
|
476
627
|
return this.toStoredMessage(message);
|
|
477
628
|
}
|
|
629
|
+
async setMessageToolResults(messageId, results) {
|
|
630
|
+
await this.db.update(agentMessage).set({
|
|
631
|
+
toolResults: results
|
|
632
|
+
}).where((0, import_drizzle_orm2.eq)(agentMessage.id, messageId));
|
|
633
|
+
}
|
|
478
634
|
async truncateFrom(threadId, messageId) {
|
|
479
635
|
const messages = await this.db.select().from(agentMessage).where((0, import_drizzle_orm2.eq)(agentMessage.threadId, threadId)).orderBy((0, import_drizzle_orm2.asc)(agentMessage.createdAt), (0, import_drizzle_orm2.asc)(agentMessage.id));
|
|
480
636
|
const cutoff = messages.findIndex((message) => message.id === messageId);
|
|
@@ -523,6 +679,39 @@ var DrizzleAgentStore = class {
|
|
|
523
679
|
}
|
|
524
680
|
await this.db.update(agentToolCall).set(updates).where((0, import_drizzle_orm2.eq)(agentToolCall.id, input.toolCallId));
|
|
525
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Of `mediaIds`, the ones a surviving message in one of this actor's threads still carries.
|
|
684
|
+
*
|
|
685
|
+
* Reads the `attachments` JSON back out and matches in memory rather than pushing the match into
|
|
686
|
+
* SQL: the column holds an array of objects, and every dialect this adapter's sibling targets
|
|
687
|
+
* spells that query differently (`jsonb` containment, `json_table`, `json_each`), while none of
|
|
688
|
+
* them can use an index for it anyway. The scan is bounded by ONE actor's attachment-bearing
|
|
689
|
+
* messages, which is a small set — attachments are rare, and a message without one is excluded
|
|
690
|
+
* by the `is not null` before any JSON is parsed.
|
|
691
|
+
*
|
|
692
|
+
* Soft-deleted threads are deliberately included: their message rows survive, so the bytes they
|
|
693
|
+
* point at are still reachable from stored state and are not garbage.
|
|
694
|
+
*/
|
|
695
|
+
async referencedMediaIds(actorRef, mediaIds) {
|
|
696
|
+
if (mediaIds.length === 0) {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
const rows = await this.db.select({
|
|
700
|
+
attachments: agentMessage.attachments
|
|
701
|
+
}).from(agentMessage).innerJoin(agentThread, (0, import_drizzle_orm2.eq)(agentMessage.threadId, agentThread.id)).where((0, import_drizzle_orm2.and)((0, import_drizzle_orm2.eq)(agentThread.actorRef, actorRef), (0, import_drizzle_orm2.isNotNull)(agentMessage.attachments)));
|
|
702
|
+
const wanted = new Set(mediaIds);
|
|
703
|
+
const found = /* @__PURE__ */ new Set();
|
|
704
|
+
for (const row of rows) {
|
|
705
|
+
for (const attachment of row.attachments ?? []) {
|
|
706
|
+
if (wanted.has(attachment.mediaId)) {
|
|
707
|
+
found.add(attachment.mediaId);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return [
|
|
712
|
+
...wanted
|
|
713
|
+
].filter((mediaId) => found.has(mediaId));
|
|
714
|
+
}
|
|
526
715
|
async recordUsage(input) {
|
|
527
716
|
await this.db.insert(agentTokenUsage).values({
|
|
528
717
|
id: crypto.randomUUID(),
|
|
@@ -557,6 +746,7 @@ var DrizzleAgentStore = class {
|
|
|
557
746
|
transient: thread.transient,
|
|
558
747
|
createdAt: thread.createdAt.toISOString(),
|
|
559
748
|
updatedAt: thread.updatedAt.toISOString(),
|
|
749
|
+
defaultAgent: thread.defaultAgent,
|
|
560
750
|
...lastContent !== void 0 ? {
|
|
561
751
|
lastMessagePreview: lastContent.slice(0, 120)
|
|
562
752
|
} : {}
|
|
@@ -574,6 +764,9 @@ var DrizzleAgentStore = class {
|
|
|
574
764
|
...message.toolResults != null ? {
|
|
575
765
|
toolResults: message.toolResults
|
|
576
766
|
} : {},
|
|
767
|
+
...message.attachments != null ? {
|
|
768
|
+
attachments: message.attachments
|
|
769
|
+
} : {},
|
|
577
770
|
...message.followUps != null ? {
|
|
578
771
|
followUps: message.followUps
|
|
579
772
|
} : {},
|
|
@@ -582,6 +775,9 @@ var DrizzleAgentStore = class {
|
|
|
582
775
|
} : {},
|
|
583
776
|
...message.agentName != null ? {
|
|
584
777
|
agentName: message.agentName
|
|
778
|
+
} : {},
|
|
779
|
+
...message.runId != null ? {
|
|
780
|
+
runId: message.runId
|
|
585
781
|
} : {}
|
|
586
782
|
};
|
|
587
783
|
}
|
|
@@ -615,7 +811,8 @@ __name(isToolKind, "isToolKind");
|
|
|
615
811
|
var RUN_STATUSES = [
|
|
616
812
|
"running",
|
|
617
813
|
"completed",
|
|
618
|
-
"failed"
|
|
814
|
+
"failed",
|
|
815
|
+
"cancelled"
|
|
619
816
|
];
|
|
620
817
|
function isRunStatus(value) {
|
|
621
818
|
return RUN_STATUSES.includes(value);
|
|
@@ -656,7 +853,8 @@ function toRecentRunRow(run) {
|
|
|
656
853
|
errorMessage: run.errorMessage ?? null,
|
|
657
854
|
retries: run.retries,
|
|
658
855
|
startedAt: run.startedAt.toISOString(),
|
|
659
|
-
promptHash: run.promptHash ?? null
|
|
856
|
+
promptHash: run.promptHash ?? null,
|
|
857
|
+
parentRunId: run.parentRunId ?? null
|
|
660
858
|
};
|
|
661
859
|
}
|
|
662
860
|
__name(toRecentRunRow, "toRecentRunRow");
|