agenthub-multiagent-mcp 1.1.5 → 1.4.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/README.md +25 -3
- package/dist/client.d.ts +162 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +139 -1
- package/dist/client.js.map +1 -1
- package/dist/heartbeat.d.ts +16 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +97 -26
- package/dist/heartbeat.js.map +1 -1
- package/dist/index.js +68 -4
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/tools/index.d.ts +15 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +458 -48
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/tools.test.js +7 -1
- package/dist/tools/tools.test.js.map +1 -1
- package/dist/websocket.d.ts +66 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +196 -0
- package/dist/websocket.js.map +1 -0
- package/package.json +6 -1
- package/src/client.ts +318 -2
- package/src/heartbeat.ts +108 -25
- package/src/index.ts +77 -4
- package/src/state.ts +1 -0
- package/src/tools/index.ts +505 -59
- package/src/tools/tools.test.ts +7 -1
- package/src/websocket.ts +237 -0
package/src/client.ts
CHANGED
|
@@ -28,6 +28,76 @@ export interface PendingTask {
|
|
|
28
28
|
created_at: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Ticket system types
|
|
32
|
+
export interface TicketTask {
|
|
33
|
+
id: string;
|
|
34
|
+
org_id: string;
|
|
35
|
+
project_id: string;
|
|
36
|
+
epic_id?: string;
|
|
37
|
+
story_id?: string;
|
|
38
|
+
key: string;
|
|
39
|
+
type: string;
|
|
40
|
+
title: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
status: string;
|
|
43
|
+
priority: string;
|
|
44
|
+
points?: number;
|
|
45
|
+
review_mode: string;
|
|
46
|
+
assignee_id?: string;
|
|
47
|
+
assigned_agent?: string;
|
|
48
|
+
claimed_by?: string;
|
|
49
|
+
labels?: string[];
|
|
50
|
+
sprint_id?: string;
|
|
51
|
+
due_date?: string;
|
|
52
|
+
created_by: string;
|
|
53
|
+
created_at: string;
|
|
54
|
+
updated_at: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TicketTaskContext {
|
|
58
|
+
task: TicketTask;
|
|
59
|
+
story?: {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
status: string;
|
|
64
|
+
};
|
|
65
|
+
epic?: {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
status: string;
|
|
70
|
+
};
|
|
71
|
+
project?: {
|
|
72
|
+
id: string;
|
|
73
|
+
key: string;
|
|
74
|
+
name: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TaskCheckpoint {
|
|
80
|
+
id: string;
|
|
81
|
+
task_id: string;
|
|
82
|
+
task_key: string;
|
|
83
|
+
agent_id: string;
|
|
84
|
+
description: string;
|
|
85
|
+
status: string;
|
|
86
|
+
reviewed_by?: string;
|
|
87
|
+
review_notes?: string;
|
|
88
|
+
reviewed_at?: string;
|
|
89
|
+
created_at: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CheckpointResponse {
|
|
93
|
+
id: string;
|
|
94
|
+
task_id: string;
|
|
95
|
+
task_key: string;
|
|
96
|
+
status: string;
|
|
97
|
+
slack_posted: boolean;
|
|
98
|
+
created_at: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
31
101
|
export interface TaskCompletionResponse {
|
|
32
102
|
task_id: string;
|
|
33
103
|
elapsed_time: string;
|
|
@@ -150,11 +220,19 @@ export class ApiClient {
|
|
|
150
220
|
async startWork(
|
|
151
221
|
agentId: string,
|
|
152
222
|
task: string,
|
|
153
|
-
project?: string
|
|
154
|
-
|
|
223
|
+
project?: string,
|
|
224
|
+
taskId?: string
|
|
225
|
+
): Promise<{
|
|
226
|
+
acknowledged: boolean;
|
|
227
|
+
slack_posted: boolean;
|
|
228
|
+
task_id?: string;
|
|
229
|
+
task_key?: string;
|
|
230
|
+
task_linked?: boolean;
|
|
231
|
+
}> {
|
|
155
232
|
return this.request("POST", `/agents/${agentId}/start-work`, {
|
|
156
233
|
task,
|
|
157
234
|
project,
|
|
235
|
+
task_id: taskId,
|
|
158
236
|
});
|
|
159
237
|
}
|
|
160
238
|
|
|
@@ -294,4 +372,242 @@ export class ApiClient {
|
|
|
294
372
|
reason,
|
|
295
373
|
});
|
|
296
374
|
}
|
|
375
|
+
|
|
376
|
+
// Ticket system methods (for agent integration)
|
|
377
|
+
|
|
378
|
+
async getAvailableTasks(params?: {
|
|
379
|
+
status?: string;
|
|
380
|
+
project_id?: string;
|
|
381
|
+
priority?: string;
|
|
382
|
+
required_type?: string;
|
|
383
|
+
}): Promise<{ tasks: TicketTask[]; total: number }> {
|
|
384
|
+
const query = new URLSearchParams();
|
|
385
|
+
if (params?.status) query.set("status", params.status);
|
|
386
|
+
if (params?.project_id) query.set("project_id", params.project_id);
|
|
387
|
+
if (params?.priority) query.set("priority", params.priority);
|
|
388
|
+
if (params?.required_type) query.set("required_type", params.required_type);
|
|
389
|
+
const queryStr = query.toString() ? `?${query}` : "";
|
|
390
|
+
return this.request("GET", `/agent-tickets/available${queryStr}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async getTicketTask(taskId: string): Promise<TicketTaskContext> {
|
|
394
|
+
return this.request("GET", `/agent-tickets/${taskId}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async claimTicketTask(
|
|
398
|
+
taskId: string,
|
|
399
|
+
agentId: string
|
|
400
|
+
): Promise<{
|
|
401
|
+
success: boolean;
|
|
402
|
+
task: TicketTask;
|
|
403
|
+
task_id: string;
|
|
404
|
+
task_key: string;
|
|
405
|
+
}> {
|
|
406
|
+
return this.request("POST", `/agent-tickets/${taskId}/claim`, {
|
|
407
|
+
agent_id: agentId,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async createCheckpoint(
|
|
412
|
+
taskId: string,
|
|
413
|
+
agentId: string,
|
|
414
|
+
description: string
|
|
415
|
+
): Promise<CheckpointResponse> {
|
|
416
|
+
return this.request("POST", `/agent-tickets/${taskId}/checkpoint`, {
|
|
417
|
+
agent_id: agentId,
|
|
418
|
+
description,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Browser-based registration flow
|
|
423
|
+
async initRegistration(agentId?: string, agentName?: string): Promise<{
|
|
424
|
+
session_token: string;
|
|
425
|
+
dashboard_url: string;
|
|
426
|
+
expires_at: string;
|
|
427
|
+
}> {
|
|
428
|
+
return this.request("POST", "/agents/register-init", {
|
|
429
|
+
agent_id: agentId,
|
|
430
|
+
agent_name: agentName,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async pollRegistrationCallback(token: string): Promise<{
|
|
435
|
+
status: "pending" | "completed" | "expired";
|
|
436
|
+
agent_id?: string;
|
|
437
|
+
agent_name?: string;
|
|
438
|
+
agent_type?: string;
|
|
439
|
+
connect_token?: string;
|
|
440
|
+
expires_at?: string;
|
|
441
|
+
}> {
|
|
442
|
+
const url = `${this.baseUrl}/agents/register-callback/${token}`;
|
|
443
|
+
const response = await fetch(url, {
|
|
444
|
+
method: "GET",
|
|
445
|
+
headers: {
|
|
446
|
+
"Content-Type": "application/json",
|
|
447
|
+
"X-API-Key": this.apiKey,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (response.status === 202) {
|
|
452
|
+
// Still pending
|
|
453
|
+
const data = await response.json() as { status: string; expires_at: string };
|
|
454
|
+
return { status: "pending", expires_at: data.expires_at };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (response.status === 410) {
|
|
458
|
+
// Expired
|
|
459
|
+
return { status: "expired" };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
const data = await response.json() as { message?: string };
|
|
464
|
+
throw new Error(data.message || `Request failed: ${response.status}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const data = await response.json() as {
|
|
468
|
+
status: string;
|
|
469
|
+
agent_id: string;
|
|
470
|
+
agent_name: string;
|
|
471
|
+
agent_type: string;
|
|
472
|
+
connect_token: string;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
status: "completed",
|
|
477
|
+
agent_id: data.agent_id,
|
|
478
|
+
agent_name: data.agent_name,
|
|
479
|
+
agent_type: data.agent_type,
|
|
480
|
+
connect_token: data.connect_token,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getBaseUrl(): string {
|
|
485
|
+
return this.baseUrl;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Ticket creation methods
|
|
489
|
+
|
|
490
|
+
async createEpic(params: {
|
|
491
|
+
project_id: string;
|
|
492
|
+
title: string;
|
|
493
|
+
description?: string;
|
|
494
|
+
source_file?: string;
|
|
495
|
+
}): Promise<{ id: string; key: string }> {
|
|
496
|
+
return this.request("POST", `/projects/${params.project_id}/epics`, {
|
|
497
|
+
title: params.title,
|
|
498
|
+
description: params.description,
|
|
499
|
+
metadata: params.source_file ? JSON.stringify({ source_file: params.source_file }) : undefined,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async createStory(params: {
|
|
504
|
+
project_id: string;
|
|
505
|
+
epic_id?: string;
|
|
506
|
+
title: string;
|
|
507
|
+
description?: string;
|
|
508
|
+
required_type?: string;
|
|
509
|
+
labels?: string[];
|
|
510
|
+
order?: number;
|
|
511
|
+
}): Promise<{ id: string; key: string }> {
|
|
512
|
+
return this.request("POST", `/projects/${params.project_id}/stories`, {
|
|
513
|
+
epic_id: params.epic_id,
|
|
514
|
+
title: params.title,
|
|
515
|
+
description: params.description,
|
|
516
|
+
required_type: params.required_type,
|
|
517
|
+
labels: params.labels,
|
|
518
|
+
backlog_order: params.order,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async createTask(params: {
|
|
523
|
+
project_id: string;
|
|
524
|
+
story_id?: string;
|
|
525
|
+
epic_id?: string;
|
|
526
|
+
title: string;
|
|
527
|
+
description?: string;
|
|
528
|
+
required_type?: string;
|
|
529
|
+
labels?: string[];
|
|
530
|
+
order?: number;
|
|
531
|
+
}): Promise<{ id: string; key: string }> {
|
|
532
|
+
return this.request("POST", `/projects/${params.project_id}/tasks`, {
|
|
533
|
+
story_id: params.story_id,
|
|
534
|
+
epic_id: params.epic_id,
|
|
535
|
+
title: params.title,
|
|
536
|
+
description: params.description,
|
|
537
|
+
required_type: params.required_type,
|
|
538
|
+
labels: params.labels,
|
|
539
|
+
backlog_order: params.order,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async addComment(params: {
|
|
544
|
+
ticket_id: string;
|
|
545
|
+
body: string;
|
|
546
|
+
}): Promise<{ id: string }> {
|
|
547
|
+
return this.request("POST", `/tickets/${params.ticket_id}/comments`, {
|
|
548
|
+
body: params.body,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async updateTicketStatus(params: {
|
|
553
|
+
ticket_id: string;
|
|
554
|
+
status: string;
|
|
555
|
+
summary?: string;
|
|
556
|
+
files_changed?: string[];
|
|
557
|
+
}): Promise<{ success: boolean }> {
|
|
558
|
+
return this.request("PATCH", `/tasks/${params.ticket_id}`, {
|
|
559
|
+
status: params.status,
|
|
560
|
+
summary: params.summary,
|
|
561
|
+
files_changed: params.files_changed,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async uploadAttachment(params: {
|
|
566
|
+
ticket_id: string;
|
|
567
|
+
ticket_type: string;
|
|
568
|
+
ticket_key: string;
|
|
569
|
+
file_path: string;
|
|
570
|
+
description?: string;
|
|
571
|
+
agent_id?: string;
|
|
572
|
+
}): Promise<{
|
|
573
|
+
id: string;
|
|
574
|
+
url: string;
|
|
575
|
+
filename: string;
|
|
576
|
+
size: number;
|
|
577
|
+
mime_type: string;
|
|
578
|
+
}> {
|
|
579
|
+
const fs = await import("fs");
|
|
580
|
+
const FormData = (await import("form-data")).default;
|
|
581
|
+
|
|
582
|
+
const form = new FormData();
|
|
583
|
+
form.append("ticket_id", params.ticket_id);
|
|
584
|
+
form.append("ticket_type", params.ticket_type);
|
|
585
|
+
form.append("ticket_key", params.ticket_key);
|
|
586
|
+
form.append("file", fs.createReadStream(params.file_path));
|
|
587
|
+
if (params.description) form.append("description", params.description);
|
|
588
|
+
if (params.agent_id) form.append("agent_id", params.agent_id);
|
|
589
|
+
|
|
590
|
+
const response = await fetch(`${this.baseUrl}/attachments`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
"X-API-Key": this.apiKey,
|
|
594
|
+
...form.getHeaders(),
|
|
595
|
+
},
|
|
596
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
597
|
+
body: form as any,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
if (!response.ok) {
|
|
601
|
+
const error = await response.json() as { message?: string };
|
|
602
|
+
throw new Error(error.message || `Upload failed: ${response.status}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return response.json() as Promise<{
|
|
606
|
+
id: string;
|
|
607
|
+
url: string;
|
|
608
|
+
filename: string;
|
|
609
|
+
size: number;
|
|
610
|
+
mime_type: string;
|
|
611
|
+
}>;
|
|
612
|
+
}
|
|
297
613
|
}
|
package/src/heartbeat.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Background heartbeat manager with enhanced logging for pending tasks/messages
|
|
3
|
+
* and automatic reconnection on failures.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { ApiClient } from "./client.js";
|
|
7
|
+
import { AgentState } from "./state.js";
|
|
8
|
+
|
|
9
|
+
// Reconnection callback type
|
|
10
|
+
export type ReconnectCallback = () => Promise<AgentState | null>;
|
|
6
11
|
|
|
7
12
|
export class HeartbeatManager {
|
|
8
13
|
private client: ApiClient;
|
|
@@ -13,16 +18,34 @@ export class HeartbeatManager {
|
|
|
13
18
|
private lastUnreadMessages = 0;
|
|
14
19
|
private heartbeatCount = 0;
|
|
15
20
|
|
|
21
|
+
// Retry state
|
|
22
|
+
private consecutiveFailures = 0;
|
|
23
|
+
private backoffMs = 1000; // Start at 1 second
|
|
24
|
+
private readonly maxBackoffMs = 30000; // Cap at 30 seconds
|
|
25
|
+
private readonly failuresBeforeReconnect = 3;
|
|
26
|
+
private reconnectCallback?: ReconnectCallback;
|
|
27
|
+
private isReconnecting = false;
|
|
28
|
+
|
|
16
29
|
constructor(client: ApiClient) {
|
|
17
30
|
this.client = client;
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Set callback for reconnection attempts
|
|
35
|
+
*/
|
|
36
|
+
setReconnectCallback(callback: ReconnectCallback): void {
|
|
37
|
+
this.reconnectCallback = callback;
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
start(agentId: string): void {
|
|
21
41
|
this.stop(); // Clear any existing interval
|
|
22
42
|
this.agentId = agentId;
|
|
23
43
|
this.heartbeatCount = 0;
|
|
24
44
|
this.lastPendingTasks = 0;
|
|
25
45
|
this.lastUnreadMessages = 0;
|
|
46
|
+
this.consecutiveFailures = 0;
|
|
47
|
+
this.backoffMs = 1000;
|
|
48
|
+
this.isReconnecting = false;
|
|
26
49
|
|
|
27
50
|
// Send heartbeat every 30 seconds
|
|
28
51
|
this.intervalId = setInterval(async () => {
|
|
@@ -31,12 +54,12 @@ export class HeartbeatManager {
|
|
|
31
54
|
try {
|
|
32
55
|
await this.sendHeartbeat();
|
|
33
56
|
} catch (error) {
|
|
34
|
-
|
|
57
|
+
await this.handleHeartbeatFailure(error);
|
|
35
58
|
}
|
|
36
59
|
}, 30_000);
|
|
37
60
|
|
|
38
61
|
// Send initial heartbeat
|
|
39
|
-
this.sendHeartbeat();
|
|
62
|
+
this.sendHeartbeat().catch((error) => this.handleHeartbeatFailure(error));
|
|
40
63
|
}
|
|
41
64
|
|
|
42
65
|
stop(): void {
|
|
@@ -57,42 +80,102 @@ export class HeartbeatManager {
|
|
|
57
80
|
|
|
58
81
|
setStatus(status: "online" | "busy"): void {
|
|
59
82
|
this.status = status;
|
|
60
|
-
this.sendHeartbeat();
|
|
83
|
+
this.sendHeartbeat().catch((error) => this.handleHeartbeatFailure(error));
|
|
61
84
|
}
|
|
62
85
|
|
|
63
86
|
private async sendHeartbeat(): Promise<void> {
|
|
64
87
|
if (!this.agentId) return;
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
const response = await this.client.heartbeat(this.agentId, this.status);
|
|
68
|
-
this.heartbeatCount++;
|
|
89
|
+
const response = await this.client.heartbeat(this.agentId, this.status);
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
// Success - reset failure state
|
|
92
|
+
this.consecutiveFailures = 0;
|
|
93
|
+
this.backoffMs = 1000;
|
|
94
|
+
this.heartbeatCount++;
|
|
71
95
|
|
|
72
|
-
|
|
73
|
-
if (pending_tasks_count > 0 && pending_tasks_count !== this.lastPendingTasks) {
|
|
74
|
-
console.error(`\n🔔 [AgentHub] You have ${pending_tasks_count} pending task(s) waiting! Use get_pending_tasks to view them.\n`);
|
|
75
|
-
}
|
|
96
|
+
const { pending_tasks_count, unread_messages_count } = response;
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
// Log if there are new pending tasks
|
|
99
|
+
if (pending_tasks_count > 0 && pending_tasks_count !== this.lastPendingTasks) {
|
|
100
|
+
console.error(`\n🔔 [AgentHub] You have ${pending_tasks_count} pending task(s) waiting! Use get_pending_tasks to view them.\n`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Log if there are new unread messages
|
|
104
|
+
if (unread_messages_count > 0 && unread_messages_count !== this.lastUnreadMessages) {
|
|
105
|
+
console.error(`\n📬 [AgentHub] You have ${unread_messages_count} unread message(s)! Use check_inbox to read them.\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Periodic reminder every 5 heartbeats (2.5 minutes) if there are pending items
|
|
109
|
+
if (this.heartbeatCount % 5 === 0) {
|
|
110
|
+
if (pending_tasks_count > 0 || unread_messages_count > 0) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
if (pending_tasks_count > 0) parts.push(`${pending_tasks_count} pending task(s)`);
|
|
113
|
+
if (unread_messages_count > 0) parts.push(`${unread_messages_count} unread message(s)`);
|
|
114
|
+
console.error(`\n⏰ [AgentHub] Reminder: You have ${parts.join(" and ")}.\n`);
|
|
80
115
|
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.lastPendingTasks = pending_tasks_count;
|
|
119
|
+
this.lastUnreadMessages = unread_messages_count;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async handleHeartbeatFailure(error: unknown): Promise<void> {
|
|
123
|
+
this.consecutiveFailures++;
|
|
124
|
+
console.error(`[AgentHub] Heartbeat failed (attempt ${this.consecutiveFailures}):`, error);
|
|
81
125
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
// Check if we should attempt full reconnection
|
|
127
|
+
if (this.consecutiveFailures >= this.failuresBeforeReconnect && !this.isReconnecting) {
|
|
128
|
+
await this.attemptReconnection();
|
|
129
|
+
} else {
|
|
130
|
+
// Schedule retry with exponential backoff
|
|
131
|
+
this.scheduleRetry();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private scheduleRetry(): void {
|
|
136
|
+
console.error(`[AgentHub] Retrying heartbeat in ${this.backoffMs}ms...`);
|
|
137
|
+
|
|
138
|
+
setTimeout(async () => {
|
|
139
|
+
if (!this.agentId) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await this.sendHeartbeat();
|
|
143
|
+
console.error("[AgentHub] Heartbeat recovered!");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
await this.handleHeartbeatFailure(error);
|
|
90
146
|
}
|
|
147
|
+
}, this.backoffMs);
|
|
148
|
+
|
|
149
|
+
// Increase backoff for next failure (exponential with cap)
|
|
150
|
+
this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async attemptReconnection(): Promise<void> {
|
|
154
|
+
if (!this.reconnectCallback) {
|
|
155
|
+
console.error("[AgentHub] No reconnect callback configured, continuing retries...");
|
|
156
|
+
this.scheduleRetry();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.isReconnecting = true;
|
|
161
|
+
console.error("[AgentHub] Attempting full reconnection...");
|
|
91
162
|
|
|
92
|
-
|
|
93
|
-
|
|
163
|
+
try {
|
|
164
|
+
const state = await this.reconnectCallback();
|
|
165
|
+
if (state) {
|
|
166
|
+
this.agentId = state.agent_id;
|
|
167
|
+
this.consecutiveFailures = 0;
|
|
168
|
+
this.backoffMs = 1000;
|
|
169
|
+
this.isReconnecting = false;
|
|
170
|
+
console.error(`[AgentHub] Reconnected successfully as ${state.agent_id}`);
|
|
171
|
+
} else {
|
|
172
|
+
throw new Error("No state available for reconnection");
|
|
173
|
+
}
|
|
94
174
|
} catch (error) {
|
|
95
|
-
console.error("
|
|
175
|
+
console.error("[AgentHub] Reconnection failed:", error);
|
|
176
|
+
this.isReconnecting = false;
|
|
177
|
+
this.consecutiveFailures = 0; // Reset to try again after more failures
|
|
178
|
+
this.scheduleRetry();
|
|
96
179
|
}
|
|
97
180
|
}
|
|
98
181
|
|
package/src/index.ts
CHANGED
|
@@ -10,11 +10,14 @@ import {
|
|
|
10
10
|
import { ApiClient } from "./client.js";
|
|
11
11
|
import { registerTools, handleToolCall } from "./tools/index.js";
|
|
12
12
|
import { HeartbeatManager } from "./heartbeat.js";
|
|
13
|
+
import { WebSocketClient } from "./websocket.js";
|
|
14
|
+
import * as state from "./state.js";
|
|
13
15
|
|
|
14
16
|
// Environment configuration
|
|
15
17
|
const AGENTHUB_URL = process.env.AGENTHUB_URL || "https://agenthub.contetial.com";
|
|
16
18
|
const AGENTHUB_API_KEY = process.env.AGENTHUB_API_KEY || "";
|
|
17
19
|
const AGENTHUB_AGENT_ID = process.env.AGENTHUB_AGENT_ID || "";
|
|
20
|
+
const AGENTHUB_AUTO_RECONNECT = process.env.AGENTHUB_AUTO_RECONNECT !== "false"; // Enabled by default
|
|
18
21
|
|
|
19
22
|
// Validate configuration
|
|
20
23
|
if (!AGENTHUB_API_KEY) {
|
|
@@ -28,14 +31,61 @@ const client = new ApiClient(AGENTHUB_URL, AGENTHUB_API_KEY);
|
|
|
28
31
|
// Initialize heartbeat manager
|
|
29
32
|
const heartbeat = new HeartbeatManager(client);
|
|
30
33
|
|
|
31
|
-
//
|
|
34
|
+
// Initialize WebSocket client for push notifications
|
|
35
|
+
const wsClient = new WebSocketClient(AGENTHUB_URL, AGENTHUB_API_KEY);
|
|
36
|
+
|
|
37
|
+
// Track current agent ID and working directory
|
|
32
38
|
let currentAgentId = AGENTHUB_AGENT_ID;
|
|
39
|
+
const workingDir = process.cwd();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attempt to reconnect using stored state
|
|
43
|
+
*/
|
|
44
|
+
async function attemptReconnect(): Promise<state.AgentState | null> {
|
|
45
|
+
const existingState = state.loadState(workingDir);
|
|
46
|
+
if (!existingState) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const owner = state.getCurrentOwner();
|
|
51
|
+
if (existingState.owner !== owner) {
|
|
52
|
+
console.error("[AgentHub] State file belongs to different user, skipping auto-reconnect");
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = await client.reconnectAgent(
|
|
58
|
+
existingState.agent_id,
|
|
59
|
+
existingState.token,
|
|
60
|
+
owner,
|
|
61
|
+
undefined, // model - keep existing
|
|
62
|
+
existingState.name
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (result.reconnected) {
|
|
66
|
+
currentAgentId = result.agent_id;
|
|
67
|
+
// Update state with any changes
|
|
68
|
+
state.saveState(workingDir, {
|
|
69
|
+
...existingState,
|
|
70
|
+
name: result.name,
|
|
71
|
+
});
|
|
72
|
+
return existingState;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error("[AgentHub] Reconnect attempt failed:", error);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Set up reconnect callback for heartbeat manager
|
|
82
|
+
heartbeat.setReconnectCallback(attemptReconnect);
|
|
33
83
|
|
|
34
84
|
// Create MCP server
|
|
35
85
|
const server = new Server(
|
|
36
86
|
{
|
|
37
87
|
name: "agenthub",
|
|
38
|
-
version: "1.
|
|
88
|
+
version: "1.2.0",
|
|
39
89
|
},
|
|
40
90
|
{
|
|
41
91
|
capabilities: {
|
|
@@ -61,9 +111,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
61
111
|
setCurrentAgentId: (id: string) => {
|
|
62
112
|
currentAgentId = id;
|
|
63
113
|
heartbeat.start(id);
|
|
114
|
+
wsClient.connect(id);
|
|
115
|
+
},
|
|
116
|
+
stopHeartbeat: () => {
|
|
117
|
+
heartbeat.stop();
|
|
118
|
+
wsClient.close();
|
|
64
119
|
},
|
|
65
|
-
stopHeartbeat: () => heartbeat.stop(),
|
|
66
120
|
getWorkingDir: () => process.cwd(),
|
|
121
|
+
getPushedItems: () => wsClient.getPushedItems(),
|
|
67
122
|
});
|
|
68
123
|
|
|
69
124
|
return {
|
|
@@ -91,11 +146,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
91
146
|
// Graceful shutdown
|
|
92
147
|
process.on("SIGINT", () => {
|
|
93
148
|
heartbeat.stop();
|
|
149
|
+
wsClient.close();
|
|
94
150
|
process.exit(0);
|
|
95
151
|
});
|
|
96
152
|
|
|
97
153
|
process.on("SIGTERM", () => {
|
|
98
154
|
heartbeat.stop();
|
|
155
|
+
wsClient.close();
|
|
99
156
|
process.exit(0);
|
|
100
157
|
});
|
|
101
158
|
|
|
@@ -105,15 +162,31 @@ async function main() {
|
|
|
105
162
|
await server.connect(transport);
|
|
106
163
|
console.error("AgentHub MCP server running");
|
|
107
164
|
|
|
108
|
-
// Auto-register if agent ID is provided
|
|
165
|
+
// Auto-register if agent ID is provided via environment
|
|
109
166
|
if (AGENTHUB_AGENT_ID) {
|
|
110
167
|
try {
|
|
111
168
|
await client.registerAgent(AGENTHUB_AGENT_ID);
|
|
112
169
|
heartbeat.start(AGENTHUB_AGENT_ID);
|
|
170
|
+
wsClient.connect(AGENTHUB_AGENT_ID);
|
|
113
171
|
console.error(`Auto-registered as agent: ${AGENTHUB_AGENT_ID}`);
|
|
114
172
|
} catch (error) {
|
|
115
173
|
console.error(`Failed to auto-register: ${error}`);
|
|
116
174
|
}
|
|
175
|
+
} else if (AGENTHUB_AUTO_RECONNECT) {
|
|
176
|
+
// Try to auto-reconnect from stored state
|
|
177
|
+
const existingState = state.loadState(workingDir);
|
|
178
|
+
if (existingState) {
|
|
179
|
+
console.error(`[AgentHub] Found existing state for agent: ${existingState.agent_id}`);
|
|
180
|
+
const reconnected = await attemptReconnect();
|
|
181
|
+
if (reconnected) {
|
|
182
|
+
heartbeat.start(reconnected.agent_id);
|
|
183
|
+
wsClient.connect(reconnected.agent_id);
|
|
184
|
+
const displayName = reconnected.name || reconnected.agent_id;
|
|
185
|
+
console.error(`[AgentHub] Auto-reconnected as: ${displayName}`);
|
|
186
|
+
} else {
|
|
187
|
+
console.error("[AgentHub] Auto-reconnect failed, manual registration required");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
117
190
|
}
|
|
118
191
|
}
|
|
119
192
|
|