@slashfi/agents-sdk 0.35.0 → 0.36.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/dist/cjs/events.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry.js +58 -10
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/server.js +73 -71
- package/dist/cjs/server.js.map +1 -1
- package/dist/events.d.ts +85 -9
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +16 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +58 -10
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +73 -71
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/events.ts +83 -9
- package/src/hooks.test.ts +439 -0
- package/src/index.ts +3 -1
- package/src/registry.ts +85 -12
- package/src/server.ts +97 -90
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tools/call/call_agent and tools/call/list_agents hooks.
|
|
3
|
+
*
|
|
4
|
+
* These go through the full production path:
|
|
5
|
+
* HTTP POST → JSON-RPC → MCP tools/call → handleToolCall → registry hooks
|
|
6
|
+
*
|
|
7
|
+
* Mirrors how atlas-os uses these hooks to intercept and route requests.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import {
|
|
12
|
+
createAgentRegistry,
|
|
13
|
+
createAgentServer,
|
|
14
|
+
defineAgent,
|
|
15
|
+
defineTool,
|
|
16
|
+
} from "./index";
|
|
17
|
+
import type {
|
|
18
|
+
AgentDefinition,
|
|
19
|
+
AgentServer,
|
|
20
|
+
CallAgentToolCallEvent,
|
|
21
|
+
ListAgentsToolCallEvent,
|
|
22
|
+
} from "./index";
|
|
23
|
+
|
|
24
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
25
|
+
// Test agents
|
|
26
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
27
|
+
|
|
28
|
+
const echoAgent = defineAgent({
|
|
29
|
+
path: "@echo",
|
|
30
|
+
entrypoint: "Echo agent",
|
|
31
|
+
config: { name: "Echo", description: "Echoes input back" },
|
|
32
|
+
visibility: "public" as const,
|
|
33
|
+
tools: [
|
|
34
|
+
defineTool({
|
|
35
|
+
name: "echo",
|
|
36
|
+
description: "Echo the input",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: { message: { type: "string" } },
|
|
40
|
+
required: ["message"],
|
|
41
|
+
},
|
|
42
|
+
execute: async (input: { message: string }) => ({
|
|
43
|
+
echoed: input.message,
|
|
44
|
+
}),
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const mathAgent = defineAgent({
|
|
50
|
+
path: "@math",
|
|
51
|
+
entrypoint: "Math agent",
|
|
52
|
+
config: { name: "Math", description: "Does math" },
|
|
53
|
+
visibility: "public" as const,
|
|
54
|
+
tools: [
|
|
55
|
+
defineTool({
|
|
56
|
+
name: "add",
|
|
57
|
+
description: "Add two numbers",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
a: { type: "number" },
|
|
62
|
+
b: { type: "number" },
|
|
63
|
+
},
|
|
64
|
+
required: ["a", "b"],
|
|
65
|
+
},
|
|
66
|
+
execute: async (input: { a: number; b: number }) => ({
|
|
67
|
+
result: input.a + input.b,
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
74
|
+
// Helper: MCP JSON-RPC call through HTTP
|
|
75
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
76
|
+
|
|
77
|
+
let nextId = 1;
|
|
78
|
+
async function mcpCall(
|
|
79
|
+
baseUrl: string,
|
|
80
|
+
toolName: string,
|
|
81
|
+
args: Record<string, unknown>,
|
|
82
|
+
) {
|
|
83
|
+
const res = await fetch(baseUrl, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
jsonrpc: "2.0",
|
|
88
|
+
id: nextId++,
|
|
89
|
+
method: "tools/call",
|
|
90
|
+
params: { name: toolName, arguments: args },
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const json = (await res.json()) as {
|
|
95
|
+
result?: { content?: Array<{ text: string }> };
|
|
96
|
+
};
|
|
97
|
+
const text = json.result?.content?.[0]?.text;
|
|
98
|
+
return text ? JSON.parse(text) : json;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
102
|
+
// tools/call/call_agent tests
|
|
103
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
104
|
+
|
|
105
|
+
describe("tools/call/call_agent hook", () => {
|
|
106
|
+
const PORT = 19870;
|
|
107
|
+
|
|
108
|
+
test("resolve() short-circuits the default handler", async () => {
|
|
109
|
+
// This mirrors atlas-os: intercept call_agent, route externally, resolve()
|
|
110
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
111
|
+
registry.register(echoAgent);
|
|
112
|
+
|
|
113
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
114
|
+
event.resolve({
|
|
115
|
+
success: true,
|
|
116
|
+
result: { intercepted: true, originalPath: event.request.path },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const server = createAgentServer(registry, { port: PORT });
|
|
121
|
+
await server.start();
|
|
122
|
+
|
|
123
|
+
const result = await mcpCall(`http://localhost:${PORT}`, "call_agent", {
|
|
124
|
+
action: "execute_tool",
|
|
125
|
+
path: "@echo",
|
|
126
|
+
tool: "echo",
|
|
127
|
+
params: { message: "hello" },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(true);
|
|
131
|
+
expect(result.result.intercepted).toBe(true);
|
|
132
|
+
expect(result.result.originalPath).toBe("@echo");
|
|
133
|
+
await server.stop();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("next() runs default handler and hook observes", async () => {
|
|
137
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
138
|
+
registry.register(echoAgent);
|
|
139
|
+
|
|
140
|
+
let hookSawRequest = false;
|
|
141
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
142
|
+
hookSawRequest = true;
|
|
143
|
+
await event.next();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const server = createAgentServer(registry, { port: PORT + 1 });
|
|
147
|
+
await server.start();
|
|
148
|
+
|
|
149
|
+
const result = await mcpCall(`http://localhost:${PORT + 1}`, "call_agent", {
|
|
150
|
+
action: "execute_tool",
|
|
151
|
+
path: "@echo",
|
|
152
|
+
tool: "echo",
|
|
153
|
+
params: { message: "passthrough" },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(hookSawRequest).toBe(true);
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
expect(result.result.echoed).toBe("passthrough");
|
|
159
|
+
await server.stop();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("next() with modified request reroutes the call", async () => {
|
|
163
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
164
|
+
registry.register(echoAgent);
|
|
165
|
+
registry.register(mathAgent);
|
|
166
|
+
|
|
167
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
168
|
+
if (event.request.action === "execute_tool" && event.request.path === "@echo") {
|
|
169
|
+
await event.next({
|
|
170
|
+
...event.request,
|
|
171
|
+
path: "@math",
|
|
172
|
+
tool: "add",
|
|
173
|
+
params: { a: 10, b: 20 },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const server = createAgentServer(registry, { port: PORT + 2 });
|
|
179
|
+
await server.start();
|
|
180
|
+
|
|
181
|
+
const result = await mcpCall(`http://localhost:${PORT + 2}`, "call_agent", {
|
|
182
|
+
action: "execute_tool",
|
|
183
|
+
path: "@echo",
|
|
184
|
+
tool: "echo",
|
|
185
|
+
params: { message: "this gets rerouted" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.success).toBe(true);
|
|
189
|
+
expect(result.result.result).toBe(30);
|
|
190
|
+
await server.stop();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("no listener = default behavior unchanged", async () => {
|
|
194
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
195
|
+
registry.register(echoAgent);
|
|
196
|
+
|
|
197
|
+
const server = createAgentServer(registry, { port: PORT + 3 });
|
|
198
|
+
await server.start();
|
|
199
|
+
|
|
200
|
+
const result = await mcpCall(`http://localhost:${PORT + 3}`, "call_agent", {
|
|
201
|
+
action: "execute_tool",
|
|
202
|
+
path: "@echo",
|
|
203
|
+
tool: "echo",
|
|
204
|
+
params: { message: "no hook" },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(result.success).toBe(true);
|
|
208
|
+
expect(result.result.echoed).toBe("no hook");
|
|
209
|
+
await server.stop();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("resolve() with error response", async () => {
|
|
213
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
214
|
+
registry.register(echoAgent);
|
|
215
|
+
|
|
216
|
+
registry.on("tools/call/call_agent", async (event) => {
|
|
217
|
+
event.resolve({
|
|
218
|
+
success: false,
|
|
219
|
+
error: "Agent not available in remote registry",
|
|
220
|
+
code: "AGENT_NOT_FOUND",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const server = createAgentServer(registry, { port: PORT + 4 });
|
|
225
|
+
await server.start();
|
|
226
|
+
|
|
227
|
+
const result = await mcpCall(`http://localhost:${PORT + 4}`, "call_agent", {
|
|
228
|
+
action: "execute_tool",
|
|
229
|
+
path: "@nonexistent",
|
|
230
|
+
tool: "foo",
|
|
231
|
+
params: {},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.success).toBe(false);
|
|
235
|
+
expect(result.error).toBe("Agent not available in remote registry");
|
|
236
|
+
await server.stop();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
241
|
+
// tools/call/list_agents tests
|
|
242
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
243
|
+
|
|
244
|
+
describe("tools/call/list_agents hook", () => {
|
|
245
|
+
const PORT = 19880;
|
|
246
|
+
|
|
247
|
+
test("next() with additional agents merges into listing", async () => {
|
|
248
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
249
|
+
registry.register(echoAgent);
|
|
250
|
+
|
|
251
|
+
const remoteAgent = defineAgent({
|
|
252
|
+
path: "@remote-db",
|
|
253
|
+
entrypoint: "Remote database agent",
|
|
254
|
+
config: { name: "RemoteDB", description: "Query remote databases" },
|
|
255
|
+
visibility: "public" as const,
|
|
256
|
+
tools: [
|
|
257
|
+
defineTool({
|
|
258
|
+
name: "query",
|
|
259
|
+
description: "Run a SQL query",
|
|
260
|
+
inputSchema: { type: "object", properties: { sql: { type: "string" } } },
|
|
261
|
+
execute: async () => ({ rows: [] }),
|
|
262
|
+
}),
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
267
|
+
await event.next([remoteAgent]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const server = createAgentServer(registry, { port: PORT });
|
|
271
|
+
await server.start();
|
|
272
|
+
|
|
273
|
+
const result = await mcpCall(`http://localhost:${PORT}`, "list_agents", {});
|
|
274
|
+
|
|
275
|
+
expect(result.success).toBe(true);
|
|
276
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
277
|
+
expect(paths).toContain("@echo");
|
|
278
|
+
expect(paths).toContain("@remote-db");
|
|
279
|
+
expect(result.total).toBe(2);
|
|
280
|
+
await server.stop();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("injected agents appear in BM25 search results", async () => {
|
|
284
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
285
|
+
registry.register(echoAgent);
|
|
286
|
+
|
|
287
|
+
const remoteAgent = defineAgent({
|
|
288
|
+
path: "@snowflake",
|
|
289
|
+
entrypoint: "Snowflake analytics",
|
|
290
|
+
config: { name: "Snowflake", description: "Analytics data warehouse queries" },
|
|
291
|
+
visibility: "public" as const,
|
|
292
|
+
tools: [
|
|
293
|
+
defineTool({
|
|
294
|
+
name: "run_query",
|
|
295
|
+
description: "Run an analytics query on Snowflake",
|
|
296
|
+
inputSchema: { type: "object", properties: { sql: { type: "string" } } },
|
|
297
|
+
execute: async () => ({ rows: [] }),
|
|
298
|
+
}),
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
303
|
+
await event.next([remoteAgent]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const server = createAgentServer(registry, { port: PORT + 1 });
|
|
307
|
+
await server.start();
|
|
308
|
+
|
|
309
|
+
const result = await mcpCall(`http://localhost:${PORT + 1}`, "list_agents", {
|
|
310
|
+
query: "analytics warehouse",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(result.success).toBe(true);
|
|
314
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
315
|
+
expect(paths).toContain("@snowflake");
|
|
316
|
+
await server.stop();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("resolve() fully replaces the response", async () => {
|
|
320
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
321
|
+
registry.register(echoAgent);
|
|
322
|
+
registry.register(mathAgent);
|
|
323
|
+
|
|
324
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
325
|
+
event.resolve({
|
|
326
|
+
success: true,
|
|
327
|
+
total: 1,
|
|
328
|
+
agents: [
|
|
329
|
+
{
|
|
330
|
+
path: "@custom-only",
|
|
331
|
+
name: "Custom",
|
|
332
|
+
description: "Only this one",
|
|
333
|
+
tools: ["do_stuff"],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const server = createAgentServer(registry, { port: PORT + 2 });
|
|
340
|
+
await server.start();
|
|
341
|
+
|
|
342
|
+
const result = await mcpCall(`http://localhost:${PORT + 2}`, "list_agents", {});
|
|
343
|
+
|
|
344
|
+
expect(result.success).toBe(true);
|
|
345
|
+
expect(result.agents.length).toBe(1);
|
|
346
|
+
expect(result.agents[0].path).toBe("@custom-only");
|
|
347
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
348
|
+
expect(paths).not.toContain("@echo");
|
|
349
|
+
expect(paths).not.toContain("@math");
|
|
350
|
+
await server.stop();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("no listener = default behavior unchanged", async () => {
|
|
354
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
355
|
+
registry.register(echoAgent);
|
|
356
|
+
registry.register(mathAgent);
|
|
357
|
+
|
|
358
|
+
const server = createAgentServer(registry, { port: PORT + 3 });
|
|
359
|
+
await server.start();
|
|
360
|
+
|
|
361
|
+
const result = await mcpCall(`http://localhost:${PORT + 3}`, "list_agents", {});
|
|
362
|
+
|
|
363
|
+
expect(result.success).toBe(true);
|
|
364
|
+
expect(result.total).toBe(2);
|
|
365
|
+
const paths = result.agents.map((a: { path: string }) => a.path);
|
|
366
|
+
expect(paths).toContain("@echo");
|
|
367
|
+
expect(paths).toContain("@math");
|
|
368
|
+
await server.stop();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("deduplicates by path — injected agent overrides local", async () => {
|
|
372
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
373
|
+
registry.register(echoAgent);
|
|
374
|
+
|
|
375
|
+
const overrideEcho = defineAgent({
|
|
376
|
+
path: "@echo",
|
|
377
|
+
entrypoint: "Overridden echo",
|
|
378
|
+
config: { name: "Echo Override", description: "This replaced the original" },
|
|
379
|
+
visibility: "public" as const,
|
|
380
|
+
tools: [
|
|
381
|
+
defineTool({
|
|
382
|
+
name: "echo",
|
|
383
|
+
description: "Overridden echo tool",
|
|
384
|
+
inputSchema: { type: "object", properties: { message: { type: "string" } } },
|
|
385
|
+
execute: async () => ({ overridden: true }),
|
|
386
|
+
}),
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
391
|
+
await event.next([overrideEcho]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const server = createAgentServer(registry, { port: PORT + 4 });
|
|
395
|
+
await server.start();
|
|
396
|
+
|
|
397
|
+
const result = await mcpCall(`http://localhost:${PORT + 4}`, "list_agents", {});
|
|
398
|
+
|
|
399
|
+
expect(result.success).toBe(true);
|
|
400
|
+
expect(result.total).toBe(1);
|
|
401
|
+
expect(result.agents[0].description).toBe("This replaced the original");
|
|
402
|
+
await server.stop();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("event carries query/limit/cursor params", async () => {
|
|
406
|
+
const registry = createAgentRegistry({ defaultVisibility: "public" });
|
|
407
|
+
registry.register(echoAgent);
|
|
408
|
+
|
|
409
|
+
let capturedEvent: {
|
|
410
|
+
query?: string;
|
|
411
|
+
limit?: number;
|
|
412
|
+
cursor?: string;
|
|
413
|
+
baseAgentCount: number;
|
|
414
|
+
} | null = null;
|
|
415
|
+
|
|
416
|
+
registry.on("tools/call/list_agents", async (event) => {
|
|
417
|
+
capturedEvent = {
|
|
418
|
+
query: event.query,
|
|
419
|
+
limit: event.limit,
|
|
420
|
+
cursor: event.cursor,
|
|
421
|
+
baseAgentCount: event.baseAgents.length,
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const server = createAgentServer(registry, { port: PORT + 5 });
|
|
426
|
+
await server.start();
|
|
427
|
+
|
|
428
|
+
await mcpCall(`http://localhost:${PORT + 5}`, "list_agents", {
|
|
429
|
+
query: "echo",
|
|
430
|
+
limit: 5,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(capturedEvent).not.toBeNull();
|
|
434
|
+
expect(capturedEvent!.query).toBe("echo");
|
|
435
|
+
expect(capturedEvent!.limit).toBe(5);
|
|
436
|
+
expect(capturedEvent!.baseAgentCount).toBe(1);
|
|
437
|
+
await server.stop();
|
|
438
|
+
});
|
|
439
|
+
});
|
package/src/index.ts
CHANGED
package/src/registry.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { dirname, resolve } from "node:path";
|
|
8
|
-
import type { AgentEvent, BaseEvent, CustomEventMap, EventCallback, EventType } from "./events.js";
|
|
8
|
+
import type { AgentEvent, BaseEvent, CallAgentToolCallEvent, CustomEventMap, EventCallback, EventType, ListAgentsResult, ListAgentsToolCallEvent } from "./events.js";
|
|
9
9
|
import { createEventBus } from "./events.js";
|
|
10
10
|
import type { SerializedAgentDefinition } from "./serialized.js";
|
|
11
11
|
import type {
|
|
@@ -96,6 +96,21 @@ export interface AgentRegistry {
|
|
|
96
96
|
/** Call an agent (execute action) */
|
|
97
97
|
call(request: CallAgentRequest): Promise<CallAgentResponse>;
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* List agents with hook support.
|
|
101
|
+
* Emits `tools/call/list_agents` event so hosts can inject additional agents
|
|
102
|
+
* (e.g., from remote registries or consumer config) before the callback runs.
|
|
103
|
+
*
|
|
104
|
+
* @param params - Query/pagination params from the MCP tool call
|
|
105
|
+
* @param callback - Processes the (possibly augmented) agent list into the final result.
|
|
106
|
+
* Receives the merged agent list; responsible for visibility, BM25, pagination.
|
|
107
|
+
* @returns The ListAgentsResult from either the callback or an intercepting listener
|
|
108
|
+
*/
|
|
109
|
+
listAgents(
|
|
110
|
+
params: { query?: string; limit?: number; cursor?: string },
|
|
111
|
+
callback: (agents: AgentDefinition[]) => Promise<ListAgentsResult>,
|
|
112
|
+
): Promise<ListAgentsResult>;
|
|
113
|
+
|
|
99
114
|
/** Register an event listener (global scope — fires for all agents) */
|
|
100
115
|
on<T extends EventType>(eventType: T, callback: EventCallback<T>): void;
|
|
101
116
|
|
|
@@ -183,6 +198,18 @@ export function agentFromSerialized(
|
|
|
183
198
|
};
|
|
184
199
|
}
|
|
185
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Deduplicate agents by path. Later entries (from additionalAgents) override
|
|
203
|
+
* earlier ones from the base set.
|
|
204
|
+
*/
|
|
205
|
+
function dedupeAgents(agents: AgentDefinition[]): AgentDefinition[] {
|
|
206
|
+
const seen = new Map<string, AgentDefinition>();
|
|
207
|
+
for (const agent of agents) {
|
|
208
|
+
seen.set(agent.path, agent);
|
|
209
|
+
}
|
|
210
|
+
return Array.from(seen.values());
|
|
211
|
+
}
|
|
212
|
+
|
|
186
213
|
export function createAgentRegistry(
|
|
187
214
|
options: AgentRegistryOptions = {},
|
|
188
215
|
): AgentRegistry {
|
|
@@ -484,6 +511,46 @@ export function createAgentRegistry(
|
|
|
484
511
|
return Array.from(agents.keys());
|
|
485
512
|
},
|
|
486
513
|
|
|
514
|
+
async listAgents(
|
|
515
|
+
params: { query?: string; limit?: number; cursor?: string },
|
|
516
|
+
callback: (agents: AgentDefinition[]) => Promise<ListAgentsResult>,
|
|
517
|
+
): Promise<ListAgentsResult> {
|
|
518
|
+
const baseAgents = Array.from(agents.values());
|
|
519
|
+
let intercepted: ListAgentsResult | undefined;
|
|
520
|
+
let nextCalled = false;
|
|
521
|
+
let nextResult: ListAgentsResult | undefined;
|
|
522
|
+
|
|
523
|
+
const nextFn = async (additionalAgents?: AgentDefinition[]) => {
|
|
524
|
+
nextCalled = true;
|
|
525
|
+
const merged = additionalAgents
|
|
526
|
+
? dedupeAgents([...baseAgents, ...additionalAgents])
|
|
527
|
+
: baseAgents;
|
|
528
|
+
nextResult = await callback(merged);
|
|
529
|
+
return nextResult;
|
|
530
|
+
};
|
|
531
|
+
const resolveFn = (result: ListAgentsResult) => {
|
|
532
|
+
intercepted = result;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
await eventBus.emit({
|
|
536
|
+
type: "tools/call/list_agents",
|
|
537
|
+
agentPath: "*",
|
|
538
|
+
timestamp: Date.now(),
|
|
539
|
+
baseAgents,
|
|
540
|
+
query: params.query,
|
|
541
|
+
limit: params.limit,
|
|
542
|
+
cursor: params.cursor,
|
|
543
|
+
next: nextFn,
|
|
544
|
+
resolve: resolveFn,
|
|
545
|
+
} satisfies ListAgentsToolCallEvent);
|
|
546
|
+
|
|
547
|
+
if (intercepted) return intercepted;
|
|
548
|
+
if (nextCalled) return nextResult!;
|
|
549
|
+
|
|
550
|
+
// No listener engaged — run default with base agents
|
|
551
|
+
return callback(baseAgents);
|
|
552
|
+
},
|
|
553
|
+
|
|
487
554
|
on<T extends EventType>(eventType: T, callback: EventCallback<T>): void {
|
|
488
555
|
eventBus.on(eventType, callback);
|
|
489
556
|
},
|
|
@@ -500,26 +567,32 @@ export function createAgentRegistry(
|
|
|
500
567
|
},
|
|
501
568
|
|
|
502
569
|
async call(request: CallAgentRequest): Promise<CallAgentResponse> {
|
|
503
|
-
// Emit call event — listeners can next()/resolve() to control flow
|
|
570
|
+
// Emit tools/call/call_agent event — listeners can next()/resolve() to control flow
|
|
504
571
|
let intercepted: CallAgentResponse | undefined;
|
|
505
572
|
let nextCalled = false;
|
|
506
573
|
let nextResult: CallAgentResponse | undefined;
|
|
574
|
+
|
|
575
|
+
const nextFn = async (overrideRequest?: CallAgentRequest) => {
|
|
576
|
+
nextCalled = true;
|
|
577
|
+
nextResult = await callInternal(overrideRequest ?? request);
|
|
578
|
+
return nextResult;
|
|
579
|
+
};
|
|
580
|
+
const resolveFn = (response: CallAgentResponse) => {
|
|
581
|
+
intercepted = response;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Emit the new namespaced event
|
|
507
585
|
await eventBus.emit({
|
|
508
|
-
type: "call",
|
|
586
|
+
type: "tools/call/call_agent",
|
|
509
587
|
agentPath: request.path,
|
|
510
588
|
timestamp: Date.now(),
|
|
511
589
|
request,
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return nextResult;
|
|
516
|
-
},
|
|
517
|
-
resolve(response: CallAgentResponse) {
|
|
518
|
-
intercepted = response;
|
|
519
|
-
},
|
|
520
|
-
});
|
|
590
|
+
next: nextFn,
|
|
591
|
+
resolve: resolveFn,
|
|
592
|
+
} satisfies CallAgentToolCallEvent);
|
|
521
593
|
if (intercepted) return intercepted;
|
|
522
594
|
if (nextCalled) return nextResult!;
|
|
595
|
+
|
|
523
596
|
// No listener engaged — run default
|
|
524
597
|
return callInternal(request);
|
|
525
598
|
},
|