@dudousxd/nestjs-agent-store-drizzle 0.8.2 → 0.10.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 +126 -3
- package/dist/index.cjs +808 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1965 -534
- package/dist/index.d.ts +1965 -534
- package/dist/index.js +792 -21
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -36,9 +36,132 @@ export class AppModule {}
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
The package ships the `agentSchema` (Drizzle tables), `ensureAgentSchema` (a non-destructive
|
|
39
|
-
|
|
40
|
-
`
|
|
41
|
-
`ensureAgentSchema` helper.
|
|
39
|
+
DDL helper for a quick start), `DrizzleAgentStore`, `DrizzleGovernanceQueries`, and
|
|
40
|
+
`DrizzleRagIngestionLog`. For production,
|
|
41
|
+
prefer your normal drizzle-kit migrations over the `ensureAgentSchema` helper.
|
|
42
|
+
|
|
43
|
+
## The read a turn makes
|
|
44
|
+
|
|
45
|
+
The agent loop does not call `getThread` to build a prompt. This store implements the core SPI's
|
|
46
|
+
`ThreadTurnReader`, so the loop asks it for `loadThreadForTurn({ threadId, messageLimit })` instead:
|
|
47
|
+
the thread's newest `messageLimit` messages oldest-first, bounded by the database rather than in
|
|
48
|
+
memory, projected to the columns a model turn reads (`usage`, `follow_ups` and `run_id` stay in the
|
|
49
|
+
table), plus the title, the default agent, and whether the thread has EVER been answered.
|
|
50
|
+
|
|
51
|
+
`messageLimit` is the configured `HistoryPolicy.maxMessages`. A policy that summarizes, or whose
|
|
52
|
+
ceiling is only a token budget, names no row bound and the whole thread is read — see the core
|
|
53
|
+
README. Nothing to wire: the loop probes for the method, and `getThread` remains the right read for
|
|
54
|
+
a client rendering a transcript.
|
|
55
|
+
|
|
56
|
+
## Memory
|
|
57
|
+
|
|
58
|
+
`DrizzleMemoryProvider` is a `MemoryProvider` over `agent_memory`, a table `ensureAgentSchema`
|
|
59
|
+
creates alongside the other six.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { DrizzleAgentStoreModule, DrizzleMemoryProvider } from '@dudousxd/nestjs-agent-store-drizzle';
|
|
63
|
+
|
|
64
|
+
AgentModule.forRootAsync({
|
|
65
|
+
// AGENT_STORE already comes from DrizzleAgentStoreModule.forRoot({ db }), which is global.
|
|
66
|
+
externalStore: true,
|
|
67
|
+
inject: [DrizzleMemoryProvider],
|
|
68
|
+
useFactory: (memory: DrizzleMemoryProvider) => ({
|
|
69
|
+
model,
|
|
70
|
+
actorResolver,
|
|
71
|
+
memory: { provider: memory },
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The store module exports the provider but never binds `AGENT_MEMORY`: that token's **presence** is
|
|
77
|
+
what turns memory on, so binding it here would switch the feature on for every host that installs the
|
|
78
|
+
store. Naming the provider is how you opt in.
|
|
79
|
+
|
|
80
|
+
What it holds to:
|
|
81
|
+
|
|
82
|
+
- `list` filters `scope in (…)` in the **query**. The library drops out-of-scope records it is handed,
|
|
83
|
+
but that is a backstop — a memory held for another actor or another tenant is never selected, so it
|
|
84
|
+
is unreachable rather than outranked.
|
|
85
|
+
- `write` upserts on the (`scope`, `key`) unique index in one statement, so two turns concluding the
|
|
86
|
+
same key at once cannot race to a duplicate-key insert. The conflict `set` names exactly what a
|
|
87
|
+
rewrite may change, so `pinned`, `created_at` and `id` are left alone by construction: a rewrite
|
|
88
|
+
must not unpin a record, lose when the belief was formed, or invalidate the delete handle a person
|
|
89
|
+
was already shown.
|
|
90
|
+
- `write` refuses an **agent-authored** record at any scope but the actor's own. A human-authored one
|
|
91
|
+
above it is allowed, because that is what a console publishing an organisation's policy does, and
|
|
92
|
+
whether that person may write there is what `memoryWriteVerdict` answers.
|
|
93
|
+
- `forget` deletes only from the actor's own scope, so an id alone cannot reach a tenant's memory.
|
|
94
|
+
- `pin({ id, pinned })` is the operator act the SPI has no method for — a pin grants a fact a
|
|
95
|
+
permanent place in every future prompt, so nothing an agent can reach may set it. Authorize it in
|
|
96
|
+
your own console.
|
|
97
|
+
|
|
98
|
+
**`search` is not implemented**, deliberately. It buys a block filled by relevance once the applicable
|
|
99
|
+
set outgrows `maxMemories`, and that needs an index over the memories themselves — whose shape depends
|
|
100
|
+
entirely on what you already run. Without it, `list` reads the applicable scopes whole and the ceiling
|
|
101
|
+
never bites. The signal that it is worth building is `MemoryDigest.omitted` going non-zero, and
|
|
102
|
+
`pinnedOmitted` especially: a standing policy that stopped reaching any prompt. Both are on the
|
|
103
|
+
`aviary:agent:memory.resolved` diagnostic.
|
|
104
|
+
|
|
105
|
+
## Upgrading a database that already exists
|
|
106
|
+
|
|
107
|
+
`ensureAgentSchema` is additive only — `CREATE TABLE IF NOT EXISTS` plus `CREATE INDEX IF NOT
|
|
108
|
+
EXISTS`, and an explicit add-column pass for columns this package introduced after a table shipped,
|
|
109
|
+
since the `CREATE TABLE` guard is inert against a table that already exists. Calling it on boot is
|
|
110
|
+
enough; it never drops a column or changes a type.
|
|
111
|
+
|
|
112
|
+
On your own drizzle-kit migrations, those additive statements are yours to write:
|
|
113
|
+
|
|
114
|
+
```sql
|
|
115
|
+
ALTER TABLE agent_thread ADD COLUMN default_agent TEXT;
|
|
116
|
+
ALTER TABLE agent_message ADD COLUMN run_id TEXT;
|
|
117
|
+
ALTER TABLE agent_message ADD COLUMN attachments TEXT;
|
|
118
|
+
ALTER TABLE agent_run ADD COLUMN parent_run_id TEXT;
|
|
119
|
+
CREATE INDEX agent_tool_call_message_idx ON agent_tool_call (message_id);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The index is not optional on Postgres: `agent_tool_call.message_id` carries a foreign key, and
|
|
123
|
+
Postgres — unlike MySQL — does not index one for you, while both message-scoped reads (the thread
|
|
124
|
+
reader's `IN (…)` and `truncateFrom`'s delete) filter on it.
|
|
125
|
+
|
|
126
|
+
## Attachment housekeeping
|
|
127
|
+
|
|
128
|
+
`referencedMediaIds(actorRef, mediaIds)` answers which of a set of media ids a message that still
|
|
129
|
+
exists carries, for one actor — the inverse of the host's own staged-media inventory, and the half a
|
|
130
|
+
sweep cannot work out for itself. It needs **no schema change**: it reads the `attachments` JSON
|
|
131
|
+
column messages have carried since attachments shipped, so an existing database answers correctly
|
|
132
|
+
the moment you upgrade, with nothing to backfill.
|
|
133
|
+
|
|
134
|
+
The match happens in memory rather than in SQL. The column holds an array of objects, every dialect
|
|
135
|
+
spells that query differently, and none of them can use an index for it; the scan is bounded to one
|
|
136
|
+
actor's attachment-bearing messages, and a message with no attachments never leaves the database.
|
|
137
|
+
|
|
138
|
+
A thread that was soft-deleted still counts as holding its references: the message rows survive, so
|
|
139
|
+
the bytes are still reachable from stored state. Delete the thread for real and the cascade makes
|
|
140
|
+
them collectable on the next sweep.
|
|
141
|
+
|
|
142
|
+
## RAG ingestion outcomes
|
|
143
|
+
|
|
144
|
+
`rag_ingestion_log` holds the latest outcome for every RAG document — ingested, skipped, failed,
|
|
145
|
+
removed — one row per document id, overwritten on re-ingest. It answers the question a vector store
|
|
146
|
+
structurally cannot: *which documents failed to index, and why*. A document whose extraction came
|
|
147
|
+
back empty, whose mime type had no extractor, or whose embedding call threw produces zero chunks, so
|
|
148
|
+
`VectorStore.listDocuments()` cannot tell it from one nobody ever uploaded. Pair the two: the index
|
|
149
|
+
is the truth about what is retrievable, this table is the truth about what was attempted.
|
|
150
|
+
|
|
151
|
+
`DrizzleRagIngestionLog` fills it by subscribing to the `aviary:rag:*` diagnostics
|
|
152
|
+
`@dudousxd/nestjs-agent-rag-media` publishes — the RAG package owns no storage of its own. It is
|
|
153
|
+
bound and exported by `DrizzleAgentStoreModule.forRoot({ db })` by default; pass
|
|
154
|
+
`{ ragIngestionLog: false }` to bind nothing at all. `ensureAgentSchema` creates the table, indexed
|
|
155
|
+
by (`collection`, `updated_at`) for the per-collection listing.
|
|
156
|
+
|
|
157
|
+
Reads: `list` / `listPage` (a page plus the unpaginated total), `get`, `remove`,
|
|
158
|
+
`removeByCollection`, a delete-safe keyset `iterate`, and `listDocumentIds` for an orphan sweep that
|
|
159
|
+
wants a collection's id set without hydrating every stack trace in the table. The paging order is
|
|
160
|
+
exported as `RAG_INGESTION_LOG_PAGE_ORDER` — `updated_at desc` tiebroken on the primary key, which
|
|
161
|
+
is what keeps consecutive pages disjoint when a bulk upload stamps a whole batch with one timestamp.
|
|
162
|
+
|
|
163
|
+
Writes are best-effort: the recorder runs detached on a diagnostics channel, so a failed write is
|
|
164
|
+
reported and dropped rather than taking down the ingestion that triggered it.
|
|
42
165
|
|
|
43
166
|
## Cost accounting
|
|
44
167
|
|