anorion 0.1.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 +87 -0
- package/agents/001.yaml +32 -0
- package/agents/example.yaml +6 -0
- package/bin/anorion.js +8093 -0
- package/package.json +72 -0
- package/scripts/cli.ts +182 -0
- package/scripts/postinstall.js +6 -0
- package/scripts/setup.ts +255 -0
- package/src/agents/pipeline.ts +231 -0
- package/src/agents/registry.ts +153 -0
- package/src/agents/runtime.ts +593 -0
- package/src/agents/session.ts +338 -0
- package/src/agents/subagent.ts +185 -0
- package/src/bridge/client.ts +221 -0
- package/src/bridge/federator.ts +221 -0
- package/src/bridge/protocol.ts +88 -0
- package/src/bridge/server.ts +221 -0
- package/src/channels/base.ts +43 -0
- package/src/channels/router.ts +122 -0
- package/src/channels/telegram.ts +592 -0
- package/src/channels/webhook.ts +143 -0
- package/src/cli/index.ts +1036 -0
- package/src/cli/interactive.ts +26 -0
- package/src/gateway/routes-v2.ts +165 -0
- package/src/gateway/server.ts +512 -0
- package/src/gateway/ws.ts +75 -0
- package/src/index.ts +182 -0
- package/src/llm/provider.ts +243 -0
- package/src/llm/providers.ts +381 -0
- package/src/memory/context.ts +125 -0
- package/src/memory/store.ts +214 -0
- package/src/scheduler/cron.ts +239 -0
- package/src/shared/audit.ts +231 -0
- package/src/shared/config.ts +129 -0
- package/src/shared/db/index.ts +165 -0
- package/src/shared/db/prepared.ts +111 -0
- package/src/shared/db/schema.ts +84 -0
- package/src/shared/events.ts +79 -0
- package/src/shared/logger.ts +10 -0
- package/src/shared/metrics.ts +190 -0
- package/src/shared/rbac.ts +151 -0
- package/src/shared/token-budget.ts +157 -0
- package/src/shared/types.ts +166 -0
- package/src/tools/builtin/echo.ts +19 -0
- package/src/tools/builtin/file-read.ts +78 -0
- package/src/tools/builtin/file-write.ts +64 -0
- package/src/tools/builtin/http-request.ts +63 -0
- package/src/tools/builtin/memory.ts +71 -0
- package/src/tools/builtin/shell.ts +94 -0
- package/src/tools/builtin/web-search.ts +22 -0
- package/src/tools/executor.ts +126 -0
- package/src/tools/registry.ts +56 -0
- package/src/tools/skill-manager.ts +252 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Bridge Client — outbound connection to a peer gateway
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BridgeMessage,
|
|
5
|
+
HelloPayload,
|
|
6
|
+
MessageForwardPayload,
|
|
7
|
+
MessageResponsePayload,
|
|
8
|
+
} from './protocol';
|
|
9
|
+
import {
|
|
10
|
+
createBridgeMessage,
|
|
11
|
+
parseBridgeMessage,
|
|
12
|
+
BRIDGE_VERSION,
|
|
13
|
+
} from './protocol';
|
|
14
|
+
import { logger } from '../shared/logger';
|
|
15
|
+
import { agentRegistry } from '../agents/registry';
|
|
16
|
+
|
|
17
|
+
export type ClientState = 'disconnected' | 'connecting' | 'connected';
|
|
18
|
+
|
|
19
|
+
interface PendingRequest {
|
|
20
|
+
resolve: (msg: BridgeMessage) => void;
|
|
21
|
+
reject: (err: Error) => void;
|
|
22
|
+
timer: ReturnType<typeof setTimeout>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class BridgeClient {
|
|
26
|
+
readonly peerUrl: string;
|
|
27
|
+
readonly secret: string;
|
|
28
|
+
state: ClientState = 'disconnected';
|
|
29
|
+
private ws: WebSocket | null = null;
|
|
30
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
31
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
32
|
+
private backoffMs = 1000;
|
|
33
|
+
private queue: BridgeMessage[] = [];
|
|
34
|
+
private pendingRequests = new Map<string, PendingRequest>();
|
|
35
|
+
private peerGatewayId: string | null = null;
|
|
36
|
+
private onMessage: ((msg: BridgeMessage) => void) | null = null;
|
|
37
|
+
private onClose: (() => void) | null = null;
|
|
38
|
+
private stopped = false;
|
|
39
|
+
|
|
40
|
+
constructor(peerUrl: string, secret: string) {
|
|
41
|
+
this.peerUrl = peerUrl;
|
|
42
|
+
this.secret = secret;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get connected(): boolean {
|
|
46
|
+
return this.state === 'connected' && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setHandlers(onMessage: (msg: BridgeMessage) => void, onClose: () => void) {
|
|
50
|
+
this.onMessage = onMessage;
|
|
51
|
+
this.onClose = onClose;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async start(ownGatewayId: string): Promise<void> {
|
|
55
|
+
this.stopped = false;
|
|
56
|
+
this.connect(ownGatewayId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
stop(): void {
|
|
60
|
+
this.stopped = true;
|
|
61
|
+
this.cleanup();
|
|
62
|
+
// Reject pending
|
|
63
|
+
for (const [id, req] of this.pendingRequests) {
|
|
64
|
+
clearTimeout(req.timer);
|
|
65
|
+
req.reject(new Error('Client stopped'));
|
|
66
|
+
this.pendingRequests.delete(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
send(msg: BridgeMessage): void {
|
|
71
|
+
if (this.connected) {
|
|
72
|
+
this.ws!.send(JSON.stringify(msg));
|
|
73
|
+
} else {
|
|
74
|
+
this.queue.push(msg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async request(msg: BridgeMessage, timeoutMs = 30000): Promise<BridgeMessage> {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
this.pendingRequests.delete(msg.messageId);
|
|
82
|
+
reject(new Error('Bridge request timeout'));
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
this.pendingRequests.set(msg.messageId, { resolve, reject, timer });
|
|
85
|
+
this.send(msg);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private connect(ownGatewayId: string): void {
|
|
90
|
+
if (this.stopped) return;
|
|
91
|
+
this.state = 'connecting';
|
|
92
|
+
logger.info({ url: this.peerUrl, backoff: this.backoffMs }, 'Bridge client connecting');
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const url = new URL(this.peerUrl);
|
|
96
|
+
url.searchParams.set('secret', this.secret);
|
|
97
|
+
|
|
98
|
+
this.ws = new WebSocket(url.toString());
|
|
99
|
+
this.ws.onopen = () => {
|
|
100
|
+
this.state = 'connected';
|
|
101
|
+
this.backoffMs = 1000;
|
|
102
|
+
logger.info({ url: this.peerUrl }, 'Bridge client connected');
|
|
103
|
+
|
|
104
|
+
// Send hello
|
|
105
|
+
const hello = createBridgeMessage('hello', ownGatewayId, {
|
|
106
|
+
secret: this.secret,
|
|
107
|
+
gatewayId: ownGatewayId,
|
|
108
|
+
agents: agentRegistry.list().map((a) => ({ id: a.id, name: a.name, status: a.state })),
|
|
109
|
+
} as HelloPayload);
|
|
110
|
+
this.ws!.send(JSON.stringify(hello));
|
|
111
|
+
|
|
112
|
+
// Flush queue
|
|
113
|
+
for (const msg of this.queue) {
|
|
114
|
+
this.ws!.send(JSON.stringify(msg));
|
|
115
|
+
}
|
|
116
|
+
this.queue = [];
|
|
117
|
+
|
|
118
|
+
// Start heartbeat
|
|
119
|
+
this.heartbeatTimer = setInterval(() => {
|
|
120
|
+
if (this.connected) {
|
|
121
|
+
this.send(createBridgeMessage('health-ping', ownGatewayId, {}));
|
|
122
|
+
}
|
|
123
|
+
}, 30_000);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.ws.onmessage = (event) => {
|
|
127
|
+
const msg = parseBridgeMessage(event.data as string);
|
|
128
|
+
if (!msg) return;
|
|
129
|
+
|
|
130
|
+
// Handle pending requests
|
|
131
|
+
const pending = this.pendingRequests.get(msg.messageId);
|
|
132
|
+
if (pending) {
|
|
133
|
+
clearTimeout(pending.timer);
|
|
134
|
+
this.pendingRequests.delete(msg.messageId);
|
|
135
|
+
pending.resolve(msg);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (msg.type === 'hello-ack') {
|
|
140
|
+
this.peerGatewayId = msg.gatewayId;
|
|
141
|
+
logger.info({ peerGatewayId: this.peerGatewayId }, 'Bridge peer acknowledged');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (msg.type === 'message-forward') {
|
|
145
|
+
// Handle incoming message-forward from peer (we are the target)
|
|
146
|
+
void this.handleMessageForward(msg, ownGatewayId);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.onMessage?.(msg);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
this.ws.onclose = () => {
|
|
154
|
+
logger.info({ url: this.peerUrl }, 'Bridge client disconnected');
|
|
155
|
+
this.state = 'disconnected';
|
|
156
|
+
this.onClose?.();
|
|
157
|
+
this.scheduleReconnect(ownGatewayId);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
this.ws.onerror = (err) => {
|
|
161
|
+
logger.error({ url: this.peerUrl, error: String(err) }, 'Bridge client error');
|
|
162
|
+
};
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logger.error({ url: this.peerUrl, error: (err as Error).message }, 'Bridge client connect failed');
|
|
165
|
+
this.scheduleReconnect(ownGatewayId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private async handleMessageForward(msg: BridgeMessage, ownGatewayId: string): Promise<void> {
|
|
170
|
+
const payload = msg.payload as MessageForwardPayload;
|
|
171
|
+
const { sendMessage } = await import('../agents/runtime');
|
|
172
|
+
try {
|
|
173
|
+
const result = await sendMessage({
|
|
174
|
+
agentId: payload.targetAgentId,
|
|
175
|
+
sessionId: payload.sessionId,
|
|
176
|
+
text: payload.text,
|
|
177
|
+
channelId: payload.channelId,
|
|
178
|
+
});
|
|
179
|
+
const response = createBridgeMessage('message-response', ownGatewayId, {
|
|
180
|
+
requestMessageId: msg.messageId,
|
|
181
|
+
content: result.content,
|
|
182
|
+
} as MessageResponsePayload);
|
|
183
|
+
this.send(response);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
const response = createBridgeMessage('message-response', ownGatewayId, {
|
|
186
|
+
requestMessageId: msg.messageId,
|
|
187
|
+
content: '',
|
|
188
|
+
error: (err as Error).message,
|
|
189
|
+
} as MessageResponsePayload);
|
|
190
|
+
this.send(response);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private scheduleReconnect(ownGatewayId: string): void {
|
|
195
|
+
if (this.stopped) return;
|
|
196
|
+
this.cleanup();
|
|
197
|
+
this.reconnectTimer = setTimeout(() => {
|
|
198
|
+
this.connect(ownGatewayId);
|
|
199
|
+
}, this.backoffMs);
|
|
200
|
+
this.backoffMs = Math.min(this.backoffMs * 2, 30_000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private cleanup(): void {
|
|
204
|
+
if (this.reconnectTimer) {
|
|
205
|
+
clearTimeout(this.reconnectTimer);
|
|
206
|
+
this.reconnectTimer = null;
|
|
207
|
+
}
|
|
208
|
+
if (this.heartbeatTimer) {
|
|
209
|
+
clearInterval(this.heartbeatTimer);
|
|
210
|
+
this.heartbeatTimer = null;
|
|
211
|
+
}
|
|
212
|
+
if (this.ws) {
|
|
213
|
+
this.ws.onclose = null;
|
|
214
|
+
this.ws.onerror = null;
|
|
215
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
216
|
+
this.ws.close();
|
|
217
|
+
}
|
|
218
|
+
this.ws = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Federator — peer table management, agent discovery, message routing
|
|
2
|
+
|
|
3
|
+
import { createBridgeMessage, type MessageForwardPayload, type MessageResponsePayload } from './protocol';
|
|
4
|
+
import { BridgeClient } from './client';
|
|
5
|
+
import { BridgeServer } from './server';
|
|
6
|
+
import { agentRegistry } from '../agents/registry';
|
|
7
|
+
import { logger } from '../shared/logger';
|
|
8
|
+
|
|
9
|
+
export interface PeerInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
url: string;
|
|
12
|
+
status: 'connecting' | 'connected' | 'disconnected';
|
|
13
|
+
agentCount: number;
|
|
14
|
+
connectedAt?: number;
|
|
15
|
+
lastPing: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RemoteAgent {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
status: string;
|
|
22
|
+
gatewayId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_QUEUE_SIZE = 100;
|
|
26
|
+
|
|
27
|
+
export class Federator {
|
|
28
|
+
private ownGatewayId: string;
|
|
29
|
+
private secret: string;
|
|
30
|
+
private clients = new Map<string, BridgeClient>(); // url -> client
|
|
31
|
+
private bridgeServer: BridgeServer;
|
|
32
|
+
private messageQueue: Array<{ targetAgentId: string; payload: MessageForwardPayload; resolve: (v: unknown) => void; reject: (e: Error) => void }> = [];
|
|
33
|
+
private stats = { messagesForwarded: 0, messagesReceived: 0, queueDropped: 0, startTime: Date.now() };
|
|
34
|
+
|
|
35
|
+
constructor(ownGatewayId: string, secret: string, bridgeServer: BridgeServer) {
|
|
36
|
+
this.ownGatewayId = ownGatewayId;
|
|
37
|
+
this.secret = secret;
|
|
38
|
+
this.bridgeServer = bridgeServer;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async connectPeer(url: string, secret: string): Promise<void> {
|
|
42
|
+
if (this.clients.has(url)) {
|
|
43
|
+
logger.warn({ url }, 'Peer already configured');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const client = new BridgeClient(url, secret);
|
|
48
|
+
|
|
49
|
+
client.setHandlers(
|
|
50
|
+
(msg) => this.handleClientMessage(url, msg),
|
|
51
|
+
() => logger.info({ url }, 'Bridge client closed'),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
this.clients.set(url, client);
|
|
55
|
+
await client.start(this.ownGatewayId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
disconnectPeer(url: string): void {
|
|
59
|
+
const client = this.clients.get(url);
|
|
60
|
+
if (client) {
|
|
61
|
+
client.stop();
|
|
62
|
+
this.clients.delete(url);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private handleClientMessage(_url: string, _msg: unknown): void {
|
|
67
|
+
// Client-level messages handled internally (hello-ack etc.)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get all agents: local + remote
|
|
71
|
+
getAllAgents(): { local: ReturnType<typeof agentRegistry.list>; remote: RemoteAgent[] } {
|
|
72
|
+
const local = agentRegistry.list();
|
|
73
|
+
const remote: RemoteAgent[] = [];
|
|
74
|
+
|
|
75
|
+
// From server-side connections
|
|
76
|
+
for (const [, conn] of this.bridgeServer.getConnections()) {
|
|
77
|
+
for (const agent of conn.agents) {
|
|
78
|
+
remote.push({ ...agent, gatewayId: conn.gatewayId });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { local, remote };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if an agent is remote (hosted on a peer)
|
|
86
|
+
findRemoteAgent(agentId: string): { gatewayId: string; gatewayUrl?: string } | null {
|
|
87
|
+
// Check server-side connections (inbound)
|
|
88
|
+
for (const [gwId, conn] of this.bridgeServer.getConnections()) {
|
|
89
|
+
if (conn.agents.some((a) => a.id === agentId)) {
|
|
90
|
+
return { gatewayId: gwId };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Route a message to an agent (local or remote)
|
|
97
|
+
async routeMessage(targetAgentId: string, text: string, sessionId?: string, channelId?: string): Promise<{ content: string; error?: string }> {
|
|
98
|
+
// Check local first
|
|
99
|
+
const local = agentRegistry.get(targetAgentId) || agentRegistry.getByName(targetAgentId);
|
|
100
|
+
if (local) {
|
|
101
|
+
const { sendMessage } = await import('../agents/runtime');
|
|
102
|
+
const result = await sendMessage({ agentId: local.id, sessionId, text, channelId });
|
|
103
|
+
return { content: result.content };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check remote
|
|
107
|
+
const remote = this.findRemoteAgent(targetAgentId);
|
|
108
|
+
if (remote) {
|
|
109
|
+
return this.sendToRemote(remote.gatewayId, targetAgentId, text, sessionId, channelId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check remote by name too
|
|
113
|
+
const { remote: allRemote } = this.getAllAgents();
|
|
114
|
+
const namedRemote = allRemote.find((a) => a.name === targetAgentId);
|
|
115
|
+
if (namedRemote) {
|
|
116
|
+
return this.sendToRemote(namedRemote.gatewayId, namedRemote.id, text, sessionId, channelId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Queue if not found (maybe agent will appear)
|
|
120
|
+
if (this.messageQueue.length >= MAX_QUEUE_SIZE) {
|
|
121
|
+
// Drop the oldest queued message to cap memory
|
|
122
|
+
const oldest = this.messageQueue.shift();
|
|
123
|
+
if (oldest) {
|
|
124
|
+
oldest.reject(new Error('Message dropped: queue full'));
|
|
125
|
+
this.stats.queueDropped++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
this.messageQueue.push({ targetAgentId, payload: { targetAgentId, text, sessionId, channelId }, resolve, reject });
|
|
131
|
+
// Auto-reject after 30s
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
const idx = this.messageQueue.findIndex((m) => m.resolve === resolve);
|
|
134
|
+
if (idx !== -1) {
|
|
135
|
+
this.messageQueue.splice(idx, 1);
|
|
136
|
+
reject(new Error(`Agent not found: ${targetAgentId}`));
|
|
137
|
+
}
|
|
138
|
+
}, 30_000);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async sendToRemote(gatewayId: string, targetAgentId: string, text: string, sessionId?: string, channelId?: string): Promise<{ content: string; error?: string }> {
|
|
143
|
+
const msg = createBridgeMessage('message-forward', this.ownGatewayId, {
|
|
144
|
+
targetAgentId,
|
|
145
|
+
sessionId,
|
|
146
|
+
text,
|
|
147
|
+
channelId,
|
|
148
|
+
} as MessageForwardPayload);
|
|
149
|
+
|
|
150
|
+
this.stats.messagesForwarded++;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const response = await this.bridgeServer.sendToPeer(gatewayId, msg);
|
|
154
|
+
const payload = response.payload as MessageResponsePayload;
|
|
155
|
+
this.stats.messagesReceived++;
|
|
156
|
+
return { content: payload.content, error: payload.error };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { content: '', error: (err as Error).message };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Process queued messages if agents become available
|
|
163
|
+
flushQueue(): void {
|
|
164
|
+
const remaining = [];
|
|
165
|
+
for (const item of this.messageQueue) {
|
|
166
|
+
const local = agentRegistry.get(item.targetAgentId);
|
|
167
|
+
const remote = this.findRemoteAgent(item.targetAgentId);
|
|
168
|
+
if (local || remote) {
|
|
169
|
+
this.routeMessage(item.targetAgentId, item.payload.text, item.payload.sessionId, item.payload.channelId)
|
|
170
|
+
.then(item.resolve)
|
|
171
|
+
.catch(item.reject);
|
|
172
|
+
} else {
|
|
173
|
+
remaining.push(item);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.messageQueue = remaining;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Peer status for API
|
|
180
|
+
getPeers(): PeerInfo[] {
|
|
181
|
+
const peers: PeerInfo[] = [];
|
|
182
|
+
|
|
183
|
+
// Outbound clients
|
|
184
|
+
for (const [url, client] of this.clients) {
|
|
185
|
+
const gwId = 'outbound-' + url.replace(/[^a-zA-Z0-9]/g, '-');
|
|
186
|
+
peers.push({
|
|
187
|
+
id: gwId,
|
|
188
|
+
url,
|
|
189
|
+
status: client.state,
|
|
190
|
+
agentCount: 0,
|
|
191
|
+
lastPing: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Inbound connections
|
|
196
|
+
for (const [gwId, conn] of this.bridgeServer.getConnections()) {
|
|
197
|
+
peers.push({
|
|
198
|
+
id: gwId,
|
|
199
|
+
url: 'inbound',
|
|
200
|
+
status: 'connected',
|
|
201
|
+
agentCount: conn.agents.length,
|
|
202
|
+
connectedAt: conn.connectedAt,
|
|
203
|
+
lastPing: conn.lastPong,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return peers;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getStatus() {
|
|
211
|
+
return {
|
|
212
|
+
enabled: true,
|
|
213
|
+
peerCount: this.clients.size + this.bridgeServer.getConnections().size,
|
|
214
|
+
queueSize: this.messageQueue.length,
|
|
215
|
+
messagesForwarded: this.stats.messagesForwarded,
|
|
216
|
+
messagesReceived: this.stats.messagesReceived,
|
|
217
|
+
queueDropped: this.stats.queueDropped,
|
|
218
|
+
uptime: Date.now() - this.stats.startTime,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Bridge wire protocol for gateway-to-gateway communication
|
|
2
|
+
|
|
3
|
+
export const BRIDGE_VERSION = '1.0.0';
|
|
4
|
+
|
|
5
|
+
export type BridgeMessageType =
|
|
6
|
+
| 'hello'
|
|
7
|
+
| 'agent-register'
|
|
8
|
+
| 'agent-update'
|
|
9
|
+
| 'message-forward'
|
|
10
|
+
| 'message-response'
|
|
11
|
+
| 'health-ping'
|
|
12
|
+
| 'health-pong'
|
|
13
|
+
| 'unsubscribe'
|
|
14
|
+
| 'hello-ack';
|
|
15
|
+
|
|
16
|
+
export interface BridgeMessage {
|
|
17
|
+
version: string;
|
|
18
|
+
type: BridgeMessageType;
|
|
19
|
+
messageId: string;
|
|
20
|
+
gatewayId: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
payload: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Payloads
|
|
26
|
+
|
|
27
|
+
export interface HelloPayload {
|
|
28
|
+
secret: string;
|
|
29
|
+
gatewayId: string;
|
|
30
|
+
agents: { id: string; name: string; status: string }[];
|
|
31
|
+
capabilities?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HelloAckPayload {
|
|
35
|
+
gatewayId: string;
|
|
36
|
+
agents: { id: string; name: string; status: string }[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AgentRegisterPayload {
|
|
40
|
+
agents: { id: string; name: string; status: string }[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AgentUpdatePayload {
|
|
44
|
+
agentId: string;
|
|
45
|
+
status: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MessageForwardPayload {
|
|
49
|
+
targetAgentId: string;
|
|
50
|
+
messageId: string;
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
text: string;
|
|
53
|
+
channelId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MessageResponsePayload {
|
|
57
|
+
requestMessageId: string;
|
|
58
|
+
content: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createBridgeMessage(
|
|
63
|
+
type: BridgeMessageType,
|
|
64
|
+
gatewayId: string,
|
|
65
|
+
payload: unknown,
|
|
66
|
+
messageId?: string,
|
|
67
|
+
): BridgeMessage {
|
|
68
|
+
return {
|
|
69
|
+
version: BRIDGE_VERSION,
|
|
70
|
+
type,
|
|
71
|
+
messageId: messageId || crypto.randomUUID(),
|
|
72
|
+
gatewayId,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
payload,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseBridgeMessage(data: string): BridgeMessage | null {
|
|
79
|
+
try {
|
|
80
|
+
const msg = JSON.parse(data);
|
|
81
|
+
if (msg.version && msg.type && msg.messageId && msg.gatewayId && msg.timestamp !== undefined) {
|
|
82
|
+
return msg as BridgeMessage;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|