@dex-ai/memory 0.3.3 → 0.3.5

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 CHANGED
@@ -1,36 +1,34 @@
1
- # @dex-ai/memory-sqlite
1
+ # @dex-ai/memory
2
2
 
3
- SQLite-backed memory Extension for [`@dex-ai/sdk`](https://github.com/klxdev/dex-ai-sdk). Three memory types — **episodic**, **semantic**, **procedural** — in one extension, one SQLite file.
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-sqlite
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` is an optional peer dep. Skip it if you pass your own `embed()` function.
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 { DexAgent } from '@dex-ai/runtime';
16
+ import { Agent } from '@dex-ai/sdk';
19
17
  import { openai } from '@dex-ai/openai';
20
- import { memoryExtensionSqlite } from '@dex-ai/memory-sqlite';
18
+ import { memoryExtension } from '@dex-ai/memory';
21
19
 
22
- const agent = new DexAgent({
20
+ const agent = await Agent.create({
23
21
  provider: openai({ modelId: 'gpt-4.1' }),
24
22
  extensions: [
25
- memoryExtensionSqlite({
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 (e.g. a remote endpoint).
33
- // If omitted, a local Transformers.js embedder (all-MiniLM-L6-v2) is used.
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 fires a background task:
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; `agent.dispose()` awaits them.
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
- At recall time (`onRequest`), the extension fetches:
51
- - The last **3 most-recent** episodes for the user.
52
- - The **3 most-similar** episodes via sqlite-vec cosine against the user's last message.
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 iteration stop: the LLM extracts durable claims from the turn and upserts them.
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
- At recall time, **all of the user's facts** are injected as a synthetic system message. The set is typically small (tens to low hundreds); if it grows we'll add top-k filtering later.
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
- **Auto-inject**: at recall time, the extension does a vector similarity lookup against the user's last message. If the top match scores above the threshold (default 0.5), its full body is prepended to the prompt as a synthetic system message.
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
- memoryExtensionSqlite({
84
+ memoryExtension({
82
85
  // ...
83
86
  llm: {
84
87
  provider: customProvider, // optional — overrides the agent's provider entirely
@@ -90,63 +93,75 @@ 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 (captured from `actx.provider` in `onAgentStart`).
94
- 3. If `llm.model` is set, it's passed via `providerOptions.model` on every memory-internal request. Providers that merge `providerOptions` over the default body — like `@dex-ai/openai` — honor this override per-call. Providers that don't merge `providerOptions` require passing a full `llm.provider` instance.
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 MemoryExtensionSqliteOptions {
102
+ interface MemoryExtensionOptions {
100
103
  path: string; // SQLite file path or ':memory:'
101
- userId: string; // owner for episodic + semantic (procedural is global)
104
+ userId: string; // owner for episodic + semantic memory
105
+
106
+ vecPath?: string; // optional explicit sqlite-vec extension path
107
+ sqlitePath?: string; // advanced: override auto-detected Homebrew SQLite dylib
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 3
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 prepends a single synthetic system message to each provider request. Example:
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
- Known facts:
131
+ Relevant facts:
123
132
  - user prefers TypeScript
124
133
  - project uses PostgreSQL 15
125
134
  - user is on macOS
126
135
 
127
- Relevant runbook deploy-dex (similarity 0.71):
128
- 1. bun run typecheck
129
- 2. bun run test
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's a per-turn rewrite via the `onRequest` reducer, which never mutates the agent's canonical history.
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 (ships prebuilt binaries for macOS, Linux, Windows). Extension construction throws if the binary isn't loadable for your platform.
138
- - **Transformers.js first-run download**: ~25 MB ONNX model, cached in `~/.cache/huggingface`. Initial `embed()` takes 10-30s; subsequent calls are fast.
139
- - **Background writes are fire-and-forget.** If the process is killed mid-turn, that turn's memory may not be persisted. `agent.dispose()` awaits in-flight writes normally.
140
- - **Prompt quality of auto-extraction** depends on the configured model. Cheap models (gpt-4o-mini, Haiku) work but may produce noisier facts. Review with `memory.list_facts` (or just inspect the DB) occasionally and use `memory.forget_fact` to prune.
141
- - **Procedural is global**, not user-scoped, in v0.1. If you need per-user runbooks, open an issue and we'll add a user_id column.
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 and restart Dex:
147
+
148
+ ```bash
149
+ brew install sqlite
150
+ ```
151
+
152
+ - **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.
153
+ - **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.
154
+ - **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.
155
+ - **Procedural memory is global**, not user-scoped. Episodic and semantic memory are scoped by `userId`.
142
156
 
143
157
  ## Schema
144
158
 
145
- ```
159
+ ```text
146
160
  episodic id | user_id | summary | metadata(JSON) | created_at
147
161
  episodic_vec id | embedding(FLOAT[384]) -- sqlite-vec
148
162
 
149
163
  semantic id | user_id | subject | predicate | object | source | created_at | updated_at
164
+ semantic_vec id | embedding(FLOAT[384]) -- sqlite-vec
150
165
  UNIQUE (user_id, subject, predicate)
151
166
 
152
167
  procedural id | title(UNIQUE) | body | tags(JSON) | created_at | updated_at
@@ -161,4 +176,6 @@ _schema_migrations name(PK) | applied_at
161
176
  bun test
162
177
  ```
163
178
 
164
- 33 tests as of v0.1: schema/migrations, each memory type's read/write path, LLM helpers, and two end-to-end tests that exercise the full Agent + Extension flow.
179
+ On macOS, install/configure Homebrew SQLite first if you see `This build of sqlite3 does not support dynamic extension loading`.
180
+
181
+ 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",
3
+ "version": "0.3.5",
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.test.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { Database } from 'bun:sqlite';
1
2
  import { describe, expect, test } from 'bun:test';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { MemoryDb, EMBED_DIMS } from './db';
3
5
 
4
6
  describe('MemoryDb', () => {
@@ -40,6 +42,16 @@ describe('MemoryDb', () => {
40
42
  }
41
43
  });
42
44
 
45
+ test('tolerates SQLite being loaded before custom sqlite configuration', () => {
46
+ const alreadyOpen = new Database(':memory:');
47
+ try {
48
+ const db = new MemoryDb({ path: ':memory:', sqlitePath: fileURLToPath(import.meta.url) });
49
+ db.close();
50
+ } finally {
51
+ alreadyOpen.close();
52
+ }
53
+ });
54
+
43
55
  test('EMBED_DIMS is 384', () => {
44
56
  expect(EMBED_DIMS).toBe(384);
45
57
  });
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 'bun:sqlite';
13
- import * as sqliteVec from 'sqlite-vec';
14
- import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
15
- import { join, dirname } from 'node:path';
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
- /** Absolute path to the DB file, or ':memory:'. Created if missing. */
23
- path: string;
24
- /**
25
- * Optional path to the sqlite-vec loadable extension (vec0.so / vec0.dylib).
26
- * When running inside a compiled binary, the normal require.resolve() won't
27
- * find the .so — pass the extracted path here instead.
28
- */
29
- vecPath?: string;
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
- name: '001_init',
35
- sql: `
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
- name: '002_vec',
71
- // vec0 virtual tables. Must run AFTER sqlite-vec is loaded.
72
- sql: `
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,134 @@ const MIGRATIONS: Array<{ name: string; sql: string }> = [
79
84
  embedding FLOAT[${EMBED_DIMS}]
80
85
  );
81
86
  `,
82
- },
83
- {
84
- name: '003_semantic_vec',
85
- sql: `
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 PACKAGE_NAME = "@dex-ai/memory";
100
+ const SQLITE_DYLIB_ENV = "DEX_SQLITE_DYLIB";
101
+
102
+ const HOMEBREW_SQLITE_DYLIBS = [
103
+ "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib",
104
+ "/usr/local/opt/sqlite/lib/libsqlite3.dylib",
105
+ ];
106
+
107
+ let customSqliteConfigured = false;
108
+ let customSqliteAttempted = false;
109
+
110
+ function isSQLiteAlreadyLoadedError(err: unknown): boolean {
111
+ return err instanceof Error && /SQLite already loaded/i.test(err.message);
112
+ }
113
+
114
+ function configureCustomSQLite(sqlitePath?: string): void {
115
+ if (customSqliteConfigured || platform !== "darwin") return;
116
+ if (sqlitePath === undefined && customSqliteAttempted) return;
117
+ customSqliteAttempted = true;
118
+
119
+ const candidate =
120
+ sqlitePath ??
121
+ Bun.env[SQLITE_DYLIB_ENV] ??
122
+ HOMEBREW_SQLITE_DYLIBS.find(existsSync);
123
+ if (candidate === undefined) return;
124
+
125
+ if (!existsSync(candidate)) {
126
+ throw new Error(
127
+ `${PACKAGE_NAME}: configured SQLite dylib does not exist: ${candidate}`,
128
+ );
129
+ }
130
+
131
+ try {
132
+ if (!Database.setCustomSQLite(candidate)) {
133
+ throw new Error(`failed to configure custom SQLite dylib: ${candidate}`);
134
+ }
135
+ customSqliteConfigured = true;
136
+ } catch (err) {
137
+ // Bun only allows setCustomSQLite() before SQLite is loaded anywhere in
138
+ // the process. Dex may load another SQLite-backed extension first. In
139
+ // that case, keep using the already-loaded SQLite and let sqlite-vec
140
+ // loading below determine whether the current build supports extensions.
141
+ if (isSQLiteAlreadyLoadedError(err)) return;
142
+ throw new Error(
143
+ `${PACKAGE_NAME}: ${err instanceof Error ? err.message : String(err)}`,
144
+ );
145
+ }
146
+ }
147
+
148
+ function customSQLiteHelp(): string {
149
+ if (platform !== "darwin") return "";
150
+ return " On macOS, Bun uses Apple SQLite by default, which disables loadable extensions. Install SQLite with `brew install sqlite`, then restart Dex.";
151
+ }
152
+
153
+ // Configure Bun to use Homebrew SQLite as soon as this module is imported.
154
+ // Users should only need to install SQLite; no environment variables are
155
+ // required for the common Homebrew locations.
156
+ configureCustomSQLite();
157
+
94
158
  function applyMigrations(db: Database): void {
95
- db.exec(`
159
+ db.exec(`
96
160
  CREATE TABLE IF NOT EXISTS _schema_migrations (
97
161
  name TEXT PRIMARY KEY,
98
162
  applied_at INTEGER NOT NULL
99
163
  );
100
164
  `);
101
- const already = new Set(
102
- db
103
- .prepare('SELECT name FROM _schema_migrations')
104
- .all()
105
- .map((r) => (r as { name: string }).name),
106
- );
107
- const record = db.prepare('INSERT INTO _schema_migrations(name, applied_at) VALUES(?, ?)');
108
- for (const m of MIGRATIONS) {
109
- if (already.has(m.name)) continue;
110
- db.transaction(() => {
111
- db.exec(m.sql);
112
- record.run(m.name, Date.now());
113
- })();
114
- }
165
+ const already = new Set(
166
+ db
167
+ .prepare("SELECT name FROM _schema_migrations")
168
+ .all()
169
+ .map((r) => (r as { name: string }).name),
170
+ );
171
+ const record = db.prepare(
172
+ "INSERT INTO _schema_migrations(name, applied_at) VALUES(?, ?)",
173
+ );
174
+ for (const m of MIGRATIONS) {
175
+ if (already.has(m.name)) continue;
176
+ db.transaction(() => {
177
+ db.exec(m.sql);
178
+ record.run(m.name, Date.now());
179
+ })();
180
+ }
115
181
  }
116
182
 
117
183
  export class MemoryDb {
118
- readonly db: Database;
119
-
120
- constructor(opts: OpenDbOptions) {
121
- this.db = new Database(opts.path, { create: true });
122
- this.db.exec('PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');
123
-
124
- // Load sqlite-vec. Failure here is fatal — no point constructing the
125
- // extension if vector tables won't work.
126
- try {
127
- if (opts.vecPath) {
128
- // Compiled binary mode: load from explicit path
129
- this.db.loadExtension(opts.vecPath);
130
- } else {
131
- // Normal mode: sqlite-vec resolves the platform-specific .so via require.resolve()
132
- (sqliteVec as { load: (db: unknown) => void }).load(this.db);
133
- }
134
- } catch (err) {
135
- this.db.close();
136
- throw new Error(
137
- `@dex-ai/memory-sqlite: failed to load sqlite-vec extension. ` +
138
- `Ensure the sqlite-vec npm package's prebuilt binary is available for this platform. ` +
139
- `Original error: ${err instanceof Error ? err.message : String(err)}`,
140
- );
141
- }
184
+ readonly db: Database;
185
+
186
+ constructor(opts: OpenDbOptions) {
187
+ configureCustomSQLite(opts.sqlitePath);
188
+ this.db = new Database(opts.path, { create: true });
189
+ this.db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
190
+
191
+ // Load sqlite-vec. Failure here is fatal — no point constructing the
192
+ // extension if vector tables won't work.
193
+ try {
194
+ if (opts.vecPath) {
195
+ // Compiled binary mode: load from explicit path
196
+ this.db.loadExtension(opts.vecPath);
197
+ } else {
198
+ // Normal mode: sqlite-vec resolves the platform-specific .so via require.resolve()
199
+ (sqliteVec as { load: (db: unknown) => void }).load(this.db);
200
+ }
201
+ } catch (err) {
202
+ this.db.close();
203
+ throw new Error(
204
+ `${PACKAGE_NAME}: failed to load sqlite-vec extension. ` +
205
+ `Ensure the sqlite-vec npm package's prebuilt binary is available for this platform.` +
206
+ customSQLiteHelp() +
207
+ ` Original error: ${err instanceof Error ? err.message : String(err)}`,
208
+ );
209
+ }
142
210
 
143
- applyMigrations(this.db);
144
- }
211
+ applyMigrations(this.db);
212
+ }
145
213
 
146
- close(): void {
147
- this.db.close();
148
- }
214
+ close(): void {
215
+ this.db.close();
216
+ }
149
217
  }
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-sqlite — SQLite-backed memory Extension for Dex.
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-sqlite';
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 './extension';
27
- export type { MemoryExtensionOptions } from './extension';
26
+ export { memoryExtension } from "./extension";
27
+ export type { MemoryExtensionOptions } from "./extension";
28
28
 
