agent-office 0.3.2 → 0.4.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/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 +7 -1
- package/dist/db/postgresql-storage.js +39 -14
- package/dist/db/sqlite-storage.d.ts +7 -1
- package/dist/db/sqlite-storage.js +53 -12
- package/dist/db/storage-base.d.ts +7 -1
- package/dist/db/storage-base.js +1 -1
- package/dist/db/storage.d.ts +7 -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 +57 -253
- package/package.json +4 -4
- package/dist/server/memory.d.ts +0 -87
- package/dist/server/memory.js +0 -348
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# agent-office
|
|
2
2
|
|
|
3
|
-
An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) coding sessions as named coworkers with inter-agent messaging, scheduled tasks,
|
|
3
|
+
An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) coding sessions as named coworkers with inter-agent messaging, scheduled tasks, and a terminal UI for human oversight.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`agent-office` sits between a human operator and one or more AI coding agents running on an OpenCode server. It wraps raw OpenCode sessions with named identities, a messaging system, cron-based scheduling
|
|
7
|
+
`agent-office` sits between a human operator and one or more AI coding agents running on an OpenCode server. It wraps raw OpenCode sessions with named identities, a messaging system, and cron-based scheduling -- creating an "office" where AI agents are coworkers that communicate, take direction, and operate autonomously.
|
|
8
8
|
|
|
9
9
|
**For humans**, there are three interfaces:
|
|
10
10
|
|
|
11
|
-
- **`serve`** -- an HTTP server that manages sessions, messages, cron jobs,
|
|
11
|
+
- **`serve`** -- an HTTP server that manages sessions, messages, and cron jobs, backed by PostgreSQL and a running OpenCode server
|
|
12
12
|
- **`manage`** -- a full-screen terminal UI (React Ink) for creating coworkers, sending messages, browsing mail, managing cron jobs, and observing agent activity
|
|
13
13
|
- **`communicator web`** -- a browser-based chat interface for conversing with a specific agent in real time
|
|
14
14
|
|
|
15
15
|
**For AI agents**, there is a CLI:
|
|
16
16
|
|
|
17
|
-
- **`worker`** -- subcommands that agents invoke from within their OpenCode sessions to clock in, message coworkers, set status, manage cron jobs
|
|
17
|
+
- **`worker`** -- subcommands that agents invoke from within their OpenCode sessions to clock in, message coworkers, set status, and manage cron jobs
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
+---------------------+
|
|
@@ -27,10 +27,10 @@ An office for your AI agents. Manage multiple [OpenCode](https://opencode.ai) co
|
|
|
27
27
|
| Ink/React | | :7654 | | |
|
|
28
28
|
+--------------+ | | +--------------+
|
|
29
29
|
| CronScheduler |
|
|
30
|
-
+--------------+ |
|
|
31
|
-
| Communicator |<-->|
|
|
32
|
-
| Web (HTMX) | | |
|
|
33
|
-
| :7655 | +---------+-----------+
|
|
30
|
+
+--------------+ | |
|
|
31
|
+
| Communicator |<-->| |
|
|
32
|
+
| Web (HTMX) | | |
|
|
33
|
+
| :7655 | +---------+-----------+
|
|
34
34
|
+--------------+ |
|
|
35
35
|
| /worker/* endpoints
|
|
36
36
|
+---------+-----------+
|
|
@@ -69,7 +69,7 @@ agent-office serve \
|
|
|
69
69
|
--password mysecret
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
The server runs migrations automatically on startup
|
|
72
|
+
The server runs migrations automatically on startup and initializes the cron scheduler.
|
|
73
73
|
|
|
74
74
|
### 2. Open the manager
|
|
75
75
|
|
|
@@ -97,11 +97,11 @@ Opens a web-based chat interface at `http://127.0.0.1:7655` for real-time conver
|
|
|
97
97
|
|
|
98
98
|
2. **Clock In** -- The AI agent sees the enrollment message in its session, runs `agent-office worker clock-in <token>`, and receives a welcome briefing with its name, the human manager's identity, all available CLI commands, and a privacy notice.
|
|
99
99
|
|
|
100
|
-
3. **Work**
|
|
100
|
+
3. **Work** - - The agent operates in its OpenCode session. It communicates with the human and other agents by running `agent-office worker send-message`. It can set its status and create cron jobs.
|
|
101
101
|
|
|
102
102
|
4. **Message Delivery** -- Messages are stored in PostgreSQL and simultaneously injected as prompts into the recipient's OpenCode session. This means agents see messages immediately in their active session context.
|
|
103
103
|
|
|
104
|
-
5. **Reset**
|
|
104
|
+
5. **Reset** - - A session can be reverted to its initial state (clearing all conversation history) and re-enrolled.
|
|
105
105
|
|
|
106
106
|
6. **Delete** -- Deleting a coworker removes both the OpenCode session and the database record.
|
|
107
107
|
|
|
@@ -139,7 +139,7 @@ cp .env.example .env
|
|
|
139
139
|
|
|
140
140
|
### `agent-office serve`
|
|
141
141
|
|
|
142
|
-
Starts the HTTP server. Connects to PostgreSQL, runs migrations, initializes the cron scheduler
|
|
142
|
+
Starts the HTTP server. Connects to PostgreSQL, runs migrations, and initializes the cron scheduler.
|
|
143
143
|
|
|
144
144
|
```
|
|
145
145
|
Options:
|
|
@@ -147,7 +147,6 @@ Options:
|
|
|
147
147
|
--opencode-url <url> OpenCode server URL (default: http://localhost:4096)
|
|
148
148
|
--host <host> Bind host (default: 127.0.0.1)
|
|
149
149
|
--port <port> Bind port (default: 7654)
|
|
150
|
-
--memory-path <path> Directory for memory storage (default: ./.memory)
|
|
151
150
|
--password <password> REQUIRED. API password (env: AGENT_OFFICE_PASSWORD)
|
|
152
151
|
```
|
|
153
152
|
|
|
@@ -165,7 +164,7 @@ Options:
|
|
|
165
164
|
|
|
166
165
|
| Screen | Description |
|
|
167
166
|
|---|---|
|
|
168
|
-
| **Coworkers** | Table of all agents showing name, status, mode, session ID, and masked agent code. Keyboard: `c` create, `d` delete, `r` reveal code, `g` regenerate code, `x` revert session, `X` revert all, `t` tail messages, `i` inject text, `m` coworker mail
|
|
167
|
+
| **Coworkers** | Table of all agents showing name, status, mode, session ID, and masked agent code. Keyboard: `c` create, `d` delete, `r` reveal code, `g` regenerate code, `x` revert session, `X` revert all, `t` tail messages, `i` inject text, `m` coworker mail |
|
|
169
168
|
| **Send message** | Select a recipient and compose a message |
|
|
170
169
|
| **My mail** | View received and sent messages. `r` reply, `m` mark read, `a` mark all read. Tab between received/sent |
|
|
171
170
|
| **Cron jobs** | Table of scheduled tasks with name, coworker, schedule, next run, and status. Create, delete, enable/disable, view history |
|
|
@@ -217,11 +216,6 @@ agent-office worker cron enable <token> <id>
|
|
|
217
216
|
agent-office worker cron disable <token> <id>
|
|
218
217
|
agent-office worker cron history <token> <id>
|
|
219
218
|
|
|
220
|
-
# Persistent memory (survives session resets)
|
|
221
|
-
agent-office worker memory add --content "The auth module uses JWT with RS256" <token>
|
|
222
|
-
agent-office worker memory search --query "authentication" <token>
|
|
223
|
-
agent-office worker memory list <token>
|
|
224
|
-
agent-office worker memory forget <token> <memory-id>
|
|
225
219
|
```
|
|
226
220
|
|
|
227
221
|
## REST API
|
|
@@ -251,12 +245,7 @@ agent-office worker memory forget <token> <memory-id>
|
|
|
251
245
|
| `POST` | `/crons/:id/enable` | Enable a cron job |
|
|
252
246
|
| `POST` | `/crons/:id/disable` | Disable a cron job |
|
|
253
247
|
| `GET` | `/crons/:id/history` | Cron execution history. Query: `?limit=N` |
|
|
254
|
-
|
|
255
|
-
| `POST` | `/sessions/:name/memories` | Add a memory. Body: `{ content, metadata? }` |
|
|
256
|
-
| `GET` | `/sessions/:name/memories/:id` | Get a memory |
|
|
257
|
-
| `PUT` | `/sessions/:name/memories/:id` | Update a memory. Body: `{ content, metadata? }` |
|
|
258
|
-
| `DELETE` | `/sessions/:name/memories/:id` | Delete a memory |
|
|
259
|
-
| `POST` | `/sessions/:name/memories/search` | Search memories. Body: `{ query, limit? }` |
|
|
248
|
+
|
|
260
249
|
|
|
261
250
|
### Worker Endpoints (agent code auth via `?code=<uuid>`)
|
|
262
251
|
|
|
@@ -272,10 +261,7 @@ agent-office worker memory forget <token> <memory-id>
|
|
|
272
261
|
| `POST` | `/worker/crons/:id/enable` | Enable own cron job |
|
|
273
262
|
| `POST` | `/worker/crons/:id/disable` | Disable own cron job |
|
|
274
263
|
| `GET` | `/worker/crons/:id/history` | View own cron job history |
|
|
275
|
-
|
|
276
|
-
| `POST` | `/worker/memory/search` | Search memories |
|
|
277
|
-
| `GET` | `/worker/memory/list` | List memories |
|
|
278
|
-
| `DELETE` | `/worker/memory/:memoryId` | Delete a memory |
|
|
264
|
+
|
|
279
265
|
|
|
280
266
|
## Architecture
|
|
281
267
|
|
|
@@ -289,14 +275,6 @@ The server uses PostgreSQL with automatic migrations. Tables:
|
|
|
289
275
|
- **`cron_jobs`** -- Scheduled tasks tied to sessions with cron expressions and timezone support
|
|
290
276
|
- **`cron_history`** -- Execution log for cron jobs with success/failure tracking
|
|
291
277
|
|
|
292
|
-
### Memory System
|
|
293
|
-
|
|
294
|
-
Each agent gets a private SQLite database (via [fastmemory](https://github.com/nichochar/fastmemory)) stored in the `--memory-path` directory. Memories support:
|
|
295
|
-
|
|
296
|
-
- **Hybrid search** -- BM25 full-text search combined with vector semantic search using Reciprocal Rank Fusion
|
|
297
|
-
- **Persistence across resets** -- Memories are stored outside the OpenCode session, so they survive session reverts
|
|
298
|
-
- **Quantized embeddings** -- Uses `q4` dtype for efficient storage
|
|
299
|
-
|
|
300
278
|
### Agentic Coding Server Abstraction
|
|
301
279
|
|
|
302
280
|
The application does not depend on the OpenCode SDK directly. Instead, all OpenCode interactions go through an `AgenticCodingServer` interface (`src/lib/agentic-coding-server.ts`) with six methods:
|
package/dist/cli.js
CHANGED
|
@@ -13,10 +13,8 @@ program
|
|
|
13
13
|
.option("--sqlite <path>", "SQLite database file path (alternative to PostgreSQL)", process.env.AGENT_OFFICE_SQLITE)
|
|
14
14
|
.option("--host <host>", "Host to bind to", "127.0.0.1")
|
|
15
15
|
.option("--port <port>", "Port to serve on", "7654")
|
|
16
|
-
.option("--memory-path <path>", "Directory for memory storage (default: ./.memory)", "./.memory")
|
|
17
16
|
.option("--password <password>", "REQUIRED. API password", process.env.AGENT_OFFICE_PASSWORD)
|
|
18
17
|
.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)")
|
|
20
18
|
.action(async (options) => {
|
|
21
19
|
const { serve } = await import("./commands/serve.js");
|
|
22
20
|
await serve(options);
|
|
@@ -34,8 +32,8 @@ const appCmd = program
|
|
|
34
32
|
.command("app")
|
|
35
33
|
.description("[HUMAN ONLY] Interactive visual applications");
|
|
36
34
|
appCmd
|
|
37
|
-
.command("
|
|
38
|
-
.description("
|
|
35
|
+
.command("chat")
|
|
36
|
+
.description("Web chat interface for human to chat to coworkers")
|
|
39
37
|
.option("--url <url>", "URL of the agent-office serve endpoint (e.g. http://127.0.0.1:7654)", process.env.AGENT_OFFICE_URL ?? "http://127.0.0.1:7654")
|
|
40
38
|
.option("--password <password>", "API password for the agent-office server", process.env.AGENT_OFFICE_PASSWORD ?? "secret")
|
|
41
39
|
.option("--host <host>", "Host to bind the web server to", "127.0.0.1")
|
|
@@ -44,9 +42,8 @@ appCmd
|
|
|
44
42
|
const { appCoworkerChatWeb } = await import("./commands/communicator.js");
|
|
45
43
|
await appCoworkerChatWeb(options);
|
|
46
44
|
});
|
|
47
|
-
appCmd
|
|
48
|
-
.
|
|
49
|
-
.description("[HUMAN ONLY] Launch a visualization of recent mail activity (live screensaver)")
|
|
45
|
+
appCmd.command("screensaver")
|
|
46
|
+
.description("3D visualization of recent mail activity")
|
|
50
47
|
.option("--url <url>", "URL of the agent-office serve endpoint (e.g. http://127.0.0.1:7654)", process.env.AGENT_OFFICE_URL ?? "http://127.0.0.1:7654")
|
|
51
48
|
.option("--password <password>", "API password for the agent-office server", process.env.AGENT_OFFICE_PASSWORD ?? "secret")
|
|
52
49
|
.option("--host <host>", "Host to bind the screensaver web server to", "127.0.0.1")
|
|
@@ -55,6 +52,19 @@ appCmd
|
|
|
55
52
|
const { appScreensaver } = await import("./commands/screensaver.js");
|
|
56
53
|
await appScreensaver(options);
|
|
57
54
|
});
|
|
55
|
+
appCmd
|
|
56
|
+
.command('notifier')
|
|
57
|
+
.description('Notify human by email when unread messages have been waiting over certain amount of time')
|
|
58
|
+
.option('--agent-office-url <url>', 'Agent Office server URL', process.env.AGENT_OFFICE_URL ?? 'http://127.0.0.1:7654')
|
|
59
|
+
.option('--password <pw>', 'API password', process.env.AGENT_OFFICE_PASSWORD)
|
|
60
|
+
.option('--to-email <email>', 'Recipient email address', process.env.TO_EMAIL)
|
|
61
|
+
.option('--resend-api-key <key>', 'Resend API key', process.env.RESEND_API_KEY)
|
|
62
|
+
.option('--domain <domain>', 'Sender domain (e.g. coworker.innercontext.com)', process.env.EMAIL_DOMAIN)
|
|
63
|
+
.option('--wait-minutes <minutes>', 'Minutes a message must be unread before notifying', '15')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
const { notifier } = await import('./commands/notifier.js');
|
|
66
|
+
await notifier(options);
|
|
67
|
+
});
|
|
58
68
|
const workerCmd = program
|
|
59
69
|
.command("worker")
|
|
60
70
|
.description("Worker agent commands");
|
|
@@ -178,47 +188,4 @@ cronCmd
|
|
|
178
188
|
const { cronHistory } = await import("./commands/worker.js");
|
|
179
189
|
await cronHistory(token, cronId);
|
|
180
190
|
});
|
|
181
|
-
// ── Worker Memory Commands (nested) ──────────────────────────────────────────
|
|
182
|
-
const memoryCmd = workerCmd
|
|
183
|
-
.command("memory")
|
|
184
|
-
.description("Manage your persistent memories");
|
|
185
|
-
memoryCmd
|
|
186
|
-
.command("add")
|
|
187
|
-
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
188
|
-
.description("Add a new memory")
|
|
189
|
-
.requiredOption("--content <content>", "Memory content to store")
|
|
190
|
-
.action(async (token, options) => {
|
|
191
|
-
const { memoryAdd } = await import("./commands/worker.js");
|
|
192
|
-
await memoryAdd(token, options.content);
|
|
193
|
-
});
|
|
194
|
-
memoryCmd
|
|
195
|
-
.command("search")
|
|
196
|
-
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
197
|
-
.description("Search memories using hybrid search (keyword + semantic)")
|
|
198
|
-
.requiredOption("--query <query>", "Search query")
|
|
199
|
-
.option("--limit <limit>", "Maximum results (default 10)", "10")
|
|
200
|
-
.action(async (token, options) => {
|
|
201
|
-
const limit = parseInt(options.limit ?? "10", 10);
|
|
202
|
-
const { memorySearch } = await import("./commands/worker.js");
|
|
203
|
-
await memorySearch(token, options.query, limit);
|
|
204
|
-
});
|
|
205
|
-
memoryCmd
|
|
206
|
-
.command("list")
|
|
207
|
-
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
208
|
-
.description("List all stored memories")
|
|
209
|
-
.option("--limit <limit>", "Maximum memories to list (default 50)", "50")
|
|
210
|
-
.action(async (token, options) => {
|
|
211
|
-
const limit = parseInt(options.limit ?? "50", 10);
|
|
212
|
-
const { memoryList } = await import("./commands/worker.js");
|
|
213
|
-
await memoryList(token, limit);
|
|
214
|
-
});
|
|
215
|
-
memoryCmd
|
|
216
|
-
.command("forget")
|
|
217
|
-
.argument("<token>", "Agent token in the format <agent_code>@<server-url>")
|
|
218
|
-
.argument("<memoryId>", "ID of the memory to forget")
|
|
219
|
-
.description("Delete a memory by ID")
|
|
220
|
-
.action(async (token, memoryId) => {
|
|
221
|
-
const { memoryForget } = await import("./commands/worker.js");
|
|
222
|
-
await memoryForget(token, memoryId);
|
|
223
|
-
});
|
|
224
191
|
program.parse();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentOfficeNotifier } from "../lib/notifier.js";
|
|
2
|
+
type Options = {
|
|
3
|
+
agentOfficeUrl?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
toEmail?: string;
|
|
6
|
+
resendApiKey?: string;
|
|
7
|
+
domain?: string;
|
|
8
|
+
waitMinutes?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function notifier(options: Options, customNotifier?: AgentOfficeNotifier): Promise<void>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Cron } from "croner";
|
|
2
|
+
import { ResendNotifier } from "../lib/notifier.js";
|
|
3
|
+
export async function notifier(options, customNotifier) {
|
|
4
|
+
const toEmail = options.toEmail;
|
|
5
|
+
if (!toEmail) {
|
|
6
|
+
console.error("Error: --to-email or TO_EMAIL env required");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const domain = options.domain;
|
|
10
|
+
if (!domain) {
|
|
11
|
+
console.error("Error: --domain or EMAIL_DOMAIN env required");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const agentOfficeUrl = options.agentOfficeUrl ?? "http://127.0.0.1:7654";
|
|
15
|
+
const password = options.password;
|
|
16
|
+
if (!password) {
|
|
17
|
+
console.error("Error: --password or AGENT_OFFICE_PASSWORD env required");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const waitMinutes = parseInt(options.waitMinutes ?? "15", 10);
|
|
21
|
+
const waitHours = waitMinutes / 60;
|
|
22
|
+
let notify;
|
|
23
|
+
if (customNotifier) {
|
|
24
|
+
notify = customNotifier;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const resendApiKey = options.resendApiKey;
|
|
28
|
+
if (!resendApiKey) {
|
|
29
|
+
console.error("Error: --resend-api-key or RESEND_API_KEY env required");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
notify = new ResendNotifier(resendApiKey);
|
|
33
|
+
}
|
|
34
|
+
const authHeaders = {
|
|
35
|
+
Authorization: `Bearer ${password}`,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
};
|
|
38
|
+
const check = async () => {
|
|
39
|
+
try {
|
|
40
|
+
// Fetch messages old enough to notify about
|
|
41
|
+
const resp = await fetch(`${agentOfficeUrl}/human/unread-old?hours=${waitHours}`, {
|
|
42
|
+
headers: authHeaders,
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok) {
|
|
45
|
+
console.error(`GET /human/unread-old failed: ${resp.status}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const qualifying = (await resp.json());
|
|
49
|
+
if (qualifying.length === 0) {
|
|
50
|
+
// Check if there are any unread messages at all (just not old enough yet)
|
|
51
|
+
const allResp = await fetch(`${agentOfficeUrl}/human/unread-old?hours=0`, {
|
|
52
|
+
headers: authHeaders,
|
|
53
|
+
});
|
|
54
|
+
if (allResp.ok) {
|
|
55
|
+
const allUnread = (await allResp.json());
|
|
56
|
+
if (allUnread.length > 0) {
|
|
57
|
+
console.log(`${allUnread.length} unread message(s) exist but haven't been waiting >${waitMinutes}m yet — skipping notification`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log("No unread messages — nothing to notify");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const senders = [...new Set(qualifying.map((m) => m.from_name))];
|
|
66
|
+
for (const sender of senders) {
|
|
67
|
+
const fromAddress = `${sender} <${sender.replace(/\s+/g, "+")}@${domain}>`;
|
|
68
|
+
await notify.send({
|
|
69
|
+
from: fromAddress,
|
|
70
|
+
to: toEmail,
|
|
71
|
+
subject: "Agent Office: Message Waiting",
|
|
72
|
+
text: "There is a message waiting for you at Agent Office.",
|
|
73
|
+
});
|
|
74
|
+
console.log(`Sent out notification for waiting mail | from: ${fromAddress} | to: ${toEmail}`);
|
|
75
|
+
}
|
|
76
|
+
const ids = qualifying.map((m) => m.id);
|
|
77
|
+
const markResp = await fetch(`${agentOfficeUrl}/human/mark-notified`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: authHeaders,
|
|
80
|
+
body: JSON.stringify({ ids }),
|
|
81
|
+
});
|
|
82
|
+
if (!markResp.ok) {
|
|
83
|
+
console.error(`POST /human/mark-notified failed: ${markResp.status}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
console.error("Notifier cron error:", e);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const cron = new Cron("0 * * * *", check);
|
|
91
|
+
console.log(`Agent Office notifier started. Notifying for messages unread >${waitMinutes}m. Checking ${agentOfficeUrl} every hour. ^C to stop.`);
|
|
92
|
+
await check();
|
|
93
|
+
const shutdown = async () => {
|
|
94
|
+
console.log("\nShutting down...");
|
|
95
|
+
cron.stop();
|
|
96
|
+
process.exit(0);
|
|
97
|
+
};
|
|
98
|
+
process.on("SIGINT", shutdown);
|
|
99
|
+
process.on("SIGTERM", shutdown);
|
|
100
|
+
}
|
package/dist/commands/serve.d.ts
CHANGED
package/dist/commands/serve.js
CHANGED
|
@@ -3,7 +3,6 @@ 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 { AgentOfficeFastMemory, AgentOfficeSimpleMemory } from "../server/memory.js";
|
|
7
6
|
export async function serve(options) {
|
|
8
7
|
const password = options.password;
|
|
9
8
|
if (!password) {
|
|
@@ -44,33 +43,10 @@ export async function serve(options) {
|
|
|
44
43
|
const agenticCodingServer = new OpenCodeCodingServer(options.opencodeUrl);
|
|
45
44
|
console.log(`Connecting to OpenCode server at ${options.opencodeUrl}...`);
|
|
46
45
|
const serverUrl = `http://${options.host}:${port}`;
|
|
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);
|
|
52
|
-
await memoryManager.warmup();
|
|
53
|
-
}
|
|
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
|
-
}
|
|
69
|
-
}
|
|
70
46
|
// Create cron scheduler
|
|
71
47
|
const cronScheduler = new CronScheduler();
|
|
72
48
|
// Create Express app
|
|
73
|
-
const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler
|
|
49
|
+
const app = createApp(storage, agenticCodingServer, password, serverUrl, cronScheduler);
|
|
74
50
|
// Start cron scheduler
|
|
75
51
|
await cronScheduler.start(storage, agenticCodingServer, serverUrl);
|
|
76
52
|
// Start server
|
|
@@ -82,7 +58,6 @@ export async function serve(options) {
|
|
|
82
58
|
console.log("\nShutting down...");
|
|
83
59
|
server.close(async () => {
|
|
84
60
|
cronScheduler.stop();
|
|
85
|
-
memoryManager.closeAll();
|
|
86
61
|
await storage.close();
|
|
87
62
|
console.log("Goodbye.");
|
|
88
63
|
process.exit(0);
|
|
@@ -13,7 +13,3 @@ export declare function deleteCron(token: string, cronId: number): Promise<void>
|
|
|
13
13
|
export declare function enableCron(token: string, cronId: number): Promise<void>;
|
|
14
14
|
export declare function disableCron(token: string, cronId: number): Promise<void>;
|
|
15
15
|
export declare function cronHistory(token: string, cronId: number): Promise<void>;
|
|
16
|
-
export declare function memoryAdd(token: string, content: string): Promise<void>;
|
|
17
|
-
export declare function memorySearch(token: string, query: string, limit: number): Promise<void>;
|
|
18
|
-
export declare function memoryList(token: string, limit: number): Promise<void>;
|
|
19
|
-
export declare function memoryForget(token: string, memoryId: string): Promise<void>;
|
package/dist/commands/worker.js
CHANGED
|
@@ -139,66 +139,3 @@ export async function cronHistory(token, cronId) {
|
|
|
139
139
|
const history = await fetchWorker(token, `/worker/crons/${cronId}/history`);
|
|
140
140
|
console.log(JSON.stringify(history, null, 2));
|
|
141
141
|
}
|
|
142
|
-
// ── Memory Commands ──────────────────────────────────────────────────────────
|
|
143
|
-
export async function memoryAdd(token, content) {
|
|
144
|
-
const result = await postWorker(token, "/worker/memory/add", { content });
|
|
145
|
-
console.log(JSON.stringify(result, null, 2));
|
|
146
|
-
}
|
|
147
|
-
export async function memorySearch(token, query, limit) {
|
|
148
|
-
const result = await postWorker(token, "/worker/memory/search", { query, limit });
|
|
149
|
-
console.log(JSON.stringify(result, null, 2));
|
|
150
|
-
}
|
|
151
|
-
export async function memoryList(token, limit) {
|
|
152
|
-
const { agentCode, serverUrl } = parseToken(token);
|
|
153
|
-
const url = `${serverUrl}/worker/memory/list?code=${encodeURIComponent(agentCode)}&limit=${limit}`;
|
|
154
|
-
let res;
|
|
155
|
-
try {
|
|
156
|
-
res = await fetch(url);
|
|
157
|
-
}
|
|
158
|
-
catch (err) {
|
|
159
|
-
console.error(`Error: could not reach ${serverUrl}`);
|
|
160
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
let body;
|
|
164
|
-
try {
|
|
165
|
-
body = await res.json();
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
console.error(`Error: invalid response from server`);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
if (!res.ok) {
|
|
172
|
-
const msg = body.error ?? `HTTP ${res.status}`;
|
|
173
|
-
console.error(`Error: ${msg}`);
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
console.log(JSON.stringify(body, null, 2));
|
|
177
|
-
}
|
|
178
|
-
export async function memoryForget(token, memoryId) {
|
|
179
|
-
const { agentCode, serverUrl } = parseToken(token);
|
|
180
|
-
const url = `${serverUrl}/worker/memory/${encodeURIComponent(memoryId)}?code=${encodeURIComponent(agentCode)}`;
|
|
181
|
-
let res;
|
|
182
|
-
try {
|
|
183
|
-
res = await fetch(url, { method: "DELETE" });
|
|
184
|
-
}
|
|
185
|
-
catch (err) {
|
|
186
|
-
console.error(`Error: could not reach ${serverUrl}`);
|
|
187
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
let body;
|
|
191
|
-
try {
|
|
192
|
-
body = await res.json();
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
console.error(`Error: invalid response from server`);
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
if (!res.ok) {
|
|
199
|
-
const msg = body.error ?? `HTTP ${res.status}`;
|
|
200
|
-
console.error(`Error: ${msg}`);
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
console.log(JSON.stringify(body, null, 2));
|
|
204
|
-
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -19,12 +19,18 @@ export declare class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase
|
|
|
19
19
|
getAllConfig(): Promise<ConfigRow[]>;
|
|
20
20
|
getConfig(key: string): Promise<string | null>;
|
|
21
21
|
setConfig(key: string, value: string): Promise<void>;
|
|
22
|
-
listMessagesForRecipient(name: string,
|
|
22
|
+
listMessagesForRecipient(name: string, filters?: {
|
|
23
|
+
unread?: boolean;
|
|
24
|
+
olderThanHours?: number;
|
|
25
|
+
notified?: boolean;
|
|
26
|
+
}): Promise<MessageRow[]>;
|
|
23
27
|
listMessagesFromSender(name: string): Promise<MessageRow[]>;
|
|
24
28
|
countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
|
|
29
|
+
lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
|
|
25
30
|
createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
|
|
26
31
|
markMessageAsRead(id: number): Promise<MessageRow | null>;
|
|
27
32
|
markMessageAsInjected(id: number): Promise<void>;
|
|
33
|
+
markMessagesAsNotified(ids: number[]): Promise<void>;
|
|
28
34
|
listCronJobs(): Promise<CronJobRow[]>;
|
|
29
35
|
listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
|
|
30
36
|
getCronJobById(id: number): Promise<CronJobRow | null>;
|
|
@@ -107,21 +107,19 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
|
|
|
107
107
|
`;
|
|
108
108
|
}
|
|
109
109
|
// Messages
|
|
110
|
-
async listMessagesForRecipient(name,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
async listMessagesForRecipient(name, filters) {
|
|
111
|
+
const whereClauses = ["to_name = $1"];
|
|
112
|
+
const params = [name];
|
|
113
|
+
if (filters?.unread)
|
|
114
|
+
whereClauses.push("read = FALSE");
|
|
115
|
+
if (filters?.notified === false)
|
|
116
|
+
whereClauses.push("notified = FALSE");
|
|
117
|
+
if (filters?.olderThanHours !== undefined) {
|
|
118
|
+
whereClauses.push(`created_at < NOW() - INTERVAL '${filters.olderThanHours} hours'`);
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
WHERE to_name = ${name}
|
|
123
|
-
ORDER BY created_at DESC
|
|
124
|
-
`;
|
|
120
|
+
const whereSQL = whereClauses.join(" AND ");
|
|
121
|
+
const rows = await this.sql.unsafe(`SELECT id, from_name, to_name, body, read, injected, created_at, notified FROM messages WHERE ${whereSQL} ORDER BY created_at DESC`, params);
|
|
122
|
+
return rows;
|
|
125
123
|
}
|
|
126
124
|
async listMessagesFromSender(name) {
|
|
127
125
|
return this.sql `
|
|
@@ -144,6 +142,21 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
|
|
|
144
142
|
}
|
|
145
143
|
return result;
|
|
146
144
|
}
|
|
145
|
+
async lastMessageAtByCoworker(humanName) {
|
|
146
|
+
const rows = await this.sql `
|
|
147
|
+
SELECT
|
|
148
|
+
CASE WHEN from_name = ${humanName} THEN to_name ELSE from_name END AS coworker,
|
|
149
|
+
MAX(created_at) AS last_at
|
|
150
|
+
FROM messages
|
|
151
|
+
WHERE from_name = ${humanName} OR to_name = ${humanName}
|
|
152
|
+
GROUP BY coworker
|
|
153
|
+
`;
|
|
154
|
+
const result = new Map();
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
result.set(row.coworker, row.last_at);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
147
160
|
async createMessageImpl(from, to, body) {
|
|
148
161
|
const [row] = await this.sql `
|
|
149
162
|
INSERT INTO messages (from_name, to_name, body)
|
|
@@ -162,6 +175,11 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
|
|
|
162
175
|
async markMessageAsInjected(id) {
|
|
163
176
|
await this.sql `UPDATE messages SET injected = TRUE WHERE id = ${id}`;
|
|
164
177
|
}
|
|
178
|
+
async markMessagesAsNotified(ids) {
|
|
179
|
+
if (ids.length === 0)
|
|
180
|
+
return;
|
|
181
|
+
await this.sql `UPDATE messages SET notified = TRUE WHERE id = ANY(${ids})`;
|
|
182
|
+
}
|
|
165
183
|
// Cron Jobs
|
|
166
184
|
async listCronJobs() {
|
|
167
185
|
return this.sql `
|
|
@@ -343,6 +361,13 @@ export class AgentOfficePostgresqlStorage extends AgentOfficeStorageBase {
|
|
|
343
361
|
name: "rename_mode_to_agent",
|
|
344
362
|
sql: `
|
|
345
363
|
ALTER TABLE sessions RENAME COLUMN mode TO agent;
|
|
364
|
+
`,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
version: 9,
|
|
368
|
+
name: "add_notified_to_messages",
|
|
369
|
+
sql: `
|
|
370
|
+
ALTER TABLE messages ADD COLUMN IF NOT EXISTS notified BOOLEAN NOT NULL DEFAULT FALSE;
|
|
346
371
|
`,
|
|
347
372
|
},
|
|
348
373
|
];
|
|
@@ -20,12 +20,18 @@ export declare class AgentOfficeSqliteStorage extends AgentOfficeStorageBase {
|
|
|
20
20
|
getAllConfig(): Promise<ConfigRow[]>;
|
|
21
21
|
getConfig(key: string): Promise<string | null>;
|
|
22
22
|
setConfig(key: string, value: string): Promise<void>;
|
|
23
|
-
listMessagesForRecipient(name: string,
|
|
23
|
+
listMessagesForRecipient(name: string, filters?: {
|
|
24
|
+
unread?: boolean;
|
|
25
|
+
olderThanHours?: number;
|
|
26
|
+
notified?: boolean;
|
|
27
|
+
}): Promise<MessageRow[]>;
|
|
24
28
|
listMessagesFromSender(name: string): Promise<MessageRow[]>;
|
|
25
29
|
countUnreadBySender(recipientName: string): Promise<Map<string, number>>;
|
|
30
|
+
lastMessageAtByCoworker(humanName: string): Promise<Map<string, Date>>;
|
|
26
31
|
createMessageImpl(from: string, to: string, body: string): Promise<MessageRow>;
|
|
27
32
|
markMessageAsRead(id: number): Promise<MessageRow | null>;
|
|
28
33
|
markMessageAsInjected(id: number): Promise<void>;
|
|
34
|
+
markMessagesAsNotified(ids: number[]): Promise<void>;
|
|
29
35
|
listCronJobs(): Promise<CronJobRow[]>;
|
|
30
36
|
listCronJobsForSession(sessionName: string): Promise<CronJobRow[]>;
|
|
31
37
|
getCronJobById(id: number): Promise<CronJobRow | null>;
|