agent-office 0.3.2 → 0.4.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 +15 -37
- package/dist/cli.js +17 -50
- package/dist/commands/notifier.d.ts +11 -0
- package/dist/commands/notifier.js +100 -0
- package/dist/commands/serve.d.ts +0 -2
- package/dist/commands/serve.js +1 -26
- package/dist/commands/worker.d.ts +0 -4
- package/dist/commands/worker.js +0 -63
- package/dist/db/index.d.ts +1 -0
- package/dist/db/postgresql-storage.d.ts +6 -1
- package/dist/db/postgresql-storage.js +24 -14
- package/dist/db/sqlite-storage.d.ts +6 -1
- package/dist/db/sqlite-storage.js +37 -12
- package/dist/db/storage-base.d.ts +6 -1
- package/dist/db/storage-base.js +1 -1
- package/dist/db/storage.d.ts +6 -1
- package/dist/lib/notifier.d.ts +18 -0
- package/dist/lib/notifier.js +15 -0
- package/dist/manage/components/SessionList.js +0 -266
- package/dist/manage/hooks/useApi.d.ts +0 -24
- package/dist/manage/hooks/useApi.js +0 -24
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js +3 -3
- package/dist/server/routes.d.ts +2 -3
- package/dist/server/routes.js +36 -249
- package/package.json +4 -4
- package/dist/server/memory.d.ts +0 -87
- package/dist/server/memory.js +0 -348
package/dist/server/memory.js
DELETED
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
import { createAgentMemory } from "fastmemory";
|
|
2
|
-
import { mkdirSync, existsSync } from "fs";
|
|
3
|
-
import { join, resolve } from "path";
|
|
4
|
-
import Database from "better-sqlite3";
|
|
5
|
-
/**
|
|
6
|
-
* Abstract base class for agent memory backends.
|
|
7
|
-
* Consumers should depend on this type rather than any concrete implementation.
|
|
8
|
-
*/
|
|
9
|
-
export class AgentOfficeMemory {
|
|
10
|
-
}
|
|
11
|
-
// ── AgentOfficeSimpleMemory ───────────────────────────────────────────────────
|
|
12
|
-
/**
|
|
13
|
-
* Lightweight memory backend using a single SQLite database file at
|
|
14
|
-
* <memoryPath>/simple-memory.db. All sessions share the same file; rows are
|
|
15
|
-
* partitioned by session_name. Full-text search is provided by SQLite's
|
|
16
|
-
* built-in FTS5 (BM25 ranking). No embeddings, no external dependencies
|
|
17
|
-
* beyond better-sqlite3.
|
|
18
|
-
*
|
|
19
|
-
* Activate with: agent-office serve --simple-memory
|
|
20
|
-
*/
|
|
21
|
-
export class AgentOfficeSimpleMemory extends AgentOfficeMemory {
|
|
22
|
-
basePath;
|
|
23
|
-
db;
|
|
24
|
-
constructor(memoryPath) {
|
|
25
|
-
super();
|
|
26
|
-
this.basePath = resolve(memoryPath);
|
|
27
|
-
if (!existsSync(this.basePath)) {
|
|
28
|
-
mkdirSync(this.basePath, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
const dbPath = join(this.basePath, "simple-memory.db");
|
|
31
|
-
this.db = new Database(dbPath);
|
|
32
|
-
this.db.pragma("journal_mode = WAL");
|
|
33
|
-
this.db.pragma("foreign_keys = ON");
|
|
34
|
-
this._migrate();
|
|
35
|
-
}
|
|
36
|
-
_migrate() {
|
|
37
|
-
this.db.exec(`
|
|
38
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
39
|
-
id TEXT PRIMARY KEY,
|
|
40
|
-
session_name TEXT NOT NULL,
|
|
41
|
-
content TEXT NOT NULL,
|
|
42
|
-
metadata TEXT NOT NULL DEFAULT '{}',
|
|
43
|
-
created_at TEXT NOT NULL
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
CREATE INDEX IF NOT EXISTS memories_session_idx ON memories (session_name);
|
|
47
|
-
|
|
48
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
49
|
-
content,
|
|
50
|
-
session_name UNINDEXED,
|
|
51
|
-
memory_id UNINDEXED,
|
|
52
|
-
content='memories',
|
|
53
|
-
content_rowid='rowid',
|
|
54
|
-
tokenize='porter unicode61'
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
58
|
-
INSERT INTO memories_fts(rowid, content, session_name, memory_id)
|
|
59
|
-
VALUES (new.rowid, new.content, new.session_name, new.id);
|
|
60
|
-
END;
|
|
61
|
-
|
|
62
|
-
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
63
|
-
INSERT INTO memories_fts(memories_fts, rowid, content, session_name, memory_id)
|
|
64
|
-
VALUES ('delete', old.rowid, old.content, old.session_name, old.id);
|
|
65
|
-
END;
|
|
66
|
-
|
|
67
|
-
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
68
|
-
INSERT INTO memories_fts(memories_fts, rowid, content, session_name, memory_id)
|
|
69
|
-
VALUES ('delete', old.rowid, old.content, old.session_name, old.id);
|
|
70
|
-
INSERT INTO memories_fts(rowid, content, session_name, memory_id)
|
|
71
|
-
VALUES (new.rowid, new.content, new.session_name, new.id);
|
|
72
|
-
END;
|
|
73
|
-
`);
|
|
74
|
-
}
|
|
75
|
-
newId() {
|
|
76
|
-
// Compact random UUID without hyphens
|
|
77
|
-
return crypto.randomUUID().replace(/-/g, "");
|
|
78
|
-
}
|
|
79
|
-
sanitizeFts5Query(query) {
|
|
80
|
-
return query
|
|
81
|
-
.split(/\s+/)
|
|
82
|
-
.filter((t) => t.length > 0)
|
|
83
|
-
.map((t) => `"${t.replace(/"/g, '""')}"`)
|
|
84
|
-
.join(" ");
|
|
85
|
-
}
|
|
86
|
-
async addMemory(sessionName, content, metadata = {}) {
|
|
87
|
-
const id = this.newId();
|
|
88
|
-
const createdAt = new Date().toISOString();
|
|
89
|
-
this.db.prepare(`INSERT INTO memories (id, session_name, content, metadata, created_at) VALUES (?, ?, ?, ?, ?)`).run(id, sessionName, content, JSON.stringify(metadata), createdAt);
|
|
90
|
-
return id;
|
|
91
|
-
}
|
|
92
|
-
async searchMemories(sessionName, query, limit = 10) {
|
|
93
|
-
if (!query.trim())
|
|
94
|
-
return [];
|
|
95
|
-
const safeQuery = this.sanitizeFts5Query(query);
|
|
96
|
-
const rows = this.db.prepare(`
|
|
97
|
-
SELECT m.id, m.content, m.metadata, m.created_at,
|
|
98
|
-
-bm25(memories_fts) AS score
|
|
99
|
-
FROM memories_fts
|
|
100
|
-
JOIN memories m ON m.id = memories_fts.memory_id
|
|
101
|
-
WHERE memories_fts MATCH ?
|
|
102
|
-
AND memories_fts.session_name = ?
|
|
103
|
-
ORDER BY score DESC
|
|
104
|
-
LIMIT ?
|
|
105
|
-
`).all(safeQuery, sessionName, limit);
|
|
106
|
-
return rows.map((r) => ({
|
|
107
|
-
id: r.id,
|
|
108
|
-
content: r.content,
|
|
109
|
-
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
110
|
-
createdAt: r.created_at,
|
|
111
|
-
score: r.score,
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
listMemories(sessionName, limit = 50) {
|
|
115
|
-
const rows = this.db.prepare(`SELECT id, content, metadata, created_at FROM memories WHERE session_name = ? ORDER BY created_at DESC LIMIT ?`).all(sessionName, limit);
|
|
116
|
-
return rows.map((r) => ({
|
|
117
|
-
id: r.id,
|
|
118
|
-
content: r.content,
|
|
119
|
-
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
120
|
-
createdAt: r.created_at,
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
getMemory(sessionName, memoryId) {
|
|
124
|
-
const row = this.db.prepare(`SELECT id, content, metadata, created_at FROM memories WHERE id = ? AND session_name = ?`).get(memoryId, sessionName);
|
|
125
|
-
if (!row)
|
|
126
|
-
return null;
|
|
127
|
-
return {
|
|
128
|
-
id: row.id,
|
|
129
|
-
content: row.content,
|
|
130
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
131
|
-
createdAt: row.created_at,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
deleteMemory(sessionName, memoryId) {
|
|
135
|
-
const result = this.db.prepare(`DELETE FROM memories WHERE id = ? AND session_name = ?`).run(memoryId, sessionName);
|
|
136
|
-
return result.changes > 0;
|
|
137
|
-
}
|
|
138
|
-
async updateMemory(sessionName, memoryId, content, metadata) {
|
|
139
|
-
const existing = this.getMemory(sessionName, memoryId);
|
|
140
|
-
if (!existing)
|
|
141
|
-
return false;
|
|
142
|
-
const finalMetadata = metadata ?? existing.metadata;
|
|
143
|
-
const result = this.db.prepare(`UPDATE memories SET content = ?, metadata = ? WHERE id = ? AND session_name = ?`).run(content, JSON.stringify(finalMetadata), memoryId, sessionName);
|
|
144
|
-
return result.changes > 0;
|
|
145
|
-
}
|
|
146
|
-
getStats(sessionName) {
|
|
147
|
-
const row = this.db.prepare(`SELECT COUNT(*) as total FROM memories WHERE session_name = ?`).get(sessionName);
|
|
148
|
-
return { total: row.total };
|
|
149
|
-
}
|
|
150
|
-
async warmup() {
|
|
151
|
-
// No model to warm up — DB is already open and ready.
|
|
152
|
-
}
|
|
153
|
-
closeAll() {
|
|
154
|
-
try {
|
|
155
|
-
this.db.close();
|
|
156
|
-
}
|
|
157
|
-
catch { /* ignore */ }
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Concrete AgentOfficeMemory implementation backed by fastmemory (SQLite + embeddings).
|
|
162
|
-
* Each session gets its own .db file under <memoryPath>/<sessionName>.db
|
|
163
|
-
*/
|
|
164
|
-
export class AgentOfficeFastMemory extends AgentOfficeMemory {
|
|
165
|
-
basePath;
|
|
166
|
-
stores = new Map();
|
|
167
|
-
constructor(memoryPath) {
|
|
168
|
-
super();
|
|
169
|
-
this.basePath = resolve(memoryPath);
|
|
170
|
-
if (!existsSync(this.basePath)) {
|
|
171
|
-
mkdirSync(this.basePath, { recursive: true });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
dbPathFor(sessionName) {
|
|
175
|
-
// Sanitize session name for filesystem safety
|
|
176
|
-
const safe = sessionName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
177
|
-
return join(this.basePath, `${safe}.db`);
|
|
178
|
-
}
|
|
179
|
-
async getStore(sessionName) {
|
|
180
|
-
if (this.stores.has(sessionName)) {
|
|
181
|
-
return this.stores.get(sessionName);
|
|
182
|
-
}
|
|
183
|
-
const dbPath = this.dbPathFor(sessionName);
|
|
184
|
-
const cacheDir = join(this.basePath, ".model-cache");
|
|
185
|
-
const store = await createAgentMemory({
|
|
186
|
-
dbPath,
|
|
187
|
-
cacheDir,
|
|
188
|
-
dtype: "q4",
|
|
189
|
-
});
|
|
190
|
-
this.stores.set(sessionName, store);
|
|
191
|
-
return store;
|
|
192
|
-
}
|
|
193
|
-
async addMemory(sessionName, content, metadata = {}) {
|
|
194
|
-
const store = await this.getStore(sessionName);
|
|
195
|
-
return await store.add(content, metadata);
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Sanitize a query string for FTS5 MATCH syntax.
|
|
199
|
-
* Wraps each whitespace-delimited token in double quotes so that
|
|
200
|
-
* special characters (apostrophes, parens, asterisks, etc.) are
|
|
201
|
-
* treated as literals rather than FTS5 operators.
|
|
202
|
-
*/
|
|
203
|
-
sanitizeFts5Query(query) {
|
|
204
|
-
return query
|
|
205
|
-
.split(/\s+/)
|
|
206
|
-
.filter((t) => t.length > 0)
|
|
207
|
-
.map((t) => `"${t.replace(/"/g, '""')}"`)
|
|
208
|
-
.join(" ");
|
|
209
|
-
}
|
|
210
|
-
async searchMemories(sessionName, query, limit = 10) {
|
|
211
|
-
const store = await this.getStore(sessionName);
|
|
212
|
-
const safeQuery = this.sanitizeFts5Query(query);
|
|
213
|
-
return await store.searchHybrid(safeQuery, limit);
|
|
214
|
-
}
|
|
215
|
-
listMemories(sessionName, limit = 50) {
|
|
216
|
-
const dbPath = this.dbPathFor(sessionName);
|
|
217
|
-
if (!existsSync(dbPath))
|
|
218
|
-
return [];
|
|
219
|
-
const db = new Database(dbPath);
|
|
220
|
-
db.pragma("journal_mode = WAL");
|
|
221
|
-
try {
|
|
222
|
-
const rows = db.prepare(`SELECT id, content, metadata, created_at FROM memories ORDER BY created_at DESC LIMIT ?`).all(limit);
|
|
223
|
-
return rows.map((r) => ({
|
|
224
|
-
id: r.id,
|
|
225
|
-
content: r.content,
|
|
226
|
-
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
227
|
-
createdAt: r.created_at,
|
|
228
|
-
}));
|
|
229
|
-
}
|
|
230
|
-
finally {
|
|
231
|
-
db.close();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
getMemory(sessionName, memoryId) {
|
|
235
|
-
const dbPath = this.dbPathFor(sessionName);
|
|
236
|
-
if (!existsSync(dbPath))
|
|
237
|
-
return null;
|
|
238
|
-
const db = new Database(dbPath);
|
|
239
|
-
db.pragma("journal_mode = WAL");
|
|
240
|
-
try {
|
|
241
|
-
const row = db.prepare(`SELECT id, content, metadata, created_at FROM memories WHERE id = ?`).get(memoryId);
|
|
242
|
-
if (!row)
|
|
243
|
-
return null;
|
|
244
|
-
return {
|
|
245
|
-
id: row.id,
|
|
246
|
-
content: row.content,
|
|
247
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
248
|
-
createdAt: row.created_at,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
db.close();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
deleteMemory(sessionName, memoryId) {
|
|
256
|
-
const dbPath = this.dbPathFor(sessionName);
|
|
257
|
-
if (!existsSync(dbPath))
|
|
258
|
-
return false;
|
|
259
|
-
const db = new Database(dbPath);
|
|
260
|
-
db.pragma("journal_mode = WAL");
|
|
261
|
-
try {
|
|
262
|
-
const result = db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
|
|
263
|
-
return result.changes > 0;
|
|
264
|
-
}
|
|
265
|
-
finally {
|
|
266
|
-
db.close();
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
async updateMemory(sessionName, memoryId, content, metadata) {
|
|
270
|
-
const existing = this.getMemory(sessionName, memoryId);
|
|
271
|
-
if (!existing)
|
|
272
|
-
return false;
|
|
273
|
-
// Delete old and re-add with new content (to get new embedding)
|
|
274
|
-
this.deleteMemory(sessionName, memoryId);
|
|
275
|
-
// Close the cached store so it picks up the direct DB changes
|
|
276
|
-
if (this.stores.has(sessionName)) {
|
|
277
|
-
this.stores.get(sessionName).close();
|
|
278
|
-
this.stores.delete(sessionName);
|
|
279
|
-
}
|
|
280
|
-
const store = await this.getStore(sessionName);
|
|
281
|
-
const finalMetadata = metadata ?? existing.metadata;
|
|
282
|
-
await store.add(content, finalMetadata);
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
getStats(sessionName) {
|
|
286
|
-
const dbPath = this.dbPathFor(sessionName);
|
|
287
|
-
if (!existsSync(dbPath))
|
|
288
|
-
return { total: 0 };
|
|
289
|
-
const db = new Database(dbPath);
|
|
290
|
-
db.pragma("journal_mode = WAL");
|
|
291
|
-
try {
|
|
292
|
-
const row = db.prepare(`SELECT COUNT(*) as total FROM memories`).get();
|
|
293
|
-
return { total: row.total };
|
|
294
|
-
}
|
|
295
|
-
finally {
|
|
296
|
-
db.close();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Warm up the embedding model and verify it loads correctly.
|
|
301
|
-
* Uses a temporary DB so no real session data is affected.
|
|
302
|
-
* Throws if the model is corrupt or fails to produce embeddings.
|
|
303
|
-
*/
|
|
304
|
-
async warmup() {
|
|
305
|
-
const testDbPath = join(this.basePath, "_warmup_test.db");
|
|
306
|
-
const cacheDir = join(this.basePath, ".model-cache");
|
|
307
|
-
const store = await createAgentMemory({
|
|
308
|
-
dbPath: testDbPath,
|
|
309
|
-
cacheDir,
|
|
310
|
-
dtype: "q4",
|
|
311
|
-
});
|
|
312
|
-
try {
|
|
313
|
-
// Force an embedding by adding and searching
|
|
314
|
-
await store.add("warmup test memory");
|
|
315
|
-
const results = await store.searchHybrid("warmup test", 1);
|
|
316
|
-
if (results.length === 0) {
|
|
317
|
-
throw new Error("Embedding model warmup: search returned no results");
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
finally {
|
|
321
|
-
store.close();
|
|
322
|
-
// Clean up the temp DB files
|
|
323
|
-
try {
|
|
324
|
-
const { unlinkSync } = await import("fs");
|
|
325
|
-
unlinkSync(testDbPath);
|
|
326
|
-
// SQLite WAL/SHM files
|
|
327
|
-
try {
|
|
328
|
-
unlinkSync(testDbPath + "-wal");
|
|
329
|
-
}
|
|
330
|
-
catch { /* may not exist */ }
|
|
331
|
-
try {
|
|
332
|
-
unlinkSync(testDbPath + "-shm");
|
|
333
|
-
}
|
|
334
|
-
catch { /* may not exist */ }
|
|
335
|
-
}
|
|
336
|
-
catch { /* ignore cleanup errors */ }
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
closeAll() {
|
|
340
|
-
for (const [, store] of this.stores) {
|
|
341
|
-
try {
|
|
342
|
-
store.close();
|
|
343
|
-
}
|
|
344
|
-
catch { /* ignore */ }
|
|
345
|
-
}
|
|
346
|
-
this.stores.clear();
|
|
347
|
-
}
|
|
348
|
-
}
|