@dex-ai/memory 0.3.3 → 0.3.4
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 +67 -48
- package/package.json +1 -2
- package/src/db.ts +116 -70
- package/src/extension.ts +9 -0
- package/src/index.ts +7 -7
- package/src/tools.ts +114 -64
package/README.md
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
# @dex-ai/memory
|
|
1
|
+
# @dex-ai/memory
|
|
2
2
|
|
|
3
|
-
SQLite-backed memory
|
|
3
|
+
SQLite-backed memory extension for [`@dex-ai/sdk`](https://github.com/klxdev/dex-ai-sdk). Three memory types — **episodic**, **semantic**, and **procedural** — in one extension and one SQLite file.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun add @dex-ai/memory
|
|
9
|
-
# optional: default local embedder (ONNX via Transformers.js)
|
|
10
|
-
bun add @xenova/transformers
|
|
8
|
+
bun add @dex-ai/memory
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
`@xenova/transformers
|
|
11
|
+
The default local embedder uses `@xenova/transformers`. Pass your own `embed()` function if you want to use a remote embedding service instead.
|
|
14
12
|
|
|
15
13
|
## Usage
|
|
16
14
|
|
|
17
15
|
```ts
|
|
18
|
-
import {
|
|
16
|
+
import { Agent } from '@dex-ai/sdk';
|
|
19
17
|
import { openai } from '@dex-ai/openai';
|
|
20
|
-
import {
|
|
18
|
+
import { memoryExtension } from '@dex-ai/memory';
|
|
21
19
|
|
|
22
|
-
const agent =
|
|
20
|
+
const agent = await Agent.create({
|
|
23
21
|
provider: openai({ modelId: 'gpt-4.1' }),
|
|
24
22
|
extensions: [
|
|
25
|
-
|
|
23
|
+
memoryExtension({
|
|
26
24
|
path: '~/.dex/memory.db',
|
|
27
25
|
userId: 'alice',
|
|
28
26
|
|
|
29
27
|
// Optional: a cheaper model for memory's summarize + extract calls.
|
|
30
28
|
llm: { model: 'gpt-4o-mini' },
|
|
31
29
|
|
|
32
|
-
// Optional: bring your own embedder
|
|
33
|
-
// If omitted, a local Transformers.js embedder
|
|
30
|
+
// Optional: bring your own embedder, e.g. a remote endpoint.
|
|
31
|
+
// If omitted, a local Transformers.js embedder is used.
|
|
34
32
|
// embed: async (texts) => await myRemoteEmbedder(texts),
|
|
35
33
|
}),
|
|
36
34
|
],
|
|
@@ -41,44 +39,49 @@ const agent = new DexAgent({
|
|
|
41
39
|
|
|
42
40
|
### Episodic memory — past turns, auto-summarized
|
|
43
41
|
|
|
44
|
-
Every `generate()` iteration, the extension
|
|
42
|
+
Every `generate()` iteration, the extension can fire a background task:
|
|
43
|
+
|
|
45
44
|
1. Summarize the turn's new messages into 1-3 sentences via the LLM.
|
|
46
45
|
2. Embed the summary and write both to SQLite.
|
|
47
46
|
|
|
48
|
-
Background writes are tracked; `
|
|
47
|
+
Background writes are tracked; `session-stop` awaits them before closing the database.
|
|
48
|
+
|
|
49
|
+
At recall time (`model-start`), the extension fetches:
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
- The
|
|
52
|
-
-
|
|
53
|
-
- De-duplicated by id, sorted newest-first.
|
|
51
|
+
- The last **5 most-recent** episodes for the user by default.
|
|
52
|
+
- The **3 most-similar** episodes via sqlite-vec against the user's last message.
|
|
53
|
+
- De-duplicated results, sorted newest-first.
|
|
54
54
|
|
|
55
55
|
### Semantic memory — durable facts
|
|
56
56
|
|
|
57
57
|
Facts are `(subject, predicate, object)` tuples, unique by `(userId, subject, predicate)`. Written in two ways:
|
|
58
58
|
|
|
59
|
-
**Automatic extraction** at each
|
|
59
|
+
**Automatic extraction** at each generate stop: the LLM extracts durable claims from the turn and upserts them.
|
|
60
60
|
|
|
61
61
|
**Model-driven** via tools:
|
|
62
|
-
- `memory.remember_fact({ subject, predicate, object })` — upsert by key.
|
|
63
|
-
- `memory.forget_fact({ subject, predicate })` — delete by key.
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
- `memory_remember_fact({ subject, predicate, object })` — upsert by key.
|
|
64
|
+
- `memory_forget_fact({ subject, predicate })` — delete by key.
|
|
65
|
+
- `memory_recall_facts({ query, limit? })` — search facts by vector similarity.
|
|
66
|
+
|
|
67
|
+
At recall time, the most relevant facts for the user's last message are injected as a synthetic memory message.
|
|
66
68
|
|
|
67
69
|
### Procedural memory — runbooks
|
|
68
70
|
|
|
69
71
|
Long-form how-to content. Stored by unique `title`, tagged, with an embedding over `title + body`.
|
|
70
72
|
|
|
71
73
|
**Tools**:
|
|
72
|
-
- `memory.store_procedure({ title, body, tags? })` — upsert by title.
|
|
73
|
-
- `memory.list_procedures({ query?, tag?, limit? })` — with `query`, returns vector-ranked results; with `tag`, filters; with neither, returns most-recently-updated.
|
|
74
|
-
- `memory.get_procedure({ title })` — fetch full body.
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
- `memory_store_procedure({ title, body, tags? })` — upsert by title.
|
|
76
|
+
- `memory_list_procedures({ query?, tag?, limit? })` — with `query`, returns vector-ranked results; with `tag`, filters; with neither, returns most-recently-updated.
|
|
77
|
+
- `memory_get_procedure({ title })` — fetch full body by exact title.
|
|
78
|
+
|
|
79
|
+
**Auto-inject**: at recall time, the extension does a vector similarity lookup against the user's last message. Matching procedure titles are injected; use `memory_get_procedure` to fetch the full runbook when needed.
|
|
77
80
|
|
|
78
81
|
## LLM configuration
|
|
79
82
|
|
|
80
83
|
```ts
|
|
81
|
-
|
|
84
|
+
memoryExtension({
|
|
82
85
|
// ...
|
|
83
86
|
llm: {
|
|
84
87
|
provider: customProvider, // optional — overrides the agent's provider entirely
|
|
@@ -90,63 +93,77 @@ memoryExtensionSqlite({
|
|
|
90
93
|
Resolution rule:
|
|
91
94
|
|
|
92
95
|
1. If `llm.provider` is set, use it.
|
|
93
|
-
2. Otherwise use the agent's provider
|
|
94
|
-
3. If `llm.model` is set, it
|
|
96
|
+
2. Otherwise use the agent's model/provider.
|
|
97
|
+
3. If `llm.model` is set, it is passed via `providerOptions.model` on every memory-internal request.
|
|
95
98
|
|
|
96
99
|
## Extension options
|
|
97
100
|
|
|
98
101
|
```ts
|
|
99
|
-
interface
|
|
102
|
+
interface MemoryExtensionOptions {
|
|
100
103
|
path: string; // SQLite file path or ':memory:'
|
|
101
|
-
userId: string; // owner for episodic + semantic
|
|
104
|
+
userId: string; // owner for episodic + semantic memory
|
|
105
|
+
|
|
106
|
+
vecPath?: string; // optional explicit sqlite-vec extension path
|
|
107
|
+
sqlitePath?: string; // optional SQLite dylib with extension loading enabled
|
|
102
108
|
|
|
103
109
|
llm?: { provider?: Provider; model?: string };
|
|
104
|
-
embed?: (texts: string[]) => Promise<number[][]>; // 384-dim
|
|
110
|
+
embed?: (texts: string[]) => Promise<number[][]>; // 384-dim vectors
|
|
105
111
|
|
|
106
|
-
episodicRecent?: number; // default
|
|
112
|
+
episodicRecent?: number; // default 5
|
|
107
113
|
episodicSimilar?: number; // default 3
|
|
114
|
+
semanticLimit?: number; // default 10
|
|
108
115
|
proceduralThreshold?: number; // default 0.5
|
|
109
116
|
autoWrite?: boolean; // default true; set false to disable auto-summarize+extract
|
|
117
|
+
autoWriteMinMessages?: number; // default 2
|
|
110
118
|
}
|
|
111
119
|
```
|
|
112
120
|
|
|
113
121
|
## Injected prompt shape
|
|
114
122
|
|
|
115
|
-
When memory is available, the extension
|
|
123
|
+
When memory is available, the extension inserts a synthetic memory message before the latest user message. Example:
|
|
116
124
|
|
|
117
|
-
```
|
|
125
|
+
```text
|
|
126
|
+
<memory>
|
|
118
127
|
Recent context:
|
|
119
128
|
- 5m ago: discussed the auth flow; chose JWT over session cookies.
|
|
120
129
|
- 1d ago: debugged a test flake in session-sqlite.
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
Relevant facts:
|
|
123
132
|
- user prefers TypeScript
|
|
124
133
|
- project uses PostgreSQL 15
|
|
125
134
|
- user is on macOS
|
|
126
135
|
|
|
127
|
-
Relevant
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
3. git tag vX.Y.Z && git push --tags
|
|
136
|
+
Relevant procedures (use memory_get_procedure to fetch details):
|
|
137
|
+
- [deploy-dex] (id: proc_123, similarity: 0.71)
|
|
138
|
+
</memory>
|
|
131
139
|
```
|
|
132
140
|
|
|
133
|
-
This message is **not** persisted to `actx.messages` — it
|
|
141
|
+
This message is **not** persisted to `actx.messages` — it is a per-turn rewrite during `model-start`, which never mutates the agent's canonical history.
|
|
134
142
|
|
|
135
143
|
## Requirements + gotchas
|
|
136
144
|
|
|
137
|
-
- **sqlite-vec**: loaded via the `sqlite-vec` npm package
|
|
138
|
-
- **
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
- **sqlite-vec**: loaded via the `sqlite-vec` npm package, which ships prebuilt binaries for macOS, Linux, and Windows. Extension construction throws if the binary is not loadable for your platform.
|
|
146
|
+
- **Bun + macOS SQLite**: Bun uses Apple SQLite on macOS by default, and Apple SQLite disables loadable extensions. The extension automatically uses Homebrew SQLite when it exists at `/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib` or `/usr/local/opt/sqlite/lib/libsqlite3.dylib`. If needed, install it with `brew install sqlite`, set `DEX_SQLITE_DYLIB=/path/to/libsqlite3.dylib`, or pass `sqlitePath` to `memoryExtension()`.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
brew install sqlite
|
|
150
|
+
export DEX_SQLITE_DYLIB="$(brew --prefix sqlite)/lib/libsqlite3.dylib"
|
|
151
|
+
bun test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- **Transformers.js first-run download**: the default local embedder downloads an ONNX model and caches it in `~/.cache/huggingface`. Initial `embed()` may take 10-30s; subsequent calls are faster.
|
|
155
|
+
- **Background writes are fire-and-forget.** If the process is killed mid-turn, that turn's memory may not be persisted. `session-stop` awaits in-flight writes normally.
|
|
156
|
+
- **Prompt quality of auto-extraction** depends on the configured model. Cheap models work but may produce noisier facts. Review with `memory_recall_facts` or inspect the DB, and use `memory_forget_fact` to prune.
|
|
157
|
+
- **Procedural memory is global**, not user-scoped. Episodic and semantic memory are scoped by `userId`.
|
|
142
158
|
|
|
143
159
|
## Schema
|
|
144
160
|
|
|
145
|
-
```
|
|
161
|
+
```text
|
|
146
162
|
episodic id | user_id | summary | metadata(JSON) | created_at
|
|
147
163
|
episodic_vec id | embedding(FLOAT[384]) -- sqlite-vec
|
|
148
164
|
|
|
149
165
|
semantic id | user_id | subject | predicate | object | source | created_at | updated_at
|
|
166
|
+
semantic_vec id | embedding(FLOAT[384]) -- sqlite-vec
|
|
150
167
|
UNIQUE (user_id, subject, predicate)
|
|
151
168
|
|
|
152
169
|
procedural id | title(UNIQUE) | body | tags(JSON) | created_at | updated_at
|
|
@@ -161,4 +178,6 @@ _schema_migrations name(PK) | applied_at
|
|
|
161
178
|
bun test
|
|
162
179
|
```
|
|
163
180
|
|
|
164
|
-
|
|
181
|
+
On macOS, install/configure Homebrew SQLite first if you see `This build of sqlite3 does not support dynamic extension loading`.
|
|
182
|
+
|
|
183
|
+
The test suite covers schema/migrations, each memory type's read/write path, LLM helpers, and end-to-end Agent + extension flow.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dex-ai/memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "SQLite-backed memory Extension for @dex-ai/sdk — episodic, semantic, and procedural memory in one package.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"@types/bun": "latest",
|
|
33
33
|
"bun-types": "latest",
|
|
34
34
|
"zod": "^3.23.8",
|
|
35
|
-
"@xenova/transformers": "^2.17.2",
|
|
36
35
|
"@changesets/cli": "^2.29.0"
|
|
37
36
|
},
|
|
38
37
|
"sideEffects": false,
|
package/src/db.ts
CHANGED
|
@@ -9,30 +9,35 @@
|
|
|
9
9
|
* platform), we throw a descriptive error at construction time so apps see it
|
|
10
10
|
* immediately rather than on first recall.
|
|
11
11
|
*/
|
|
12
|
-
import { Database } from
|
|
13
|
-
import * as sqliteVec from
|
|
14
|
-
import { existsSync
|
|
15
|
-
import {
|
|
16
|
-
import { tmpdir } from 'node:os';
|
|
12
|
+
import { Database } from "bun:sqlite";
|
|
13
|
+
import * as sqliteVec from "sqlite-vec";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { platform } from "node:process";
|
|
17
16
|
|
|
18
17
|
/** Embedding dimensions — matches Xenova/all-MiniLM-L6-v2. */
|
|
19
18
|
export const EMBED_DIMS = 384;
|
|
20
19
|
|
|
21
20
|
export interface OpenDbOptions {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
/** Absolute path to the DB file, or ':memory:'. Created if missing. */
|
|
22
|
+
path: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional path to the sqlite-vec loadable extension (vec0.so / vec0.dylib).
|
|
25
|
+
* When running inside a compiled binary, the normal require.resolve() won't
|
|
26
|
+
* find the .so — pass the extracted path here instead.
|
|
27
|
+
*/
|
|
28
|
+
vecPath?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional path to a SQLite dynamic library with extension loading enabled.
|
|
31
|
+
* On macOS, Bun defaults to Apple SQLite, which disables loadable extensions.
|
|
32
|
+
* Pass a Homebrew SQLite dylib path here, or set DEX_SQLITE_DYLIB.
|
|
33
|
+
*/
|
|
34
|
+
sqlitePath?: string;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
const MIGRATIONS: Array<{ name: string; sql: string }> = [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
{
|
|
39
|
+
name: "001_init",
|
|
40
|
+
sql: `
|
|
36
41
|
CREATE TABLE IF NOT EXISTS episodic (
|
|
37
42
|
id TEXT PRIMARY KEY,
|
|
38
43
|
user_id TEXT NOT NULL,
|
|
@@ -65,11 +70,11 @@ const MIGRATIONS: Array<{ name: string; sql: string }> = [
|
|
|
65
70
|
);
|
|
66
71
|
CREATE INDEX IF NOT EXISTS idx_procedural_title ON procedural(title);
|
|
67
72
|
`,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "002_vec",
|
|
76
|
+
// vec0 virtual tables. Must run AFTER sqlite-vec is loaded.
|
|
77
|
+
sql: `
|
|
73
78
|
CREATE VIRTUAL TABLE IF NOT EXISTS episodic_vec USING vec0(
|
|
74
79
|
id TEXT PRIMARY KEY,
|
|
75
80
|
embedding FLOAT[${EMBED_DIMS}]
|
|
@@ -79,71 +84,112 @@ const MIGRATIONS: Array<{ name: string; sql: string }> = [
|
|
|
79
84
|
embedding FLOAT[${EMBED_DIMS}]
|
|
80
85
|
);
|
|
81
86
|
`,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "003_semantic_vec",
|
|
90
|
+
sql: `
|
|
86
91
|
CREATE VIRTUAL TABLE IF NOT EXISTS semantic_vec USING vec0(
|
|
87
92
|
id TEXT PRIMARY KEY,
|
|
88
93
|
embedding FLOAT[${EMBED_DIMS}]
|
|
89
94
|
);
|
|
90
95
|
`,
|
|
91
|
-
|
|
96
|
+
},
|
|
92
97
|
];
|
|
93
98
|
|
|
99
|
+
const SQLITE_DYLIB_ENV = "DEX_SQLITE_DYLIB";
|
|
100
|
+
|
|
101
|
+
const HOMEBREW_SQLITE_DYLIBS = [
|
|
102
|
+
"/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib",
|
|
103
|
+
"/usr/local/opt/sqlite/lib/libsqlite3.dylib",
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
let customSqliteConfigured = false;
|
|
107
|
+
|
|
108
|
+
function configureCustomSQLite(sqlitePath?: string): void {
|
|
109
|
+
if (customSqliteConfigured || platform !== "darwin") return;
|
|
110
|
+
|
|
111
|
+
const candidate =
|
|
112
|
+
sqlitePath ??
|
|
113
|
+
Bun.env[SQLITE_DYLIB_ENV] ??
|
|
114
|
+
HOMEBREW_SQLITE_DYLIBS.find(existsSync);
|
|
115
|
+
if (candidate === undefined) return;
|
|
116
|
+
|
|
117
|
+
if (!existsSync(candidate)) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`@dex-ai/memory-sqlite: configured SQLite dylib does not exist: ${candidate}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!Database.setCustomSQLite(candidate)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`@dex-ai/memory-sqlite: failed to configure custom SQLite dylib: ${candidate}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
customSqliteConfigured = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function customSQLiteHelp(): string {
|
|
132
|
+
if (platform !== "darwin") return "";
|
|
133
|
+
return ` On macOS, Bun uses Apple SQLite by default, which disables loadable extensions. Install SQLite with \`brew install sqlite\`, then either set ${SQLITE_DYLIB_ENV}=/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib or pass sqlitePath.`;
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
function applyMigrations(db: Database): void {
|
|
95
|
-
|
|
137
|
+
db.exec(`
|
|
96
138
|
CREATE TABLE IF NOT EXISTS _schema_migrations (
|
|
97
139
|
name TEXT PRIMARY KEY,
|
|
98
140
|
applied_at INTEGER NOT NULL
|
|
99
141
|
);
|
|
100
142
|
`);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
143
|
+
const already = new Set(
|
|
144
|
+
db
|
|
145
|
+
.prepare("SELECT name FROM _schema_migrations")
|
|
146
|
+
.all()
|
|
147
|
+
.map((r) => (r as { name: string }).name),
|
|
148
|
+
);
|
|
149
|
+
const record = db.prepare(
|
|
150
|
+
"INSERT INTO _schema_migrations(name, applied_at) VALUES(?, ?)",
|
|
151
|
+
);
|
|
152
|
+
for (const m of MIGRATIONS) {
|
|
153
|
+
if (already.has(m.name)) continue;
|
|
154
|
+
db.transaction(() => {
|
|
155
|
+
db.exec(m.sql);
|
|
156
|
+
record.run(m.name, Date.now());
|
|
157
|
+
})();
|
|
158
|
+
}
|
|
115
159
|
}
|
|
116
160
|
|
|
117
161
|
export class MemoryDb {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
readonly db: Database;
|
|
163
|
+
|
|
164
|
+
constructor(opts: OpenDbOptions) {
|
|
165
|
+
configureCustomSQLite(opts.sqlitePath);
|
|
166
|
+
this.db = new Database(opts.path, { create: true });
|
|
167
|
+
this.db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
|
|
168
|
+
|
|
169
|
+
// Load sqlite-vec. Failure here is fatal — no point constructing the
|
|
170
|
+
// extension if vector tables won't work.
|
|
171
|
+
try {
|
|
172
|
+
if (opts.vecPath) {
|
|
173
|
+
// Compiled binary mode: load from explicit path
|
|
174
|
+
this.db.loadExtension(opts.vecPath);
|
|
175
|
+
} else {
|
|
176
|
+
// Normal mode: sqlite-vec resolves the platform-specific .so via require.resolve()
|
|
177
|
+
(sqliteVec as { load: (db: unknown) => void }).load(this.db);
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
this.db.close();
|
|
181
|
+
throw new Error(
|
|
182
|
+
`@dex-ai/memory-sqlite: failed to load sqlite-vec extension. ` +
|
|
183
|
+
`Ensure the sqlite-vec npm package's prebuilt binary is available for this platform.` +
|
|
184
|
+
customSQLiteHelp() +
|
|
185
|
+
` Original error: ${err instanceof Error ? err.message : String(err)}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
142
188
|
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
applyMigrations(this.db);
|
|
190
|
+
}
|
|
145
191
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
192
|
+
close(): void {
|
|
193
|
+
this.db.close();
|
|
194
|
+
}
|
|
149
195
|
}
|
package/src/extension.ts
CHANGED
|
@@ -57,6 +57,12 @@ export interface MemoryExtensionOptions {
|
|
|
57
57
|
* cannot locate the platform package.
|
|
58
58
|
*/
|
|
59
59
|
vecPath?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Explicit path to a SQLite dynamic library with extension loading enabled.
|
|
62
|
+
* Useful on macOS, where Bun defaults to Apple SQLite, which disables
|
|
63
|
+
* loadable extensions. Can also be configured with DEX_SQLITE_DYLIB.
|
|
64
|
+
*/
|
|
65
|
+
sqlitePath?: string;
|
|
60
66
|
|
|
61
67
|
/**
|
|
62
68
|
* LLM override for summarization + fact extraction.
|
|
@@ -365,6 +371,9 @@ export function memoryExtension(
|
|
|
365
371
|
...(resolvedOpts.vecPath !== undefined
|
|
366
372
|
? { vecPath: resolvedOpts.vecPath }
|
|
367
373
|
: {}),
|
|
374
|
+
...(resolvedOpts.sqlitePath !== undefined
|
|
375
|
+
? { sqlitePath: resolvedOpts.sqlitePath }
|
|
376
|
+
: {}),
|
|
368
377
|
});
|
|
369
378
|
const episodic = new EpisodicStore(db.db);
|
|
370
379
|
const semantic = new SemanticStore(db.db);
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @dex-ai/memory
|
|
2
|
+
* @dex-ai/memory — SQLite-backed memory Extension for Dex.
|
|
3
3
|
*
|
|
4
4
|
* Three memory types in one extension, one SQLite file:
|
|
5
5
|
* - Episodic: summarized past turns, recalled by recency + similarity.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* import { Agent } from '@dex-ai/sdk';
|
|
12
12
|
* import { openai } from '@dex-ai/openai';
|
|
13
|
-
* import { memoryExtension } from '@dex-ai/memory
|
|
13
|
+
* import { memoryExtension } from '@dex-ai/memory';
|
|
14
14
|
*
|
|
15
15
|
* const agent = await Agent.create({
|
|
16
16
|
* provider: openai({ modelId: 'gpt-4.1' }),
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
* ],
|
|
24
24
|
* });
|
|
25
25
|
*/
|
|
26
|
-
export { memoryExtension } from
|
|
27
|
-
export type { MemoryExtensionOptions } from
|
|
26
|
+
export { memoryExtension } from "./extension";
|
|
27
|
+
export type { MemoryExtensionOptions } from "./extension";
|
|
28
28
|
|
|
29
|
-
export { localEmbedder } from
|
|
30
|
-
export type { Embedder } from
|
|
29
|
+
export { localEmbedder } from "./embedder";
|
|
30
|
+
export type { Embedder } from "./embedder";
|
|
31
31
|
|
|
32
|
-
export type { MemoryLlmOptions } from
|
|
32
|
+
export type { MemoryLlmOptions } from "./summarize";
|
package/src/tools.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Tools are namespaced `memory_<name>` so they don't collide with other
|
|
6
6
|
* extensions' tool names.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
9
|
-
import { z } from "zod";
|
|
8
|
+
import { Schema, type Tool, type ToolOutput } from "@dex-ai/sdk";
|
|
10
9
|
import type { Embedder } from "./embedder";
|
|
11
10
|
import type { SemanticStore } from "./semantic";
|
|
12
11
|
import type { ProceduralStore } from "./procedural";
|
|
@@ -16,6 +15,13 @@ function err(msg: string): ToolOutput {
|
|
|
16
15
|
return { type: "error-text", value: msg };
|
|
17
16
|
}
|
|
18
17
|
|
|
18
|
+
type RecallFactsInput = { query: string; limit?: number };
|
|
19
|
+
type RememberFactInput = { subject: string; predicate: string; object: string };
|
|
20
|
+
type ForgetFactInput = { subject: string; predicate: string };
|
|
21
|
+
type ListProceduresInput = { query?: string; tag?: string; limit?: number };
|
|
22
|
+
type GetProcedureInput = { title: string };
|
|
23
|
+
type StoreProcedureInput = { title: string; body: string; tags?: string[] };
|
|
24
|
+
|
|
19
25
|
/* ------------------------------------------------------------------ */
|
|
20
26
|
/* Semantic tools */
|
|
21
27
|
/* ------------------------------------------------------------------ */
|
|
@@ -24,19 +30,23 @@ export function recallFactsTool(
|
|
|
24
30
|
store: SemanticStore,
|
|
25
31
|
userId: string,
|
|
26
32
|
embed: Embedder,
|
|
27
|
-
): Tool {
|
|
28
|
-
const schema =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
): Tool<RecallFactsInput> {
|
|
34
|
+
const schema = Schema.fromJsonSchema<RecallFactsInput>({
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
query: {
|
|
38
|
+
type: "string",
|
|
39
|
+
minLength: 1,
|
|
40
|
+
description: "Free-text query to search for relevant facts by similarity.",
|
|
41
|
+
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: "integer",
|
|
44
|
+
minimum: 1,
|
|
45
|
+
maximum: 50,
|
|
46
|
+
description: "Max results. Default 20.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
required: ["query"],
|
|
40
50
|
});
|
|
41
51
|
return {
|
|
42
52
|
name: "memory_recall_facts",
|
|
@@ -46,7 +56,7 @@ export function recallFactsTool(
|
|
|
46
56
|
"Search long-term semantic memory for facts relevant to a query. Returns the top matching facts by vector similarity. Use this to look up specific knowledge about the user, project, or environment.",
|
|
47
57
|
parameters: schema,
|
|
48
58
|
async execute(input): Promise<ToolOutput> {
|
|
49
|
-
const parsed =
|
|
59
|
+
const parsed = input;
|
|
50
60
|
const limit = parsed.limit ?? 20;
|
|
51
61
|
try {
|
|
52
62
|
const [vec] = await embed([parsed.query]);
|
|
@@ -77,17 +87,27 @@ export function rememberFactTool(
|
|
|
77
87
|
store: SemanticStore,
|
|
78
88
|
userId: string,
|
|
79
89
|
embed: Embedder,
|
|
80
|
-
): Tool {
|
|
81
|
-
const schema =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
): Tool<RememberFactInput> {
|
|
91
|
+
const schema = Schema.fromJsonSchema<RememberFactInput>({
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
subject: {
|
|
95
|
+
type: "string",
|
|
96
|
+
minLength: 1,
|
|
97
|
+
description: "Subject of the fact, e.g. 'user', 'project'.",
|
|
98
|
+
},
|
|
99
|
+
predicate: {
|
|
100
|
+
type: "string",
|
|
101
|
+
minLength: 1,
|
|
102
|
+
description: "Predicate linking subject to object, e.g. 'prefers', 'uses'.",
|
|
103
|
+
},
|
|
104
|
+
object: {
|
|
105
|
+
type: "string",
|
|
106
|
+
minLength: 1,
|
|
107
|
+
description: "The value of the claim.",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ["subject", "predicate", "object"],
|
|
91
111
|
});
|
|
92
112
|
return {
|
|
93
113
|
name: "memory_remember_fact",
|
|
@@ -97,7 +117,7 @@ export function rememberFactTool(
|
|
|
97
117
|
"Store a durable fact (subject, predicate, object) in long-term semantic memory. Upsert by (subject, predicate); the latest object replaces any prior one.",
|
|
98
118
|
parameters: schema,
|
|
99
119
|
async execute(input): Promise<ToolOutput> {
|
|
100
|
-
const parsed =
|
|
120
|
+
const parsed = input;
|
|
101
121
|
try {
|
|
102
122
|
const factText = `${parsed.subject} ${parsed.predicate} ${parsed.object}`;
|
|
103
123
|
const [vec] = await embed([factText]);
|
|
@@ -119,10 +139,17 @@ export function rememberFactTool(
|
|
|
119
139
|
};
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
export function forgetFactTool(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
export function forgetFactTool(
|
|
143
|
+
store: SemanticStore,
|
|
144
|
+
userId: string,
|
|
145
|
+
): Tool<ForgetFactInput> {
|
|
146
|
+
const schema = Schema.fromJsonSchema<ForgetFactInput>({
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
subject: { type: "string", minLength: 1 },
|
|
150
|
+
predicate: { type: "string", minLength: 1 },
|
|
151
|
+
},
|
|
152
|
+
required: ["subject", "predicate"],
|
|
126
153
|
});
|
|
127
154
|
return {
|
|
128
155
|
name: "memory_forget_fact",
|
|
@@ -132,7 +159,7 @@ export function forgetFactTool(store: SemanticStore, userId: string): Tool {
|
|
|
132
159
|
"Delete a stored semantic fact by (subject, predicate). Returns { deleted: boolean }.",
|
|
133
160
|
parameters: schema,
|
|
134
161
|
async execute(input): Promise<ToolOutput> {
|
|
135
|
-
const parsed =
|
|
162
|
+
const parsed = input;
|
|
136
163
|
const res = await store.delete({
|
|
137
164
|
userId,
|
|
138
165
|
subject: parsed.subject,
|
|
@@ -150,23 +177,25 @@ export function forgetFactTool(store: SemanticStore, userId: string): Tool {
|
|
|
150
177
|
export function listProceduresTool(
|
|
151
178
|
store: ProceduralStore,
|
|
152
179
|
embed: Embedder,
|
|
153
|
-
): Tool {
|
|
154
|
-
const schema =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
): Tool<ListProceduresInput> {
|
|
181
|
+
const schema = Schema.fromJsonSchema<ListProceduresInput>({
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
query: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "Free-text task to search by similarity.",
|
|
187
|
+
},
|
|
188
|
+
tag: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "Filter to procedures tagged with this string.",
|
|
191
|
+
},
|
|
192
|
+
limit: {
|
|
193
|
+
type: "integer",
|
|
194
|
+
minimum: 1,
|
|
195
|
+
maximum: 50,
|
|
196
|
+
description: "Max results. Default 10.",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
170
199
|
});
|
|
171
200
|
return {
|
|
172
201
|
name: "memory_list_procedures",
|
|
@@ -176,7 +205,7 @@ export function listProceduresTool(
|
|
|
176
205
|
"List runbooks/procedures. With `query`, returns vector-similarity-ranked results. With `tag`, filters by tag. With neither, returns most-recent.",
|
|
177
206
|
parameters: schema,
|
|
178
207
|
async execute(input): Promise<ToolOutput> {
|
|
179
|
-
const parsed =
|
|
208
|
+
const parsed = input;
|
|
180
209
|
const limit = parsed.limit ?? 10;
|
|
181
210
|
try {
|
|
182
211
|
if (parsed.query !== undefined && parsed.query.trim() !== "") {
|
|
@@ -201,9 +230,17 @@ export function listProceduresTool(
|
|
|
201
230
|
};
|
|
202
231
|
}
|
|
203
232
|
|
|
204
|
-
export function getProcedureTool(store: ProceduralStore): Tool {
|
|
205
|
-
const schema =
|
|
206
|
-
|
|
233
|
+
export function getProcedureTool(store: ProceduralStore): Tool<GetProcedureInput> {
|
|
234
|
+
const schema = Schema.fromJsonSchema<GetProcedureInput>({
|
|
235
|
+
type: "object",
|
|
236
|
+
properties: {
|
|
237
|
+
title: {
|
|
238
|
+
type: "string",
|
|
239
|
+
minLength: 1,
|
|
240
|
+
description: "Exact title of the procedure to fetch.",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
required: ["title"],
|
|
207
244
|
});
|
|
208
245
|
return {
|
|
209
246
|
name: "memory_get_procedure",
|
|
@@ -213,7 +250,7 @@ export function getProcedureTool(store: ProceduralStore): Tool {
|
|
|
213
250
|
"Fetch a procedure by title. Returns the full body and tags, or null.",
|
|
214
251
|
parameters: schema,
|
|
215
252
|
async execute(input): Promise<ToolOutput> {
|
|
216
|
-
const parsed =
|
|
253
|
+
const parsed = input;
|
|
217
254
|
const row = await store.getByTitle(parsed.title);
|
|
218
255
|
return { type: "json", value: row };
|
|
219
256
|
},
|
|
@@ -223,14 +260,27 @@ export function getProcedureTool(store: ProceduralStore): Tool {
|
|
|
223
260
|
export function storeProcedureTool(
|
|
224
261
|
store: ProceduralStore,
|
|
225
262
|
embed: Embedder,
|
|
226
|
-
): Tool {
|
|
227
|
-
const schema =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
): Tool<StoreProcedureInput> {
|
|
264
|
+
const schema = Schema.fromJsonSchema<StoreProcedureInput>({
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
title: {
|
|
268
|
+
type: "string",
|
|
269
|
+
minLength: 1,
|
|
270
|
+
description: "Unique title of the runbook.",
|
|
271
|
+
},
|
|
272
|
+
body: {
|
|
273
|
+
type: "string",
|
|
274
|
+
minLength: 1,
|
|
275
|
+
description: "The runbook contents, typically markdown.",
|
|
276
|
+
},
|
|
277
|
+
tags: {
|
|
278
|
+
type: "array",
|
|
279
|
+
items: { type: "string" },
|
|
280
|
+
description: "Tags for filtering.",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
required: ["title", "body"],
|
|
234
284
|
});
|
|
235
285
|
return {
|
|
236
286
|
name: "memory_store_procedure",
|
|
@@ -240,7 +290,7 @@ export function storeProcedureTool(
|
|
|
240
290
|
"Upsert a runbook by title. Overwrites body+tags for an existing title. Vector embedding is computed from title and body automatically.",
|
|
241
291
|
parameters: schema,
|
|
242
292
|
async execute(input): Promise<ToolOutput> {
|
|
243
|
-
const parsed =
|
|
293
|
+
const parsed = input;
|
|
244
294
|
try {
|
|
245
295
|
const [vec] = await embed([`${parsed.title}\n\n${parsed.body}`]);
|
|
246
296
|
if (vec === undefined) return err("embedder returned no vector");
|