@desplega.ai/agent-swarm 1.2.1 → 1.9.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/.claude/settings.local.json +20 -1
- package/.env.docker.example +22 -1
- package/.env.example +17 -0
- package/.github/workflows/docker-publish.yml +92 -0
- package/CONTRIBUTING.md +270 -0
- package/DEPLOYMENT.md +391 -0
- package/Dockerfile.worker +29 -1
- package/FAQ.md +19 -0
- package/LICENSE +21 -0
- package/MCP.md +249 -0
- package/README.md +103 -207
- package/assets/agent-swarm-logo-orange.png +0 -0
- package/assets/agent-swarm-logo.png +0 -0
- package/docker-compose.example.yml +137 -0
- package/docker-entrypoint.sh +223 -7
- package/package.json +8 -3
- package/{cc-plugin → plugin}/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/codebase-analyzer.md +143 -0
- package/plugin/agents/codebase-locator.md +122 -0
- package/plugin/agents/codebase-pattern-finder.md +227 -0
- package/plugin/agents/web-search-researcher.md +109 -0
- package/plugin/commands/create-plan.md +415 -0
- package/plugin/commands/implement-plan.md +89 -0
- package/plugin/commands/research.md +200 -0
- package/plugin/commands/start-leader.md +101 -0
- package/plugin/commands/start-worker.md +56 -0
- package/plugin/commands/swarm-chat.md +78 -0
- package/plugin/commands/todos.md +66 -0
- package/plugin/commands/work-on-task.md +44 -0
- package/plugin/skills/.gitkeep +0 -0
- package/scripts/generate-mcp-docs.ts +415 -0
- package/slack-manifest.json +69 -0
- package/src/be/db.ts +1431 -25
- package/src/cli.tsx +135 -11
- package/src/commands/lead.ts +13 -0
- package/src/commands/runner.ts +255 -0
- package/src/commands/worker.ts +8 -220
- package/src/hooks/hook.ts +102 -14
- package/src/http.ts +361 -5
- package/src/prompts/base-prompt.ts +131 -0
- package/src/server.ts +56 -0
- package/src/slack/app.ts +73 -0
- package/src/slack/commands.ts +88 -0
- package/src/slack/handlers.ts +281 -0
- package/src/slack/index.ts +3 -0
- package/src/slack/responses.ts +175 -0
- package/src/slack/router.ts +170 -0
- package/src/slack/types.ts +20 -0
- package/src/slack/watcher.ts +119 -0
- package/src/tools/create-channel.ts +80 -0
- package/src/tools/get-tasks.ts +54 -21
- package/src/tools/join-swarm.ts +28 -4
- package/src/tools/list-channels.ts +37 -0
- package/src/tools/list-services.ts +110 -0
- package/src/tools/poll-task.ts +46 -3
- package/src/tools/post-message.ts +87 -0
- package/src/tools/read-messages.ts +192 -0
- package/src/tools/register-service.ts +118 -0
- package/src/tools/send-task.ts +80 -7
- package/src/tools/store-progress.ts +9 -3
- package/src/tools/task-action.ts +211 -0
- package/src/tools/unregister-service.ts +110 -0
- package/src/tools/update-profile.ts +105 -0
- package/src/tools/update-service-status.ts +118 -0
- package/src/types.ts +110 -3
- package/src/utils/pretty-print.ts +224 -0
- package/thoughts/shared/plans/.gitkeep +0 -0
- package/thoughts/shared/plans/2025-12-18-inverse-teleport.md +1142 -0
- package/thoughts/shared/plans/2025-12-18-slack-integration.md +1195 -0
- package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +732 -0
- package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +361 -0
- package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +501 -0
- package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +560 -0
- package/thoughts/shared/research/.gitkeep +0 -0
- package/thoughts/shared/research/2025-12-18-slack-integration.md +442 -0
- package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +339 -0
- package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +390 -0
- package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +376 -0
- package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +264 -0
- package/tsconfig.json +3 -1
- package/ui/bun.lock +692 -0
- package/ui/index.html +22 -0
- package/ui/package.json +32 -0
- package/ui/pnpm-lock.yaml +3034 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/logo.png +0 -0
- package/ui/src/App.tsx +43 -0
- package/ui/src/components/ActivityFeed.tsx +415 -0
- package/ui/src/components/AgentDetailPanel.tsx +534 -0
- package/ui/src/components/AgentsPanel.tsx +549 -0
- package/ui/src/components/ChatPanel.tsx +1820 -0
- package/ui/src/components/ConfigModal.tsx +232 -0
- package/ui/src/components/Dashboard.tsx +534 -0
- package/ui/src/components/Header.tsx +168 -0
- package/ui/src/components/ServicesPanel.tsx +612 -0
- package/ui/src/components/StatsBar.tsx +288 -0
- package/ui/src/components/StatusBadge.tsx +124 -0
- package/ui/src/components/TaskDetailPanel.tsx +807 -0
- package/ui/src/components/TasksPanel.tsx +575 -0
- package/ui/src/hooks/queries.ts +170 -0
- package/ui/src/index.css +235 -0
- package/ui/src/lib/api.ts +161 -0
- package/ui/src/lib/config.ts +35 -0
- package/ui/src/lib/theme.ts +214 -0
- package/ui/src/lib/utils.ts +48 -0
- package/ui/src/main.tsx +32 -0
- package/ui/src/types/api.ts +164 -0
- package/ui/src/vite-env.d.ts +1 -0
- package/ui/tailwind.config.js +35 -0
- package/ui/tsconfig.json +31 -0
- package/ui/vite.config.ts +22 -0
- package/cc-plugin/README.md +0 -49
- package/cc-plugin/commands/setup-leader.md +0 -73
- package/cc-plugin/commands/start-worker.md +0 -64
- package/docker-compose.worker.yml +0 -35
- package/example-req-meta.json +0 -24
- /package/{cc-plugin → plugin}/hooks/hooks.json +0 -0
package/src/be/db.ts
CHANGED
|
@@ -5,8 +5,14 @@ import type {
|
|
|
5
5
|
AgentLogEventType,
|
|
6
6
|
AgentStatus,
|
|
7
7
|
AgentTask,
|
|
8
|
+
AgentTaskSource,
|
|
8
9
|
AgentTaskStatus,
|
|
9
10
|
AgentWithTasks,
|
|
11
|
+
Channel,
|
|
12
|
+
ChannelMessage,
|
|
13
|
+
ChannelType,
|
|
14
|
+
Service,
|
|
15
|
+
ServiceStatus,
|
|
10
16
|
} from "../types";
|
|
11
17
|
|
|
12
18
|
let db: Database | null = null;
|
|
@@ -26,22 +32,37 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
26
32
|
name TEXT NOT NULL,
|
|
27
33
|
isLead INTEGER NOT NULL DEFAULT 0,
|
|
28
34
|
status TEXT NOT NULL CHECK(status IN ('idle', 'busy', 'offline')),
|
|
35
|
+
description TEXT,
|
|
36
|
+
role TEXT,
|
|
37
|
+
capabilities TEXT DEFAULT '[]',
|
|
29
38
|
createdAt TEXT NOT NULL,
|
|
30
39
|
lastUpdatedAt TEXT NOT NULL
|
|
31
40
|
);
|
|
32
41
|
|
|
33
42
|
CREATE TABLE IF NOT EXISTS agent_tasks (
|
|
34
43
|
id TEXT PRIMARY KEY,
|
|
35
|
-
agentId TEXT
|
|
44
|
+
agentId TEXT,
|
|
45
|
+
creatorAgentId TEXT,
|
|
36
46
|
task TEXT NOT NULL,
|
|
37
|
-
status TEXT NOT NULL
|
|
47
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
48
|
+
source TEXT NOT NULL DEFAULT 'mcp',
|
|
49
|
+
taskType TEXT,
|
|
50
|
+
tags TEXT DEFAULT '[]',
|
|
51
|
+
priority INTEGER DEFAULT 50,
|
|
52
|
+
dependsOn TEXT DEFAULT '[]',
|
|
53
|
+
offeredTo TEXT,
|
|
54
|
+
offeredAt TEXT,
|
|
55
|
+
acceptedAt TEXT,
|
|
56
|
+
rejectionReason TEXT,
|
|
57
|
+
slackChannelId TEXT,
|
|
58
|
+
slackThreadTs TEXT,
|
|
59
|
+
slackUserId TEXT,
|
|
38
60
|
createdAt TEXT NOT NULL,
|
|
39
61
|
lastUpdatedAt TEXT NOT NULL,
|
|
40
62
|
finishedAt TEXT,
|
|
41
63
|
failureReason TEXT,
|
|
42
64
|
output TEXT,
|
|
43
|
-
progress TEXT
|
|
44
|
-
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE
|
|
65
|
+
progress TEXT
|
|
45
66
|
);
|
|
46
67
|
|
|
47
68
|
CREATE INDEX IF NOT EXISTS idx_agent_tasks_agentId ON agent_tasks(agentId);
|
|
@@ -62,8 +83,237 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
62
83
|
CREATE INDEX IF NOT EXISTS idx_agent_log_taskId ON agent_log(taskId);
|
|
63
84
|
CREATE INDEX IF NOT EXISTS idx_agent_log_eventType ON agent_log(eventType);
|
|
64
85
|
CREATE INDEX IF NOT EXISTS idx_agent_log_createdAt ON agent_log(createdAt);
|
|
86
|
+
|
|
87
|
+
-- Channels table
|
|
88
|
+
CREATE TABLE IF NOT EXISTS channels (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
name TEXT NOT NULL UNIQUE,
|
|
91
|
+
description TEXT,
|
|
92
|
+
type TEXT NOT NULL DEFAULT 'public' CHECK(type IN ('public', 'dm')),
|
|
93
|
+
createdBy TEXT,
|
|
94
|
+
participants TEXT DEFAULT '[]',
|
|
95
|
+
createdAt TEXT NOT NULL,
|
|
96
|
+
FOREIGN KEY (createdBy) REFERENCES agents(id) ON DELETE SET NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
-- Channel messages table
|
|
100
|
+
CREATE TABLE IF NOT EXISTS channel_messages (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
channelId TEXT NOT NULL,
|
|
103
|
+
agentId TEXT,
|
|
104
|
+
content TEXT NOT NULL,
|
|
105
|
+
replyToId TEXT,
|
|
106
|
+
mentions TEXT DEFAULT '[]',
|
|
107
|
+
createdAt TEXT NOT NULL,
|
|
108
|
+
FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE,
|
|
109
|
+
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
|
|
110
|
+
FOREIGN KEY (replyToId) REFERENCES channel_messages(id) ON DELETE SET NULL
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_channel_messages_channelId ON channel_messages(channelId);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_channel_messages_agentId ON channel_messages(agentId);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_channel_messages_createdAt ON channel_messages(createdAt);
|
|
116
|
+
|
|
117
|
+
-- Channel read state table
|
|
118
|
+
CREATE TABLE IF NOT EXISTS channel_read_state (
|
|
119
|
+
agentId TEXT NOT NULL,
|
|
120
|
+
channelId TEXT NOT NULL,
|
|
121
|
+
lastReadAt TEXT NOT NULL,
|
|
122
|
+
PRIMARY KEY (agentId, channelId),
|
|
123
|
+
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
|
|
124
|
+
FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
-- Services table (for PM2/background services)
|
|
128
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
agentId TEXT NOT NULL,
|
|
131
|
+
name TEXT NOT NULL,
|
|
132
|
+
port INTEGER NOT NULL DEFAULT 3000,
|
|
133
|
+
description TEXT,
|
|
134
|
+
url TEXT,
|
|
135
|
+
healthCheckPath TEXT DEFAULT '/health',
|
|
136
|
+
status TEXT NOT NULL DEFAULT 'starting' CHECK(status IN ('starting', 'healthy', 'unhealthy', 'stopped')),
|
|
137
|
+
-- PM2 configuration for ecosystem-based restart
|
|
138
|
+
script TEXT NOT NULL DEFAULT '',
|
|
139
|
+
cwd TEXT,
|
|
140
|
+
interpreter TEXT,
|
|
141
|
+
args TEXT, -- JSON array
|
|
142
|
+
env TEXT, -- JSON object
|
|
143
|
+
metadata TEXT DEFAULT '{}',
|
|
144
|
+
createdAt TEXT NOT NULL,
|
|
145
|
+
lastUpdatedAt TEXT NOT NULL,
|
|
146
|
+
FOREIGN KEY (agentId) REFERENCES agents(id) ON DELETE CASCADE,
|
|
147
|
+
UNIQUE(agentId, name)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_services_agentId ON services(agentId);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_services_status ON services(status);
|
|
65
152
|
`);
|
|
66
153
|
|
|
154
|
+
// Seed default general channel if it doesn't exist
|
|
155
|
+
// Use a stable UUID for the general channel so it's consistent across restarts
|
|
156
|
+
const generalChannelId = "00000000-0000-4000-8000-000000000001";
|
|
157
|
+
try {
|
|
158
|
+
// Migration: Fix old 'general' channel ID that wasn't a valid UUID
|
|
159
|
+
db.run(`UPDATE channels SET id = ? WHERE id = 'general'`, [generalChannelId]);
|
|
160
|
+
db.run(`UPDATE channel_messages SET channelId = ? WHERE channelId = 'general'`, [
|
|
161
|
+
generalChannelId,
|
|
162
|
+
]);
|
|
163
|
+
db.run(`UPDATE channel_read_state SET channelId = ? WHERE channelId = 'general'`, [
|
|
164
|
+
generalChannelId,
|
|
165
|
+
]);
|
|
166
|
+
} catch {
|
|
167
|
+
/* Migration not needed or already applied */
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
db.run(
|
|
171
|
+
`
|
|
172
|
+
INSERT OR IGNORE INTO channels (id, name, description, type, createdAt)
|
|
173
|
+
VALUES (?, 'general', 'Default channel for all agents', 'public', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
174
|
+
`,
|
|
175
|
+
[generalChannelId],
|
|
176
|
+
);
|
|
177
|
+
} catch {
|
|
178
|
+
/* Channel already exists */
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Migration: Add new columns to existing databases (SQLite doesn't support IF NOT EXISTS for columns)
|
|
182
|
+
// Agent task columns
|
|
183
|
+
try {
|
|
184
|
+
db.run(
|
|
185
|
+
`ALTER TABLE agent_tasks ADD COLUMN source TEXT NOT NULL DEFAULT 'mcp' CHECK(source IN ('mcp', 'slack', 'api'))`,
|
|
186
|
+
);
|
|
187
|
+
} catch {
|
|
188
|
+
/* exists */
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN slackChannelId TEXT`);
|
|
192
|
+
} catch {
|
|
193
|
+
/* exists */
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN slackThreadTs TEXT`);
|
|
197
|
+
} catch {
|
|
198
|
+
/* exists */
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN slackUserId TEXT`);
|
|
202
|
+
} catch {
|
|
203
|
+
/* exists */
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN taskType TEXT`);
|
|
207
|
+
} catch {
|
|
208
|
+
/* exists */
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN tags TEXT DEFAULT '[]'`);
|
|
212
|
+
} catch {
|
|
213
|
+
/* exists */
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN priority INTEGER DEFAULT 50`);
|
|
217
|
+
} catch {
|
|
218
|
+
/* exists */
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN dependsOn TEXT DEFAULT '[]'`);
|
|
222
|
+
} catch {
|
|
223
|
+
/* exists */
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN offeredTo TEXT`);
|
|
227
|
+
} catch {
|
|
228
|
+
/* exists */
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN offeredAt TEXT`);
|
|
232
|
+
} catch {
|
|
233
|
+
/* exists */
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN acceptedAt TEXT`);
|
|
237
|
+
} catch {
|
|
238
|
+
/* exists */
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN rejectionReason TEXT`);
|
|
242
|
+
} catch {
|
|
243
|
+
/* exists */
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN creatorAgentId TEXT`);
|
|
247
|
+
} catch {
|
|
248
|
+
/* exists */
|
|
249
|
+
}
|
|
250
|
+
// Mention-to-task columns
|
|
251
|
+
try {
|
|
252
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN mentionMessageId TEXT`);
|
|
253
|
+
} catch {
|
|
254
|
+
/* exists */
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
db.run(`ALTER TABLE agent_tasks ADD COLUMN mentionChannelId TEXT`);
|
|
258
|
+
} catch {
|
|
259
|
+
/* exists */
|
|
260
|
+
}
|
|
261
|
+
// Agent profile columns
|
|
262
|
+
try {
|
|
263
|
+
db.run(`ALTER TABLE agents ADD COLUMN description TEXT`);
|
|
264
|
+
} catch {
|
|
265
|
+
/* exists */
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
db.run(`ALTER TABLE agents ADD COLUMN role TEXT`);
|
|
269
|
+
} catch {
|
|
270
|
+
/* exists */
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
db.run(`ALTER TABLE agents ADD COLUMN capabilities TEXT DEFAULT '[]'`);
|
|
274
|
+
} catch {
|
|
275
|
+
/* exists */
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Service PM2 columns migration
|
|
279
|
+
try {
|
|
280
|
+
db.run(`ALTER TABLE services ADD COLUMN script TEXT NOT NULL DEFAULT ''`);
|
|
281
|
+
} catch {
|
|
282
|
+
/* exists */
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
db.run(`ALTER TABLE services ADD COLUMN cwd TEXT`);
|
|
286
|
+
} catch {
|
|
287
|
+
/* exists */
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
db.run(`ALTER TABLE services ADD COLUMN interpreter TEXT`);
|
|
291
|
+
} catch {
|
|
292
|
+
/* exists */
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
db.run(`ALTER TABLE services ADD COLUMN args TEXT`);
|
|
296
|
+
} catch {
|
|
297
|
+
/* exists */
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
db.run(`ALTER TABLE services ADD COLUMN env TEXT`);
|
|
301
|
+
} catch {
|
|
302
|
+
/* exists */
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Create indexes on new columns (after migrations add them)
|
|
306
|
+
try {
|
|
307
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_offeredTo ON agent_tasks(offeredTo)`);
|
|
308
|
+
} catch {
|
|
309
|
+
/* exists or column missing */
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_agent_tasks_taskType ON agent_tasks(taskType)`);
|
|
313
|
+
} catch {
|
|
314
|
+
/* exists or column missing */
|
|
315
|
+
}
|
|
316
|
+
|
|
67
317
|
return db;
|
|
68
318
|
}
|
|
69
319
|
|
|
@@ -90,6 +340,9 @@ type AgentRow = {
|
|
|
90
340
|
name: string;
|
|
91
341
|
isLead: number;
|
|
92
342
|
status: AgentStatus;
|
|
343
|
+
description: string | null;
|
|
344
|
+
role: string | null;
|
|
345
|
+
capabilities: string | null;
|
|
93
346
|
createdAt: string;
|
|
94
347
|
lastUpdatedAt: string;
|
|
95
348
|
};
|
|
@@ -100,6 +353,9 @@ function rowToAgent(row: AgentRow): Agent {
|
|
|
100
353
|
name: row.name,
|
|
101
354
|
isLead: row.isLead === 1,
|
|
102
355
|
status: row.status,
|
|
356
|
+
description: row.description ?? undefined,
|
|
357
|
+
role: row.role ?? undefined,
|
|
358
|
+
capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
|
|
103
359
|
createdAt: row.createdAt,
|
|
104
360
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
105
361
|
};
|
|
@@ -177,9 +433,24 @@ export function deleteAgent(id: string): boolean {
|
|
|
177
433
|
|
|
178
434
|
type AgentTaskRow = {
|
|
179
435
|
id: string;
|
|
180
|
-
agentId: string;
|
|
436
|
+
agentId: string | null;
|
|
437
|
+
creatorAgentId: string | null;
|
|
181
438
|
task: string;
|
|
182
439
|
status: AgentTaskStatus;
|
|
440
|
+
source: AgentTaskSource;
|
|
441
|
+
taskType: string | null;
|
|
442
|
+
tags: string | null;
|
|
443
|
+
priority: number;
|
|
444
|
+
dependsOn: string | null;
|
|
445
|
+
offeredTo: string | null;
|
|
446
|
+
offeredAt: string | null;
|
|
447
|
+
acceptedAt: string | null;
|
|
448
|
+
rejectionReason: string | null;
|
|
449
|
+
slackChannelId: string | null;
|
|
450
|
+
slackThreadTs: string | null;
|
|
451
|
+
slackUserId: string | null;
|
|
452
|
+
mentionMessageId: string | null;
|
|
453
|
+
mentionChannelId: string | null;
|
|
183
454
|
createdAt: string;
|
|
184
455
|
lastUpdatedAt: string;
|
|
185
456
|
finishedAt: string | null;
|
|
@@ -192,8 +463,23 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
192
463
|
return {
|
|
193
464
|
id: row.id,
|
|
194
465
|
agentId: row.agentId,
|
|
466
|
+
creatorAgentId: row.creatorAgentId ?? undefined,
|
|
195
467
|
task: row.task,
|
|
196
468
|
status: row.status,
|
|
469
|
+
source: row.source,
|
|
470
|
+
taskType: row.taskType ?? undefined,
|
|
471
|
+
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
472
|
+
priority: row.priority ?? 50,
|
|
473
|
+
dependsOn: row.dependsOn ? JSON.parse(row.dependsOn) : [],
|
|
474
|
+
offeredTo: row.offeredTo ?? undefined,
|
|
475
|
+
offeredAt: row.offeredAt ?? undefined,
|
|
476
|
+
acceptedAt: row.acceptedAt ?? undefined,
|
|
477
|
+
rejectionReason: row.rejectionReason ?? undefined,
|
|
478
|
+
slackChannelId: row.slackChannelId ?? undefined,
|
|
479
|
+
slackThreadTs: row.slackThreadTs ?? undefined,
|
|
480
|
+
slackUserId: row.slackUserId ?? undefined,
|
|
481
|
+
mentionMessageId: row.mentionMessageId ?? undefined,
|
|
482
|
+
mentionChannelId: row.mentionChannelId ?? undefined,
|
|
197
483
|
createdAt: row.createdAt,
|
|
198
484
|
lastUpdatedAt: row.lastUpdatedAt,
|
|
199
485
|
finishedAt: row.finishedAt ?? undefined,
|
|
@@ -205,9 +491,21 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
205
491
|
|
|
206
492
|
export const taskQueries = {
|
|
207
493
|
insert: () =>
|
|
208
|
-
getDb().prepare<
|
|
209
|
-
|
|
210
|
-
|
|
494
|
+
getDb().prepare<
|
|
495
|
+
AgentTaskRow,
|
|
496
|
+
[
|
|
497
|
+
string,
|
|
498
|
+
string,
|
|
499
|
+
string,
|
|
500
|
+
AgentTaskStatus,
|
|
501
|
+
AgentTaskSource,
|
|
502
|
+
string | null,
|
|
503
|
+
string | null,
|
|
504
|
+
string | null,
|
|
505
|
+
]
|
|
506
|
+
>(
|
|
507
|
+
`INSERT INTO agent_tasks (id, agentId, task, status, source, slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt)
|
|
508
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
|
|
211
509
|
),
|
|
212
510
|
|
|
213
511
|
getById: () => getDb().prepare<AgentTaskRow, [string]>("SELECT * FROM agent_tasks WHERE id = ?"),
|
|
@@ -246,12 +544,39 @@ export const taskQueries = {
|
|
|
246
544
|
delete: () => getDb().prepare<null, [string]>("DELETE FROM agent_tasks WHERE id = ?"),
|
|
247
545
|
};
|
|
248
546
|
|
|
249
|
-
export function createTask(
|
|
547
|
+
export function createTask(
|
|
548
|
+
agentId: string,
|
|
549
|
+
task: string,
|
|
550
|
+
options?: {
|
|
551
|
+
source?: AgentTaskSource;
|
|
552
|
+
slackChannelId?: string;
|
|
553
|
+
slackThreadTs?: string;
|
|
554
|
+
slackUserId?: string;
|
|
555
|
+
},
|
|
556
|
+
): AgentTask {
|
|
250
557
|
const id = crypto.randomUUID();
|
|
251
|
-
const
|
|
558
|
+
const source = options?.source ?? "mcp";
|
|
559
|
+
const row = taskQueries
|
|
560
|
+
.insert()
|
|
561
|
+
.get(
|
|
562
|
+
id,
|
|
563
|
+
agentId,
|
|
564
|
+
task,
|
|
565
|
+
"pending",
|
|
566
|
+
source,
|
|
567
|
+
options?.slackChannelId ?? null,
|
|
568
|
+
options?.slackThreadTs ?? null,
|
|
569
|
+
options?.slackUserId ?? null,
|
|
570
|
+
);
|
|
252
571
|
if (!row) throw new Error("Failed to create task");
|
|
253
572
|
try {
|
|
254
|
-
createLogEntry({
|
|
573
|
+
createLogEntry({
|
|
574
|
+
eventType: "task_created",
|
|
575
|
+
agentId,
|
|
576
|
+
taskId: id,
|
|
577
|
+
newValue: "pending",
|
|
578
|
+
metadata: { source },
|
|
579
|
+
});
|
|
255
580
|
} catch {}
|
|
256
581
|
return rowToAgentTask(row);
|
|
257
582
|
}
|
|
@@ -278,7 +603,7 @@ export function startTask(taskId: string): AgentTask | null {
|
|
|
278
603
|
createLogEntry({
|
|
279
604
|
eventType: "task_status_change",
|
|
280
605
|
taskId,
|
|
281
|
-
agentId: row.agentId,
|
|
606
|
+
agentId: row.agentId ?? undefined,
|
|
282
607
|
oldValue: oldTask.status,
|
|
283
608
|
newValue: "in_progress",
|
|
284
609
|
});
|
|
@@ -300,21 +625,127 @@ export function getTasksByStatus(status: AgentTaskStatus): AgentTask[] {
|
|
|
300
625
|
return taskQueries.getByStatus().all(status).map(rowToAgentTask);
|
|
301
626
|
}
|
|
302
627
|
|
|
303
|
-
export
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
628
|
+
export interface TaskFilters {
|
|
629
|
+
status?: AgentTaskStatus;
|
|
630
|
+
agentId?: string;
|
|
631
|
+
search?: string;
|
|
632
|
+
// New filters
|
|
633
|
+
unassigned?: boolean;
|
|
634
|
+
offeredTo?: string;
|
|
635
|
+
readyOnly?: boolean;
|
|
636
|
+
taskType?: string;
|
|
637
|
+
tags?: string[];
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function getAllTasks(filters?: TaskFilters): AgentTask[] {
|
|
641
|
+
const conditions: string[] = [];
|
|
642
|
+
const params: (string | AgentTaskStatus)[] = [];
|
|
643
|
+
|
|
644
|
+
if (filters?.status) {
|
|
645
|
+
conditions.push("status = ?");
|
|
646
|
+
params.push(filters.status);
|
|
311
647
|
}
|
|
648
|
+
|
|
649
|
+
if (filters?.agentId) {
|
|
650
|
+
conditions.push("agentId = ?");
|
|
651
|
+
params.push(filters.agentId);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (filters?.search) {
|
|
655
|
+
conditions.push("task LIKE ?");
|
|
656
|
+
params.push(`%${filters.search}%`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// New filters
|
|
660
|
+
if (filters?.unassigned) {
|
|
661
|
+
conditions.push("(agentId IS NULL OR status = 'unassigned')");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (filters?.offeredTo) {
|
|
665
|
+
conditions.push("offeredTo = ?");
|
|
666
|
+
params.push(filters.offeredTo);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (filters?.taskType) {
|
|
670
|
+
conditions.push("taskType = ?");
|
|
671
|
+
params.push(filters.taskType);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
675
|
+
// Match any of the tags
|
|
676
|
+
const tagConditions = filters.tags.map(() => "tags LIKE ?");
|
|
677
|
+
conditions.push(`(${tagConditions.join(" OR ")})`);
|
|
678
|
+
for (const tag of filters.tags) {
|
|
679
|
+
params.push(`%"${tag}"%`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
684
|
+
const query = `SELECT * FROM agent_tasks ${whereClause} ORDER BY lastUpdatedAt DESC, priority DESC`;
|
|
685
|
+
|
|
686
|
+
let tasks = getDb()
|
|
687
|
+
.prepare<AgentTaskRow, (string | AgentTaskStatus)[]>(query)
|
|
688
|
+
.all(...params)
|
|
689
|
+
.map(rowToAgentTask);
|
|
690
|
+
|
|
691
|
+
// Filter for ready tasks (dependencies met) if requested
|
|
692
|
+
if (filters?.readyOnly) {
|
|
693
|
+
tasks = tasks.filter((task) => {
|
|
694
|
+
if (!task.dependsOn || task.dependsOn.length === 0) return true;
|
|
695
|
+
return checkDependencies(task.id).ready;
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return tasks;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export function getCompletedSlackTasks(): AgentTask[] {
|
|
703
|
+
return getDb()
|
|
704
|
+
.prepare<AgentTaskRow, []>(
|
|
705
|
+
`SELECT * FROM agent_tasks
|
|
706
|
+
WHERE source = 'slack'
|
|
707
|
+
AND slackChannelId IS NOT NULL
|
|
708
|
+
AND status IN ('completed', 'failed')
|
|
709
|
+
ORDER BY lastUpdatedAt DESC`,
|
|
710
|
+
)
|
|
711
|
+
.all()
|
|
712
|
+
.map(rowToAgentTask);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function getInProgressSlackTasks(): AgentTask[] {
|
|
312
716
|
return getDb()
|
|
313
|
-
.prepare<AgentTaskRow, []>(
|
|
717
|
+
.prepare<AgentTaskRow, []>(
|
|
718
|
+
`SELECT * FROM agent_tasks
|
|
719
|
+
WHERE source = 'slack'
|
|
720
|
+
AND slackChannelId IS NOT NULL
|
|
721
|
+
AND status = 'in_progress'
|
|
722
|
+
ORDER BY lastUpdatedAt DESC`,
|
|
723
|
+
)
|
|
314
724
|
.all()
|
|
315
725
|
.map(rowToAgentTask);
|
|
316
726
|
}
|
|
317
727
|
|
|
728
|
+
/**
|
|
729
|
+
* Find an agent that has an active task (in_progress or pending) in a specific Slack thread.
|
|
730
|
+
* Used for routing thread follow-up messages to the same agent.
|
|
731
|
+
*/
|
|
732
|
+
export function getAgentWorkingOnThread(channelId: string, threadTs: string): Agent | null {
|
|
733
|
+
const row = getDb()
|
|
734
|
+
.prepare<AgentTaskRow, [string, string]>(
|
|
735
|
+
`SELECT * FROM agent_tasks
|
|
736
|
+
WHERE source = 'slack'
|
|
737
|
+
AND slackChannelId = ?
|
|
738
|
+
AND slackThreadTs = ?
|
|
739
|
+
AND status IN ('in_progress', 'pending')
|
|
740
|
+
ORDER BY createdAt DESC
|
|
741
|
+
LIMIT 1`,
|
|
742
|
+
)
|
|
743
|
+
.get(channelId, threadTs);
|
|
744
|
+
|
|
745
|
+
if (!row || !row.agentId) return null;
|
|
746
|
+
return getAgentById(row.agentId);
|
|
747
|
+
}
|
|
748
|
+
|
|
318
749
|
export function completeTask(id: string, output?: string): AgentTask | null {
|
|
319
750
|
const oldTask = getTaskById(id);
|
|
320
751
|
const finishedAt = new Date().toISOString();
|
|
@@ -330,7 +761,7 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
330
761
|
createLogEntry({
|
|
331
762
|
eventType: "task_status_change",
|
|
332
763
|
taskId: id,
|
|
333
|
-
agentId: row.agentId,
|
|
764
|
+
agentId: row.agentId ?? undefined,
|
|
334
765
|
oldValue: oldTask.status,
|
|
335
766
|
newValue: "completed",
|
|
336
767
|
});
|
|
@@ -349,7 +780,7 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
349
780
|
createLogEntry({
|
|
350
781
|
eventType: "task_status_change",
|
|
351
782
|
taskId: id,
|
|
352
|
-
agentId: row.agentId,
|
|
783
|
+
agentId: row.agentId ?? undefined,
|
|
353
784
|
oldValue: oldTask.status,
|
|
354
785
|
newValue: "failed",
|
|
355
786
|
metadata: { reason },
|
|
@@ -371,7 +802,7 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
|
|
|
371
802
|
createLogEntry({
|
|
372
803
|
eventType: "task_progress",
|
|
373
804
|
taskId: id,
|
|
374
|
-
agentId: row.agentId,
|
|
805
|
+
agentId: row.agentId ?? undefined,
|
|
375
806
|
newValue: progress,
|
|
376
807
|
});
|
|
377
808
|
} catch {}
|
|
@@ -507,9 +938,984 @@ export function getLogsByTaskIdChronological(taskId: string): AgentLog[] {
|
|
|
507
938
|
export function getAllLogs(limit?: number): AgentLog[] {
|
|
508
939
|
if (limit) {
|
|
509
940
|
return getDb()
|
|
510
|
-
.prepare<AgentLogRow, [number]>(
|
|
941
|
+
.prepare<AgentLogRow, [number]>(
|
|
942
|
+
"SELECT * FROM agent_log WHERE eventType != 'agent_status_change' ORDER BY createdAt DESC LIMIT ?",
|
|
943
|
+
)
|
|
511
944
|
.all(limit)
|
|
512
945
|
.map(rowToAgentLog);
|
|
513
946
|
}
|
|
514
947
|
return logQueries.getAll().all().map(rowToAgentLog);
|
|
515
948
|
}
|
|
949
|
+
|
|
950
|
+
// ============================================================================
|
|
951
|
+
// Task Pool Operations
|
|
952
|
+
// ============================================================================
|
|
953
|
+
|
|
954
|
+
export interface CreateTaskOptions {
|
|
955
|
+
agentId?: string | null;
|
|
956
|
+
creatorAgentId?: string;
|
|
957
|
+
source?: AgentTaskSource;
|
|
958
|
+
taskType?: string;
|
|
959
|
+
tags?: string[];
|
|
960
|
+
priority?: number;
|
|
961
|
+
dependsOn?: string[];
|
|
962
|
+
offeredTo?: string;
|
|
963
|
+
slackChannelId?: string;
|
|
964
|
+
slackThreadTs?: string;
|
|
965
|
+
slackUserId?: string;
|
|
966
|
+
mentionMessageId?: string;
|
|
967
|
+
mentionChannelId?: string;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function createTaskExtended(task: string, options?: CreateTaskOptions): AgentTask {
|
|
971
|
+
const id = crypto.randomUUID();
|
|
972
|
+
const now = new Date().toISOString();
|
|
973
|
+
const status: AgentTaskStatus = options?.offeredTo
|
|
974
|
+
? "offered"
|
|
975
|
+
: options?.agentId
|
|
976
|
+
? "pending"
|
|
977
|
+
: "unassigned";
|
|
978
|
+
|
|
979
|
+
const row = getDb()
|
|
980
|
+
.prepare<AgentTaskRow, (string | number | null)[]>(
|
|
981
|
+
`INSERT INTO agent_tasks (
|
|
982
|
+
id, agentId, creatorAgentId, task, status, source,
|
|
983
|
+
taskType, tags, priority, dependsOn, offeredTo, offeredAt,
|
|
984
|
+
slackChannelId, slackThreadTs, slackUserId,
|
|
985
|
+
mentionMessageId, mentionChannelId, createdAt, lastUpdatedAt
|
|
986
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
987
|
+
)
|
|
988
|
+
.get(
|
|
989
|
+
id,
|
|
990
|
+
options?.agentId ?? null,
|
|
991
|
+
options?.creatorAgentId ?? null,
|
|
992
|
+
task,
|
|
993
|
+
status,
|
|
994
|
+
options?.source ?? "mcp",
|
|
995
|
+
options?.taskType ?? null,
|
|
996
|
+
JSON.stringify(options?.tags ?? []),
|
|
997
|
+
options?.priority ?? 50,
|
|
998
|
+
JSON.stringify(options?.dependsOn ?? []),
|
|
999
|
+
options?.offeredTo ?? null,
|
|
1000
|
+
options?.offeredTo ? now : null,
|
|
1001
|
+
options?.slackChannelId ?? null,
|
|
1002
|
+
options?.slackThreadTs ?? null,
|
|
1003
|
+
options?.slackUserId ?? null,
|
|
1004
|
+
options?.mentionMessageId ?? null,
|
|
1005
|
+
options?.mentionChannelId ?? null,
|
|
1006
|
+
now,
|
|
1007
|
+
now,
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
if (!row) throw new Error("Failed to create task");
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
createLogEntry({
|
|
1014
|
+
eventType: status === "offered" ? "task_offered" : "task_created",
|
|
1015
|
+
agentId: options?.creatorAgentId,
|
|
1016
|
+
taskId: id,
|
|
1017
|
+
newValue: status,
|
|
1018
|
+
metadata: { source: options?.source ?? "mcp" },
|
|
1019
|
+
});
|
|
1020
|
+
} catch {}
|
|
1021
|
+
|
|
1022
|
+
return rowToAgentTask(row);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export function claimTask(taskId: string, agentId: string): AgentTask | null {
|
|
1026
|
+
const task = getTaskById(taskId);
|
|
1027
|
+
if (!task) return null;
|
|
1028
|
+
if (task.status !== "unassigned") return null;
|
|
1029
|
+
|
|
1030
|
+
const now = new Date().toISOString();
|
|
1031
|
+
const row = getDb()
|
|
1032
|
+
.prepare<AgentTaskRow, [string, string, string]>(
|
|
1033
|
+
`UPDATE agent_tasks SET agentId = ?, status = 'pending', lastUpdatedAt = ?
|
|
1034
|
+
WHERE id = ? AND status = 'unassigned' RETURNING *`,
|
|
1035
|
+
)
|
|
1036
|
+
.get(agentId, now, taskId);
|
|
1037
|
+
|
|
1038
|
+
if (row) {
|
|
1039
|
+
try {
|
|
1040
|
+
createLogEntry({
|
|
1041
|
+
eventType: "task_claimed",
|
|
1042
|
+
agentId,
|
|
1043
|
+
taskId,
|
|
1044
|
+
oldValue: "unassigned",
|
|
1045
|
+
newValue: "pending",
|
|
1046
|
+
});
|
|
1047
|
+
} catch {}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return row ? rowToAgentTask(row) : null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export function releaseTask(taskId: string): AgentTask | null {
|
|
1054
|
+
const task = getTaskById(taskId);
|
|
1055
|
+
if (!task) return null;
|
|
1056
|
+
if (task.status !== "pending") return null;
|
|
1057
|
+
|
|
1058
|
+
const now = new Date().toISOString();
|
|
1059
|
+
const row = getDb()
|
|
1060
|
+
.prepare<AgentTaskRow, [string, string]>(
|
|
1061
|
+
`UPDATE agent_tasks SET agentId = NULL, status = 'unassigned', lastUpdatedAt = ?
|
|
1062
|
+
WHERE id = ? AND status = 'pending' RETURNING *`,
|
|
1063
|
+
)
|
|
1064
|
+
.get(now, taskId);
|
|
1065
|
+
|
|
1066
|
+
if (row) {
|
|
1067
|
+
try {
|
|
1068
|
+
createLogEntry({
|
|
1069
|
+
eventType: "task_released",
|
|
1070
|
+
agentId: task.agentId ?? undefined,
|
|
1071
|
+
taskId,
|
|
1072
|
+
oldValue: "pending",
|
|
1073
|
+
newValue: "unassigned",
|
|
1074
|
+
});
|
|
1075
|
+
} catch {}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return row ? rowToAgentTask(row) : null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function acceptTask(taskId: string, agentId: string): AgentTask | null {
|
|
1082
|
+
const task = getTaskById(taskId);
|
|
1083
|
+
if (!task) return null;
|
|
1084
|
+
if (task.status !== "offered" || task.offeredTo !== agentId) return null;
|
|
1085
|
+
|
|
1086
|
+
const now = new Date().toISOString();
|
|
1087
|
+
const row = getDb()
|
|
1088
|
+
.prepare<AgentTaskRow, [string, string, string, string]>(
|
|
1089
|
+
`UPDATE agent_tasks SET agentId = ?, status = 'pending', acceptedAt = ?, lastUpdatedAt = ?
|
|
1090
|
+
WHERE id = ? AND status = 'offered' RETURNING *`,
|
|
1091
|
+
)
|
|
1092
|
+
.get(agentId, now, now, taskId);
|
|
1093
|
+
|
|
1094
|
+
if (row) {
|
|
1095
|
+
try {
|
|
1096
|
+
createLogEntry({
|
|
1097
|
+
eventType: "task_accepted",
|
|
1098
|
+
agentId,
|
|
1099
|
+
taskId,
|
|
1100
|
+
oldValue: "offered",
|
|
1101
|
+
newValue: "pending",
|
|
1102
|
+
});
|
|
1103
|
+
} catch {}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return row ? rowToAgentTask(row) : null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
export function rejectTask(taskId: string, agentId: string, reason?: string): AgentTask | null {
|
|
1110
|
+
const task = getTaskById(taskId);
|
|
1111
|
+
if (!task) return null;
|
|
1112
|
+
if (task.status !== "offered" || task.offeredTo !== agentId) return null;
|
|
1113
|
+
|
|
1114
|
+
const now = new Date().toISOString();
|
|
1115
|
+
const row = getDb()
|
|
1116
|
+
.prepare<AgentTaskRow, [string | null, string, string]>(
|
|
1117
|
+
`UPDATE agent_tasks SET
|
|
1118
|
+
status = 'unassigned', offeredTo = NULL, offeredAt = NULL,
|
|
1119
|
+
rejectionReason = ?, lastUpdatedAt = ?
|
|
1120
|
+
WHERE id = ? AND status = 'offered' RETURNING *`,
|
|
1121
|
+
)
|
|
1122
|
+
.get(reason ?? null, now, taskId);
|
|
1123
|
+
|
|
1124
|
+
if (row) {
|
|
1125
|
+
try {
|
|
1126
|
+
createLogEntry({
|
|
1127
|
+
eventType: "task_rejected",
|
|
1128
|
+
agentId,
|
|
1129
|
+
taskId,
|
|
1130
|
+
oldValue: "offered",
|
|
1131
|
+
newValue: "unassigned",
|
|
1132
|
+
metadata: reason ? { reason } : undefined,
|
|
1133
|
+
});
|
|
1134
|
+
} catch {}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return row ? rowToAgentTask(row) : null;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export function getOfferedTasksForAgent(agentId: string): AgentTask[] {
|
|
1141
|
+
return getDb()
|
|
1142
|
+
.prepare<AgentTaskRow, [string]>(
|
|
1143
|
+
"SELECT * FROM agent_tasks WHERE offeredTo = ? AND status = 'offered' ORDER BY createdAt ASC",
|
|
1144
|
+
)
|
|
1145
|
+
.all(agentId)
|
|
1146
|
+
.map(rowToAgentTask);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
export function getUnassignedTasksCount(): number {
|
|
1150
|
+
const result = getDb()
|
|
1151
|
+
.prepare<{ count: number }, []>(
|
|
1152
|
+
"SELECT COUNT(*) as count FROM agent_tasks WHERE status = 'unassigned'",
|
|
1153
|
+
)
|
|
1154
|
+
.get();
|
|
1155
|
+
return result?.count ?? 0;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ============================================================================
|
|
1159
|
+
// Dependency Checking
|
|
1160
|
+
// ============================================================================
|
|
1161
|
+
|
|
1162
|
+
export function checkDependencies(taskId: string): {
|
|
1163
|
+
ready: boolean;
|
|
1164
|
+
blockedBy: string[];
|
|
1165
|
+
} {
|
|
1166
|
+
const task = getTaskById(taskId);
|
|
1167
|
+
if (!task || !task.dependsOn || task.dependsOn.length === 0) {
|
|
1168
|
+
return { ready: true, blockedBy: [] };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const blockedBy: string[] = [];
|
|
1172
|
+
for (const depId of task.dependsOn) {
|
|
1173
|
+
const depTask = getTaskById(depId);
|
|
1174
|
+
if (!depTask || depTask.status !== "completed") {
|
|
1175
|
+
blockedBy.push(depId);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return { ready: blockedBy.length === 0, blockedBy };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ============================================================================
|
|
1183
|
+
// Agent Profile Operations
|
|
1184
|
+
// ============================================================================
|
|
1185
|
+
|
|
1186
|
+
export function updateAgentProfile(
|
|
1187
|
+
id: string,
|
|
1188
|
+
updates: {
|
|
1189
|
+
description?: string;
|
|
1190
|
+
role?: string;
|
|
1191
|
+
capabilities?: string[];
|
|
1192
|
+
},
|
|
1193
|
+
): Agent | null {
|
|
1194
|
+
const agent = getAgentById(id);
|
|
1195
|
+
if (!agent) return null;
|
|
1196
|
+
|
|
1197
|
+
const now = new Date().toISOString();
|
|
1198
|
+
const row = getDb()
|
|
1199
|
+
.prepare<AgentRow, [string | null, string | null, string | null, string, string]>(
|
|
1200
|
+
`UPDATE agents SET
|
|
1201
|
+
description = COALESCE(?, description),
|
|
1202
|
+
role = COALESCE(?, role),
|
|
1203
|
+
capabilities = COALESCE(?, capabilities),
|
|
1204
|
+
lastUpdatedAt = ?
|
|
1205
|
+
WHERE id = ? RETURNING *`,
|
|
1206
|
+
)
|
|
1207
|
+
.get(
|
|
1208
|
+
updates.description ?? null,
|
|
1209
|
+
updates.role ?? null,
|
|
1210
|
+
updates.capabilities ? JSON.stringify(updates.capabilities) : null,
|
|
1211
|
+
now,
|
|
1212
|
+
id,
|
|
1213
|
+
);
|
|
1214
|
+
|
|
1215
|
+
return row ? rowToAgent(row) : null;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// ============================================================================
|
|
1219
|
+
// Channel Operations
|
|
1220
|
+
// ============================================================================
|
|
1221
|
+
|
|
1222
|
+
type ChannelRow = {
|
|
1223
|
+
id: string;
|
|
1224
|
+
name: string;
|
|
1225
|
+
description: string | null;
|
|
1226
|
+
type: ChannelType;
|
|
1227
|
+
createdBy: string | null;
|
|
1228
|
+
participants: string | null;
|
|
1229
|
+
createdAt: string;
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
function rowToChannel(row: ChannelRow): Channel {
|
|
1233
|
+
return {
|
|
1234
|
+
id: row.id,
|
|
1235
|
+
name: row.name,
|
|
1236
|
+
description: row.description ?? undefined,
|
|
1237
|
+
type: row.type,
|
|
1238
|
+
createdBy: row.createdBy ?? undefined,
|
|
1239
|
+
participants: row.participants ? JSON.parse(row.participants) : [],
|
|
1240
|
+
createdAt: row.createdAt,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
type ChannelMessageRow = {
|
|
1245
|
+
id: string;
|
|
1246
|
+
channelId: string;
|
|
1247
|
+
agentId: string | null;
|
|
1248
|
+
content: string;
|
|
1249
|
+
replyToId: string | null;
|
|
1250
|
+
mentions: string | null;
|
|
1251
|
+
createdAt: string;
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
function rowToChannelMessage(row: ChannelMessageRow, agentName?: string): ChannelMessage {
|
|
1255
|
+
return {
|
|
1256
|
+
id: row.id,
|
|
1257
|
+
channelId: row.channelId,
|
|
1258
|
+
agentId: row.agentId,
|
|
1259
|
+
agentName: agentName ?? (row.agentId ? undefined : "Human"),
|
|
1260
|
+
content: row.content,
|
|
1261
|
+
replyToId: row.replyToId ?? undefined,
|
|
1262
|
+
mentions: row.mentions ? JSON.parse(row.mentions) : [],
|
|
1263
|
+
createdAt: row.createdAt,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
export function createChannel(
|
|
1268
|
+
name: string,
|
|
1269
|
+
options?: {
|
|
1270
|
+
description?: string;
|
|
1271
|
+
type?: ChannelType;
|
|
1272
|
+
createdBy?: string;
|
|
1273
|
+
participants?: string[];
|
|
1274
|
+
},
|
|
1275
|
+
): Channel {
|
|
1276
|
+
const id = crypto.randomUUID();
|
|
1277
|
+
const now = new Date().toISOString();
|
|
1278
|
+
|
|
1279
|
+
const row = getDb()
|
|
1280
|
+
.prepare<
|
|
1281
|
+
ChannelRow,
|
|
1282
|
+
[string, string, string | null, ChannelType, string | null, string, string]
|
|
1283
|
+
>(
|
|
1284
|
+
`INSERT INTO channels (id, name, description, type, createdBy, participants, createdAt)
|
|
1285
|
+
VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1286
|
+
)
|
|
1287
|
+
.get(
|
|
1288
|
+
id,
|
|
1289
|
+
name,
|
|
1290
|
+
options?.description ?? null,
|
|
1291
|
+
options?.type ?? "public",
|
|
1292
|
+
options?.createdBy ?? null,
|
|
1293
|
+
JSON.stringify(options?.participants ?? []),
|
|
1294
|
+
now,
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
if (!row) throw new Error("Failed to create channel");
|
|
1298
|
+
return rowToChannel(row);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
export function getMessageById(id: string): ChannelMessage | null {
|
|
1302
|
+
const row = getDb()
|
|
1303
|
+
.prepare<ChannelMessageRow, [string]>("SELECT * FROM channel_messages WHERE id = ?")
|
|
1304
|
+
.get(id);
|
|
1305
|
+
if (!row) return null;
|
|
1306
|
+
const agent = row.agentId ? getAgentById(row.agentId) : null;
|
|
1307
|
+
return rowToChannelMessage(row, agent?.name);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
export function getChannelById(id: string): Channel | null {
|
|
1311
|
+
const row = getDb().prepare<ChannelRow, [string]>("SELECT * FROM channels WHERE id = ?").get(id);
|
|
1312
|
+
return row ? rowToChannel(row) : null;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
export function getChannelByName(name: string): Channel | null {
|
|
1316
|
+
const row = getDb()
|
|
1317
|
+
.prepare<ChannelRow, [string]>("SELECT * FROM channels WHERE name = ?")
|
|
1318
|
+
.get(name);
|
|
1319
|
+
return row ? rowToChannel(row) : null;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export function getAllChannels(): Channel[] {
|
|
1323
|
+
return getDb()
|
|
1324
|
+
.prepare<ChannelRow, []>("SELECT * FROM channels ORDER BY name")
|
|
1325
|
+
.all()
|
|
1326
|
+
.map(rowToChannel);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
export function postMessage(
|
|
1330
|
+
channelId: string,
|
|
1331
|
+
agentId: string | null,
|
|
1332
|
+
content: string,
|
|
1333
|
+
options?: {
|
|
1334
|
+
replyToId?: string;
|
|
1335
|
+
mentions?: string[];
|
|
1336
|
+
},
|
|
1337
|
+
): ChannelMessage {
|
|
1338
|
+
const id = crypto.randomUUID();
|
|
1339
|
+
const now = new Date().toISOString();
|
|
1340
|
+
|
|
1341
|
+
// Detect /task prefix - only create tasks when explicitly requested
|
|
1342
|
+
const isTaskMessage = content.trimStart().startsWith("/task ");
|
|
1343
|
+
const messageContent = isTaskMessage ? content.replace(/^\s*\/task\s+/, "") : content;
|
|
1344
|
+
|
|
1345
|
+
const row = getDb()
|
|
1346
|
+
.prepare<
|
|
1347
|
+
ChannelMessageRow,
|
|
1348
|
+
[string, string, string | null, string, string | null, string, string]
|
|
1349
|
+
>(
|
|
1350
|
+
`INSERT INTO channel_messages (id, channelId, agentId, content, replyToId, mentions, createdAt)
|
|
1351
|
+
VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1352
|
+
)
|
|
1353
|
+
.get(
|
|
1354
|
+
id,
|
|
1355
|
+
channelId,
|
|
1356
|
+
agentId,
|
|
1357
|
+
messageContent,
|
|
1358
|
+
options?.replyToId ?? null,
|
|
1359
|
+
JSON.stringify(options?.mentions ?? []),
|
|
1360
|
+
now,
|
|
1361
|
+
);
|
|
1362
|
+
|
|
1363
|
+
if (!row) throw new Error("Failed to post message");
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
createLogEntry({
|
|
1367
|
+
eventType: "channel_message",
|
|
1368
|
+
agentId: agentId ?? undefined,
|
|
1369
|
+
metadata: { channelId, messageId: id },
|
|
1370
|
+
});
|
|
1371
|
+
} catch {}
|
|
1372
|
+
|
|
1373
|
+
// Determine which agents should receive task notifications
|
|
1374
|
+
let targetMentions = options?.mentions ?? [];
|
|
1375
|
+
|
|
1376
|
+
// Thread follow-up: If no explicit mentions and this is a reply, inherit from parent message
|
|
1377
|
+
// Note: Only for notifications, not for task creation (requires explicit /task)
|
|
1378
|
+
if (targetMentions.length === 0 && options?.replyToId) {
|
|
1379
|
+
const parentMessage = getMessageById(options.replyToId);
|
|
1380
|
+
if (parentMessage?.mentions && parentMessage.mentions.length > 0) {
|
|
1381
|
+
targetMentions = parentMessage.mentions;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Only create tasks when /task prefix is used
|
|
1386
|
+
if (isTaskMessage && targetMentions.length > 0) {
|
|
1387
|
+
const sender = agentId ? getAgentById(agentId) : null;
|
|
1388
|
+
const channel = getChannelById(channelId);
|
|
1389
|
+
const senderName = sender?.name ?? "Human";
|
|
1390
|
+
const channelName = channel?.name ?? "unknown";
|
|
1391
|
+
const truncated =
|
|
1392
|
+
messageContent.length > 80 ? `${messageContent.slice(0, 80)}...` : messageContent;
|
|
1393
|
+
|
|
1394
|
+
// Dedupe mentions (self-mentions allowed - agents can create tasks for themselves)
|
|
1395
|
+
const uniqueMentions = [...new Set(targetMentions)];
|
|
1396
|
+
const createdTaskIds: string[] = [];
|
|
1397
|
+
|
|
1398
|
+
for (const mentionedAgentId of uniqueMentions) {
|
|
1399
|
+
// Skip if agent doesn't exist
|
|
1400
|
+
const mentionedAgent = getAgentById(mentionedAgentId);
|
|
1401
|
+
if (!mentionedAgent) continue;
|
|
1402
|
+
|
|
1403
|
+
const taskDescription = `Task from ${senderName} in #${channelName}: "${truncated}"`;
|
|
1404
|
+
|
|
1405
|
+
const task = createTaskExtended(taskDescription, {
|
|
1406
|
+
agentId: mentionedAgentId, // Direct assignment
|
|
1407
|
+
creatorAgentId: agentId ?? undefined,
|
|
1408
|
+
source: "mcp",
|
|
1409
|
+
taskType: "task",
|
|
1410
|
+
priority: 50,
|
|
1411
|
+
mentionMessageId: id,
|
|
1412
|
+
mentionChannelId: channelId,
|
|
1413
|
+
});
|
|
1414
|
+
createdTaskIds.push(task.id);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Append task links to message content (markdown format for frontend)
|
|
1418
|
+
if (createdTaskIds.length > 0) {
|
|
1419
|
+
const taskLinks = createdTaskIds
|
|
1420
|
+
.map((taskId) => `[#${taskId.slice(0, 8)}](task:${taskId})`)
|
|
1421
|
+
.join(" ");
|
|
1422
|
+
const updatedContent = `${messageContent}\n\n→ Created: ${taskLinks}`;
|
|
1423
|
+
getDb()
|
|
1424
|
+
.prepare(`UPDATE channel_messages SET content = ? WHERE id = ?`)
|
|
1425
|
+
.run(updatedContent, id);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Get agent name for the response - re-fetch to get updated content
|
|
1430
|
+
const agent = agentId ? getAgentById(agentId) : null;
|
|
1431
|
+
const updatedRow = getDb()
|
|
1432
|
+
.prepare<ChannelMessageRow, [string]>(
|
|
1433
|
+
`SELECT m.*, a.name as agentName FROM channel_messages m
|
|
1434
|
+
LEFT JOIN agents a ON m.agentId = a.id WHERE m.id = ?`,
|
|
1435
|
+
)
|
|
1436
|
+
.get(id);
|
|
1437
|
+
return rowToChannelMessage(updatedRow ?? row, agent?.name);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
export function getChannelMessages(
|
|
1441
|
+
channelId: string,
|
|
1442
|
+
options?: {
|
|
1443
|
+
limit?: number;
|
|
1444
|
+
since?: string;
|
|
1445
|
+
before?: string;
|
|
1446
|
+
},
|
|
1447
|
+
): ChannelMessage[] {
|
|
1448
|
+
let query =
|
|
1449
|
+
"SELECT m.*, a.name as agentName FROM channel_messages m LEFT JOIN agents a ON m.agentId = a.id WHERE m.channelId = ?";
|
|
1450
|
+
const params: (string | number)[] = [channelId];
|
|
1451
|
+
|
|
1452
|
+
if (options?.since) {
|
|
1453
|
+
query += " AND m.createdAt > ?";
|
|
1454
|
+
params.push(options.since);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (options?.before) {
|
|
1458
|
+
query += " AND m.createdAt < ?";
|
|
1459
|
+
params.push(options.before);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
query += " ORDER BY m.createdAt DESC";
|
|
1463
|
+
|
|
1464
|
+
if (options?.limit) {
|
|
1465
|
+
query += " LIMIT ?";
|
|
1466
|
+
params.push(options.limit);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
type MessageWithAgentRow = ChannelMessageRow & { agentName: string | null };
|
|
1470
|
+
|
|
1471
|
+
return getDb()
|
|
1472
|
+
.prepare<MessageWithAgentRow, (string | number)[]>(query)
|
|
1473
|
+
.all(...params)
|
|
1474
|
+
.map((row) => rowToChannelMessage(row, row.agentName ?? undefined))
|
|
1475
|
+
.reverse(); // Return in chronological order
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
export function updateReadState(agentId: string, channelId: string): void {
|
|
1479
|
+
const now = new Date().toISOString();
|
|
1480
|
+
getDb().run(
|
|
1481
|
+
`INSERT INTO channel_read_state (agentId, channelId, lastReadAt)
|
|
1482
|
+
VALUES (?, ?, ?)
|
|
1483
|
+
ON CONFLICT(agentId, channelId) DO UPDATE SET lastReadAt = ?`,
|
|
1484
|
+
[agentId, channelId, now, now],
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
export function getLastReadAt(agentId: string, channelId: string): string | null {
|
|
1489
|
+
const result = getDb()
|
|
1490
|
+
.prepare<{ lastReadAt: string }, [string, string]>(
|
|
1491
|
+
"SELECT lastReadAt FROM channel_read_state WHERE agentId = ? AND channelId = ?",
|
|
1492
|
+
)
|
|
1493
|
+
.get(agentId, channelId);
|
|
1494
|
+
return result?.lastReadAt ?? null;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
export function getUnreadMessages(agentId: string, channelId: string): ChannelMessage[] {
|
|
1498
|
+
const lastReadAt = getLastReadAt(agentId, channelId);
|
|
1499
|
+
|
|
1500
|
+
let query = `SELECT m.*, a.name as agentName FROM channel_messages m
|
|
1501
|
+
LEFT JOIN agents a ON m.agentId = a.id
|
|
1502
|
+
WHERE m.channelId = ?`;
|
|
1503
|
+
const params: string[] = [channelId];
|
|
1504
|
+
|
|
1505
|
+
if (lastReadAt) {
|
|
1506
|
+
query += " AND m.createdAt > ?";
|
|
1507
|
+
params.push(lastReadAt);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
query += " ORDER BY m.createdAt ASC";
|
|
1511
|
+
|
|
1512
|
+
type MessageWithAgentRow = ChannelMessageRow & { agentName: string | null };
|
|
1513
|
+
|
|
1514
|
+
return getDb()
|
|
1515
|
+
.prepare<MessageWithAgentRow, string[]>(query)
|
|
1516
|
+
.all(...params)
|
|
1517
|
+
.map((row) => rowToChannelMessage(row, row.agentName ?? undefined));
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
export function getMentionsForAgent(
|
|
1521
|
+
agentId: string,
|
|
1522
|
+
options?: { unreadOnly?: boolean; channelId?: string },
|
|
1523
|
+
): ChannelMessage[] {
|
|
1524
|
+
let query = `SELECT m.*, a.name as agentName FROM channel_messages m
|
|
1525
|
+
LEFT JOIN agents a ON m.agentId = a.id
|
|
1526
|
+
WHERE m.mentions LIKE ?`;
|
|
1527
|
+
const params: string[] = [`%"${agentId}"%`];
|
|
1528
|
+
|
|
1529
|
+
if (options?.channelId) {
|
|
1530
|
+
query += " AND m.channelId = ?";
|
|
1531
|
+
params.push(options.channelId);
|
|
1532
|
+
|
|
1533
|
+
if (options?.unreadOnly) {
|
|
1534
|
+
const lastReadAt = getLastReadAt(agentId, options.channelId);
|
|
1535
|
+
if (lastReadAt) {
|
|
1536
|
+
query += " AND m.createdAt > ?";
|
|
1537
|
+
params.push(lastReadAt);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
query += " ORDER BY m.createdAt DESC LIMIT 50";
|
|
1543
|
+
|
|
1544
|
+
type MessageWithAgentRow = ChannelMessageRow & { agentName: string | null };
|
|
1545
|
+
|
|
1546
|
+
return getDb()
|
|
1547
|
+
.prepare<MessageWithAgentRow, string[]>(query)
|
|
1548
|
+
.all(...params)
|
|
1549
|
+
.map((row) => rowToChannelMessage(row, row.agentName ?? undefined));
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// ============================================================================
|
|
1553
|
+
// Inbox Summary (for system tray)
|
|
1554
|
+
// ============================================================================
|
|
1555
|
+
|
|
1556
|
+
export interface MentionPreview {
|
|
1557
|
+
channelName: string;
|
|
1558
|
+
agentName: string;
|
|
1559
|
+
content: string;
|
|
1560
|
+
createdAt: string;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
export interface InboxSummary {
|
|
1564
|
+
unreadCount: number;
|
|
1565
|
+
mentionsCount: number;
|
|
1566
|
+
offeredTasksCount: number;
|
|
1567
|
+
poolTasksCount: number;
|
|
1568
|
+
inProgressCount: number;
|
|
1569
|
+
recentMentions: MentionPreview[]; // Up to 3 recent @mentions
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
export function getInboxSummary(agentId: string): InboxSummary {
|
|
1573
|
+
const db = getDb();
|
|
1574
|
+
const channels = getAllChannels();
|
|
1575
|
+
let unreadCount = 0;
|
|
1576
|
+
let mentionsCount = 0;
|
|
1577
|
+
|
|
1578
|
+
for (const channel of channels) {
|
|
1579
|
+
const lastReadAt = getLastReadAt(agentId, channel.id);
|
|
1580
|
+
const baseCondition = lastReadAt ? `AND m.createdAt > '${lastReadAt}'` : "";
|
|
1581
|
+
|
|
1582
|
+
// Count unread (excluding own messages)
|
|
1583
|
+
const channelUnread = db
|
|
1584
|
+
.prepare<{ count: number }, [string]>(
|
|
1585
|
+
`SELECT COUNT(*) as count FROM channel_messages m
|
|
1586
|
+
WHERE m.channelId = ? AND (m.agentId != '${agentId}' OR m.agentId IS NULL) ${baseCondition}`,
|
|
1587
|
+
)
|
|
1588
|
+
.get(channel.id);
|
|
1589
|
+
unreadCount += channelUnread?.count ?? 0;
|
|
1590
|
+
|
|
1591
|
+
// Count mentions in unread
|
|
1592
|
+
const channelMentions = db
|
|
1593
|
+
.prepare<{ count: number }, [string, string]>(
|
|
1594
|
+
`SELECT COUNT(*) as count FROM channel_messages m
|
|
1595
|
+
WHERE m.channelId = ? AND m.mentions LIKE ? ${baseCondition}`,
|
|
1596
|
+
)
|
|
1597
|
+
.get(channel.id, `%"${agentId}"%`);
|
|
1598
|
+
mentionsCount += channelMentions?.count ?? 0;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Count offered tasks for this agent
|
|
1602
|
+
const offeredResult = db
|
|
1603
|
+
.prepare<{ count: number }, [string]>(
|
|
1604
|
+
"SELECT COUNT(*) as count FROM agent_tasks WHERE offeredTo = ? AND status = 'offered'",
|
|
1605
|
+
)
|
|
1606
|
+
.get(agentId);
|
|
1607
|
+
|
|
1608
|
+
// Count unassigned tasks in pool
|
|
1609
|
+
const poolResult = db
|
|
1610
|
+
.prepare<{ count: number }, []>(
|
|
1611
|
+
"SELECT COUNT(*) as count FROM agent_tasks WHERE status = 'unassigned'",
|
|
1612
|
+
)
|
|
1613
|
+
.get();
|
|
1614
|
+
|
|
1615
|
+
// Count my in-progress tasks
|
|
1616
|
+
const inProgressResult = db
|
|
1617
|
+
.prepare<{ count: number }, [string]>(
|
|
1618
|
+
"SELECT COUNT(*) as count FROM agent_tasks WHERE agentId = ? AND status = 'in_progress'",
|
|
1619
|
+
)
|
|
1620
|
+
.get(agentId);
|
|
1621
|
+
|
|
1622
|
+
// Get recent unread @mentions (up to 3)
|
|
1623
|
+
const recentMentions: MentionPreview[] = [];
|
|
1624
|
+
const mentionMessages = getMentionsForAgent(agentId, { unreadOnly: false });
|
|
1625
|
+
|
|
1626
|
+
// Filter to only unread mentions and limit to 3
|
|
1627
|
+
for (const msg of mentionMessages) {
|
|
1628
|
+
if (recentMentions.length >= 3) break;
|
|
1629
|
+
|
|
1630
|
+
// Check if message is unread (by checking against read state per channel)
|
|
1631
|
+
const lastReadAt = getLastReadAt(agentId, msg.channelId);
|
|
1632
|
+
if (lastReadAt && new Date(msg.createdAt) <= new Date(lastReadAt)) {
|
|
1633
|
+
continue; // Already read
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Get channel name
|
|
1637
|
+
const channel = getChannelById(msg.channelId);
|
|
1638
|
+
|
|
1639
|
+
recentMentions.push({
|
|
1640
|
+
channelName: channel?.name ?? "unknown",
|
|
1641
|
+
agentName: msg.agentName ?? "Unknown",
|
|
1642
|
+
content: msg.content.length > 100 ? `${msg.content.slice(0, 100)}...` : msg.content,
|
|
1643
|
+
createdAt: msg.createdAt,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
return {
|
|
1648
|
+
unreadCount,
|
|
1649
|
+
mentionsCount,
|
|
1650
|
+
offeredTasksCount: offeredResult?.count ?? 0,
|
|
1651
|
+
poolTasksCount: poolResult?.count ?? 0,
|
|
1652
|
+
inProgressCount: inProgressResult?.count ?? 0,
|
|
1653
|
+
recentMentions,
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// ============================================================================
|
|
1658
|
+
// Service Operations (PM2/background services)
|
|
1659
|
+
// ============================================================================
|
|
1660
|
+
|
|
1661
|
+
type ServiceRow = {
|
|
1662
|
+
id: string;
|
|
1663
|
+
agentId: string;
|
|
1664
|
+
name: string;
|
|
1665
|
+
port: number;
|
|
1666
|
+
description: string | null;
|
|
1667
|
+
url: string | null;
|
|
1668
|
+
healthCheckPath: string | null;
|
|
1669
|
+
status: ServiceStatus;
|
|
1670
|
+
// PM2 configuration
|
|
1671
|
+
script: string;
|
|
1672
|
+
cwd: string | null;
|
|
1673
|
+
interpreter: string | null;
|
|
1674
|
+
args: string | null; // JSON array
|
|
1675
|
+
env: string | null; // JSON object
|
|
1676
|
+
metadata: string | null;
|
|
1677
|
+
createdAt: string;
|
|
1678
|
+
lastUpdatedAt: string;
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
function rowToService(row: ServiceRow): Service {
|
|
1682
|
+
return {
|
|
1683
|
+
id: row.id,
|
|
1684
|
+
agentId: row.agentId,
|
|
1685
|
+
name: row.name,
|
|
1686
|
+
port: row.port,
|
|
1687
|
+
description: row.description ?? undefined,
|
|
1688
|
+
url: row.url ?? undefined,
|
|
1689
|
+
healthCheckPath: row.healthCheckPath ?? "/health",
|
|
1690
|
+
status: row.status,
|
|
1691
|
+
// PM2 configuration
|
|
1692
|
+
script: row.script,
|
|
1693
|
+
cwd: row.cwd ?? undefined,
|
|
1694
|
+
interpreter: row.interpreter ?? undefined,
|
|
1695
|
+
args: row.args ? JSON.parse(row.args) : undefined,
|
|
1696
|
+
env: row.env ? JSON.parse(row.env) : undefined,
|
|
1697
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
1698
|
+
createdAt: row.createdAt,
|
|
1699
|
+
lastUpdatedAt: row.lastUpdatedAt,
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
export interface CreateServiceOptions {
|
|
1704
|
+
port?: number;
|
|
1705
|
+
description?: string;
|
|
1706
|
+
url?: string;
|
|
1707
|
+
healthCheckPath?: string;
|
|
1708
|
+
// PM2 configuration
|
|
1709
|
+
script: string; // Required
|
|
1710
|
+
cwd?: string;
|
|
1711
|
+
interpreter?: string;
|
|
1712
|
+
args?: string[];
|
|
1713
|
+
env?: Record<string, string>;
|
|
1714
|
+
metadata?: Record<string, unknown>;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
export function createService(
|
|
1718
|
+
agentId: string,
|
|
1719
|
+
name: string,
|
|
1720
|
+
options: CreateServiceOptions,
|
|
1721
|
+
): Service {
|
|
1722
|
+
const id = crypto.randomUUID();
|
|
1723
|
+
const now = new Date().toISOString();
|
|
1724
|
+
|
|
1725
|
+
const row = getDb()
|
|
1726
|
+
.prepare<ServiceRow, (string | number | null)[]>(
|
|
1727
|
+
`INSERT INTO services (id, agentId, name, port, description, url, healthCheckPath, status, script, cwd, interpreter, args, env, metadata, createdAt, lastUpdatedAt)
|
|
1728
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
1729
|
+
)
|
|
1730
|
+
.get(
|
|
1731
|
+
id,
|
|
1732
|
+
agentId,
|
|
1733
|
+
name,
|
|
1734
|
+
options.port ?? 3000,
|
|
1735
|
+
options.description ?? null,
|
|
1736
|
+
options.url ?? null,
|
|
1737
|
+
options.healthCheckPath ?? "/health",
|
|
1738
|
+
options.script,
|
|
1739
|
+
options.cwd ?? null,
|
|
1740
|
+
options.interpreter ?? null,
|
|
1741
|
+
options.args ? JSON.stringify(options.args) : null,
|
|
1742
|
+
options.env ? JSON.stringify(options.env) : null,
|
|
1743
|
+
JSON.stringify(options.metadata ?? {}),
|
|
1744
|
+
now,
|
|
1745
|
+
now,
|
|
1746
|
+
);
|
|
1747
|
+
|
|
1748
|
+
if (!row) throw new Error("Failed to create service");
|
|
1749
|
+
|
|
1750
|
+
try {
|
|
1751
|
+
createLogEntry({
|
|
1752
|
+
eventType: "service_registered",
|
|
1753
|
+
agentId,
|
|
1754
|
+
newValue: name,
|
|
1755
|
+
metadata: { serviceId: id, port: options?.port ?? 3000 },
|
|
1756
|
+
});
|
|
1757
|
+
} catch {}
|
|
1758
|
+
|
|
1759
|
+
return rowToService(row);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export function getServiceById(id: string): Service | null {
|
|
1763
|
+
const row = getDb().prepare<ServiceRow, [string]>("SELECT * FROM services WHERE id = ?").get(id);
|
|
1764
|
+
return row ? rowToService(row) : null;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
export function getServiceByAgentAndName(agentId: string, name: string): Service | null {
|
|
1768
|
+
const row = getDb()
|
|
1769
|
+
.prepare<ServiceRow, [string, string]>("SELECT * FROM services WHERE agentId = ? AND name = ?")
|
|
1770
|
+
.get(agentId, name);
|
|
1771
|
+
return row ? rowToService(row) : null;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
export function getServicesByAgentId(agentId: string): Service[] {
|
|
1775
|
+
return getDb()
|
|
1776
|
+
.prepare<ServiceRow, [string]>("SELECT * FROM services WHERE agentId = ? ORDER BY name")
|
|
1777
|
+
.all(agentId)
|
|
1778
|
+
.map(rowToService);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
export interface ServiceFilters {
|
|
1782
|
+
agentId?: string;
|
|
1783
|
+
name?: string;
|
|
1784
|
+
status?: ServiceStatus;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
export function getAllServices(filters?: ServiceFilters): Service[] {
|
|
1788
|
+
const conditions: string[] = [];
|
|
1789
|
+
const params: string[] = [];
|
|
1790
|
+
|
|
1791
|
+
if (filters?.agentId) {
|
|
1792
|
+
conditions.push("agentId = ?");
|
|
1793
|
+
params.push(filters.agentId);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (filters?.name) {
|
|
1797
|
+
conditions.push("name LIKE ?");
|
|
1798
|
+
params.push(`%${filters.name}%`);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (filters?.status) {
|
|
1802
|
+
conditions.push("status = ?");
|
|
1803
|
+
params.push(filters.status);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1807
|
+
const query = `SELECT * FROM services ${whereClause} ORDER BY
|
|
1808
|
+
CASE status
|
|
1809
|
+
WHEN 'healthy' THEN 1
|
|
1810
|
+
WHEN 'starting' THEN 2
|
|
1811
|
+
WHEN 'unhealthy' THEN 3
|
|
1812
|
+
WHEN 'stopped' THEN 4
|
|
1813
|
+
END, name`;
|
|
1814
|
+
|
|
1815
|
+
return getDb()
|
|
1816
|
+
.prepare<ServiceRow, string[]>(query)
|
|
1817
|
+
.all(...params)
|
|
1818
|
+
.map(rowToService);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
export function updateServiceStatus(id: string, status: ServiceStatus): Service | null {
|
|
1822
|
+
const oldService = getServiceById(id);
|
|
1823
|
+
if (!oldService) return null;
|
|
1824
|
+
|
|
1825
|
+
const now = new Date().toISOString();
|
|
1826
|
+
const row = getDb()
|
|
1827
|
+
.prepare<ServiceRow, [ServiceStatus, string, string]>(
|
|
1828
|
+
`UPDATE services SET status = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *`,
|
|
1829
|
+
)
|
|
1830
|
+
.get(status, now, id);
|
|
1831
|
+
|
|
1832
|
+
if (row && oldService.status !== status) {
|
|
1833
|
+
try {
|
|
1834
|
+
createLogEntry({
|
|
1835
|
+
eventType: "service_status_change",
|
|
1836
|
+
agentId: oldService.agentId,
|
|
1837
|
+
oldValue: oldService.status,
|
|
1838
|
+
newValue: status,
|
|
1839
|
+
metadata: { serviceId: id, serviceName: oldService.name },
|
|
1840
|
+
});
|
|
1841
|
+
} catch {}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
return row ? rowToService(row) : null;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
export function deleteService(id: string): boolean {
|
|
1848
|
+
const service = getServiceById(id);
|
|
1849
|
+
if (service) {
|
|
1850
|
+
try {
|
|
1851
|
+
createLogEntry({
|
|
1852
|
+
eventType: "service_unregistered",
|
|
1853
|
+
agentId: service.agentId,
|
|
1854
|
+
oldValue: service.name,
|
|
1855
|
+
metadata: { serviceId: id },
|
|
1856
|
+
});
|
|
1857
|
+
} catch {}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const result = getDb().run("DELETE FROM services WHERE id = ?", [id]);
|
|
1861
|
+
return result.changes > 0;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/** Upsert a service - update if exists (by agentId + name), create if not */
|
|
1865
|
+
export function upsertService(
|
|
1866
|
+
agentId: string,
|
|
1867
|
+
name: string,
|
|
1868
|
+
options: CreateServiceOptions,
|
|
1869
|
+
): Service {
|
|
1870
|
+
const existing = getServiceByAgentAndName(agentId, name);
|
|
1871
|
+
|
|
1872
|
+
if (existing) {
|
|
1873
|
+
// Update existing service
|
|
1874
|
+
const now = new Date().toISOString();
|
|
1875
|
+
const row = getDb()
|
|
1876
|
+
.prepare<ServiceRow, (string | number | null)[]>(
|
|
1877
|
+
`UPDATE services SET
|
|
1878
|
+
port = ?, description = ?, url = ?, healthCheckPath = ?,
|
|
1879
|
+
script = ?, cwd = ?, interpreter = ?, args = ?, env = ?,
|
|
1880
|
+
metadata = ?, lastUpdatedAt = ?
|
|
1881
|
+
WHERE id = ? RETURNING *`,
|
|
1882
|
+
)
|
|
1883
|
+
.get(
|
|
1884
|
+
options.port ?? existing.port,
|
|
1885
|
+
options.description ?? existing.description ?? null,
|
|
1886
|
+
options.url ?? existing.url ?? null,
|
|
1887
|
+
options.healthCheckPath ?? existing.healthCheckPath ?? "/health",
|
|
1888
|
+
options.script,
|
|
1889
|
+
options.cwd ?? null,
|
|
1890
|
+
options.interpreter ?? null,
|
|
1891
|
+
options.args ? JSON.stringify(options.args) : null,
|
|
1892
|
+
options.env ? JSON.stringify(options.env) : null,
|
|
1893
|
+
JSON.stringify(options.metadata ?? existing.metadata ?? {}),
|
|
1894
|
+
now,
|
|
1895
|
+
existing.id,
|
|
1896
|
+
);
|
|
1897
|
+
|
|
1898
|
+
if (!row) throw new Error("Failed to update service");
|
|
1899
|
+
return rowToService(row);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Create new service
|
|
1903
|
+
return createService(agentId, name, options);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
export function deleteServicesByAgentId(agentId: string): number {
|
|
1907
|
+
const services = getServicesByAgentId(agentId);
|
|
1908
|
+
for (const service of services) {
|
|
1909
|
+
try {
|
|
1910
|
+
createLogEntry({
|
|
1911
|
+
eventType: "service_unregistered",
|
|
1912
|
+
agentId,
|
|
1913
|
+
oldValue: service.name,
|
|
1914
|
+
metadata: { serviceId: service.id },
|
|
1915
|
+
});
|
|
1916
|
+
} catch {}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const result = getDb().run("DELETE FROM services WHERE agentId = ?", [agentId]);
|
|
1920
|
+
return result.changes;
|
|
1921
|
+
}
|