29
- export { localEmbedder } from './embedder';
30
- export type { Embedder } from './embedder';
29
+ export { localEmbedder } from "./embedder";
30
+ export type { Embedder } from "./embedder";
31
31
 
32
- export type { MemoryLlmOptions } from './summarize';
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 { Tool, ToolOutput } from "@dex-ai/sdk";
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 = z.object({
29
- query: z
30
- .string()
31
- .min(1)
32
- .describe("Free-text query to search for relevant facts by similarity."),
33
- limit: z
34
- .number()
35
- .int()
36
- .positive()
37
- .max(50)
38
- .optional()
39
- .describe("Max results. Default 20."),
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 = schema.parse(input);
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 = z.object({
82
- subject: z
83
- .string()
84
- .min(1)
85
- .describe("Subject of the fact, e.g. 'user', 'project'."),
86
- predicate: z
87
- .string()
88
- .min(1)
89
- .describe("Predicate linking subject to object, e.g. 'prefers', 'uses'."),
90
- object: z.string().min(1).describe("The value of the claim."),
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 = schema.parse(input);
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(store: SemanticStore, userId: string): Tool {
123
- const schema = z.object({
124
- subject: z.string().min(1),
125
- predicate: z.string().min(1),
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 = schema.parse(input);
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 = z.object({
155
- query: z
156
- .string()
157
- .optional()
158
- .describe("Free-text task to search by similarity."),
159
- tag: z
160
- .string()
161
- .optional()
162
- .describe("Filter to procedures tagged with this string."),
163
- limit: z
164
- .number()
165
- .int()
166
- .positive()
167
- .max(50)
168
- .optional()
169
- .describe("Max results. Default 10."),
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 = schema.parse(input);
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 = z.object({
206
- title: z.string().min(1).describe("Exact title of the procedure to fetch."),
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 = schema.parse(input);
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 = z.object({
228
- title: z.string().min(1).describe("Unique title of the runbook."),
229
- body: z
230
- .string()
231
- .min(1)
232
- .describe("The runbook contents, typically markdown."),
233
- tags: z.array(z.string()).optional().describe("Tags for filtering."),
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 = schema.parse(input);
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");