agenthub-multiagent-mcp 1.1.2 → 1.1.3

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.
@@ -1,666 +1,672 @@
1
- /**
2
- * MCP tool definitions and handlers
3
- */
4
-
5
- import { ApiClient, Message } from "../client.js";
6
- import type { Tool } from "@modelcontextprotocol/sdk/types.js";
7
- import * as state from "../state.js";
8
-
9
- export interface ToolContext {
10
- getCurrentAgentId: () => string;
11
- setCurrentAgentId: (id: string) => void;
12
- stopHeartbeat: () => void;
13
- getWorkingDir: () => string;
14
- }
15
-
16
- interface UrgentAction {
17
- required: true;
18
- instruction: string;
19
- message: Message;
20
- }
21
-
22
- interface WrappedResponse {
23
- result: unknown;
24
- pending_messages: Message[];
25
- pending_count: number;
26
- has_more: boolean;
27
- urgent_action: UrgentAction | null;
28
- }
29
-
30
- /**
31
- * Wraps a tool response with pending messages
32
- * This enables automatic message delivery without extra API calls
33
- */
34
- async function wrapWithMessages(
35
- client: ApiClient,
36
- agentId: string | null,
37
- result: unknown
38
- ): Promise<WrappedResponse | unknown> {
39
- // If not registered, return raw result
40
- if (!agentId) return result;
41
-
42
- try {
43
- // Fetch unread messages
44
- const inbox = await client.getInbox(agentId, true, 10);
45
- const messages = inbox.messages || [];
46
-
47
- // Sort: urgent first, then by created_at
48
- messages.sort((a, b) => {
49
- const aUrgent = isUrgentMessage(a);
50
- const bUrgent = isUrgentMessage(b);
51
- if (aUrgent && !bUrgent) return -1;
52
- if (!aUrgent && bUrgent) return 1;
53
- return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
54
- });
55
-
56
- // Find urgent message for action
57
- const urgentMsg = messages.find(isUrgentMessage);
58
-
59
- let urgentAction: UrgentAction | null = null;
60
- if (urgentMsg) {
61
- urgentAction = {
62
- required: true,
63
- instruction: buildUrgentInstruction(urgentMsg),
64
- message: urgentMsg,
65
- };
66
- }
67
-
68
- return {
69
- result,
70
- pending_messages: messages,
71
- pending_count: inbox.total,
72
- has_more: inbox.total > messages.length,
73
- urgent_action: urgentAction,
74
- };
75
- } catch (error) {
76
- // Don't break tool call if message fetch fails
77
- console.warn("Failed to fetch messages for wrapping:", error);
78
- return result;
79
- }
80
- }
81
-
82
- /**
83
- * Check if a message is urgent (requires immediate action)
84
- */
85
- function isUrgentMessage(msg: Message): boolean {
86
- return (
87
- msg.priority === "urgent" ||
88
- (msg.priority === "high" && msg.type === "question")
89
- );
90
- }
91
-
92
- /**
93
- * Build instruction text for urgent messages
94
- */
95
- function buildUrgentInstruction(msg: Message): string {
96
- return `URGENT: Agent [${msg.from_agent}] needs immediate assistance.
97
-
98
- Query: ${msg.subject}
99
-
100
- Details: ${msg.body}
101
-
102
- Spawn a subagent with Task tool to research and respond to this query. Use send_message to reply when done.`;
103
- }
104
-
105
- export function registerTools(): Tool[] {
106
- return [
107
- // Registration tools
108
- {
109
- name: "agent_register",
110
- description: "Register this agent with AgentHub. Must be called before using other tools.",
111
- inputSchema: {
112
- type: "object",
113
- properties: {
114
- id: {
115
- type: "string",
116
- description: "Unique agent identifier (e.g., 'backend-1', 'tester')",
117
- },
118
- name: {
119
- type: "string",
120
- description: "Human-friendly name for this agent",
121
- },
122
- model: {
123
- type: "string",
124
- description: "Model name (e.g., 'claude-opus-4.5', 'gpt-4-turbo', 'o3', 'gemini-2.0-pro'). Provider is auto-detected.",
125
- },
126
- },
127
- required: ["id", "model"],
128
- },
129
- },
130
- {
131
- name: "agent_start_work",
132
- description: "Declare what you're currently working on. Sends notification to Slack.",
133
- inputSchema: {
134
- type: "object",
135
- properties: {
136
- task: {
137
- type: "string",
138
- description: "Description of the task you're starting",
139
- },
140
- project: {
141
- type: "string",
142
- description: "Project name (optional)",
143
- },
144
- },
145
- required: ["task"],
146
- },
147
- },
148
- {
149
- name: "agent_set_status",
150
- description: "Update your status (online or busy)",
151
- inputSchema: {
152
- type: "object",
153
- properties: {
154
- status: {
155
- type: "string",
156
- enum: ["online", "busy"],
157
- description: "New status",
158
- },
159
- },
160
- required: ["status"],
161
- },
162
- },
163
- {
164
- name: "agent_disconnect",
165
- description: "Gracefully disconnect from AgentHub",
166
- inputSchema: {
167
- type: "object",
168
- properties: {},
169
- },
170
- },
171
-
172
- // Messaging tools
173
- {
174
- name: "send_message",
175
- description: "Send a direct message to another agent",
176
- inputSchema: {
177
- type: "object",
178
- properties: {
179
- to: {
180
- type: "string",
181
- description: "Target agent ID",
182
- },
183
- type: {
184
- type: "string",
185
- enum: ["task", "status", "question", "response", "context"],
186
- description: "Type of message",
187
- },
188
- subject: {
189
- type: "string",
190
- description: "Message subject/summary",
191
- },
192
- body: {
193
- type: "string",
194
- description: "Full message content",
195
- },
196
- priority: {
197
- type: "string",
198
- enum: ["normal", "high", "urgent"],
199
- description: "Message priority (default: normal)",
200
- },
201
- },
202
- required: ["to", "type", "subject", "body"],
203
- },
204
- },
205
- {
206
- name: "send_to_channel",
207
- description: "Send a message to all members of a channel",
208
- inputSchema: {
209
- type: "object",
210
- properties: {
211
- channel: {
212
- type: "string",
213
- description: "Target channel name",
214
- },
215
- type: {
216
- type: "string",
217
- enum: ["task", "status", "question", "response", "context"],
218
- description: "Type of message",
219
- },
220
- subject: {
221
- type: "string",
222
- description: "Message subject/summary",
223
- },
224
- body: {
225
- type: "string",
226
- description: "Full message content",
227
- },
228
- },
229
- required: ["channel", "type", "subject", "body"],
230
- },
231
- },
232
- {
233
- name: "broadcast",
234
- description: "Send a message to all registered agents",
235
- inputSchema: {
236
- type: "object",
237
- properties: {
238
- type: {
239
- type: "string",
240
- enum: ["task", "status", "question", "response", "context"],
241
- description: "Type of message",
242
- },
243
- subject: {
244
- type: "string",
245
- description: "Message subject/summary",
246
- },
247
- body: {
248
- type: "string",
249
- description: "Full message content",
250
- },
251
- },
252
- required: ["type", "subject", "body"],
253
- },
254
- },
255
- {
256
- name: "check_inbox",
257
- description: "Check for incoming messages",
258
- inputSchema: {
259
- type: "object",
260
- properties: {
261
- unread_only: {
262
- type: "boolean",
263
- description: "Only return unread messages (default: false)",
264
- },
265
- },
266
- },
267
- },
268
- {
269
- name: "mark_read",
270
- description: "Mark a message as read",
271
- inputSchema: {
272
- type: "object",
273
- properties: {
274
- message_id: {
275
- type: "string",
276
- description: "ID of the message to mark as read",
277
- },
278
- },
279
- required: ["message_id"],
280
- },
281
- },
282
- {
283
- name: "reply",
284
- description: "Reply to a message",
285
- inputSchema: {
286
- type: "object",
287
- properties: {
288
- message_id: {
289
- type: "string",
290
- description: "ID of the message to reply to",
291
- },
292
- body: {
293
- type: "string",
294
- description: "Reply content",
295
- },
296
- },
297
- required: ["message_id", "body"],
298
- },
299
- },
300
-
301
- // Discovery tools
302
- {
303
- name: "list_agents",
304
- description: "List all registered agents",
305
- inputSchema: {
306
- type: "object",
307
- properties: {
308
- status: {
309
- type: "string",
310
- enum: ["online", "busy", "offline"],
311
- description: "Filter by status (optional)",
312
- },
313
- },
314
- },
315
- },
316
- {
317
- name: "get_agent",
318
- description: "Get details about a specific agent",
319
- inputSchema: {
320
- type: "object",
321
- properties: {
322
- id: {
323
- type: "string",
324
- description: "Agent ID to look up",
325
- },
326
- },
327
- required: ["id"],
328
- },
329
- },
330
- {
331
- name: "list_channels",
332
- description: "List all available channels",
333
- inputSchema: {
334
- type: "object",
335
- properties: {},
336
- },
337
- },
338
- {
339
- name: "join_channel",
340
- description: "Subscribe to a channel to receive its messages",
341
- inputSchema: {
342
- type: "object",
343
- properties: {
344
- channel: {
345
- type: "string",
346
- description: "Channel name to join",
347
- },
348
- },
349
- required: ["channel"],
350
- },
351
- },
352
- {
353
- name: "leave_channel",
354
- description: "Unsubscribe from a channel",
355
- inputSchema: {
356
- type: "object",
357
- properties: {
358
- channel: {
359
- type: "string",
360
- description: "Channel name to leave",
361
- },
362
- },
363
- required: ["channel"],
364
- },
365
- },
366
-
367
- // Task completion tools
368
- {
369
- name: "agent_complete_task",
370
- description:
371
- "Mark current task as complete with a summary. Posts to Slack and notifies team.",
372
- inputSchema: {
373
- type: "object",
374
- properties: {
375
- summary: {
376
- type: "string",
377
- description: "Summary of what was accomplished",
378
- },
379
- outcome: {
380
- type: "string",
381
- enum: ["completed", "blocked", "partial", "needs_review", "handed_off"],
382
- description: "Task outcome status",
383
- },
384
- files_changed: {
385
- type: "array",
386
- items: { type: "string" },
387
- description: "List of files modified (optional)",
388
- },
389
- time_spent: {
390
- type: "string",
391
- description: "Override auto-calculated time (optional, e.g., '45 minutes')",
392
- },
393
- next_steps: {
394
- type: "string",
395
- description:
396
- "What comes next, can @mention agents for handoff (optional)",
397
- },
398
- },
399
- required: ["summary", "outcome"],
400
- },
401
- },
402
- {
403
- name: "get_pending_tasks",
404
- description: "Get tasks assigned to you by other agents",
405
- inputSchema: {
406
- type: "object",
407
- properties: {},
408
- },
409
- },
410
- {
411
- name: "accept_task",
412
- description: "Accept a pending task and start working on it",
413
- inputSchema: {
414
- type: "object",
415
- properties: {
416
- task_id: {
417
- type: "string",
418
- description: "ID of the task to accept",
419
- },
420
- },
421
- required: ["task_id"],
422
- },
423
- },
424
- {
425
- name: "decline_task",
426
- description: "Decline a pending task with reason",
427
- inputSchema: {
428
- type: "object",
429
- properties: {
430
- task_id: {
431
- type: "string",
432
- description: "ID of the task to decline",
433
- },
434
- reason: {
435
- type: "string",
436
- description: "Reason for declining",
437
- },
438
- },
439
- required: ["task_id", "reason"],
440
- },
441
- },
442
- ];
443
- }
444
-
445
- export async function handleToolCall(
446
- name: string,
447
- args: Record<string, unknown>,
448
- client: ApiClient,
449
- context: ToolContext
450
- ): Promise<unknown> {
451
- const agentId = context.getCurrentAgentId();
452
-
453
- switch (name) {
454
- // Registration
455
- case "agent_register": {
456
- const workingDir = context.getWorkingDir();
457
- const owner = state.getCurrentOwner();
458
- const requestedId = args.id as string;
459
-
460
- // Check for existing state file (reconnection)
461
- const existingState = state.loadState(workingDir);
462
-
463
- if (existingState) {
464
- // Verify owner matches
465
- if (existingState.owner !== owner) {
466
- // Different user - delete state and register fresh
467
- state.deleteState(workingDir);
468
- } else {
469
- // Try to reconnect
470
- try {
471
- const reconnectResult = await client.reconnectAgent(
472
- existingState.agent_id,
473
- existingState.token,
474
- owner
475
- );
476
-
477
- context.setCurrentAgentId(existingState.agent_id);
478
-
479
- // Update state with any new info
480
- state.saveState(workingDir, {
481
- ...existingState,
482
- last_task: undefined, // Cleared on reconnect
483
- });
484
-
485
- return {
486
- ...reconnectResult,
487
- mode: "reconnected",
488
- message: reconnectResult.was_offline
489
- ? `Reconnected after timeout. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.`
490
- : `Reconnected. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.`,
491
- };
492
- } catch (error) {
493
- // Reconnection failed (invalid token, agent not found, etc.)
494
- console.warn("Reconnection failed, registering fresh:", error);
495
- state.deleteState(workingDir);
496
- }
497
- }
498
- }
499
-
500
- // Fresh registration
501
- const result = await client.registerAgent(
502
- requestedId,
503
- args.name as string | undefined,
504
- owner,
505
- workingDir,
506
- args.model as string
507
- );
508
-
509
- context.setCurrentAgentId(requestedId);
510
-
511
- // Save state for future reconnection
512
- state.saveState(workingDir, {
513
- agent_id: requestedId,
514
- owner,
515
- token: result.token,
516
- registered_at: result.registered_at,
517
- });
518
-
519
- return {
520
- ...result,
521
- mode: "registered",
522
- message: `Registered as ${requestedId}. You have ${result.pending_tasks_count} pending tasks and ${result.unread_messages_count} unread messages.`,
523
- };
524
- }
525
-
526
- case "agent_start_work": {
527
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
528
- const result = await client.startWork(
529
- agentId,
530
- args.task as string,
531
- args.project as string | undefined
532
- );
533
- return wrapWithMessages(client, agentId, result);
534
- }
535
-
536
- case "agent_set_status": {
537
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
538
- const result = await client.heartbeat(agentId, args.status as "online" | "busy");
539
- return wrapWithMessages(client, agentId, result);
540
- }
541
-
542
- case "agent_disconnect": {
543
- // No wrapping - agent is going offline
544
- if (!agentId) throw new Error("Not registered.");
545
- context.stopHeartbeat();
546
- return client.disconnect(agentId);
547
- }
548
-
549
- // Messaging
550
- case "send_message": {
551
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
552
- const result = await client.sendMessage({
553
- from_agent: agentId,
554
- to_agent: args.to as string,
555
- type: args.type as string,
556
- subject: args.subject as string,
557
- body: args.body as string,
558
- priority: args.priority as string | undefined,
559
- });
560
- return wrapWithMessages(client, agentId, result);
561
- }
562
-
563
- case "send_to_channel": {
564
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
565
- const result = await client.sendMessage({
566
- from_agent: agentId,
567
- to_channel: args.channel as string,
568
- type: args.type as string,
569
- subject: args.subject as string,
570
- body: args.body as string,
571
- });
572
- return wrapWithMessages(client, agentId, result);
573
- }
574
-
575
- case "broadcast": {
576
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
577
- const result = await client.sendMessage({
578
- from_agent: agentId,
579
- broadcast: true,
580
- type: args.type as string,
581
- subject: args.subject as string,
582
- body: args.body as string,
583
- });
584
- return wrapWithMessages(client, agentId, result);
585
- }
586
-
587
- case "check_inbox": {
588
- // No wrapping - already fetching messages
589
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
590
- return client.getInbox(agentId, args.unread_only as boolean | undefined);
591
- }
592
-
593
- case "mark_read": {
594
- const result = await client.markRead(args.message_id as string);
595
- return wrapWithMessages(client, agentId, result);
596
- }
597
-
598
- case "reply": {
599
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
600
- const result = await client.reply(args.message_id as string, agentId, args.body as string);
601
- return wrapWithMessages(client, agentId, result);
602
- }
603
-
604
- // Discovery
605
- case "list_agents": {
606
- const result = await client.listAgents(args.status as string | undefined);
607
- return wrapWithMessages(client, agentId, result);
608
- }
609
-
610
- case "get_agent": {
611
- const result = await client.getAgent(args.id as string);
612
- return wrapWithMessages(client, agentId, result);
613
- }
614
-
615
- case "list_channels": {
616
- const result = await client.listChannels();
617
- return wrapWithMessages(client, agentId, result);
618
- }
619
-
620
- case "join_channel": {
621
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
622
- const result = await client.joinChannel(args.channel as string, agentId);
623
- return wrapWithMessages(client, agentId, result);
624
- }
625
-
626
- case "leave_channel": {
627
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
628
- const result = await client.leaveChannel(args.channel as string, agentId);
629
- return wrapWithMessages(client, agentId, result);
630
- }
631
-
632
- // Task completion
633
- case "agent_complete_task": {
634
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
635
- const result = await client.completeTask(agentId, {
636
- summary: args.summary as string,
637
- outcome: args.outcome as "completed" | "blocked" | "partial" | "needs_review" | "handed_off",
638
- files_changed: args.files_changed as string[] | undefined,
639
- time_spent: args.time_spent as string | undefined,
640
- next_steps: args.next_steps as string | undefined,
641
- });
642
- return wrapWithMessages(client, agentId, result);
643
- }
644
-
645
- case "get_pending_tasks": {
646
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
647
- const result = await client.getPendingTasks(agentId);
648
- return wrapWithMessages(client, agentId, result);
649
- }
650
-
651
- case "accept_task": {
652
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
653
- const result = await client.acceptTask(agentId, args.task_id as string);
654
- return wrapWithMessages(client, agentId, result);
655
- }
656
-
657
- case "decline_task": {
658
- if (!agentId) throw new Error("Not registered. Call agent_register first.");
659
- const result = await client.declineTask(agentId, args.task_id as string, args.reason as string);
660
- return wrapWithMessages(client, agentId, result);
661
- }
662
-
663
- default:
664
- throw new Error(`Unknown tool: ${name}`);
665
- }
666
- }
1
+ /**
2
+ * MCP tool definitions and handlers
3
+ */
4
+
5
+ import { ApiClient, Message } from "../client.js";
6
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
7
+ import * as state from "../state.js";
8
+
9
+ export interface ToolContext {
10
+ getCurrentAgentId: () => string;
11
+ setCurrentAgentId: (id: string) => void;
12
+ stopHeartbeat: () => void;
13
+ getWorkingDir: () => string;
14
+ }
15
+
16
+ interface UrgentAction {
17
+ required: true;
18
+ instruction: string;
19
+ message: Message;
20
+ }
21
+
22
+ interface WrappedResponse {
23
+ result: unknown;
24
+ pending_messages: Message[];
25
+ pending_count: number;
26
+ has_more: boolean;
27
+ urgent_action: UrgentAction | null;
28
+ }
29
+
30
+ /**
31
+ * Wraps a tool response with pending messages
32
+ * This enables automatic message delivery without extra API calls
33
+ */
34
+ async function wrapWithMessages(
35
+ client: ApiClient,
36
+ agentId: string | null,
37
+ result: unknown
38
+ ): Promise<WrappedResponse | unknown> {
39
+ // If not registered, return raw result
40
+ if (!agentId) return result;
41
+
42
+ try {
43
+ // Fetch unread messages
44
+ const inbox = await client.getInbox(agentId, true, 10);
45
+ const messages = inbox.messages || [];
46
+
47
+ // Sort: urgent first, then by created_at
48
+ messages.sort((a, b) => {
49
+ const aUrgent = isUrgentMessage(a);
50
+ const bUrgent = isUrgentMessage(b);
51
+ if (aUrgent && !bUrgent) return -1;
52
+ if (!aUrgent && bUrgent) return 1;
53
+ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
54
+ });
55
+
56
+ // Find urgent message for action
57
+ const urgentMsg = messages.find(isUrgentMessage);
58
+
59
+ let urgentAction: UrgentAction | null = null;
60
+ if (urgentMsg) {
61
+ urgentAction = {
62
+ required: true,
63
+ instruction: buildUrgentInstruction(urgentMsg),
64
+ message: urgentMsg,
65
+ };
66
+ }
67
+
68
+ return {
69
+ result,
70
+ pending_messages: messages,
71
+ pending_count: inbox.total,
72
+ has_more: inbox.total > messages.length,
73
+ urgent_action: urgentAction,
74
+ };
75
+ } catch (error) {
76
+ // Don't break tool call if message fetch fails
77
+ console.warn("Failed to fetch messages for wrapping:", error);
78
+ return result;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if a message is urgent (requires immediate action)
84
+ */
85
+ function isUrgentMessage(msg: Message): boolean {
86
+ return (
87
+ msg.priority === "urgent" ||
88
+ (msg.priority === "high" && msg.type === "question")
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Build instruction text for urgent messages
94
+ */
95
+ function buildUrgentInstruction(msg: Message): string {
96
+ return `URGENT: Agent [${msg.from_agent}] needs immediate assistance.
97
+
98
+ Query: ${msg.subject}
99
+
100
+ Details: ${msg.body}
101
+
102
+ Spawn a subagent with Task tool to research and respond to this query. Use send_message to reply when done.`;
103
+ }
104
+
105
+ export function registerTools(): Tool[] {
106
+ return [
107
+ // Registration tools
108
+ {
109
+ name: "agent_register",
110
+ description: "Register this agent with AgentHub. Must be called before using other tools.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ id: {
115
+ type: "string",
116
+ description: "Unique agent identifier (e.g., 'backend-1', 'tester')",
117
+ },
118
+ name: {
119
+ type: "string",
120
+ description: "Human-friendly name for this agent",
121
+ },
122
+ model: {
123
+ type: "string",
124
+ description: "Model name (e.g., 'claude-opus-4.5', 'gpt-4-turbo', 'o3', 'gemini-2.0-pro'). Provider is auto-detected.",
125
+ },
126
+ },
127
+ required: ["id", "model"],
128
+ },
129
+ },
130
+ {
131
+ name: "agent_start_work",
132
+ description: "Declare what you're currently working on. Sends notification to Slack.",
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ task: {
137
+ type: "string",
138
+ description: "Description of the task you're starting",
139
+ },
140
+ project: {
141
+ type: "string",
142
+ description: "Project name (optional)",
143
+ },
144
+ },
145
+ required: ["task"],
146
+ },
147
+ },
148
+ {
149
+ name: "agent_set_status",
150
+ description: "Update your status (online or busy)",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ status: {
155
+ type: "string",
156
+ enum: ["online", "busy"],
157
+ description: "New status",
158
+ },
159
+ },
160
+ required: ["status"],
161
+ },
162
+ },
163
+ {
164
+ name: "agent_disconnect",
165
+ description: "Gracefully disconnect from AgentHub",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {},
169
+ },
170
+ },
171
+
172
+ // Messaging tools
173
+ {
174
+ name: "send_message",
175
+ description: "Send a direct message to another agent",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ to: {
180
+ type: "string",
181
+ description: "Target agent ID",
182
+ },
183
+ type: {
184
+ type: "string",
185
+ enum: ["task", "status", "question", "response", "context"],
186
+ description: "Type of message",
187
+ },
188
+ subject: {
189
+ type: "string",
190
+ description: "Message subject/summary",
191
+ },
192
+ body: {
193
+ type: "string",
194
+ description: "Full message content",
195
+ },
196
+ priority: {
197
+ type: "string",
198
+ enum: ["normal", "high", "urgent"],
199
+ description: "Message priority (default: normal)",
200
+ },
201
+ },
202
+ required: ["to", "type", "subject", "body"],
203
+ },
204
+ },
205
+ {
206
+ name: "send_to_channel",
207
+ description: "Send a message to all members of a channel",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ channel: {
212
+ type: "string",
213
+ description: "Target channel name",
214
+ },
215
+ type: {
216
+ type: "string",
217
+ enum: ["task", "status", "question", "response", "context"],
218
+ description: "Type of message",
219
+ },
220
+ subject: {
221
+ type: "string",
222
+ description: "Message subject/summary",
223
+ },
224
+ body: {
225
+ type: "string",
226
+ description: "Full message content",
227
+ },
228
+ },
229
+ required: ["channel", "type", "subject", "body"],
230
+ },
231
+ },
232
+ {
233
+ name: "broadcast",
234
+ description: "Send a message to all registered agents",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ type: {
239
+ type: "string",
240
+ enum: ["task", "status", "question", "response", "context"],
241
+ description: "Type of message",
242
+ },
243
+ subject: {
244
+ type: "string",
245
+ description: "Message subject/summary",
246
+ },
247
+ body: {
248
+ type: "string",
249
+ description: "Full message content",
250
+ },
251
+ },
252
+ required: ["type", "subject", "body"],
253
+ },
254
+ },
255
+ {
256
+ name: "check_inbox",
257
+ description: "Check for incoming messages",
258
+ inputSchema: {
259
+ type: "object",
260
+ properties: {
261
+ unread_only: {
262
+ type: "boolean",
263
+ description: "Only return unread messages (default: false)",
264
+ },
265
+ },
266
+ },
267
+ },
268
+ {
269
+ name: "mark_read",
270
+ description: "Mark a message as read",
271
+ inputSchema: {
272
+ type: "object",
273
+ properties: {
274
+ message_id: {
275
+ type: "string",
276
+ description: "ID of the message to mark as read",
277
+ },
278
+ },
279
+ required: ["message_id"],
280
+ },
281
+ },
282
+ {
283
+ name: "reply",
284
+ description: "Reply to a message",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ message_id: {
289
+ type: "string",
290
+ description: "ID of the message to reply to",
291
+ },
292
+ body: {
293
+ type: "string",
294
+ description: "Reply content",
295
+ },
296
+ },
297
+ required: ["message_id", "body"],
298
+ },
299
+ },
300
+
301
+ // Discovery tools
302
+ {
303
+ name: "list_agents",
304
+ description: "List all registered agents",
305
+ inputSchema: {
306
+ type: "object",
307
+ properties: {
308
+ status: {
309
+ type: "string",
310
+ enum: ["online", "busy", "offline"],
311
+ description: "Filter by status (optional)",
312
+ },
313
+ },
314
+ },
315
+ },
316
+ {
317
+ name: "get_agent",
318
+ description: "Get details about a specific agent",
319
+ inputSchema: {
320
+ type: "object",
321
+ properties: {
322
+ id: {
323
+ type: "string",
324
+ description: "Agent ID to look up",
325
+ },
326
+ },
327
+ required: ["id"],
328
+ },
329
+ },
330
+ {
331
+ name: "list_channels",
332
+ description: "List all available channels",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {},
336
+ },
337
+ },
338
+ {
339
+ name: "join_channel",
340
+ description: "Subscribe to a channel to receive its messages",
341
+ inputSchema: {
342
+ type: "object",
343
+ properties: {
344
+ channel: {
345
+ type: "string",
346
+ description: "Channel name to join",
347
+ },
348
+ },
349
+ required: ["channel"],
350
+ },
351
+ },
352
+ {
353
+ name: "leave_channel",
354
+ description: "Unsubscribe from a channel",
355
+ inputSchema: {
356
+ type: "object",
357
+ properties: {
358
+ channel: {
359
+ type: "string",
360
+ description: "Channel name to leave",
361
+ },
362
+ },
363
+ required: ["channel"],
364
+ },
365
+ },
366
+
367
+ // Task completion tools
368
+ {
369
+ name: "agent_complete_task",
370
+ description:
371
+ "Mark current task as complete with a summary. Posts to Slack and notifies team.",
372
+ inputSchema: {
373
+ type: "object",
374
+ properties: {
375
+ summary: {
376
+ type: "string",
377
+ description: "Summary of what was accomplished",
378
+ },
379
+ outcome: {
380
+ type: "string",
381
+ enum: ["completed", "blocked", "partial", "needs_review", "handed_off"],
382
+ description: "Task outcome status",
383
+ },
384
+ files_changed: {
385
+ type: "array",
386
+ items: { type: "string" },
387
+ description: "List of files modified (optional)",
388
+ },
389
+ time_spent: {
390
+ type: "string",
391
+ description: "Override auto-calculated time (optional, e.g., '45 minutes')",
392
+ },
393
+ next_steps: {
394
+ type: "string",
395
+ description:
396
+ "What comes next, can @mention agents for handoff (optional)",
397
+ },
398
+ },
399
+ required: ["summary", "outcome"],
400
+ },
401
+ },
402
+ {
403
+ name: "get_pending_tasks",
404
+ description: "Get tasks assigned to you by other agents",
405
+ inputSchema: {
406
+ type: "object",
407
+ properties: {},
408
+ },
409
+ },
410
+ {
411
+ name: "accept_task",
412
+ description: "Accept a pending task and start working on it",
413
+ inputSchema: {
414
+ type: "object",
415
+ properties: {
416
+ task_id: {
417
+ type: "string",
418
+ description: "ID of the task to accept",
419
+ },
420
+ },
421
+ required: ["task_id"],
422
+ },
423
+ },
424
+ {
425
+ name: "decline_task",
426
+ description: "Decline a pending task with reason",
427
+ inputSchema: {
428
+ type: "object",
429
+ properties: {
430
+ task_id: {
431
+ type: "string",
432
+ description: "ID of the task to decline",
433
+ },
434
+ reason: {
435
+ type: "string",
436
+ description: "Reason for declining",
437
+ },
438
+ },
439
+ required: ["task_id", "reason"],
440
+ },
441
+ },
442
+ ];
443
+ }
444
+
445
+ export async function handleToolCall(
446
+ name: string,
447
+ args: Record<string, unknown>,
448
+ client: ApiClient,
449
+ context: ToolContext
450
+ ): Promise<unknown> {
451
+ const agentId = context.getCurrentAgentId();
452
+
453
+ switch (name) {
454
+ // Registration
455
+ case "agent_register": {
456
+ const workingDir = context.getWorkingDir();
457
+ const owner = state.getCurrentOwner();
458
+ const requestedId = args.id as string;
459
+
460
+ // Check for existing state file (reconnection)
461
+ const existingState = state.loadState(workingDir);
462
+
463
+ if (existingState) {
464
+ // Verify owner matches
465
+ if (existingState.owner !== owner) {
466
+ // Different user - delete state and register fresh
467
+ state.deleteState(workingDir);
468
+ } else {
469
+ // Try to reconnect
470
+ try {
471
+ const reconnectResult = await client.reconnectAgent(
472
+ existingState.agent_id,
473
+ existingState.token,
474
+ owner,
475
+ args.model as string // Pass the requested model to update it
476
+ );
477
+
478
+ context.setCurrentAgentId(existingState.agent_id);
479
+
480
+ // Update state with any new info
481
+ state.saveState(workingDir, {
482
+ ...existingState,
483
+ last_task: undefined, // Cleared on reconnect
484
+ });
485
+
486
+ return {
487
+ ...reconnectResult,
488
+ mode: "reconnected",
489
+ message: reconnectResult.was_offline
490
+ ? `Reconnected after timeout. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.`
491
+ : `Reconnected. You have ${reconnectResult.pending_tasks_count} pending tasks and ${reconnectResult.unread_messages_count} unread messages.`,
492
+ };
493
+ } catch (error) {
494
+ // Reconnection failed (invalid token, agent not found, etc.)
495
+ console.warn("Reconnection failed, registering fresh:", error);
496
+ state.deleteState(workingDir);
497
+ }
498
+ }
499
+ }
500
+
501
+ // Fresh registration
502
+ // Auto-detect model from environment or use provided value
503
+ const model = args.model as string ||
504
+ process.env.CLAUDE_MODEL ||
505
+ "claude-opus-4-5-20251101";
506
+
507
+ const result = await client.registerAgent(
508
+ requestedId,
509
+ args.name as string | undefined,
510
+ owner,
511
+ workingDir,
512
+ model
513
+ );
514
+
515
+ context.setCurrentAgentId(requestedId);
516
+
517
+ // Save state for future reconnection
518
+ state.saveState(workingDir, {
519
+ agent_id: requestedId,
520
+ owner,
521
+ token: result.token,
522
+ registered_at: result.registered_at,
523
+ });
524
+
525
+ return {
526
+ ...result,
527
+ mode: "registered",
528
+ message: `Registered as ${requestedId}. You have ${result.pending_tasks_count} pending tasks and ${result.unread_messages_count} unread messages.`,
529
+ };
530
+ }
531
+
532
+ case "agent_start_work": {
533
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
534
+ const result = await client.startWork(
535
+ agentId,
536
+ args.task as string,
537
+ args.project as string | undefined
538
+ );
539
+ return wrapWithMessages(client, agentId, result);
540
+ }
541
+
542
+ case "agent_set_status": {
543
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
544
+ const result = await client.heartbeat(agentId, args.status as "online" | "busy");
545
+ return wrapWithMessages(client, agentId, result);
546
+ }
547
+
548
+ case "agent_disconnect": {
549
+ // No wrapping - agent is going offline
550
+ if (!agentId) throw new Error("Not registered.");
551
+ context.stopHeartbeat();
552
+ return client.disconnect(agentId);
553
+ }
554
+
555
+ // Messaging
556
+ case "send_message": {
557
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
558
+ const result = await client.sendMessage({
559
+ from_agent: agentId,
560
+ to_agent: args.to as string,
561
+ type: args.type as string,
562
+ subject: args.subject as string,
563
+ body: args.body as string,
564
+ priority: args.priority as string | undefined,
565
+ });
566
+ return wrapWithMessages(client, agentId, result);
567
+ }
568
+
569
+ case "send_to_channel": {
570
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
571
+ const result = await client.sendMessage({
572
+ from_agent: agentId,
573
+ to_channel: args.channel as string,
574
+ type: args.type as string,
575
+ subject: args.subject as string,
576
+ body: args.body as string,
577
+ });
578
+ return wrapWithMessages(client, agentId, result);
579
+ }
580
+
581
+ case "broadcast": {
582
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
583
+ const result = await client.sendMessage({
584
+ from_agent: agentId,
585
+ broadcast: true,
586
+ type: args.type as string,
587
+ subject: args.subject as string,
588
+ body: args.body as string,
589
+ });
590
+ return wrapWithMessages(client, agentId, result);
591
+ }
592
+
593
+ case "check_inbox": {
594
+ // No wrapping - already fetching messages
595
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
596
+ return client.getInbox(agentId, args.unread_only as boolean | undefined);
597
+ }
598
+
599
+ case "mark_read": {
600
+ const result = await client.markRead(args.message_id as string);
601
+ return wrapWithMessages(client, agentId, result);
602
+ }
603
+
604
+ case "reply": {
605
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
606
+ const result = await client.reply(args.message_id as string, agentId, args.body as string);
607
+ return wrapWithMessages(client, agentId, result);
608
+ }
609
+
610
+ // Discovery
611
+ case "list_agents": {
612
+ const result = await client.listAgents(args.status as string | undefined);
613
+ return wrapWithMessages(client, agentId, result);
614
+ }
615
+
616
+ case "get_agent": {
617
+ const result = await client.getAgent(args.id as string);
618
+ return wrapWithMessages(client, agentId, result);
619
+ }
620
+
621
+ case "list_channels": {
622
+ const result = await client.listChannels();
623
+ return wrapWithMessages(client, agentId, result);
624
+ }
625
+
626
+ case "join_channel": {
627
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
628
+ const result = await client.joinChannel(args.channel as string, agentId);
629
+ return wrapWithMessages(client, agentId, result);
630
+ }
631
+
632
+ case "leave_channel": {
633
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
634
+ const result = await client.leaveChannel(args.channel as string, agentId);
635
+ return wrapWithMessages(client, agentId, result);
636
+ }
637
+
638
+ // Task completion
639
+ case "agent_complete_task": {
640
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
641
+ const result = await client.completeTask(agentId, {
642
+ summary: args.summary as string,
643
+ outcome: args.outcome as "completed" | "blocked" | "partial" | "needs_review" | "handed_off",
644
+ files_changed: args.files_changed as string[] | undefined,
645
+ time_spent: args.time_spent as string | undefined,
646
+ next_steps: args.next_steps as string | undefined,
647
+ });
648
+ return wrapWithMessages(client, agentId, result);
649
+ }
650
+
651
+ case "get_pending_tasks": {
652
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
653
+ const result = await client.getPendingTasks(agentId);
654
+ return wrapWithMessages(client, agentId, result);
655
+ }
656
+
657
+ case "accept_task": {
658
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
659
+ const result = await client.acceptTask(agentId, args.task_id as string);
660
+ return wrapWithMessages(client, agentId, result);
661
+ }
662
+
663
+ case "decline_task": {
664
+ if (!agentId) throw new Error("Not registered. Call agent_register first.");
665
+ const result = await client.declineTask(agentId, args.task_id as string, args.reason as string);
666
+ return wrapWithMessages(client, agentId, result);
667
+ }
668
+
669
+ default:
670
+ throw new Error(`Unknown tool: ${name}`);
671
+ }
672
+ }