agent-office 0.3.0 → 0.3.1

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 CHANGED
@@ -16,6 +16,7 @@ program
16
16
  .option("--memory-path <path>", "Directory for memory storage (default: ./.memory)", "./.memory")
17
17
  .option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
18
18
  .option("--opencode-url <url>", "URL of the OpenCode server (default: http://127.0.0.1:4096)", process.env.OPENCODE_URL ?? "http://127.0.0.1:4096")
19
+ .option("--simple-memory", "Use lightweight SQLite+BM25 memory instead of the default fastmemory (no embeddings, no model download)")
19
20
  .action(async (options) => {
20
21
  const { serve } = await import("./commands/serve.js");
21
22
  await serve(options);
@@ -6,6 +6,7 @@ interface ServeOptions {
6
6
  memoryPath: string;
7
7
  password?: string;
8
8
  opencodeUrl: string;
9
+ simpleMemory?: boolean;
9
10
  }
10
11
  export declare function serve(options: ServeOptions): Promise<void>;
11
12
  export {};
@@ -3,7 +3,7 @@ import { runMigrations } from "../db/migrate.js";
3
3
  import { OpenCodeCodingServer } from "../lib/opencode-coding-server.js";
4
4
  import { createApp } from "../server/index.js";
5
5
  import { CronScheduler } from "../server/cron.js";
6
- import { MemoryManager } from "../server/memory.js";
6
+ import { AgentOfficeFastMemory, AgentOfficeSimpleMemory } from "../server/memory.js";
7
7
  export async function serve(options) {
8
8
  const password = options.password;
9
9
  if (!password) {
@@ -44,20 +44,28 @@ export async function serve(options) {
44
44
  const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
45
45
  console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
46
46
  const serverUrl = `http://${options.host}:${port}`;
47
- // Create memory manager and verify embedding model
48
- const memoryManager = new MemoryManager(options.memoryPath);
49
- console.log("Warming up embedding model...");
50
- try {
47
+ // Create memory manager
48
+ let memoryManager;
49
+ if (options.simpleMemory) {
50
+ console.log("Using simple SQLite+BM25 memory backend.");
51
+ memoryManager = new AgentOfficeSimpleMemory(options.memoryPath);
51
52
  await memoryManager.warmup();
52
- console.log("Embedding model ready.");
53
53
  }
54
- catch (err) {
55
- console.error("Embedding model failed to load:", err);
56
- console.error("Try deleting the model cache and restarting:");
57
- console.error(` rm -rf ${options.memoryPath}/.model-cache`);
58
- memoryManager.closeAll();
59
- await storage.close();
60
- process.exit(1);
54
+ else {
55
+ memoryManager = new AgentOfficeFastMemory(options.memoryPath);
56
+ console.log("Warming up embedding model...");
57
+ try {
58
+ await memoryManager.warmup();
59
+ console.log("Embedding model ready.");
60
+ }
61
+ catch (err) {
62
+ console.error("Embedding model failed to load:", err);
63
+ console.error("Try deleting the model cache and restarting:");
64
+ console.error(` rm -rf ${options.memoryPath}/.model-cache`);
65
+ memoryManager.closeAll();
66
+ await storage.close();
67
+ process.exit(1);
68
+ }
61
69
  }
62
70
  // Create cron scheduler
63
71
  const cronScheduler = new CronScheduler();
@@ -1,5 +1,5 @@
1
1
  import type { AgentOfficeStorage } from "../db/index.js";
2
2
  import type { AgenticCodingServer } from "../lib/agentic-coding-server.js";
3
3
  import { CronScheduler } from "./cron.js";
4
- import type { MemoryManager } from "./memory.js";
5
- export declare function createApp(storage: AgentOfficeStorage, agenticCodingServer: AgenticCodingServer, password: string, serverUrl: string, cronScheduler: CronScheduler, memoryManager: MemoryManager): import("express-serve-static-core").Express;
4
+ import type { AgentOfficeMemory } from "./memory.js";
5
+ export declare function createApp(storage: AgentOfficeStorage, agenticCodingServer: AgenticCodingServer, password: string, serverUrl: string, cronScheduler: CronScheduler, memoryManager: AgentOfficeMemory): import("express-serve-static-core").Express;
@@ -1,24 +1,66 @@
1
- import { createAgentMemory, type MemoryEntry } from "fastmemory";
1
+ import { type MemoryEntry } from "fastmemory";
2
2
  export interface MemoryRecord {
3
3
  id: string;
4
4
  content: string;
5
5
  metadata: Record<string, unknown>;
6
6
  createdAt: string;
7
7
  }
8
- type MemoryStore = Awaited<ReturnType<typeof createAgentMemory>>;
8
+ export type AgentOfficeMemorySearchResult = MemoryEntry;
9
9
  /**
10
- * Manages per-session memory stores backed by fastmemory (SQLite + embeddings).
10
+ * Abstract base class for agent memory backends.
11
+ * Consumers should depend on this type rather than any concrete implementation.
12
+ */
13
+ export declare abstract class AgentOfficeMemory {
14
+ abstract addMemory(sessionName: string, content: string, metadata?: Record<string, unknown>): Promise<string>;
15
+ abstract searchMemories(sessionName: string, query: string, limit?: number): Promise<AgentOfficeMemorySearchResult[]>;
16
+ abstract listMemories(sessionName: string, limit?: number): MemoryRecord[];
17
+ abstract getMemory(sessionName: string, memoryId: string): MemoryRecord | null;
18
+ abstract deleteMemory(sessionName: string, memoryId: string): boolean;
19
+ abstract updateMemory(sessionName: string, memoryId: string, content: string, metadata?: Record<string, unknown>): Promise<boolean>;
20
+ abstract getStats(sessionName: string): {
21
+ total: number;
22
+ };
23
+ abstract warmup(): Promise<void>;
24
+ abstract closeAll(): void;
25
+ }
26
+ /**
27
+ * Lightweight memory backend using a single SQLite database file at
28
+ * <memoryPath>/simple-memory.db. All sessions share the same file; rows are
29
+ * partitioned by session_name. Full-text search is provided by SQLite's
30
+ * built-in FTS5 (BM25 ranking). No embeddings, no external dependencies
31
+ * beyond better-sqlite3.
32
+ *
33
+ * Activate with: agent-office serve --simple-memory
34
+ */
35
+ export declare class AgentOfficeSimpleMemory extends AgentOfficeMemory {
36
+ private basePath;
37
+ private db;
38
+ constructor(memoryPath: string);
39
+ private _migrate;
40
+ private newId;
41
+ private sanitizeFts5Query;
42
+ addMemory(sessionName: string, content: string, metadata?: Record<string, unknown>): Promise<string>;
43
+ searchMemories(sessionName: string, query: string, limit?: number): Promise<AgentOfficeMemorySearchResult[]>;
44
+ listMemories(sessionName: string, limit?: number): MemoryRecord[];
45
+ getMemory(sessionName: string, memoryId: string): MemoryRecord | null;
46
+ deleteMemory(sessionName: string, memoryId: string): boolean;
47
+ updateMemory(sessionName: string, memoryId: string, content: string, metadata?: Record<string, unknown>): Promise<boolean>;
48
+ getStats(sessionName: string): {
49
+ total: number;
50
+ };
51
+ warmup(): Promise<void>;
52
+ closeAll(): void;
53
+ }
54
+ /**
55
+ * Concrete AgentOfficeMemory implementation backed by fastmemory (SQLite + embeddings).
11
56
  * Each session gets its own .db file under <memoryPath>/<sessionName>.db
12
57
  */
13
- export declare class MemoryManager {
58
+ export declare class AgentOfficeFastMemory extends AgentOfficeMemory {
14
59
  private basePath;
15
60
  private stores;
16
61
  constructor(memoryPath: string);
17
62
  private dbPathFor;
18
- getStore(sessionName: string): Promise<MemoryStore>;
19
- /**
20
- * Add a memory for a session
21
- */
63
+ private getStore;
22
64
  addMemory(sessionName: string, content: string, metadata?: Record<string, unknown>): Promise<string>;
23
65
  /**
24
66
  * Sanitize a query string for FTS5 MATCH syntax.
@@ -27,29 +69,11 @@ export declare class MemoryManager {
27
69
  * treated as literals rather than FTS5 operators.
28
70
  */
29
71
  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
- */
72
+ searchMemories(sessionName: string, query: string, limit?: number): Promise<AgentOfficeMemorySearchResult[]>;
37
73
  listMemories(sessionName: string, limit?: number): MemoryRecord[];
38
- /**
39
- * Get a single memory by ID
40
- */
41
74
  getMemory(sessionName: string, memoryId: string): MemoryRecord | null;
42
- /**
43
- * Delete a memory by ID (direct SQLite - fastmemory has no delete method)
44
- */
45
75
  deleteMemory(sessionName: string, memoryId: string): boolean;
46
- /**
47
- * Update a memory's content (direct SQLite, re-embeds via delete+add)
48
- */
49
76
  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
77
  getStats(sessionName: string): {
54
78
  total: number;
55
79
  };
@@ -61,4 +85,3 @@ export declare class MemoryManager {
61
85
  warmup(): Promise<void>;
62
86
  closeAll(): void;
63
87
  }
64
- export {};
@@ -3,13 +3,169 @@ import { mkdirSync, existsSync } from "fs";
3
3
  import { join, resolve } from "path";
4
4
  import Database from "better-sqlite3";
5
5
  /**
6
- * Manages per-session memory stores backed by fastmemory (SQLite + embeddings).
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).
7
162
  * Each session gets its own .db file under <memoryPath>/<sessionName>.db
8
163
  */
9
- export class MemoryManager {
164
+ export class AgentOfficeFastMemory extends AgentOfficeMemory {
10
165
  basePath;
11
166
  stores = new Map();
12
167
  constructor(memoryPath) {
168
+ super();
13
169
  this.basePath = resolve(memoryPath);
14
170
  if (!existsSync(this.basePath)) {
15
171
  mkdirSync(this.basePath, { recursive: true });
@@ -34,9 +190,6 @@ export class MemoryManager {
34
190
  this.stores.set(sessionName, store);
35
191
  return store;
36
192
  }
37
- /**
38
- * Add a memory for a session
39
- */
40
193
  async addMemory(sessionName, content, metadata = {}) {
41
194
  const store = await this.getStore(sessionName);
42
195
  return await store.add(content, metadata);
@@ -54,17 +207,11 @@ export class MemoryManager {
54
207
  .map((t) => `"${t.replace(/"/g, '""')}"`)
55
208
  .join(" ");
56
209
  }
57
- /**
58
- * Search memories using hybrid search (BM25 + vector + RRF)
59
- */
60
210
  async searchMemories(sessionName, query, limit = 10) {
61
211
  const store = await this.getStore(sessionName);
62
212
  const safeQuery = this.sanitizeFts5Query(query);
63
213
  return await store.searchHybrid(safeQuery, limit);
64
214
  }
65
- /**
66
- * List all memories for a session (direct SQLite access since fastmemory has no list method)
67
- */
68
215
  listMemories(sessionName, limit = 50) {
69
216
  const dbPath = this.dbPathFor(sessionName);
70
217
  if (!existsSync(dbPath))
@@ -84,9 +231,6 @@ export class MemoryManager {
84
231
  db.close();
85
232
  }
86
233
  }
87
- /**
88
- * Get a single memory by ID
89
- */
90
234
  getMemory(sessionName, memoryId) {
91
235
  const dbPath = this.dbPathFor(sessionName);
92
236
  if (!existsSync(dbPath))
@@ -108,9 +252,6 @@ export class MemoryManager {
108
252
  db.close();
109
253
  }
110
254
  }
111
- /**
112
- * Delete a memory by ID (direct SQLite - fastmemory has no delete method)
113
- */
114
255
  deleteMemory(sessionName, memoryId) {
115
256
  const dbPath = this.dbPathFor(sessionName);
116
257
  if (!existsSync(dbPath))
@@ -125,9 +266,6 @@ export class MemoryManager {
125
266
  db.close();
126
267
  }
127
268
  }
128
- /**
129
- * Update a memory's content (direct SQLite, re-embeds via delete+add)
130
- */
131
269
  async updateMemory(sessionName, memoryId, content, metadata) {
132
270
  const existing = this.getMemory(sessionName, memoryId);
133
271
  if (!existing)
@@ -140,14 +278,10 @@ export class MemoryManager {
140
278
  this.stores.delete(sessionName);
141
279
  }
142
280
  const store = await this.getStore(sessionName);
143
- // Re-add with new content but preserve metadata if not provided
144
281
  const finalMetadata = metadata ?? existing.metadata;
145
282
  await store.add(content, finalMetadata);
146
283
  return true;
147
284
  }
148
- /**
149
- * Get stats for a session's memory store
150
- */
151
285
  getStats(sessionName) {
152
286
  const dbPath = this.dbPathFor(sessionName);
153
287
  if (!existsSync(dbPath))
@@ -177,7 +311,7 @@ export class MemoryManager {
177
311
  });
178
312
  try {
179
313
  // Force an embedding by adding and searching
180
- const id = await store.add("warmup test memory");
314
+ await store.add("warmup test memory");
181
315
  const results = await store.searchHybrid("warmup test", 1);
182
316
  if (results.length === 0) {
183
317
  throw new Error("Embedding model warmup: search returned no results");
@@ -2,7 +2,7 @@ import { Router } from "express";
2
2
  import type { AgentOfficeStorage } from "../db/index.js";
3
3
  import type { AgenticCodingServer } from "../lib/agentic-coding-server.js";
4
4
  import { CronScheduler } from "./cron.js";
5
- import type { MemoryManager } from "./memory.js";
5
+ import type { AgentOfficeMemory } from "./memory.js";
6
6
  /**
7
7
  * Build the persistent system-prompt briefing for a worker session.
8
8
  * This is injected as the `system` field on every `promptAsync` call so the
@@ -10,5 +10,5 @@ import type { MemoryManager } from "./memory.js";
10
10
  * without consuming a user-message turn.
11
11
  */
12
12
  export declare function generateSystemPrompt(name: string, status: string | null, humanName: string, humanDescription: string, token: string): string;
13
- export declare function createRouter(storage: AgentOfficeStorage, agenticCodingServer: AgenticCodingServer, serverUrl: string, scheduler: CronScheduler, memoryManager: MemoryManager): Router;
14
- export declare function createWorkerRouter(storage: AgentOfficeStorage, agenticCodingServer: AgenticCodingServer, serverUrl: string, memoryManager: MemoryManager): Router;
13
+ export declare function createRouter(storage: AgentOfficeStorage, agenticCodingServer: AgenticCodingServer, serverUrl: string, scheduler: CronScheduler, memoryManager: AgentOfficeMemory): Router;
14
+ export declare function createWorkerRouter(storage: AgentOfficeStorage, agenticCodingServer: AgenticCodingServer, serverUrl: string, memoryManager: AgentOfficeMemory): Router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "An office for your AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",