@desplega.ai/agent-swarm 1.0.2 → 1.2.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 +2 -1
- package/.dockerignore +58 -0
- package/.env.docker.example +12 -0
- package/Dockerfile.worker +112 -0
- package/README.md +117 -0
- package/cc-plugin/.claude-plugin/plugin.json +13 -0
- package/cc-plugin/README.md +49 -0
- package/cc-plugin/commands/setup-leader.md +73 -0
- package/cc-plugin/commands/start-worker.md +64 -0
- package/cc-plugin/hooks/hooks.json +71 -0
- package/deploy/DEPLOY.md +3 -0
- package/deploy/agent-swarm.service +3 -2
- package/deploy/install.ts +56 -6
- package/deploy/prod-db.ts +42 -0
- package/deploy/uninstall.ts +4 -2
- package/deploy/update.ts +21 -0
- package/docker-compose.worker.yml +35 -0
- package/docker-entrypoint.sh +62 -0
- package/package.json +9 -2
- package/src/be/db.ts +68 -20
- package/src/cli.tsx +96 -8
- package/src/commands/hook.ts +2 -2
- package/src/commands/setup.tsx +579 -550
- package/src/commands/worker.ts +225 -0
- package/src/hooks/hook.ts +180 -175
- package/src/server.ts +1 -2
- package/src/tools/get-task-details.ts +7 -3
- package/src/tools/join-swarm.ts +23 -11
- package/src/tools/poll-task.ts +34 -2
- package/src/tools/send-task.ts +40 -2
- package/src/tools/store-progress.ts +29 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
export interface WorkerOptions {
|
|
4
|
+
prompt?: string;
|
|
5
|
+
yolo?: boolean;
|
|
6
|
+
additionalArgs?: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface RunClaudeIterationOptions {
|
|
10
|
+
prompt: string;
|
|
11
|
+
logFile: string;
|
|
12
|
+
additionalArgs?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function runClaudeIteration(opts: RunClaudeIterationOptions): Promise<number> {
|
|
16
|
+
const CMD = [
|
|
17
|
+
"claude",
|
|
18
|
+
"--verbose",
|
|
19
|
+
"--output-format",
|
|
20
|
+
"stream-json",
|
|
21
|
+
"--dangerously-skip-permissions",
|
|
22
|
+
"--allow-dangerously-skip-permissions",
|
|
23
|
+
"--permission-mode",
|
|
24
|
+
"bypassPermissions",
|
|
25
|
+
"-p",
|
|
26
|
+
opts.prompt,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
if (opts.additionalArgs && opts.additionalArgs.length > 0) {
|
|
30
|
+
CMD.push(...opts.additionalArgs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`[worker] Running: claude ... -p "${opts.prompt}"`);
|
|
34
|
+
|
|
35
|
+
const logFileHandle = Bun.file(opts.logFile).writer();
|
|
36
|
+
|
|
37
|
+
// Collect stderr for better error reporting
|
|
38
|
+
let stderrOutput = "";
|
|
39
|
+
|
|
40
|
+
const proc = Bun.spawn(CMD, {
|
|
41
|
+
env: process.env,
|
|
42
|
+
stdout: "pipe",
|
|
43
|
+
stderr: "pipe",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
console.log(`[worker] Process spawned, PID: ${proc.pid}`);
|
|
47
|
+
console.log(`[worker] Waiting for output streams...`);
|
|
48
|
+
|
|
49
|
+
let stdoutChunks = 0;
|
|
50
|
+
let stderrChunks = 0;
|
|
51
|
+
|
|
52
|
+
// Read stdout and stderr concurrently
|
|
53
|
+
const stdoutPromise = (async () => {
|
|
54
|
+
console.log(`[worker] stdout stream: ${proc.stdout ? "available" : "not available"}`);
|
|
55
|
+
if (proc.stdout) {
|
|
56
|
+
for await (const chunk of proc.stdout) {
|
|
57
|
+
stdoutChunks++;
|
|
58
|
+
const text = new TextDecoder().decode(chunk);
|
|
59
|
+
logFileHandle.write(text);
|
|
60
|
+
console.log(`[worker] stdout chunk #${stdoutChunks} (${chunk.length} bytes)`);
|
|
61
|
+
|
|
62
|
+
// Also parse and log to console for visibility
|
|
63
|
+
const lines = text.split("\n");
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (line.trim() === "") continue;
|
|
66
|
+
try {
|
|
67
|
+
const json = JSON.parse(line.trim());
|
|
68
|
+
// Log a summary of what's happening
|
|
69
|
+
if (json.type === "assistant" && json.message) {
|
|
70
|
+
const preview = json.message.slice(0, 100);
|
|
71
|
+
console.log(
|
|
72
|
+
`[worker] Assistant: ${preview}${json.message.length > 100 ? "..." : ""}`,
|
|
73
|
+
);
|
|
74
|
+
} else if (json.type === "tool_use") {
|
|
75
|
+
console.log(`[worker] Tool: ${json.tool || json.name || "unknown"}`);
|
|
76
|
+
} else if (json.type === "result") {
|
|
77
|
+
// Log result details
|
|
78
|
+
const resultPreview = JSON.stringify(json).slice(0, 200);
|
|
79
|
+
console.log(
|
|
80
|
+
`[worker] Result: ${resultPreview}${JSON.stringify(json).length > 200 ? "..." : ""}`,
|
|
81
|
+
);
|
|
82
|
+
} else if (json.type === "error") {
|
|
83
|
+
console.error(
|
|
84
|
+
`[worker] Error from Claude: ${json.error || json.message || JSON.stringify(json)}`,
|
|
85
|
+
);
|
|
86
|
+
} else if (json.type === "system") {
|
|
87
|
+
// Log system message details
|
|
88
|
+
const msg = json.message || json.content || "";
|
|
89
|
+
const preview =
|
|
90
|
+
typeof msg === "string" ? msg.slice(0, 150) : JSON.stringify(msg).slice(0, 150);
|
|
91
|
+
console.log(`[worker] System: ${preview}${preview.length >= 150 ? "..." : ""}`);
|
|
92
|
+
} else {
|
|
93
|
+
// Log unknown event types with content
|
|
94
|
+
console.log(
|
|
95
|
+
`[worker] Event type: ${json.type} - ${JSON.stringify(json).slice(0, 100)}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Non-JSON line, just log it
|
|
100
|
+
if (line.trim()) {
|
|
101
|
+
console.log(`[worker] Raw output: ${line.trim()}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log(`[worker] stdout stream ended (total ${stdoutChunks} chunks)`);
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
const stderrPromise = (async () => {
|
|
111
|
+
console.log(`[worker] stderr stream: ${proc.stderr ? "available" : "not available"}`);
|
|
112
|
+
if (proc.stderr) {
|
|
113
|
+
for await (const chunk of proc.stderr) {
|
|
114
|
+
stderrChunks++;
|
|
115
|
+
const text = new TextDecoder().decode(chunk);
|
|
116
|
+
stderrOutput += text;
|
|
117
|
+
// Log stderr to console immediately
|
|
118
|
+
console.error(`[worker] stderr chunk #${stderrChunks}: ${text.trim()}`);
|
|
119
|
+
logFileHandle.write(
|
|
120
|
+
JSON.stringify({ type: "stderr", content: text, timestamp: new Date().toISOString() }) +
|
|
121
|
+
"\n",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
console.log(`[worker] stderr stream ended (total ${stderrChunks} chunks)`);
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
|
|
128
|
+
// Wait for both streams to finish
|
|
129
|
+
console.log(`[worker] Waiting for streams to complete...`);
|
|
130
|
+
await Promise.all([stdoutPromise, stderrPromise]);
|
|
131
|
+
|
|
132
|
+
await logFileHandle.end();
|
|
133
|
+
console.log(`[worker] Waiting for process to exit...`);
|
|
134
|
+
const exitCode = await proc.exited;
|
|
135
|
+
|
|
136
|
+
// Log final status
|
|
137
|
+
console.log(`[worker] Claude exited with code ${exitCode}`);
|
|
138
|
+
console.log(`[worker] Total stdout chunks: ${stdoutChunks}, stderr chunks: ${stderrChunks}`);
|
|
139
|
+
|
|
140
|
+
if (exitCode !== 0 && stderrOutput) {
|
|
141
|
+
console.error(`[worker] Full stderr output:\n${stderrOutput}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (stdoutChunks === 0 && stderrChunks === 0) {
|
|
145
|
+
console.warn(`[worker] WARNING: No output received from Claude at all!`);
|
|
146
|
+
console.warn(`[worker] This might indicate Claude failed to start or auth issues.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return exitCode ?? 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function runWorker(opts: WorkerOptions) {
|
|
153
|
+
const sessionId = process.env.SESSION_ID || crypto.randomUUID().slice(0, 8);
|
|
154
|
+
// WORKER_LOG_DIR env var for Docker, otherwise ./logs
|
|
155
|
+
const baseLogDir = process.env.WORKER_LOG_DIR || "./logs";
|
|
156
|
+
const logDir = `${baseLogDir}/${sessionId}`;
|
|
157
|
+
|
|
158
|
+
// Create log directory
|
|
159
|
+
await mkdir(logDir, { recursive: true });
|
|
160
|
+
|
|
161
|
+
const defaultPrompt = "/start-worker Start or continue the tasks your leader assigned you!";
|
|
162
|
+
const prompt = opts.prompt || defaultPrompt;
|
|
163
|
+
|
|
164
|
+
const isYolo = opts.yolo || process.env.WORKER_YOLO === "true";
|
|
165
|
+
|
|
166
|
+
console.log(`[worker] Starting worker`);
|
|
167
|
+
console.log(`[worker] Session ID: ${sessionId}`);
|
|
168
|
+
console.log(`[worker] Log directory: ${logDir}`);
|
|
169
|
+
console.log(`[worker] YOLO mode: ${isYolo ? "enabled" : "disabled"}`);
|
|
170
|
+
console.log(`[worker] Prompt: ${prompt}`);
|
|
171
|
+
|
|
172
|
+
let iteration = 0;
|
|
173
|
+
|
|
174
|
+
while (true) {
|
|
175
|
+
iteration++;
|
|
176
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
177
|
+
const logFile = `${logDir}/${timestamp}.jsonl`;
|
|
178
|
+
|
|
179
|
+
console.log(`\n[worker] === Iteration ${iteration} ===`);
|
|
180
|
+
console.log(`[worker] Logging to: ${logFile}`);
|
|
181
|
+
|
|
182
|
+
// Write iteration metadata at the start of each log file
|
|
183
|
+
const metadata = {
|
|
184
|
+
type: "worker_metadata",
|
|
185
|
+
sessionId,
|
|
186
|
+
iteration,
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
prompt,
|
|
189
|
+
yolo: isYolo,
|
|
190
|
+
};
|
|
191
|
+
await Bun.write(logFile, `${JSON.stringify(metadata)}\n`);
|
|
192
|
+
|
|
193
|
+
const exitCode = await runClaudeIteration({
|
|
194
|
+
prompt,
|
|
195
|
+
logFile,
|
|
196
|
+
additionalArgs: opts.additionalArgs,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (exitCode !== 0) {
|
|
200
|
+
const errorLog = {
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
iteration,
|
|
203
|
+
exitCode,
|
|
204
|
+
error: true,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Append to errors.jsonl
|
|
208
|
+
const errorsFile = `${logDir}/errors.jsonl`;
|
|
209
|
+
const existingErrors = (await Bun.file(errorsFile).exists())
|
|
210
|
+
? await Bun.file(errorsFile).text()
|
|
211
|
+
: "";
|
|
212
|
+
await Bun.write(errorsFile, `${existingErrors + JSON.stringify(errorLog)}\n`);
|
|
213
|
+
|
|
214
|
+
if (!isYolo) {
|
|
215
|
+
console.error(`[worker] Claude exited with code ${exitCode}. Stopping.`);
|
|
216
|
+
console.error(`[worker] Error logged to: ${errorsFile}`);
|
|
217
|
+
process.exit(exitCode);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.warn(`[worker] Claude exited with code ${exitCode}. YOLO mode - continuing...`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log(`[worker] Iteration ${iteration} complete. Starting next iteration...`);
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/hooks/hook.ts
CHANGED
|
@@ -3,196 +3,201 @@
|
|
|
3
3
|
import pkg from "../../package.json";
|
|
4
4
|
import type { Agent } from "../types";
|
|
5
5
|
|
|
6
|
-
// @ts-ignore
|
|
7
6
|
const SERVER_NAME = pkg.config?.name ?? "agent-swarm";
|
|
8
7
|
|
|
9
8
|
type McpServerConfig = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
url: string;
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: string;
|
|
12
|
+
"X-Agent-ID": string;
|
|
13
|
+
};
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
interface HookMessage {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
hook_event_name: string;
|
|
18
|
+
session_id?: string;
|
|
19
|
+
transcript_path?: string;
|
|
20
|
+
permission_mode?: string;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
source?: string;
|
|
23
|
+
trigger?: string;
|
|
24
|
+
custom_instructions?: string;
|
|
25
|
+
tool_name?: string;
|
|
26
|
+
tool_input?: Record<string, unknown>;
|
|
27
|
+
tool_response?: Record<string, unknown>;
|
|
28
|
+
tool_use_id?: string;
|
|
29
|
+
prompt?: string;
|
|
30
|
+
stop_hook_active?: boolean;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
34
|
* Main hook handler - processes Claude Code hook events
|
|
36
35
|
*/
|
|
37
36
|
export async function handleHook(): Promise<void> {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
37
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
38
|
+
|
|
39
|
+
let mcpConfig: McpServerConfig | undefined;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const mcpFile = Bun.file(`${projectDir}/.mcp.json`);
|
|
43
|
+
if (await mcpFile.exists()) {
|
|
44
|
+
const config = await mcpFile.json();
|
|
45
|
+
mcpConfig = config?.mcpServers?.[SERVER_NAME] as McpServerConfig;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// No config found, proceed without MCP
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let msg: HookMessage;
|
|
52
|
+
try {
|
|
53
|
+
msg = await Bun.stdin.json();
|
|
54
|
+
} catch {
|
|
55
|
+
// No stdin or invalid JSON - exit silently
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getBaseUrl = (): string => {
|
|
60
|
+
if (!mcpConfig) return "";
|
|
61
|
+
try {
|
|
62
|
+
const url = new URL(mcpConfig.url);
|
|
63
|
+
return url.origin;
|
|
64
|
+
} catch {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const hasAgentIdHeader = (): boolean => {
|
|
70
|
+
if (!mcpConfig) return false;
|
|
71
|
+
return Boolean(mcpConfig.headers["X-Agent-ID"]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const ping = async (): Promise<void> => {
|
|
75
|
+
if (!mcpConfig) return;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await fetch(`${getBaseUrl()}/ping`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: mcpConfig.headers,
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
// Silently fail - server might not be running
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const close = async (): Promise<void> => {
|
|
88
|
+
if (!mcpConfig) return;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await fetch(`${getBaseUrl()}/close`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: mcpConfig.headers,
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
// Silently fail
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const getAgentInfo = async (): Promise<Agent | undefined> => {
|
|
101
|
+
if (!mcpConfig) return;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const resp = await fetch(`${getBaseUrl()}/me`, {
|
|
105
|
+
method: "GET",
|
|
106
|
+
headers: mcpConfig.headers,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if ([400, 404].includes(resp.status)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (await resp.json()) as Agent;
|
|
114
|
+
} catch {
|
|
115
|
+
// Silently fail
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Ping the server to indicate activity
|
|
122
|
+
await ping();
|
|
123
|
+
|
|
124
|
+
// Get current agent info
|
|
125
|
+
const agentInfo = await getAgentInfo();
|
|
126
|
+
|
|
127
|
+
// Always output agent status
|
|
128
|
+
if (agentInfo) {
|
|
129
|
+
console.log(
|
|
130
|
+
`You are registered as ${agentInfo.isLead ? "lead" : "worker"} agent "${agentInfo.name}" with ID: ${agentInfo.id} (status: ${agentInfo.status}) as of ${new Date().toISOString()}.`,
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
console.log(
|
|
134
|
+
`You are not registered in the agent swarm yet. Use the join-swarm tool to register yourself, then check your status with my-agent-info.
|
|
135
|
+
|
|
136
|
+
If the ${SERVER_NAME} server is not running or disabled, disregard this message.
|
|
137
|
+
|
|
138
|
+
${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?.headers["X-Agent-ID"]}, it will be used automatically on join-swarm.` : "You do not have a pre-defined agent ID, you will receive one when you join the swarm, or optionally you can request one when calling join-swarm."}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle specific hook events
|
|
143
|
+
switch (msg.hook_event_name) {
|
|
144
|
+
case "SessionStart":
|
|
145
|
+
if (!agentInfo) break;
|
|
146
|
+
|
|
147
|
+
if (agentInfo.isLead) {
|
|
148
|
+
console.log(
|
|
149
|
+
`As the lead agent, you are responsible for coordinating the swarm to fulfill the user's request efficiently. Use the ${SERVER_NAME} tools to assign tasks to worker agents and monitor their progress.`,
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(
|
|
153
|
+
`As a worker agent, you should call the poll-task tool to wait for tasks assigned by the lead agent, unless specified otherwise.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case "PreCompact":
|
|
159
|
+
// Covered by SessionStart hook
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "PreToolUse":
|
|
163
|
+
// Nothing to do here for now
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "PostToolUse":
|
|
167
|
+
if (agentInfo) {
|
|
168
|
+
if (agentInfo.isLead) {
|
|
169
|
+
if (msg.tool_name?.endsWith("send-task")) {
|
|
170
|
+
const maybeTaskId = (msg.tool_response as { task?: { id?: string } })?.task?.id;
|
|
171
|
+
|
|
172
|
+
console.log(
|
|
173
|
+
`Task sent successfully.${maybeTaskId ? ` Task ID: ${maybeTaskId}.` : ""} Monitor progress using the get-task-details tool periodically.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.log(
|
|
178
|
+
`Remember to call store-progress periodically to update the lead agent on your progress.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case "UserPromptSubmit":
|
|
185
|
+
// Nothing specific for now
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case "Stop":
|
|
189
|
+
// Mark the agent as offline
|
|
190
|
+
await close();
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
// Run directly when executed as a script
|
|
194
199
|
const isMainModule = import.meta.main;
|
|
195
200
|
if (isMainModule) {
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
await handleHook();
|
|
202
|
+
process.exit(0);
|
|
198
203
|
}
|
package/src/server.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import pkg from "../package.json";
|
|
2
3
|
import { initDb } from "./be/db";
|
|
3
4
|
import { registerGetSwarmTool } from "./tools/get-swarm";
|
|
4
5
|
import { registerGetTaskDetailsTool } from "./tools/get-task-details";
|
|
@@ -8,8 +9,6 @@ import { registerMyAgentInfoTool } from "./tools/my-agent-info";
|
|
|
8
9
|
import { registerPollTaskTool } from "./tools/poll-task";
|
|
9
10
|
import { registerSendTaskTool } from "./tools/send-task";
|
|
10
11
|
import { registerStoreProgressTool } from "./tools/store-progress";
|
|
11
|
-
import pkg from "../package.json";
|
|
12
|
-
|
|
13
12
|
|
|
14
13
|
export function createServer() {
|
|
15
14
|
// Initialize database with WAL mode
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { getTaskById } from "@/be/db";
|
|
3
|
+
import { getLogsByTaskIdChronological, getTaskById } from "@/be/db";
|
|
4
4
|
import { createToolRegistrar } from "@/tools/utils";
|
|
5
|
-
import { AgentTaskSchema } from "@/types";
|
|
5
|
+
import { AgentLogSchema, AgentTaskSchema } from "@/types";
|
|
6
6
|
|
|
7
7
|
export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
8
8
|
createToolRegistrar(server)(
|
|
@@ -10,7 +10,7 @@ export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
|
10
10
|
{
|
|
11
11
|
title: "Get task details",
|
|
12
12
|
description:
|
|
13
|
-
"Returns detailed information about a specific task, including output and
|
|
13
|
+
"Returns detailed information about a specific task, including output, failure reason, and log history.",
|
|
14
14
|
inputSchema: z.object({
|
|
15
15
|
taskId: z.uuid().describe("The ID of the task to get details for."),
|
|
16
16
|
}),
|
|
@@ -18,6 +18,7 @@ export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
|
18
18
|
success: z.boolean(),
|
|
19
19
|
message: z.string(),
|
|
20
20
|
task: AgentTaskSchema.optional(),
|
|
21
|
+
logs: z.array(AgentLogSchema).optional(),
|
|
21
22
|
}),
|
|
22
23
|
},
|
|
23
24
|
async ({ taskId }, requestInfo, _meta) => {
|
|
@@ -34,6 +35,8 @@ export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const logs = getLogsByTaskIdChronological(taskId);
|
|
39
|
+
|
|
37
40
|
return {
|
|
38
41
|
content: [{ type: "text", text: `Task "${taskId}" details retrieved.` }],
|
|
39
42
|
structuredContent: {
|
|
@@ -41,6 +44,7 @@ export const registerGetTaskDetailsTool = (server: McpServer) => {
|
|
|
41
44
|
success: true,
|
|
42
45
|
message: `Task "${taskId}" details retrieved.`,
|
|
43
46
|
task,
|
|
47
|
+
logs,
|
|
44
48
|
},
|
|
45
49
|
};
|
|
46
50
|
},
|