agent-office 0.3.1 → 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.
@@ -4,17 +4,12 @@ const MAIL_INJECTION_BLURB = [
4
4
  ``,
5
5
  `---`,
6
6
  `You have a new message. Please review the injected message above and respond accordingly.`,
7
- `IMPORTANT: When reading or responding, note that dollar signs ($) and other special`,
8
- `characters may be interpreted as markdown. The sender may have included them but they`,
9
- `could appear differently in your session view. Interpret context accordingly.`,
10
7
  ``,
11
8
  `When responding to the sender:`,
12
9
  `- Use the \`agent-office worker send-message\` tool so they can see your reply`,
13
10
  `- Avoid excessive length - keep responses concise`,
14
- `- Use markdown front-matter with a "choices" array if offering options`,
15
- ``,
16
- `Tip: For currency or prices, use code blocks. Example: put numbers in single or`,
17
- `double quotes to preserve formatting characters like dollar signs.`,
11
+ `- Feel free to use markdown formatting`,
12
+ `- IMPORTANT: Remember when using bash commands certain characters (like dollar signs) may need to be escaped or wrapped in quotes`,
18
13
  ].join("\n");
19
14
  /**
20
15
  * Build the persistent system-prompt briefing for a worker session.
@@ -78,23 +73,22 @@ export function generateSystemPrompt(name, status, humanName, humanDescription,
78
73
  ` agent-office worker cron \\`,
79
74
  ` ${token}`,
80
75
  ``,
81
- ` Store a memory (persistent across sessions)`,
82
- ` agent-office worker memory add \\`,
83
- ` --content "your memory here" \\`,
84
- ` ${token}`,
85
- ``,
86
- ` Search your memories`,
87
- ` agent-office worker memory search \\`,
88
- ` --query "your search" \\`,
76
+ ` Create a cron job (scheduled task)`,
77
+ ` agent-office worker cron create \\`,
78
+ ` --name <job-name> \\`,
79
+ ` --schedule "<cron-expression>" \\`,
80
+ ` --message "<action-to-perform>" \\`,
81
+ ` --respond-to "<who-to-respond-to-when-done>" \\`,
89
82
  ` ${token}`,
90
83
  ``,
91
- ` List all your memories`,
92
- ` agent-office worker memory list \\`,
93
- ` ${token}`,
84
+ ` Example: Daily standup reminder at 9am`,
85
+ ` --schedule "0 9 * * *" \\`,
86
+ ` --message "Prepare your standup update" \\`,
87
+ ` --respond-to "${humanName} in the standup channel"`,
94
88
  ``,
95
- ` Forget a memory`,
96
- ` agent-office worker memory forget \\`,
97
- ` ${token} <memory-id>`,
89
+ ` The final injected message will be formatted as:`,
90
+ ` Action: <message>`,
91
+ ` Who to respond to when done: <respond-to>`,
98
92
  ``,
99
93
  `════════════════════════════════════════════════════════`,
100
94
  ` IMPORTANT: YOUR SESSIONS ARE PRIVATE`,
@@ -141,7 +135,7 @@ async function loadHumanConfig(storage) {
141
135
  humanDescription: humanDescription ?? "",
142
136
  };
143
137
  }
144
- export function createRouter(storage, agenticCodingServer, serverUrl, scheduler, memoryManager) {
138
+ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler) {
145
139
  const router = Router();
146
140
  router.get("/health", (_req, res) => {
147
141
  res.json({ ok: true });
@@ -192,6 +186,27 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler,
192
186
  res.status(500).json({ error: "Internal server error" });
193
187
  }
194
188
  });
189
+ router.get("/coworkers", async (_req, res) => {
190
+ try {
191
+ const sessions = await storage.listSessions();
192
+ const humanName = await storage.getConfig('human_name') ?? "Human";
193
+ const unreadCounts = await storage.countUnreadBySender(humanName);
194
+ const coworkers = [
195
+ { name: humanName, status: null, isHuman: true, unreadMessages: 0 },
196
+ ...sessions.map(s => ({
197
+ name: s.name,
198
+ status: s.status,
199
+ isHuman: false,
200
+ unreadMessages: unreadCounts.get(s.name) ?? 0
201
+ }))
202
+ ];
203
+ res.json(coworkers);
204
+ }
205
+ catch (err) {
206
+ console.error("GET /coworkers error:", err);
207
+ res.status(500).json({ error: "Internal server error" });
208
+ }
209
+ });
195
210
  router.post("/sessions", async (req, res) => {
196
211
  const { name, agent: agentArg } = req.body;
197
212
  if (!name || typeof name !== "string" || !name.trim()) {
@@ -441,7 +456,7 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler,
441
456
  rows = await storage.listMessagesFromSender(name);
442
457
  }
443
458
  else {
444
- rows = await storage.listMessagesForRecipient(name, unread_only === "true");
459
+ rows = await storage.listMessagesForRecipient(name, unread_only === "true" ? { unread: true } : undefined);
445
460
  }
446
461
  res.json(rows);
447
462
  }
@@ -524,6 +539,39 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler,
524
539
  res.status(500).json({ error: "Internal server error" });
525
540
  }
526
541
  });
542
+ // ── Human Notification API ─────────────────────────────────────────────────
543
+ router.get("/human/unread-old", async (req, res) => {
544
+ try {
545
+ const hoursStr = req.query.hours ?? "";
546
+ const hours = hoursStr !== "" ? parseFloat(hoursStr) : 1;
547
+ const humanName = (await storage.getConfig("human_name")) ?? "Human";
548
+ const msgs = await storage.listMessagesForRecipient(humanName, {
549
+ unread: true,
550
+ olderThanHours: hours,
551
+ notified: false,
552
+ });
553
+ res.json(msgs);
554
+ }
555
+ catch (err) {
556
+ console.error("GET /human/unread-old error:", err);
557
+ res.status(500).json({ error: "Internal server error" });
558
+ }
559
+ });
560
+ router.post("/human/mark-notified", async (req, res) => {
561
+ try {
562
+ const ids = req.body.ids;
563
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "number")) {
564
+ res.status(400).json({ error: "ids must be an array of numbers" });
565
+ return;
566
+ }
567
+ await storage.markMessagesAsNotified(ids);
568
+ res.json({ success: true, count: ids.length });
569
+ }
570
+ catch (err) {
571
+ console.error("POST /human/mark-notified error:", err);
572
+ res.status(500).json({ error: "Internal server error" });
573
+ }
574
+ });
527
575
  // ── Cron Jobs Endpoints ────────────────────────────────────────────────────
528
576
  router.get("/crons", async (req, res) => {
529
577
  try {
@@ -739,135 +787,9 @@ export function createRouter(storage, agenticCodingServer, serverUrl, scheduler,
739
787
  res.status(500).json({ error: "Internal server error" });
740
788
  }
741
789
  });
742
- // ── Memory Endpoints (authenticated, for manage TUI) ───────────────────────
743
- router.get("/sessions/:name/memories", async (req, res) => {
744
- const { name } = req.params;
745
- const limit = Math.min(parseInt(req.query.limit ?? "50", 10), 200);
746
- const sessionId = await storage.getSessionIdByName(name);
747
- if (!sessionId) {
748
- res.status(404).json({ error: `Session "${name}" not found` });
749
- return;
750
- }
751
- try {
752
- const memories = memoryManager.listMemories(name, limit);
753
- const stats = memoryManager.getStats(name);
754
- res.json({ memories, total: stats.total });
755
- }
756
- catch (err) {
757
- console.error("GET /sessions/:name/memories error:", err);
758
- res.status(500).json({ error: "Internal server error" });
759
- }
760
- });
761
- router.post("/sessions/:name/memories", async (req, res) => {
762
- const { name } = req.params;
763
- const { content, metadata } = req.body;
764
- if (!content || typeof content !== "string" || !content.trim()) {
765
- res.status(400).json({ error: "content is required" });
766
- return;
767
- }
768
- const sessionId = await storage.getSessionIdByName(name);
769
- if (!sessionId) {
770
- res.status(404).json({ error: `Session "${name}" not found` });
771
- return;
772
- }
773
- try {
774
- const id = await memoryManager.addMemory(name, content.trim(), metadata ?? {});
775
- res.status(201).json({ ok: true, id });
776
- }
777
- catch (err) {
778
- console.error("POST /sessions/:name/memories error:", err);
779
- res.status(500).json({ error: "Internal server error" });
780
- }
781
- });
782
- router.get("/sessions/:name/memories/:memoryId", async (req, res) => {
783
- const { name, memoryId } = req.params;
784
- const sessionId = await storage.getSessionIdByName(name);
785
- if (!sessionId) {
786
- res.status(404).json({ error: `Session "${name}" not found` });
787
- return;
788
- }
789
- try {
790
- const memory = memoryManager.getMemory(name, memoryId);
791
- if (!memory) {
792
- res.status(404).json({ error: "Memory not found" });
793
- return;
794
- }
795
- res.json(memory);
796
- }
797
- catch (err) {
798
- console.error("GET /sessions/:name/memories/:memoryId error:", err);
799
- res.status(500).json({ error: "Internal server error" });
800
- }
801
- });
802
- router.put("/sessions/:name/memories/:memoryId", async (req, res) => {
803
- const { name, memoryId } = req.params;
804
- const { content, metadata } = req.body;
805
- if (!content || typeof content !== "string" || !content.trim()) {
806
- res.status(400).json({ error: "content is required" });
807
- return;
808
- }
809
- const sessionId = await storage.getSessionIdByName(name);
810
- if (!sessionId) {
811
- res.status(404).json({ error: `Session "${name}" not found` });
812
- return;
813
- }
814
- try {
815
- const updated = await memoryManager.updateMemory(name, memoryId, content.trim(), metadata);
816
- if (!updated) {
817
- res.status(404).json({ error: "Memory not found" });
818
- return;
819
- }
820
- res.json({ ok: true });
821
- }
822
- catch (err) {
823
- console.error("PUT /sessions/:name/memories/:memoryId error:", err);
824
- res.status(500).json({ error: "Internal server error" });
825
- }
826
- });
827
- router.delete("/sessions/:name/memories/:memoryId", async (req, res) => {
828
- const { name, memoryId } = req.params;
829
- const sessionId = await storage.getSessionIdByName(name);
830
- if (!sessionId) {
831
- res.status(404).json({ error: `Session "${name}" not found` });
832
- return;
833
- }
834
- try {
835
- const deleted = memoryManager.deleteMemory(name, memoryId);
836
- if (!deleted) {
837
- res.status(404).json({ error: "Memory not found" });
838
- return;
839
- }
840
- res.json({ deleted: true, id: memoryId });
841
- }
842
- catch (err) {
843
- console.error("DELETE /sessions/:name/memories/:memoryId error:", err);
844
- res.status(500).json({ error: "Internal server error" });
845
- }
846
- });
847
- router.post("/sessions/:name/memories/search", async (req, res) => {
848
- const { name } = req.params;
849
- const { query, limit } = req.body;
850
- if (!query || typeof query !== "string" || !query.trim()) {
851
- res.status(400).json({ error: "query is required" });
852
- return;
853
- }
854
- const sessionId = await storage.getSessionIdByName(name);
855
- if (!sessionId) {
856
- res.status(404).json({ error: `Session "${name}" not found` });
857
- return;
858
- }
859
- try {
860
- const results = await memoryManager.searchMemories(name, query.trim(), Math.min(limit ?? 10, 50));
861
- res.json(results);
862
- }
863
- catch (err) {
864
- console.error("POST /sessions/:name/memories/search error:", err);
865
- res.status(500).json({ error: "Internal server error" });
866
- }
867
- });
868
790
  return router;
869
791
  }
870
- export function createWorkerRouter(storage, agenticCodingServer, serverUrl, memoryManager) {
792
+ export function createWorkerRouter(storage, agenticCodingServer, serverUrl) {
871
793
  const router = Router();
872
794
  router.get("/worker/list-coworkers", async (req, res) => {
873
795
  const { code } = req.query;
@@ -1255,107 +1177,5 @@ export function createWorkerRouter(storage, agenticCodingServer, serverUrl, memo
1255
1177
  res.status(500).json({ error: "Internal server error" });
1256
1178
  }
1257
1179
  });
1258
- // ── Worker Memory Endpoints (authenticated via agent_code in query) ────────
1259
- router.post("/worker/memory/add", async (req, res) => {
1260
- const { code } = req.query;
1261
- const { content, metadata } = req.body;
1262
- if (!code || typeof code !== "string") {
1263
- res.status(400).json({ error: "code query parameter is required" });
1264
- return;
1265
- }
1266
- if (!content || typeof content !== "string" || !content.trim()) {
1267
- res.status(400).json({ error: "content is required" });
1268
- return;
1269
- }
1270
- const session = await storage.getSessionByAgentCode(code);
1271
- if (!session) {
1272
- res.status(401).json({ error: "Invalid agent code" });
1273
- return;
1274
- }
1275
- const sessionName = session.name;
1276
- try {
1277
- const id = await memoryManager.addMemory(sessionName, content.trim(), metadata ?? {});
1278
- res.status(201).json({ ok: true, id });
1279
- }
1280
- catch (err) {
1281
- console.error("POST /worker/memory/add error:", err);
1282
- res.status(500).json({ error: "Internal server error" });
1283
- }
1284
- });
1285
- router.post("/worker/memory/search", async (req, res) => {
1286
- const { code } = req.query;
1287
- const { query, limit } = req.body;
1288
- if (!code || typeof code !== "string") {
1289
- res.status(400).json({ error: "code query parameter is required" });
1290
- return;
1291
- }
1292
- if (!query || typeof query !== "string" || !query.trim()) {
1293
- res.status(400).json({ error: "query is required" });
1294
- return;
1295
- }
1296
- const session = await storage.getSessionByAgentCode(code);
1297
- if (!session) {
1298
- res.status(401).json({ error: "Invalid agent code" });
1299
- return;
1300
- }
1301
- const sessionName = session.name;
1302
- try {
1303
- const results = await memoryManager.searchMemories(sessionName, query.trim(), Math.min(limit ?? 10, 50));
1304
- res.json(results);
1305
- }
1306
- catch (err) {
1307
- console.error("POST /worker/memory/search error:", err);
1308
- res.status(500).json({ error: "Internal server error" });
1309
- }
1310
- });
1311
- router.get("/worker/memory/list", async (req, res) => {
1312
- const { code, limit: limitStr } = req.query;
1313
- if (!code || typeof code !== "string") {
1314
- res.status(400).json({ error: "code query parameter is required" });
1315
- return;
1316
- }
1317
- const session = await storage.getSessionByAgentCode(code);
1318
- if (!session) {
1319
- res.status(401).json({ error: "Invalid agent code" });
1320
- return;
1321
- }
1322
- const sessionName = session.name;
1323
- const limit = Math.min(parseInt(limitStr ?? "50", 10), 200);
1324
- try {
1325
- const memories = memoryManager.listMemories(sessionName, limit);
1326
- const stats = memoryManager.getStats(sessionName);
1327
- res.json({ memories, total: stats.total });
1328
- }
1329
- catch (err) {
1330
- console.error("GET /worker/memory/list error:", err);
1331
- res.status(500).json({ error: "Internal server error" });
1332
- }
1333
- });
1334
- router.delete("/worker/memory/:memoryId", async (req, res) => {
1335
- const { code } = req.query;
1336
- const { memoryId } = req.params;
1337
- if (!code || typeof code !== "string") {
1338
- res.status(400).json({ error: "code query parameter is required" });
1339
- return;
1340
- }
1341
- const session = await storage.getSessionByAgentCode(code);
1342
- if (!session) {
1343
- res.status(401).json({ error: "Invalid agent code" });
1344
- return;
1345
- }
1346
- const sessionName = session.name;
1347
- try {
1348
- const deleted = memoryManager.deleteMemory(sessionName, memoryId);
1349
- if (!deleted) {
1350
- res.status(404).json({ error: "Memory not found" });
1351
- return;
1352
- }
1353
- res.json({ deleted: true, id: memoryId });
1354
- }
1355
- catch (err) {
1356
- console.error("DELETE /worker/memory/:memoryId error:", err);
1357
- res.status(500).json({ error: "Internal server error" });
1358
- }
1359
- });
1360
1180
  return router;
1361
1181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-office",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "An office for your AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,17 +40,17 @@
40
40
  "croner": "^10.0.1",
41
41
  "dotenv": "^17.0.0",
42
42
  "express": "^5.0.0",
43
- "fastmemory": "^0.1.5",
44
43
  "ink": "^6.0.0",
45
44
  "postgres": "^3.4.0",
46
- "react": "^19.0.0"
45
+ "react": "^19.0.0",
46
+ "resend": "^6.9.2"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/better-sqlite3": "^7.6.13",
50
50
  "@types/express": "^5.0.0",
51
51
  "@types/node": "^22.0.0",
52
52
  "@types/react": "^19.0.0",
53
- "tsx": "^4.0.0",
53
+ "tsx": "^4.21.0",
54
54
  "typescript": "^5.0.0"
55
55
  }
56
56
  }
@@ -1,87 +0,0 @@
1
- import { type MemoryEntry } from "fastmemory";
2
- export interface MemoryRecord {
3
- id: string;
4
- content: string;
5
- metadata: Record<string, unknown>;
6
- createdAt: string;
7
- }
8
- export type AgentOfficeMemorySearchResult = MemoryEntry;
9
- /**
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).
56
- * Each session gets its own .db file under <memoryPath>/<sessionName>.db
57
- */
58
- export declare class AgentOfficeFastMemory extends AgentOfficeMemory {
59
- private basePath;
60
- private stores;
61
- constructor(memoryPath: string);
62
- private dbPathFor;
63
- private getStore;
64
- addMemory(sessionName: string, content: string, metadata?: Record<string, unknown>): Promise<string>;
65
- /**
66
- * Sanitize a query string for FTS5 MATCH syntax.
67
- * Wraps each whitespace-delimited token in double quotes so that
68
- * special characters (apostrophes, parens, asterisks, etc.) are
69
- * treated as literals rather than FTS5 operators.
70
- */
71
- private sanitizeFts5Query;
72
- searchMemories(sessionName: string, query: string, limit?: number): Promise<AgentOfficeMemorySearchResult[]>;
73
- listMemories(sessionName: string, limit?: number): MemoryRecord[];
74
- getMemory(sessionName: string, memoryId: string): MemoryRecord | null;
75
- deleteMemory(sessionName: string, memoryId: string): boolean;
76
- updateMemory(sessionName: string, memoryId: string, content: string, metadata?: Record<string, unknown>): Promise<boolean>;
77
- getStats(sessionName: string): {
78
- total: number;
79
- };
80
- /**
81
- * Warm up the embedding model and verify it loads correctly.
82
- * Uses a temporary DB so no real session data is affected.
83
- * Throws if the model is corrupt or fails to produce embeddings.
84
- */
85
- warmup(): Promise<void>;
86
- closeAll(): void;
87
- }