agent-office 0.0.3 → 0.0.6
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/dist/cli.js +59 -0
- package/dist/commands/communicator.d.ts +8 -0
- package/dist/commands/communicator.js +595 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +18 -1
- package/dist/commands/worker.d.ts +4 -0
- package/dist/commands/worker.js +63 -0
- package/dist/manage/app.js +4 -3
- package/dist/manage/components/SessionList.d.ts +2 -1
- package/dist/manage/components/SessionList.js +309 -2
- package/dist/manage/hooks/useApi.d.ts +33 -0
- package/dist/manage/hooks/useApi.js +28 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +3 -3
- package/dist/server/memory.d.ts +64 -0
- package/dist/server/memory.js +214 -0
- package/dist/server/routes.d.ts +3 -2
- package/dist/server/routes.js +347 -32
- package/package.json +4 -1
package/dist/server/index.js
CHANGED
|
@@ -10,13 +10,13 @@ function authMiddleware(password) {
|
|
|
10
10
|
next();
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
export function createApp(sql, opencode, password, serverUrl, cronScheduler) {
|
|
13
|
+
export function createApp(sql, opencode, password, serverUrl, cronScheduler, memoryManager) {
|
|
14
14
|
const app = express();
|
|
15
15
|
app.use(express.json());
|
|
16
16
|
// Worker routes are unauthenticated — mounted before auth middleware
|
|
17
|
-
app.use("/", createWorkerRouter(sql, opencode, serverUrl));
|
|
17
|
+
app.use("/", createWorkerRouter(sql, opencode, serverUrl, memoryManager));
|
|
18
18
|
// Everything else requires Bearer auth
|
|
19
19
|
app.use(authMiddleware(password));
|
|
20
|
-
app.use("/", createRouter(sql, opencode, serverUrl, cronScheduler));
|
|
20
|
+
app.use("/", createRouter(sql, opencode, serverUrl, cronScheduler, memoryManager));
|
|
21
21
|
return app;
|
|
22
22
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createAgentMemory, type MemoryEntry } from "fastmemory";
|
|
2
|
+
export interface MemoryRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
content: string;
|
|
5
|
+
metadata: Record<string, unknown>;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
}
|
|
8
|
+
type MemoryStore = Awaited<ReturnType<typeof createAgentMemory>>;
|
|
9
|
+
/**
|
|
10
|
+
* Manages per-session memory stores backed by fastmemory (SQLite + embeddings).
|
|
11
|
+
* Each session gets its own .db file under <memoryPath>/<sessionName>.db
|
|
12
|
+
*/
|
|
13
|
+
export declare class MemoryManager {
|
|
14
|
+
private basePath;
|
|
15
|
+
private stores;
|
|
16
|
+
constructor(memoryPath: string);
|
|
17
|
+
private dbPathFor;
|
|
18
|
+
getStore(sessionName: string): Promise<MemoryStore>;
|
|
19
|
+
/**
|
|
20
|
+
* Add a memory for a session
|
|
21
|
+
*/
|
|
22
|
+
addMemory(sessionName: string, content: string, metadata?: Record<string, unknown>): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a query string for FTS5 MATCH syntax.
|
|
25
|
+
* Wraps each whitespace-delimited token in double quotes so that
|
|
26
|
+
* special characters (apostrophes, parens, asterisks, etc.) are
|
|
27
|
+
* treated as literals rather than FTS5 operators.
|
|
28
|
+
*/
|
|
29
|
+
private sanitizeFts5Query;
|
|
30
|
+
/**
|
|
31
|
+
* Search memories using hybrid search (BM25 + vector + RRF)
|
|
32
|
+
*/
|
|
33
|
+
searchMemories(sessionName: string, query: string, limit?: number): Promise<MemoryEntry[]>;
|
|
34
|
+
/**
|
|
35
|
+
* List all memories for a session (direct SQLite access since fastmemory has no list method)
|
|
36
|
+
*/
|
|
37
|
+
listMemories(sessionName: string, limit?: number): MemoryRecord[];
|
|
38
|
+
/**
|
|
39
|
+
* Get a single memory by ID
|
|
40
|
+
*/
|
|
41
|
+
getMemory(sessionName: string, memoryId: string): MemoryRecord | null;
|
|
42
|
+
/**
|
|
43
|
+
* Delete a memory by ID (direct SQLite - fastmemory has no delete method)
|
|
44
|
+
*/
|
|
45
|
+
deleteMemory(sessionName: string, memoryId: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Update a memory's content (direct SQLite, re-embeds via delete+add)
|
|
48
|
+
*/
|
|
49
|
+
updateMemory(sessionName: string, memoryId: string, content: string, metadata?: Record<string, unknown>): Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* Get stats for a session's memory store
|
|
52
|
+
*/
|
|
53
|
+
getStats(sessionName: string): {
|
|
54
|
+
total: number;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Warm up the embedding model and verify it loads correctly.
|
|
58
|
+
* Uses a temporary DB so no real session data is affected.
|
|
59
|
+
* Throws if the model is corrupt or fails to produce embeddings.
|
|
60
|
+
*/
|
|
61
|
+
warmup(): Promise<void>;
|
|
62
|
+
closeAll(): void;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
* Manages per-session memory stores backed by fastmemory (SQLite + embeddings).
|
|
7
|
+
* Each session gets its own .db file under <memoryPath>/<sessionName>.db
|
|
8
|
+
*/
|
|
9
|
+
export class MemoryManager {
|
|
10
|
+
basePath;
|
|
11
|
+
stores = new Map();
|
|
12
|
+
constructor(memoryPath) {
|
|
13
|
+
this.basePath = resolve(memoryPath);
|
|
14
|
+
if (!existsSync(this.basePath)) {
|
|
15
|
+
mkdirSync(this.basePath, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
dbPathFor(sessionName) {
|
|
19
|
+
// Sanitize session name for filesystem safety
|
|
20
|
+
const safe = sessionName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
21
|
+
return join(this.basePath, `${safe}.db`);
|
|
22
|
+
}
|
|
23
|
+
async getStore(sessionName) {
|
|
24
|
+
if (this.stores.has(sessionName)) {
|
|
25
|
+
return this.stores.get(sessionName);
|
|
26
|
+
}
|
|
27
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
28
|
+
const cacheDir = join(this.basePath, ".model-cache");
|
|
29
|
+
const store = await createAgentMemory({
|
|
30
|
+
dbPath,
|
|
31
|
+
cacheDir,
|
|
32
|
+
dtype: "q4",
|
|
33
|
+
});
|
|
34
|
+
this.stores.set(sessionName, store);
|
|
35
|
+
return store;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Add a memory for a session
|
|
39
|
+
*/
|
|
40
|
+
async addMemory(sessionName, content, metadata = {}) {
|
|
41
|
+
const store = await this.getStore(sessionName);
|
|
42
|
+
return await store.add(content, metadata);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sanitize a query string for FTS5 MATCH syntax.
|
|
46
|
+
* Wraps each whitespace-delimited token in double quotes so that
|
|
47
|
+
* special characters (apostrophes, parens, asterisks, etc.) are
|
|
48
|
+
* treated as literals rather than FTS5 operators.
|
|
49
|
+
*/
|
|
50
|
+
sanitizeFts5Query(query) {
|
|
51
|
+
return query
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter((t) => t.length > 0)
|
|
54
|
+
.map((t) => `"${t.replace(/"/g, '""')}"`)
|
|
55
|
+
.join(" ");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Search memories using hybrid search (BM25 + vector + RRF)
|
|
59
|
+
*/
|
|
60
|
+
async searchMemories(sessionName, query, limit = 10) {
|
|
61
|
+
const store = await this.getStore(sessionName);
|
|
62
|
+
const safeQuery = this.sanitizeFts5Query(query);
|
|
63
|
+
return await store.searchHybrid(safeQuery, limit);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* List all memories for a session (direct SQLite access since fastmemory has no list method)
|
|
67
|
+
*/
|
|
68
|
+
listMemories(sessionName, limit = 50) {
|
|
69
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
70
|
+
if (!existsSync(dbPath))
|
|
71
|
+
return [];
|
|
72
|
+
const db = new Database(dbPath);
|
|
73
|
+
db.pragma("journal_mode = WAL");
|
|
74
|
+
try {
|
|
75
|
+
const rows = db.prepare(`SELECT id, content, metadata, created_at FROM memories ORDER BY created_at DESC LIMIT ?`).all(limit);
|
|
76
|
+
return rows.map((r) => ({
|
|
77
|
+
id: r.id,
|
|
78
|
+
content: r.content,
|
|
79
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
80
|
+
createdAt: r.created_at,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
db.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get a single memory by ID
|
|
89
|
+
*/
|
|
90
|
+
getMemory(sessionName, memoryId) {
|
|
91
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
92
|
+
if (!existsSync(dbPath))
|
|
93
|
+
return null;
|
|
94
|
+
const db = new Database(dbPath);
|
|
95
|
+
db.pragma("journal_mode = WAL");
|
|
96
|
+
try {
|
|
97
|
+
const row = db.prepare(`SELECT id, content, metadata, created_at FROM memories WHERE id = ?`).get(memoryId);
|
|
98
|
+
if (!row)
|
|
99
|
+
return null;
|
|
100
|
+
return {
|
|
101
|
+
id: row.id,
|
|
102
|
+
content: row.content,
|
|
103
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
104
|
+
createdAt: row.created_at,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
db.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Delete a memory by ID (direct SQLite - fastmemory has no delete method)
|
|
113
|
+
*/
|
|
114
|
+
deleteMemory(sessionName, memoryId) {
|
|
115
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
116
|
+
if (!existsSync(dbPath))
|
|
117
|
+
return false;
|
|
118
|
+
const db = new Database(dbPath);
|
|
119
|
+
db.pragma("journal_mode = WAL");
|
|
120
|
+
try {
|
|
121
|
+
const result = db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
|
|
122
|
+
return result.changes > 0;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
db.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Update a memory's content (direct SQLite, re-embeds via delete+add)
|
|
130
|
+
*/
|
|
131
|
+
async updateMemory(sessionName, memoryId, content, metadata) {
|
|
132
|
+
const existing = this.getMemory(sessionName, memoryId);
|
|
133
|
+
if (!existing)
|
|
134
|
+
return false;
|
|
135
|
+
// Delete old and re-add with new content (to get new embedding)
|
|
136
|
+
this.deleteMemory(sessionName, memoryId);
|
|
137
|
+
// Close the cached store so it picks up the direct DB changes
|
|
138
|
+
if (this.stores.has(sessionName)) {
|
|
139
|
+
this.stores.get(sessionName).close();
|
|
140
|
+
this.stores.delete(sessionName);
|
|
141
|
+
}
|
|
142
|
+
const store = await this.getStore(sessionName);
|
|
143
|
+
// Re-add with new content but preserve metadata if not provided
|
|
144
|
+
const finalMetadata = metadata ?? existing.metadata;
|
|
145
|
+
await store.add(content, finalMetadata);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get stats for a session's memory store
|
|
150
|
+
*/
|
|
151
|
+
getStats(sessionName) {
|
|
152
|
+
const dbPath = this.dbPathFor(sessionName);
|
|
153
|
+
if (!existsSync(dbPath))
|
|
154
|
+
return { total: 0 };
|
|
155
|
+
const db = new Database(dbPath);
|
|
156
|
+
db.pragma("journal_mode = WAL");
|
|
157
|
+
try {
|
|
158
|
+
const row = db.prepare(`SELECT COUNT(*) as total FROM memories`).get();
|
|
159
|
+
return { total: row.total };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
db.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Warm up the embedding model and verify it loads correctly.
|
|
167
|
+
* Uses a temporary DB so no real session data is affected.
|
|
168
|
+
* Throws if the model is corrupt or fails to produce embeddings.
|
|
169
|
+
*/
|
|
170
|
+
async warmup() {
|
|
171
|
+
const testDbPath = join(this.basePath, "_warmup_test.db");
|
|
172
|
+
const cacheDir = join(this.basePath, ".model-cache");
|
|
173
|
+
const store = await createAgentMemory({
|
|
174
|
+
dbPath: testDbPath,
|
|
175
|
+
cacheDir,
|
|
176
|
+
dtype: "q4",
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
// Force an embedding by adding and searching
|
|
180
|
+
const id = await store.add("warmup test memory");
|
|
181
|
+
const results = await store.searchHybrid("warmup test", 1);
|
|
182
|
+
if (results.length === 0) {
|
|
183
|
+
throw new Error("Embedding model warmup: search returned no results");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
store.close();
|
|
188
|
+
// Clean up the temp DB files
|
|
189
|
+
try {
|
|
190
|
+
const { unlinkSync } = await import("fs");
|
|
191
|
+
unlinkSync(testDbPath);
|
|
192
|
+
// SQLite WAL/SHM files
|
|
193
|
+
try {
|
|
194
|
+
unlinkSync(testDbPath + "-wal");
|
|
195
|
+
}
|
|
196
|
+
catch { /* may not exist */ }
|
|
197
|
+
try {
|
|
198
|
+
unlinkSync(testDbPath + "-shm");
|
|
199
|
+
}
|
|
200
|
+
catch { /* may not exist */ }
|
|
201
|
+
}
|
|
202
|
+
catch { /* ignore cleanup errors */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
closeAll() {
|
|
206
|
+
for (const [, store] of this.stores) {
|
|
207
|
+
try {
|
|
208
|
+
store.close();
|
|
209
|
+
}
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
this.stores.clear();
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ import { Router } from "express";
|
|
|
2
2
|
import type { Sql } from "../db/index.js";
|
|
3
3
|
import type { OpencodeClient } from "../lib/opencode.js";
|
|
4
4
|
import { CronScheduler } from "./cron.js";
|
|
5
|
-
|
|
6
|
-
export declare function
|
|
5
|
+
import type { MemoryManager } from "./memory.js";
|
|
6
|
+
export declare function createRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, scheduler: CronScheduler, memoryManager: MemoryManager): Router;
|
|
7
|
+
export declare function createWorkerRouter(sql: Sql, opencode: OpencodeClient, serverUrl: string, memoryManager: MemoryManager): Router;
|