@desplega.ai/agent-swarm 1.9.0 → 1.10.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/Dockerfile +43 -5
- package/README.md +16 -0
- package/UI.md +40 -0
- package/deploy/prod-db.ts +2 -2
- package/docker-compose.example.yml +49 -50
- package/docker-entrypoint.sh +4 -1
- package/package.json +1 -1
- package/plugin/commands/review-offered-task.md +45 -0
- package/plugin/commands/start-leader.md +7 -5
- package/plugin/commands/start-worker.md +5 -0
- package/src/cli.tsx +44 -5
- package/src/commands/lead.ts +2 -0
- package/src/commands/runner.ts +273 -45
- package/src/commands/worker.ts +2 -0
- package/src/http.ts +145 -1
- package/src/prompts/base-prompt.ts +17 -3
- package/src/tests/runner-polling-api.test.ts +456 -0
- package/src/utils/pretty-print.ts +137 -120
- package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +934 -0
- package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +582 -0
package/src/commands/runner.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface RunnerConfig {
|
|
|
33
33
|
defaultPrompt: string;
|
|
34
34
|
/** Metadata type for log files, e.g., "worker_metadata" */
|
|
35
35
|
metadataType: string;
|
|
36
|
+
/** Optional capabilities of the agent */
|
|
37
|
+
capabilities?: string[];
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface RunnerOptions {
|
|
@@ -40,7 +42,9 @@ export interface RunnerOptions {
|
|
|
40
42
|
yolo?: boolean;
|
|
41
43
|
systemPrompt?: string;
|
|
42
44
|
systemPromptFile?: string;
|
|
45
|
+
logsDir?: string;
|
|
43
46
|
additionalArgs?: string[];
|
|
47
|
+
aiLoop?: boolean; // Use AI-based loop (old behavior)
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
interface RunClaudeIterationOptions {
|
|
@@ -51,6 +55,118 @@ interface RunClaudeIterationOptions {
|
|
|
51
55
|
role: string;
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
/** Trigger types returned by the poll API */
|
|
59
|
+
interface Trigger {
|
|
60
|
+
type: "task_assigned" | "task_offered" | "unread_mentions" | "pool_tasks_available";
|
|
61
|
+
taskId?: string;
|
|
62
|
+
task?: unknown;
|
|
63
|
+
mentionsCount?: number;
|
|
64
|
+
count?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Options for polling */
|
|
68
|
+
interface PollOptions {
|
|
69
|
+
apiUrl: string;
|
|
70
|
+
apiKey: string;
|
|
71
|
+
agentId: string;
|
|
72
|
+
pollInterval: number;
|
|
73
|
+
pollTimeout: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Register agent via HTTP API */
|
|
77
|
+
async function registerAgent(opts: {
|
|
78
|
+
apiUrl: string;
|
|
79
|
+
apiKey: string;
|
|
80
|
+
agentId: string;
|
|
81
|
+
name: string;
|
|
82
|
+
isLead: boolean;
|
|
83
|
+
capabilities?: string[];
|
|
84
|
+
}): Promise<void> {
|
|
85
|
+
const headers: Record<string, string> = {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"X-Agent-ID": opts.agentId,
|
|
88
|
+
};
|
|
89
|
+
if (opts.apiKey) {
|
|
90
|
+
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const response = await fetch(`${opts.apiUrl}/api/agents`, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers,
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
name: opts.name,
|
|
98
|
+
isLead: opts.isLead,
|
|
99
|
+
capabilities: opts.capabilities,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const error = await response.text();
|
|
105
|
+
throw new Error(`Failed to register agent: ${response.status} ${error}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Poll for triggers via HTTP API */
|
|
110
|
+
async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
|
|
111
|
+
const startTime = Date.now();
|
|
112
|
+
const headers: Record<string, string> = {
|
|
113
|
+
"X-Agent-ID": opts.agentId,
|
|
114
|
+
};
|
|
115
|
+
if (opts.apiKey) {
|
|
116
|
+
headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
while (Date.now() - startTime < opts.pollTimeout) {
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`${opts.apiUrl}/api/poll`, {
|
|
122
|
+
method: "GET",
|
|
123
|
+
headers,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
console.warn(`[runner] Poll request failed: ${response.status}`);
|
|
128
|
+
await Bun.sleep(opts.pollInterval);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const data = (await response.json()) as { trigger: Trigger | null };
|
|
133
|
+
if (data.trigger) {
|
|
134
|
+
return data.trigger;
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn(`[runner] Poll request error: ${error}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await Bun.sleep(opts.pollInterval);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return null; // Timeout reached, no trigger found
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Build prompt based on trigger type */
|
|
147
|
+
function buildPromptForTrigger(trigger: Trigger, defaultPrompt: string): string {
|
|
148
|
+
switch (trigger.type) {
|
|
149
|
+
case "task_assigned":
|
|
150
|
+
// Use the work-on-task command with task ID
|
|
151
|
+
return `/work-on-task ${trigger.taskId}`;
|
|
152
|
+
|
|
153
|
+
case "task_offered":
|
|
154
|
+
// Use the review-offered-task command to accept/reject
|
|
155
|
+
return `/review-offered-task ${trigger.taskId}`;
|
|
156
|
+
|
|
157
|
+
case "unread_mentions":
|
|
158
|
+
// Check messages
|
|
159
|
+
return "/swarm-chat";
|
|
160
|
+
|
|
161
|
+
case "pool_tasks_available":
|
|
162
|
+
// Let lead review and assign tasks
|
|
163
|
+
return defaultPrompt;
|
|
164
|
+
|
|
165
|
+
default:
|
|
166
|
+
return defaultPrompt;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
54
170
|
async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<number> {
|
|
55
171
|
const { role } = opts;
|
|
56
172
|
const CMD = [
|
|
@@ -139,7 +255,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
139
255
|
setupShutdownHandlers(role);
|
|
140
256
|
|
|
141
257
|
const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
|
|
142
|
-
const baseLogDir = process.env.LOG_DIR || "/logs";
|
|
258
|
+
const baseLogDir = opts.logsDir || process.env.LOG_DIR || "/logs";
|
|
143
259
|
const logDir = `${baseLogDir}/${sessionId}`;
|
|
144
260
|
|
|
145
261
|
await mkdir(logDir, { recursive: true });
|
|
@@ -149,10 +265,14 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
149
265
|
|
|
150
266
|
// Get agent identity and swarm URL for base prompt
|
|
151
267
|
const agentId = process.env.AGENT_ID || "unknown";
|
|
268
|
+
|
|
269
|
+
const apiUrl = process.env.MCP_BASE_URL || "http://localhost:3013";
|
|
152
270
|
const swarmUrl = process.env.SWARM_URL || "localhost";
|
|
153
271
|
|
|
272
|
+
const capabilities = config.capabilities;
|
|
273
|
+
|
|
154
274
|
// Generate base prompt that's always included
|
|
155
|
-
const basePrompt = getBasePrompt({ role, agentId, swarmUrl });
|
|
275
|
+
const basePrompt = getBasePrompt({ role, agentId, swarmUrl, capabilities });
|
|
156
276
|
|
|
157
277
|
// Resolve additional system prompt: CLI flag > env var
|
|
158
278
|
let additionalSystemPrompt: string | undefined;
|
|
@@ -189,67 +309,175 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
|
|
|
189
309
|
: basePrompt;
|
|
190
310
|
|
|
191
311
|
console.log(`[${role}] Starting ${role}`);
|
|
312
|
+
console.log(`[${role}] Agent ID: ${agentId}`);
|
|
192
313
|
console.log(`[${role}] Session ID: ${sessionId}`);
|
|
193
314
|
console.log(`[${role}] Log directory: ${logDir}`);
|
|
194
315
|
console.log(`[${role}] YOLO mode: ${isYolo ? "enabled" : "disabled"}`);
|
|
195
316
|
console.log(`[${role}] Prompt: ${prompt}`);
|
|
196
|
-
console.log(`[${role}]
|
|
317
|
+
console.log(`[${role}] API URL: ${apiUrl}`);
|
|
318
|
+
console.log(`[${role}] Swarm URL: ${apiUrl}`);
|
|
197
319
|
console.log(`[${role}] Base prompt: included (${basePrompt.length} chars)`);
|
|
198
320
|
console.log(
|
|
199
321
|
`[${role}] Additional system prompt: ${additionalSystemPrompt ? "provided" : "none"}`,
|
|
200
322
|
);
|
|
201
323
|
console.log(`[${role}] Total system prompt length: ${resolvedSystemPrompt.length} chars`);
|
|
202
324
|
|
|
325
|
+
const isAiLoop = opts.aiLoop || process.env.AI_LOOP === "true";
|
|
326
|
+
const apiKey = process.env.API_KEY || "";
|
|
327
|
+
|
|
328
|
+
// Constants for polling
|
|
329
|
+
const POLL_INTERVAL_MS = 2000; // 2 seconds between polls
|
|
330
|
+
const POLL_TIMEOUT_MS = 60000; // 1 minute timeout before retrying
|
|
331
|
+
|
|
203
332
|
let iteration = 0;
|
|
204
333
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
334
|
+
if (!isAiLoop) {
|
|
335
|
+
// NEW: Runner-level polling mode
|
|
336
|
+
console.log(`[${role}] Mode: runner-level polling (use --ai-loop for AI-based polling)`);
|
|
337
|
+
|
|
338
|
+
// Register agent before starting
|
|
339
|
+
const agentName = process.env.AGENT_NAME || `${role}-${agentId.slice(0, 8)}`;
|
|
340
|
+
try {
|
|
341
|
+
await registerAgent({
|
|
342
|
+
apiUrl,
|
|
343
|
+
apiKey,
|
|
344
|
+
agentId,
|
|
345
|
+
name: agentName,
|
|
346
|
+
isLead: role === "lead",
|
|
347
|
+
capabilities: config.capabilities,
|
|
348
|
+
});
|
|
349
|
+
console.log(`[${role}] Registered as "${agentName}" (ID: ${agentId})`);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error(`[${role}] Failed to register: ${error}`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
while (true) {
|
|
356
|
+
console.log(`\n[${role}] Polling for triggers...`);
|
|
357
|
+
|
|
358
|
+
const trigger = await pollForTrigger({
|
|
359
|
+
apiUrl,
|
|
360
|
+
apiKey,
|
|
361
|
+
agentId,
|
|
362
|
+
pollInterval: POLL_INTERVAL_MS,
|
|
363
|
+
pollTimeout: POLL_TIMEOUT_MS,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!trigger) {
|
|
367
|
+
console.log(`[${role}] No trigger found, polling again...`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
console.log(`[${role}] Trigger received: ${trigger.type}`);
|
|
372
|
+
|
|
373
|
+
// Build prompt based on trigger
|
|
374
|
+
const triggerPrompt = buildPromptForTrigger(trigger, prompt);
|
|
375
|
+
|
|
376
|
+
iteration++;
|
|
377
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
378
|
+
const logFile = `${logDir}/${timestamp}.jsonl`;
|
|
379
|
+
|
|
380
|
+
console.log(`\n[${role}] === Iteration ${iteration} ===`);
|
|
381
|
+
console.log(`[${role}] Logging to: ${logFile}`);
|
|
382
|
+
console.log(`[${role}] Prompt: ${triggerPrompt}`);
|
|
383
|
+
|
|
384
|
+
const metadata = {
|
|
385
|
+
type: metadataType,
|
|
386
|
+
sessionId,
|
|
234
387
|
iteration,
|
|
235
|
-
|
|
236
|
-
|
|
388
|
+
timestamp: new Date().toISOString(),
|
|
389
|
+
prompt: triggerPrompt,
|
|
390
|
+
trigger: trigger.type,
|
|
391
|
+
yolo: isYolo,
|
|
237
392
|
};
|
|
393
|
+
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
394
|
+
|
|
395
|
+
const exitCode = await runClaudeIteration({
|
|
396
|
+
prompt: triggerPrompt,
|
|
397
|
+
logFile,
|
|
398
|
+
systemPrompt: resolvedSystemPrompt,
|
|
399
|
+
additionalArgs: opts.additionalArgs,
|
|
400
|
+
role,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
if (exitCode !== 0) {
|
|
404
|
+
const errorLog = {
|
|
405
|
+
timestamp: new Date().toISOString(),
|
|
406
|
+
iteration,
|
|
407
|
+
exitCode,
|
|
408
|
+
trigger: trigger.type,
|
|
409
|
+
error: true,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const errorsFile = `${logDir}/errors.jsonl`;
|
|
413
|
+
const errorsFileRef = Bun.file(errorsFile);
|
|
414
|
+
const existingErrors = (await errorsFileRef.exists()) ? await errorsFileRef.text() : "";
|
|
415
|
+
await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
|
|
416
|
+
|
|
417
|
+
if (!isYolo) {
|
|
418
|
+
console.error(`[${role}] Claude exited with code ${exitCode}. Stopping.`);
|
|
419
|
+
console.error(`[${role}] Error logged to: ${errorsFile}`);
|
|
420
|
+
process.exit(exitCode);
|
|
421
|
+
}
|
|
238
422
|
|
|
239
|
-
|
|
240
|
-
const errorsFileRef = Bun.file(errorsFile);
|
|
241
|
-
const existingErrors = (await errorsFileRef.exists()) ? await errorsFileRef.text() : "";
|
|
242
|
-
await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
|
|
243
|
-
|
|
244
|
-
if (!isYolo) {
|
|
245
|
-
console.error(`[${role}] Claude exited with code ${exitCode}. Stopping.`);
|
|
246
|
-
console.error(`[${role}] Error logged to: ${errorsFile}`);
|
|
247
|
-
process.exit(exitCode);
|
|
423
|
+
console.warn(`[${role}] Claude exited with code ${exitCode}. YOLO mode - continuing...`);
|
|
248
424
|
}
|
|
249
425
|
|
|
250
|
-
console.
|
|
426
|
+
console.log(`[${role}] Iteration ${iteration} complete. Polling for next trigger...`);
|
|
251
427
|
}
|
|
428
|
+
} else {
|
|
429
|
+
// Original AI-loop mode (existing behavior)
|
|
430
|
+
console.log(`[${role}] Mode: AI-based polling (legacy)`);
|
|
431
|
+
|
|
432
|
+
while (true) {
|
|
433
|
+
iteration++;
|
|
434
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
435
|
+
const logFile = `${logDir}/${timestamp}.jsonl`;
|
|
436
|
+
|
|
437
|
+
console.log(`\n[${role}] === Iteration ${iteration} ===`);
|
|
438
|
+
console.log(`[${role}] Logging to: ${logFile}`);
|
|
252
439
|
|
|
253
|
-
|
|
440
|
+
const metadata = {
|
|
441
|
+
type: metadataType,
|
|
442
|
+
sessionId,
|
|
443
|
+
iteration,
|
|
444
|
+
timestamp: new Date().toISOString(),
|
|
445
|
+
prompt,
|
|
446
|
+
yolo: isYolo,
|
|
447
|
+
};
|
|
448
|
+
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
449
|
+
|
|
450
|
+
const exitCode = await runClaudeIteration({
|
|
451
|
+
prompt,
|
|
452
|
+
logFile,
|
|
453
|
+
systemPrompt: resolvedSystemPrompt,
|
|
454
|
+
additionalArgs: opts.additionalArgs,
|
|
455
|
+
role,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (exitCode !== 0) {
|
|
459
|
+
const errorLog = {
|
|
460
|
+
timestamp: new Date().toISOString(),
|
|
461
|
+
iteration,
|
|
462
|
+
exitCode,
|
|
463
|
+
error: true,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const errorsFile = `${logDir}/errors.jsonl`;
|
|
467
|
+
const errorsFileRef = Bun.file(errorsFile);
|
|
468
|
+
const existingErrors = (await errorsFileRef.exists()) ? await errorsFileRef.text() : "";
|
|
469
|
+
await Bun.write(errorsFile, `${existingErrors}${JSON.stringify(errorLog)}\n`);
|
|
470
|
+
|
|
471
|
+
if (!isYolo) {
|
|
472
|
+
console.error(`[${role}] Claude exited with code ${exitCode}. Stopping.`);
|
|
473
|
+
console.error(`[${role}] Error logged to: ${errorsFile}`);
|
|
474
|
+
process.exit(exitCode);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
console.warn(`[${role}] Claude exited with code ${exitCode}. YOLO mode - continuing...`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log(`[${role}] Iteration ${iteration} complete. Starting next iteration...`);
|
|
481
|
+
}
|
|
254
482
|
}
|
|
255
483
|
}
|
package/src/commands/worker.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getEnabledCapabilities } from "@/server.ts";
|
|
1
2
|
import { type RunnerConfig, type RunnerOptions, runAgent } from "./runner.ts";
|
|
2
3
|
|
|
3
4
|
export type WorkerOptions = RunnerOptions;
|
|
@@ -6,6 +7,7 @@ const workerConfig: RunnerConfig = {
|
|
|
6
7
|
role: "worker",
|
|
7
8
|
defaultPrompt: "/start-worker",
|
|
8
9
|
metadataType: "worker_metadata",
|
|
10
|
+
capabilities: getEnabledCapabilities(),
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
export async function runWorker(opts: WorkerOptions) {
|
package/src/http.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
10
10
|
import { createServer } from "@/server";
|
|
11
11
|
import {
|
|
12
12
|
closeDb,
|
|
13
|
+
createAgent,
|
|
13
14
|
getAgentById,
|
|
14
15
|
getAgentWithTasks,
|
|
15
16
|
getAllAgents,
|
|
@@ -24,8 +25,11 @@ import {
|
|
|
24
25
|
getInboxSummary,
|
|
25
26
|
getLogsByAgentId,
|
|
26
27
|
getLogsByTaskId,
|
|
28
|
+
getOfferedTasksForAgent,
|
|
29
|
+
getPendingTaskForAgent,
|
|
27
30
|
getServicesByAgentId,
|
|
28
31
|
getTaskById,
|
|
32
|
+
getUnassignedTasksCount,
|
|
29
33
|
postMessage,
|
|
30
34
|
updateAgentStatus,
|
|
31
35
|
} from "./be/db";
|
|
@@ -209,6 +213,147 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
209
213
|
return;
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Runner-Level Polling Endpoints
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
221
|
+
|
|
222
|
+
// POST /api/agents - Register a new agent (or return existing if already registered)
|
|
223
|
+
if (
|
|
224
|
+
req.method === "POST" &&
|
|
225
|
+
pathSegments[0] === "api" &&
|
|
226
|
+
pathSegments[1] === "agents" &&
|
|
227
|
+
!pathSegments[2]
|
|
228
|
+
) {
|
|
229
|
+
// Parse request body
|
|
230
|
+
const chunks: Buffer[] = [];
|
|
231
|
+
for await (const chunk of req) {
|
|
232
|
+
chunks.push(chunk);
|
|
233
|
+
}
|
|
234
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
235
|
+
|
|
236
|
+
// Validate required fields
|
|
237
|
+
if (!body.name || typeof body.name !== "string") {
|
|
238
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
239
|
+
res.end(JSON.stringify({ error: "Missing or invalid 'name' field" }));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Use X-Agent-ID header if provided, otherwise generate new UUID
|
|
244
|
+
const agentId = myAgentId || crypto.randomUUID();
|
|
245
|
+
|
|
246
|
+
// Use transaction to ensure atomicity of check-and-create/update
|
|
247
|
+
const result = getDb().transaction(() => {
|
|
248
|
+
// Check if agent already exists
|
|
249
|
+
const existingAgent = getAgentById(agentId);
|
|
250
|
+
if (existingAgent) {
|
|
251
|
+
// Update status to idle if offline
|
|
252
|
+
if (existingAgent.status === "offline") {
|
|
253
|
+
updateAgentStatus(existingAgent.id, "idle");
|
|
254
|
+
}
|
|
255
|
+
return { agent: getAgentById(agentId), created: false };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Create new agent
|
|
259
|
+
const agent = createAgent({
|
|
260
|
+
id: agentId,
|
|
261
|
+
name: body.name,
|
|
262
|
+
isLead: body.isLead ?? false,
|
|
263
|
+
status: "idle",
|
|
264
|
+
description: body.description,
|
|
265
|
+
role: body.role,
|
|
266
|
+
capabilities: body.capabilities,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return { agent, created: true };
|
|
270
|
+
})();
|
|
271
|
+
|
|
272
|
+
res.writeHead(result.created ? 201 : 200, { "Content-Type": "application/json" });
|
|
273
|
+
res.end(JSON.stringify(result.agent));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// GET /api/poll - Poll for triggers (tasks, mentions, etc.)
|
|
278
|
+
if (req.method === "GET" && pathSegments[0] === "api" && pathSegments[1] === "poll") {
|
|
279
|
+
if (!myAgentId) {
|
|
280
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
281
|
+
res.end(JSON.stringify({ error: "Missing X-Agent-ID header" }));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Use transaction for consistent reads across all trigger checks
|
|
286
|
+
const result = getDb().transaction(() => {
|
|
287
|
+
const agent = getAgentById(myAgentId);
|
|
288
|
+
if (!agent) {
|
|
289
|
+
return { error: "Agent not found", status: 404 };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check for offered tasks first (highest priority)
|
|
293
|
+
const offeredTasks = getOfferedTasksForAgent(myAgentId);
|
|
294
|
+
const firstOfferedTask = offeredTasks[0];
|
|
295
|
+
if (firstOfferedTask) {
|
|
296
|
+
return {
|
|
297
|
+
trigger: {
|
|
298
|
+
type: "task_offered",
|
|
299
|
+
taskId: firstOfferedTask.id,
|
|
300
|
+
task: firstOfferedTask,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check for pending tasks (assigned directly to this agent)
|
|
306
|
+
const pendingTask = getPendingTaskForAgent(myAgentId);
|
|
307
|
+
if (pendingTask) {
|
|
308
|
+
return {
|
|
309
|
+
trigger: {
|
|
310
|
+
type: "task_assigned",
|
|
311
|
+
taskId: pendingTask.id,
|
|
312
|
+
task: pendingTask,
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// For lead agents, check for unread mentions
|
|
318
|
+
if (agent.isLead) {
|
|
319
|
+
const inbox = getInboxSummary(myAgentId);
|
|
320
|
+
if (inbox.mentionsCount > 0) {
|
|
321
|
+
return {
|
|
322
|
+
trigger: {
|
|
323
|
+
type: "unread_mentions",
|
|
324
|
+
mentionsCount: inbox.mentionsCount,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check for tasks needing assignment (unassigned tasks in pool)
|
|
330
|
+
const unassignedCount = getUnassignedTasksCount();
|
|
331
|
+
if (unassignedCount > 0) {
|
|
332
|
+
return {
|
|
333
|
+
trigger: {
|
|
334
|
+
type: "pool_tasks_available",
|
|
335
|
+
count: unassignedCount,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// No trigger found
|
|
342
|
+
return { trigger: null };
|
|
343
|
+
})();
|
|
344
|
+
|
|
345
|
+
// Handle error case
|
|
346
|
+
if ("error" in result) {
|
|
347
|
+
res.writeHead(result.status ?? 500, { "Content-Type": "application/json" });
|
|
348
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
353
|
+
res.end(JSON.stringify(result));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
212
357
|
// GET /ecosystem - Generate PM2 ecosystem config for agent's services
|
|
213
358
|
if (req.method === "GET" && req.url === "/ecosystem") {
|
|
214
359
|
if (!myAgentId) {
|
|
@@ -249,7 +394,6 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
249
394
|
// REST API Endpoints (for frontend dashboard)
|
|
250
395
|
// ============================================================================
|
|
251
396
|
|
|
252
|
-
const pathSegments = getPathSegments(req.url || "");
|
|
253
397
|
const queryParams = parseQueryParams(req.url || "");
|
|
254
398
|
|
|
255
399
|
// GET /api/agents - List all agents (optionally with tasks)
|
|
@@ -38,9 +38,9 @@ As a worker agent of the swarm, you are responsible for executing tasks assigned
|
|
|
38
38
|
#### Useful tools for workers
|
|
39
39
|
|
|
40
40
|
- poll-task: Automatically waits for new tasks assigned by the lead or claimed from the unassigned pool.
|
|
41
|
-
- read-messages: To read messages sent to you by the lead or other workers, by default when a task is found, it will auto-assign it to you.
|
|
42
41
|
- store-progress: Critical tool to save your work and progress on tasks!
|
|
43
42
|
- task-action: Manage tasks with different actions like claim, release, accept, reject, and complete.
|
|
43
|
+
- read-messages: If communications enabled, use it to read messages sent to you by the lead or other workers, by default when a task is found, it will auto-assign it to you.
|
|
44
44
|
`;
|
|
45
45
|
|
|
46
46
|
const BASE_PROMPT_FILESYSTEM = `
|
|
@@ -57,8 +57,7 @@ const BASE_PROMPT_FILESYSTEM = `
|
|
|
57
57
|
const BASE_PROMPT_GUIDELINES = `
|
|
58
58
|
### Agent Swarm Operational Guidelines
|
|
59
59
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
60
|
+
- Follow the communicationes ettiquette and protocols established for the swarm. If not stated, do not use the chat features, focus on your tasks.
|
|
62
61
|
`;
|
|
63
62
|
|
|
64
63
|
const BASE_PROMPT_SYSTEM = `
|
|
@@ -67,7 +66,9 @@ const BASE_PROMPT_SYSTEM = `
|
|
|
67
66
|
You have a full Ubuntu environment with some packages pre-installed: node, bun, python3, curl, wget, git, gh, jq, etc.
|
|
68
67
|
|
|
69
68
|
If you need to install additional packages, use "sudo apt-get install {package_name}".
|
|
69
|
+
`;
|
|
70
70
|
|
|
71
|
+
const BASE_PROMPT_SERVICES = `
|
|
71
72
|
### External Swarm Access & Service Registry
|
|
72
73
|
|
|
73
74
|
Port 3000 is exposed for web apps or APIs. Use PM2 for robust process management:
|
|
@@ -108,6 +109,7 @@ export type BasePromptArgs = {
|
|
|
108
109
|
role: string;
|
|
109
110
|
agentId: string;
|
|
110
111
|
swarmUrl: string;
|
|
112
|
+
capabilities?: string[];
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
export const getBasePrompt = (args: BasePromptArgs): string => {
|
|
@@ -127,5 +129,17 @@ export const getBasePrompt = (args: BasePromptArgs): string => {
|
|
|
127
129
|
prompt += BASE_PROMPT_GUIDELINES;
|
|
128
130
|
prompt += BASE_PROMPT_SYSTEM.replace("{swarmUrl}", swarmUrl);
|
|
129
131
|
|
|
132
|
+
if (!args.capabilities || args.capabilities.includes("services")) {
|
|
133
|
+
prompt += BASE_PROMPT_SERVICES;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (args.capabilities) {
|
|
137
|
+
prompt += `
|
|
138
|
+
### Capabilities enabled for this agent:
|
|
139
|
+
|
|
140
|
+
- ${args.capabilities.join("\n- ")}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
130
144
|
return prompt;
|
|
131
145
|
};
|