claude-code-swarm 0.3.5 → 0.3.7
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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.claude-plugin/run-agent-inbox-mcp.sh +22 -3
- package/.gitattributes +3 -0
- package/.opentasks/config.json +9 -0
- package/.opentasks/graph.jsonl +0 -0
- package/e2e/helpers/opentasks-daemon.mjs +149 -0
- package/e2e/tier6-live-inbox-flow.test.mjs +938 -0
- package/e2e/tier7-hooks.test.mjs +992 -0
- package/e2e/tier7-minimem.test.mjs +461 -0
- package/e2e/tier7-opentasks.test.mjs +513 -0
- package/e2e/tier7-skilltree.test.mjs +506 -0
- package/e2e/vitest.config.e2e.mjs +1 -1
- package/package.json +6 -2
- package/references/agent-inbox/package-lock.json +2 -2
- package/references/agent-inbox/package.json +1 -1
- package/references/agent-inbox/src/index.ts +16 -2
- package/references/agent-inbox/src/ipc/ipc-server.ts +58 -0
- package/references/agent-inbox/src/mcp/mcp-proxy.ts +326 -0
- package/references/agent-inbox/src/types.ts +26 -0
- package/references/agent-inbox/test/ipc-new-commands.test.ts +200 -0
- package/references/agent-inbox/test/mcp-proxy.test.ts +191 -0
- package/references/minimem/package-lock.json +2 -2
- package/references/minimem/package.json +1 -1
- package/scripts/bootstrap.mjs +8 -1
- package/scripts/map-hook.mjs +6 -2
- package/scripts/map-sidecar.mjs +19 -0
- package/scripts/team-loader.mjs +15 -8
- package/skills/swarm/SKILL.md +16 -22
- package/src/__tests__/agent-generator.test.mjs +9 -10
- package/src/__tests__/context-output.test.mjs +13 -14
- package/src/__tests__/e2e-inbox-integration.test.mjs +732 -0
- package/src/__tests__/e2e-live-inbox.test.mjs +597 -0
- package/src/__tests__/inbox-integration.test.mjs +298 -0
- package/src/__tests__/integration.test.mjs +12 -11
- package/src/__tests__/skilltree-client.test.mjs +47 -1
- package/src/agent-generator.mjs +79 -88
- package/src/bootstrap.mjs +24 -3
- package/src/context-output.mjs +238 -64
- package/src/index.mjs +2 -0
- package/src/sidecar-server.mjs +30 -0
- package/src/skilltree-client.mjs +50 -5
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-proxy.ts — MCP server that proxies all tools to an existing inbox IPC socket.
|
|
3
|
+
*
|
|
4
|
+
* Instead of creating its own storage/router, this connects to a running
|
|
5
|
+
* agent-inbox IPC server (e.g., the sidecar's inbox instance) and translates
|
|
6
|
+
* MCP tool calls into IPC commands.
|
|
7
|
+
*
|
|
8
|
+
* This ensures a single source of truth for messages, agents, and routing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as net from "node:net";
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import type { IpcResponse } from "../types.js";
|
|
16
|
+
|
|
17
|
+
const IPC_TIMEOUT_MS = 5000;
|
|
18
|
+
const CONNECT_RETRY_MS = 500;
|
|
19
|
+
const CONNECT_MAX_RETRIES = 10;
|
|
20
|
+
|
|
21
|
+
export class InboxMcpProxy {
|
|
22
|
+
private mcp: McpServer;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private socketPath: string,
|
|
26
|
+
private defaultAgentId: string = "anonymous",
|
|
27
|
+
private defaultScope: string = "default"
|
|
28
|
+
) {
|
|
29
|
+
this.mcp = new McpServer({
|
|
30
|
+
name: "agent-inbox",
|
|
31
|
+
version: "0.1.0",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.registerTools();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Send an IPC command to the inbox socket and return the response.
|
|
39
|
+
* Retries connection if socket is not yet available.
|
|
40
|
+
*/
|
|
41
|
+
private async sendIpc(command: Record<string, unknown>): Promise<IpcResponse> {
|
|
42
|
+
let lastError: Error | null = null;
|
|
43
|
+
|
|
44
|
+
for (let attempt = 0; attempt < CONNECT_MAX_RETRIES; attempt++) {
|
|
45
|
+
try {
|
|
46
|
+
return await this.sendIpcOnce(command);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
49
|
+
// Only retry on connection errors (socket not ready yet)
|
|
50
|
+
if (lastError.message.includes("ENOENT") || lastError.message.includes("ECONNREFUSED")) {
|
|
51
|
+
if (attempt < CONNECT_MAX_RETRIES - 1) {
|
|
52
|
+
await new Promise((r) => setTimeout(r, CONNECT_RETRY_MS));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ok: false, error: `Inbox unavailable: ${lastError?.message ?? "unknown error"}` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private sendIpcOnce(command: Record<string, unknown>): Promise<IpcResponse> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const client = net.createConnection(this.socketPath);
|
|
66
|
+
let buffer = "";
|
|
67
|
+
let settled = false;
|
|
68
|
+
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
if (!settled) {
|
|
71
|
+
settled = true;
|
|
72
|
+
client.destroy();
|
|
73
|
+
reject(new Error("IPC timeout"));
|
|
74
|
+
}
|
|
75
|
+
}, IPC_TIMEOUT_MS);
|
|
76
|
+
|
|
77
|
+
client.on("connect", () => {
|
|
78
|
+
client.write(JSON.stringify(command) + "\n");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
client.on("data", (data) => {
|
|
82
|
+
buffer += data.toString();
|
|
83
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
84
|
+
if (newlineIdx !== -1) {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
settled = true;
|
|
87
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
88
|
+
client.destroy();
|
|
89
|
+
try {
|
|
90
|
+
resolve(JSON.parse(line) as IpcResponse);
|
|
91
|
+
} catch {
|
|
92
|
+
reject(new Error("Invalid IPC response"));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
client.on("error", (err) => {
|
|
98
|
+
if (!settled) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
settled = true;
|
|
101
|
+
reject(err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private registerTools(): void {
|
|
108
|
+
this.mcp.tool(
|
|
109
|
+
"send_message",
|
|
110
|
+
"Send a message to one or more agents. Supports replies (inReplyTo), threading (threadTag), and federated addressing (agent@system).",
|
|
111
|
+
{
|
|
112
|
+
to: z
|
|
113
|
+
.union([z.string(), z.array(z.string())])
|
|
114
|
+
.describe(
|
|
115
|
+
"Recipient agent ID(s). Use 'agent@system' for federated addressing."
|
|
116
|
+
),
|
|
117
|
+
body: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Plain text message body (shorthand for content)"),
|
|
121
|
+
content: z
|
|
122
|
+
.object({ type: z.string() })
|
|
123
|
+
.passthrough()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Structured message content"),
|
|
126
|
+
from: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("Sender agent ID (defaults to caller)"),
|
|
130
|
+
threadTag: z
|
|
131
|
+
.string()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe("Thread tag for grouping related messages"),
|
|
134
|
+
inReplyTo: z
|
|
135
|
+
.string()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("Message ID this is a reply to"),
|
|
138
|
+
importance: z
|
|
139
|
+
.enum(["low", "normal", "high", "urgent"])
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Message importance level"),
|
|
142
|
+
subject: z.string().optional().describe("Message subject"),
|
|
143
|
+
},
|
|
144
|
+
async ({ to, body, content, from, threadTag, inReplyTo, importance, subject }) => {
|
|
145
|
+
const payload = content ?? body ?? "";
|
|
146
|
+
const senderId = from ?? this.defaultAgentId;
|
|
147
|
+
const resp = await this.sendIpc({
|
|
148
|
+
action: "send",
|
|
149
|
+
from: senderId,
|
|
150
|
+
to,
|
|
151
|
+
payload,
|
|
152
|
+
threadTag,
|
|
153
|
+
inReplyTo,
|
|
154
|
+
importance,
|
|
155
|
+
subject,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text" as const,
|
|
161
|
+
text: JSON.stringify(
|
|
162
|
+
resp.ok
|
|
163
|
+
? { ok: true, messageId: resp.messageId }
|
|
164
|
+
: { ok: false, error: resp.error }
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
this.mcp.tool(
|
|
173
|
+
"check_inbox",
|
|
174
|
+
"Check inbox for messages addressed to an agent. Auto-registers the agent if not already registered.",
|
|
175
|
+
{
|
|
176
|
+
agentId: z.string().describe("Agent ID to check inbox for"),
|
|
177
|
+
unreadOnly: z
|
|
178
|
+
.boolean()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Only return unread messages (default true)"),
|
|
181
|
+
limit: z
|
|
182
|
+
.number()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe("Max messages to return"),
|
|
185
|
+
},
|
|
186
|
+
async ({ agentId, unreadOnly, limit }) => {
|
|
187
|
+
const resp = await this.sendIpc({
|
|
188
|
+
action: "check_inbox",
|
|
189
|
+
agentId,
|
|
190
|
+
unreadOnly: unreadOnly ?? true,
|
|
191
|
+
clear: true, // Mark as read after retrieval
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!resp.ok) {
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{ type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let messages = resp.messages ?? [];
|
|
203
|
+
if (limit && messages.length > limit) {
|
|
204
|
+
messages = messages.slice(0, limit);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text" as const,
|
|
211
|
+
text: JSON.stringify({
|
|
212
|
+
count: messages.length,
|
|
213
|
+
messages: messages.map((m) => ({
|
|
214
|
+
id: m.id,
|
|
215
|
+
from: m.sender_id,
|
|
216
|
+
subject: m.subject,
|
|
217
|
+
content: m.content,
|
|
218
|
+
threadTag: m.thread_tag,
|
|
219
|
+
importance: m.importance,
|
|
220
|
+
createdAt: m.created_at,
|
|
221
|
+
inReplyTo: m.in_reply_to,
|
|
222
|
+
})),
|
|
223
|
+
}),
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
this.mcp.tool(
|
|
231
|
+
"read_thread",
|
|
232
|
+
"Read all messages in a thread (by thread_tag)",
|
|
233
|
+
{
|
|
234
|
+
threadTag: z.string().describe("Thread tag to read"),
|
|
235
|
+
scope: z
|
|
236
|
+
.string()
|
|
237
|
+
.optional()
|
|
238
|
+
.describe("Scope (defaults to 'default')"),
|
|
239
|
+
},
|
|
240
|
+
async ({ threadTag, scope }) => {
|
|
241
|
+
const resp = await this.sendIpc({
|
|
242
|
+
action: "read_thread",
|
|
243
|
+
threadTag,
|
|
244
|
+
scope: scope ?? this.defaultScope,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!resp.ok) {
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{ type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const messages = resp.messages ?? [];
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: "text" as const,
|
|
260
|
+
text: JSON.stringify({
|
|
261
|
+
threadTag,
|
|
262
|
+
count: messages.length,
|
|
263
|
+
messages: messages.map((m) => ({
|
|
264
|
+
id: m.id,
|
|
265
|
+
from: m.sender_id,
|
|
266
|
+
content: m.content,
|
|
267
|
+
createdAt: m.created_at,
|
|
268
|
+
inReplyTo: m.in_reply_to,
|
|
269
|
+
})),
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
this.mcp.tool(
|
|
278
|
+
"list_agents",
|
|
279
|
+
"List agents registered in the inbox (local and optionally federated)",
|
|
280
|
+
{
|
|
281
|
+
scope: z.string().optional().describe("Filter by scope"),
|
|
282
|
+
includeFederated: z
|
|
283
|
+
.boolean()
|
|
284
|
+
.optional()
|
|
285
|
+
.describe("Include agents known from federation routing table"),
|
|
286
|
+
},
|
|
287
|
+
async ({ scope, includeFederated }) => {
|
|
288
|
+
const resp = await this.sendIpc({
|
|
289
|
+
action: "list_agents",
|
|
290
|
+
scope,
|
|
291
|
+
includeFederated,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!resp.ok) {
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{ type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "text" as const,
|
|
306
|
+
text: JSON.stringify({
|
|
307
|
+
count: resp.count ?? resp.agents?.length ?? 0,
|
|
308
|
+
agents: resp.agents ?? [],
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async start(): Promise<void> {
|
|
318
|
+
const transport = new StdioServerTransport();
|
|
319
|
+
await this.mcp.connect(transport);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Expose for testing */
|
|
323
|
+
get server(): McpServer {
|
|
324
|
+
return this.mcp;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -140,6 +140,18 @@ export interface IpcCheckInboxCommand {
|
|
|
140
140
|
clear?: boolean;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export interface IpcReadThreadCommand {
|
|
144
|
+
action: "read_thread";
|
|
145
|
+
threadTag: string;
|
|
146
|
+
scope?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface IpcListAgentsCommand {
|
|
150
|
+
action: "list_agents";
|
|
151
|
+
scope?: string;
|
|
152
|
+
includeFederated?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
export interface IpcPingCommand {
|
|
144
156
|
action: "ping";
|
|
145
157
|
}
|
|
@@ -149,12 +161,26 @@ export type IpcCommand =
|
|
|
149
161
|
| IpcEmitCommand
|
|
150
162
|
| IpcNotifyCommand
|
|
151
163
|
| IpcCheckInboxCommand
|
|
164
|
+
| IpcReadThreadCommand
|
|
165
|
+
| IpcListAgentsCommand
|
|
152
166
|
| IpcPingCommand;
|
|
153
167
|
|
|
154
168
|
export interface IpcResponse {
|
|
155
169
|
ok: boolean;
|
|
156
170
|
messageId?: string;
|
|
157
171
|
messages?: Message[];
|
|
172
|
+
agents?: Array<{
|
|
173
|
+
agentId: string;
|
|
174
|
+
name?: string;
|
|
175
|
+
scope: string;
|
|
176
|
+
status: string;
|
|
177
|
+
program?: string;
|
|
178
|
+
lastActive?: string;
|
|
179
|
+
location: "local" | "federated";
|
|
180
|
+
peerId?: string;
|
|
181
|
+
}>;
|
|
182
|
+
count?: number;
|
|
183
|
+
threadTag?: string;
|
|
158
184
|
error?: string;
|
|
159
185
|
pid?: number;
|
|
160
186
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import { InMemoryStorage } from "../src/storage/memory.js";
|
|
7
|
+
import { MessageRouter } from "../src/router/message-router.js";
|
|
8
|
+
import { IpcServer } from "../src/ipc/ipc-server.js";
|
|
9
|
+
|
|
10
|
+
function tmpSocketPath(): string {
|
|
11
|
+
return path.join(os.tmpdir(), `inbox-test-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sendCommand(socketPath: string, command: object): Promise<Record<string, unknown>> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const client = net.createConnection(socketPath, () => {
|
|
17
|
+
client.write(JSON.stringify(command) + "\n");
|
|
18
|
+
});
|
|
19
|
+
let buffer = "";
|
|
20
|
+
client.on("data", (data) => {
|
|
21
|
+
buffer += data.toString();
|
|
22
|
+
const idx = buffer.indexOf("\n");
|
|
23
|
+
if (idx !== -1) {
|
|
24
|
+
const line = buffer.slice(0, idx);
|
|
25
|
+
client.end();
|
|
26
|
+
resolve(JSON.parse(line));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
client.on("error", reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("IPC read_thread command", () => {
|
|
34
|
+
let storage: InMemoryStorage;
|
|
35
|
+
let events: EventEmitter;
|
|
36
|
+
let router: MessageRouter;
|
|
37
|
+
let server: IpcServer;
|
|
38
|
+
let socketPath: string;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
storage = new InMemoryStorage();
|
|
42
|
+
events = new EventEmitter();
|
|
43
|
+
router = new MessageRouter(storage, events, "default");
|
|
44
|
+
socketPath = tmpSocketPath();
|
|
45
|
+
server = new IpcServer(socketPath, router, storage);
|
|
46
|
+
await server.start();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await server.stop();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return empty thread for unknown tag", async () => {
|
|
54
|
+
const resp = await sendCommand(socketPath, {
|
|
55
|
+
action: "read_thread",
|
|
56
|
+
threadTag: "nonexistent",
|
|
57
|
+
});
|
|
58
|
+
expect(resp.ok).toBe(true);
|
|
59
|
+
expect(resp.threadTag).toBe("nonexistent");
|
|
60
|
+
expect(resp.count).toBe(0);
|
|
61
|
+
expect(resp.messages).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return messages in a thread", async () => {
|
|
65
|
+
// Send messages with same threadTag
|
|
66
|
+
await sendCommand(socketPath, {
|
|
67
|
+
action: "send",
|
|
68
|
+
from: "alice",
|
|
69
|
+
to: "bob",
|
|
70
|
+
payload: "first message",
|
|
71
|
+
threadTag: "task-42",
|
|
72
|
+
});
|
|
73
|
+
await sendCommand(socketPath, {
|
|
74
|
+
action: "send",
|
|
75
|
+
from: "bob",
|
|
76
|
+
to: "alice",
|
|
77
|
+
payload: "reply to first",
|
|
78
|
+
threadTag: "task-42",
|
|
79
|
+
});
|
|
80
|
+
// Different thread
|
|
81
|
+
await sendCommand(socketPath, {
|
|
82
|
+
action: "send",
|
|
83
|
+
from: "alice",
|
|
84
|
+
to: "charlie",
|
|
85
|
+
payload: "unrelated",
|
|
86
|
+
threadTag: "task-99",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const resp = await sendCommand(socketPath, {
|
|
90
|
+
action: "read_thread",
|
|
91
|
+
threadTag: "task-42",
|
|
92
|
+
scope: "default",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(resp.ok).toBe(true);
|
|
96
|
+
expect(resp.count).toBe(2);
|
|
97
|
+
const messages = resp.messages as Array<{ sender_id: string }>;
|
|
98
|
+
expect(messages).toHaveLength(2);
|
|
99
|
+
const senders = messages.map((m) => m.sender_id);
|
|
100
|
+
expect(senders).toContain("alice");
|
|
101
|
+
expect(senders).toContain("bob");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("IPC list_agents command", () => {
|
|
106
|
+
let storage: InMemoryStorage;
|
|
107
|
+
let events: EventEmitter;
|
|
108
|
+
let router: MessageRouter;
|
|
109
|
+
let server: IpcServer;
|
|
110
|
+
let socketPath: string;
|
|
111
|
+
|
|
112
|
+
beforeEach(async () => {
|
|
113
|
+
storage = new InMemoryStorage();
|
|
114
|
+
events = new EventEmitter();
|
|
115
|
+
router = new MessageRouter(storage, events, "default");
|
|
116
|
+
socketPath = tmpSocketPath();
|
|
117
|
+
server = new IpcServer(socketPath, router, storage);
|
|
118
|
+
await server.start();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(async () => {
|
|
122
|
+
await server.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should return empty list with no agents", async () => {
|
|
126
|
+
const resp = await sendCommand(socketPath, {
|
|
127
|
+
action: "list_agents",
|
|
128
|
+
});
|
|
129
|
+
expect(resp.ok).toBe(true);
|
|
130
|
+
expect(resp.count).toBe(0);
|
|
131
|
+
expect(resp.agents).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should list registered agents", async () => {
|
|
135
|
+
// Register agents via notify
|
|
136
|
+
await sendCommand(socketPath, {
|
|
137
|
+
action: "notify",
|
|
138
|
+
event: {
|
|
139
|
+
type: "agent.spawn",
|
|
140
|
+
agent: {
|
|
141
|
+
agentId: "gsd-executor",
|
|
142
|
+
name: "executor",
|
|
143
|
+
scopes: ["swarm:gsd"],
|
|
144
|
+
metadata: { role: "executor" },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
await sendCommand(socketPath, {
|
|
149
|
+
action: "notify",
|
|
150
|
+
event: {
|
|
151
|
+
type: "agent.spawn",
|
|
152
|
+
agent: {
|
|
153
|
+
agentId: "gsd-verifier",
|
|
154
|
+
name: "verifier",
|
|
155
|
+
scopes: ["swarm:gsd"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const resp = await sendCommand(socketPath, {
|
|
161
|
+
action: "list_agents",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(resp.ok).toBe(true);
|
|
165
|
+
expect(resp.count).toBe(2);
|
|
166
|
+
const agents = resp.agents as Array<{ agentId: string; location: string }>;
|
|
167
|
+
expect(agents).toHaveLength(2);
|
|
168
|
+
const ids = agents.map((a) => a.agentId);
|
|
169
|
+
expect(ids).toContain("gsd-executor");
|
|
170
|
+
expect(ids).toContain("gsd-verifier");
|
|
171
|
+
expect(agents[0].location).toBe("local");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should filter agents by scope", async () => {
|
|
175
|
+
await sendCommand(socketPath, {
|
|
176
|
+
action: "notify",
|
|
177
|
+
event: {
|
|
178
|
+
type: "agent.spawn",
|
|
179
|
+
agent: { agentId: "team1-a", name: "a", scopes: ["team1"] },
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
await sendCommand(socketPath, {
|
|
183
|
+
action: "notify",
|
|
184
|
+
event: {
|
|
185
|
+
type: "agent.spawn",
|
|
186
|
+
agent: { agentId: "team2-b", name: "b", scopes: ["team2"] },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const resp = await sendCommand(socketPath, {
|
|
191
|
+
action: "list_agents",
|
|
192
|
+
scope: "team1",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(resp.ok).toBe(true);
|
|
196
|
+
expect(resp.count).toBe(1);
|
|
197
|
+
const agents = resp.agents as Array<{ agentId: string }>;
|
|
198
|
+
expect(agents[0].agentId).toBe("team1-a");
|
|
199
|
+
});
|
|
200
|
+
});
|