agentnet 0.0.2 → 0.0.4
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 +69 -1
- package/examples/simple/simple.js +2 -2
- package/jest.config.js +1 -0
- package/package.json +5 -2
- package/src/agent/agent-loader.js +75 -12
- package/src/agent/agent.js +4 -2
- package/src/agent/runtime.js +7 -4
- package/src/llm/base.js +131 -0
- package/src/llm/gemini.js +137 -122
- package/src/llm/gpt.js +131 -109
- package/src/tests/agent.test.js +350 -0
- package/src/tools/migrate-version.js +250 -0
- package/src/transport/README.md +123 -0
- package/src/transport/base.js +237 -0
- package/src/transport/index.js +89 -0
- package/src/transport/kafka.js +474 -0
- package/src/transport/nats.js +521 -0
- package/src/transport/rabbitmq.js +722 -0
- package/src/transport/redis.js +532 -0
- package/src/utils/version.js +212 -0
- package/src/agent/runtimes/nats.js +0 -506
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RabbitMQ Transport implementation
|
|
3
|
+
*/
|
|
4
|
+
import { Transport, DiscoveryMessage, safeConnect } from './base.js';
|
|
5
|
+
import { Message } from '../index.js';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import {
|
|
8
|
+
TransportError,
|
|
9
|
+
DiscoveryError,
|
|
10
|
+
HandoffError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
withTimeout
|
|
13
|
+
} from '../errors/index.js';
|
|
14
|
+
|
|
15
|
+
// Constants
|
|
16
|
+
const HEARTBEAT_INTERVAL = 1000;
|
|
17
|
+
const TIMEOUT_TASK_REQUEST = 60000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* RabbitMQ implementation of the Transport interface
|
|
21
|
+
*/
|
|
22
|
+
export class RabbitMQTransport extends Transport {
|
|
23
|
+
constructor() {
|
|
24
|
+
super('RabbitMQ');
|
|
25
|
+
this.connection = null;
|
|
26
|
+
this.channel = null;
|
|
27
|
+
this.subscriptions = new Map(); // Map of subscription queues to consumers
|
|
28
|
+
this.requestMap = new Map(); // For request-response pattern
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Connect to RabbitMQ
|
|
33
|
+
* @param {Object} config - RabbitMQ connection configuration
|
|
34
|
+
* @returns {Promise<any>} - The RabbitMQ connection
|
|
35
|
+
*/
|
|
36
|
+
async connect(config) {
|
|
37
|
+
if (this.connected && this.connection) {
|
|
38
|
+
return this.connection;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Implementation will depend on the RabbitMQ client library used
|
|
43
|
+
// For example, with amqplib:
|
|
44
|
+
// const amqp = require('amqplib');
|
|
45
|
+
// const { url, options } = config;
|
|
46
|
+
//
|
|
47
|
+
// this.connection = await amqp.connect(url, options);
|
|
48
|
+
// this.channel = await this.connection.createChannel();
|
|
49
|
+
//
|
|
50
|
+
// // Set prefetch to handle one message at a time
|
|
51
|
+
// await this.channel.prefetch(1);
|
|
52
|
+
//
|
|
53
|
+
// // Handle connection close events
|
|
54
|
+
// this.connection.on('close', () => {
|
|
55
|
+
// this.connected = false;
|
|
56
|
+
// logger.warn('RabbitMQ connection closed');
|
|
57
|
+
// });
|
|
58
|
+
|
|
59
|
+
this.connected = true;
|
|
60
|
+
return this.connection;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new TransportError(
|
|
63
|
+
`Failed to connect to RabbitMQ: ${error.message}`,
|
|
64
|
+
this.transportType,
|
|
65
|
+
{ details: error.message }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Disconnect from RabbitMQ
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async disconnect() {
|
|
75
|
+
await super.disconnect();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Close the channel and connection
|
|
79
|
+
if (this.channel) await this.channel.close();
|
|
80
|
+
if (this.connection) await this.connection.close();
|
|
81
|
+
|
|
82
|
+
this.channel = null;
|
|
83
|
+
this.connection = null;
|
|
84
|
+
this.subscriptions.clear();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.warn('Error disconnecting from RabbitMQ', { error });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ensure exchange exists
|
|
92
|
+
* @param {string} exchange - The exchange name
|
|
93
|
+
* @param {string} type - The exchange type (direct, fanout, topic)
|
|
94
|
+
* @returns {Promise<void>}
|
|
95
|
+
*/
|
|
96
|
+
async ensureExchange(exchange, type = 'topic') {
|
|
97
|
+
if (!this.connected || !this.channel) {
|
|
98
|
+
throw new TransportError(
|
|
99
|
+
'Cannot ensure exchange: not connected to RabbitMQ',
|
|
100
|
+
this.transportType
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// await this.channel.assertExchange(exchange, type, { durable: true });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new TransportError(
|
|
108
|
+
`Failed to assert exchange ${exchange}: ${error.message}`,
|
|
109
|
+
this.transportType,
|
|
110
|
+
{ exchange }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ensure queue exists
|
|
117
|
+
* @param {string} queue - The queue name
|
|
118
|
+
* @param {Object} options - Queue options
|
|
119
|
+
* @returns {Promise<Object>} - The queue information
|
|
120
|
+
*/
|
|
121
|
+
async ensureQueue(queue, options = {}) {
|
|
122
|
+
if (!this.connected || !this.channel) {
|
|
123
|
+
throw new TransportError(
|
|
124
|
+
'Cannot ensure queue: not connected to RabbitMQ',
|
|
125
|
+
this.transportType
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// return await this.channel.assertQueue(queue, {
|
|
131
|
+
// durable: true,
|
|
132
|
+
// ...options
|
|
133
|
+
// });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new TransportError(
|
|
136
|
+
`Failed to assert queue ${queue}: ${error.message}`,
|
|
137
|
+
this.transportType,
|
|
138
|
+
{ queue }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Bind a queue to an exchange
|
|
145
|
+
* @param {string} queue - The queue name
|
|
146
|
+
* @param {string} exchange - The exchange name
|
|
147
|
+
* @param {string} routingKey - The routing key
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
async bindQueue(queue, exchange, routingKey) {
|
|
151
|
+
if (!this.connected || !this.channel) {
|
|
152
|
+
throw new TransportError(
|
|
153
|
+
'Cannot bind queue: not connected to RabbitMQ',
|
|
154
|
+
this.transportType
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// await this.channel.bindQueue(queue, exchange, routingKey);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new TransportError(
|
|
162
|
+
`Failed to bind queue ${queue} to exchange ${exchange}: ${error.message}`,
|
|
163
|
+
this.transportType,
|
|
164
|
+
{ queue, exchange, routingKey }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Publish a message to a RabbitMQ exchange
|
|
171
|
+
* @param {string} exchange - The exchange to publish to
|
|
172
|
+
* @param {string} routingKey - The routing key
|
|
173
|
+
* @param {string} message - The message to publish
|
|
174
|
+
* @param {Object} options - Publish options
|
|
175
|
+
* @returns {Promise<void>}
|
|
176
|
+
*/
|
|
177
|
+
async publish(exchange, routingKey, message, options = {}) {
|
|
178
|
+
if (!this.connected || !this.channel) {
|
|
179
|
+
throw new TransportError(
|
|
180
|
+
'Cannot publish: not connected to RabbitMQ',
|
|
181
|
+
this.transportType
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Ensure the exchange exists
|
|
187
|
+
await this.ensureExchange(exchange);
|
|
188
|
+
|
|
189
|
+
// Convert string to Buffer if needed
|
|
190
|
+
const content = Buffer.isBuffer(message) ? message : Buffer.from(message);
|
|
191
|
+
|
|
192
|
+
// await this.channel.publish(exchange, routingKey, content, {
|
|
193
|
+
// persistent: true,
|
|
194
|
+
// ...options
|
|
195
|
+
// });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
throw new TransportError(
|
|
198
|
+
`Failed to publish to exchange ${exchange} with routing key ${routingKey}: ${error.message}`,
|
|
199
|
+
this.transportType,
|
|
200
|
+
{ exchange, routingKey }
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Subscribe to a RabbitMQ queue
|
|
207
|
+
* @param {string} queue - The queue to subscribe to
|
|
208
|
+
* @param {string} exchange - The exchange (if binding is needed)
|
|
209
|
+
* @param {string} routingKey - The routing key (if binding is needed)
|
|
210
|
+
* @param {Object} options - Subscription options
|
|
211
|
+
* @returns {Promise<string>} - The consumer tag
|
|
212
|
+
*/
|
|
213
|
+
async subscribe(queue, exchange = null, routingKey = null, options = {}) {
|
|
214
|
+
if (!this.connected || !this.channel) {
|
|
215
|
+
throw new TransportError(
|
|
216
|
+
'Cannot subscribe: not connected to RabbitMQ',
|
|
217
|
+
this.transportType
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Ensure the queue exists
|
|
223
|
+
await this.ensureQueue(queue, options.queue || {});
|
|
224
|
+
|
|
225
|
+
// If exchange and routing key are provided, bind the queue
|
|
226
|
+
if (exchange && routingKey) {
|
|
227
|
+
await this.ensureExchange(exchange);
|
|
228
|
+
await this.bindQueue(queue, exchange, routingKey);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Create the consumer
|
|
232
|
+
// const { consumerTag } = await this.channel.consume(
|
|
233
|
+
// queue,
|
|
234
|
+
// (msg) => {
|
|
235
|
+
// if (msg === null) {
|
|
236
|
+
// // Consumer cancelled by server
|
|
237
|
+
// logger.warn(`Consumer for queue ${queue} was cancelled by the server`);
|
|
238
|
+
// this.subscriptions.delete(queue);
|
|
239
|
+
// return;
|
|
240
|
+
// }
|
|
241
|
+
//
|
|
242
|
+
// const content = msg.content.toString();
|
|
243
|
+
// const handler = options.messageHandler;
|
|
244
|
+
//
|
|
245
|
+
// if (handler) {
|
|
246
|
+
// Promise.resolve(handler(content, msg)).then(() => {
|
|
247
|
+
// // Acknowledge message after successful processing
|
|
248
|
+
// this.channel.ack(msg);
|
|
249
|
+
// }).catch((error) => {
|
|
250
|
+
// logger.error(`Error processing message from queue ${queue}`, { error });
|
|
251
|
+
// // Reject the message and requeue it
|
|
252
|
+
// this.channel.nack(msg, false, true);
|
|
253
|
+
// });
|
|
254
|
+
// } else {
|
|
255
|
+
// // No handler, just acknowledge
|
|
256
|
+
// this.channel.ack(msg);
|
|
257
|
+
// }
|
|
258
|
+
// },
|
|
259
|
+
// { noAck: false }
|
|
260
|
+
// );
|
|
261
|
+
|
|
262
|
+
// Store the consumer
|
|
263
|
+
// this.subscriptions.set(queue, consumerTag);
|
|
264
|
+
|
|
265
|
+
const consumerTag = `consumer_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
266
|
+
this.subscriptions.set(queue, consumerTag);
|
|
267
|
+
return consumerTag;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new TransportError(
|
|
270
|
+
`Failed to subscribe to queue ${queue}: ${error.message}`,
|
|
271
|
+
this.transportType,
|
|
272
|
+
{ queue, exchange, routingKey }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Unsubscribe from a RabbitMQ queue
|
|
279
|
+
* @param {string} queue - The queue to unsubscribe from
|
|
280
|
+
* @returns {Promise<void>}
|
|
281
|
+
*/
|
|
282
|
+
async unsubscribe(queue) {
|
|
283
|
+
if (!this.connected || !this.channel) {
|
|
284
|
+
return; // Already disconnected
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const consumerTag = this.subscriptions.get(queue);
|
|
289
|
+
if (consumerTag) {
|
|
290
|
+
// await this.channel.cancel(consumerTag);
|
|
291
|
+
this.subscriptions.delete(queue);
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
logger.warn(`Error unsubscribing from queue ${queue}`, { error });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Send a request and wait for a response
|
|
300
|
+
* Implements request-response pattern using RabbitMQ and temp reply queues
|
|
301
|
+
* @param {string} exchange - The exchange to use
|
|
302
|
+
* @param {string} routingKey - The routing key (usually the service name)
|
|
303
|
+
* @param {string} message - The message to send
|
|
304
|
+
* @param {Object} options - Request options
|
|
305
|
+
* @returns {Promise<any>} - The response
|
|
306
|
+
*/
|
|
307
|
+
async request(exchange, routingKey, message, options = {}) {
|
|
308
|
+
if (!this.connected || !this.channel) {
|
|
309
|
+
throw new TransportError(
|
|
310
|
+
'Cannot send request: not connected to RabbitMQ',
|
|
311
|
+
this.transportType
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const correlationId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
316
|
+
const timeout = options.timeout || TIMEOUT_TASK_REQUEST;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
// Create a temporary exclusive queue for the response
|
|
320
|
+
// const { queue: replyQueue } = await this.channel.assertQueue('', {
|
|
321
|
+
// exclusive: true,
|
|
322
|
+
// autoDelete: true
|
|
323
|
+
// });
|
|
324
|
+
const replyQueue = `reply_${correlationId}`;
|
|
325
|
+
|
|
326
|
+
// Create a promise that will be resolved when the response is received
|
|
327
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
328
|
+
// Set a timeout
|
|
329
|
+
const timeoutId = setTimeout(() => {
|
|
330
|
+
this.unsubscribe(replyQueue).catch(() => {});
|
|
331
|
+
this.requestMap.delete(correlationId);
|
|
332
|
+
reject(new TimeoutError(`Request to ${routingKey} timed out after ${timeout}ms`));
|
|
333
|
+
}, timeout);
|
|
334
|
+
|
|
335
|
+
// Store the resolver and timeout
|
|
336
|
+
this.requestMap.set(correlationId, {
|
|
337
|
+
resolve,
|
|
338
|
+
reject,
|
|
339
|
+
timeoutId
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Set up handler for the reply
|
|
343
|
+
const messageHandler = (content, msg) => {
|
|
344
|
+
const requestData = this.requestMap.get(correlationId);
|
|
345
|
+
if (requestData && msg.properties.correlationId === correlationId) {
|
|
346
|
+
clearTimeout(requestData.timeoutId);
|
|
347
|
+
this.requestMap.delete(correlationId);
|
|
348
|
+
this.unsubscribe(replyQueue).catch(() => {});
|
|
349
|
+
resolve({ string: () => content });
|
|
350
|
+
return true; // Handled the message
|
|
351
|
+
}
|
|
352
|
+
return false; // Not handled
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Subscribe to the reply queue
|
|
356
|
+
this.subscribe(replyQueue, null, null, {
|
|
357
|
+
queue: { exclusive: true, autoDelete: true },
|
|
358
|
+
messageHandler
|
|
359
|
+
}).catch(reject);
|
|
360
|
+
|
|
361
|
+
// Publish the request
|
|
362
|
+
const content = Buffer.from(message);
|
|
363
|
+
// this.channel.publish(exchange, routingKey, content, {
|
|
364
|
+
// persistent: true,
|
|
365
|
+
// correlationId,
|
|
366
|
+
// replyTo: replyQueue,
|
|
367
|
+
// expiration: timeout.toString()
|
|
368
|
+
// });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Wait for the response
|
|
372
|
+
return await responsePromise;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// Clean up
|
|
375
|
+
const requestData = this.requestMap.get(correlationId);
|
|
376
|
+
if (requestData) {
|
|
377
|
+
clearTimeout(requestData.timeoutId);
|
|
378
|
+
this.requestMap.delete(correlationId);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
throw new TransportError(
|
|
382
|
+
`Request to ${routingKey} failed: ${error.message}`,
|
|
383
|
+
this.transportType,
|
|
384
|
+
{ exchange, routingKey }
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Set up discovery subscription
|
|
391
|
+
* @param {string} discoveryExchange - The exchange for discovery messages
|
|
392
|
+
* @param {string} namespace - The agent namespace
|
|
393
|
+
* @param {string} agentName - The agent name
|
|
394
|
+
* @param {Object} discoveredAgents - Map to store discovered agents
|
|
395
|
+
* @param {Array} acceptedNetworks - List of accepted networks
|
|
396
|
+
* @returns {Promise<void>}
|
|
397
|
+
*/
|
|
398
|
+
async setupDiscoverySubscription(discoveryExchange, namespace, agentName, discoveredAgents, acceptedNetworks) {
|
|
399
|
+
const discoveryQueue = `discovery_${namespace}_${agentName}`;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
// Process discovery message handler
|
|
403
|
+
const messageHandler = async (content, msg) => {
|
|
404
|
+
try {
|
|
405
|
+
// Parse and validate the discovery message
|
|
406
|
+
let discoveryMessage;
|
|
407
|
+
try {
|
|
408
|
+
discoveryMessage = DiscoveryMessage.fromString(content);
|
|
409
|
+
} catch (parseError) {
|
|
410
|
+
logger.warn('Invalid discovery message format', { error: parseError.message });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const network = discoveryMessage.network;
|
|
415
|
+
const networkNamespace = network.split(".")[0];
|
|
416
|
+
const networkName = network.split(".")[1];
|
|
417
|
+
|
|
418
|
+
// Skip self
|
|
419
|
+
if (network === `${namespace}.${agentName}`) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check if network is accepted
|
|
424
|
+
let isAccepted = false;
|
|
425
|
+
for (const acceptedNetwork of acceptedNetworks) {
|
|
426
|
+
const acceptedNetworkNamespace = acceptedNetwork.split(".")[0];
|
|
427
|
+
const acceptedNetworkName = acceptedNetwork.split(".")[1];
|
|
428
|
+
|
|
429
|
+
if (
|
|
430
|
+
(acceptedNetworkNamespace === '*' || acceptedNetworkNamespace === networkNamespace) &&
|
|
431
|
+
(acceptedNetworkName === '*' || acceptedNetworkName === networkName)
|
|
432
|
+
) {
|
|
433
|
+
isAccepted = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!isAccepted) {
|
|
439
|
+
logger.warn(`Agent ${agentName} does not accept network ${network}`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Process the schemas from the discovery message
|
|
444
|
+
for (const schema of discoveryMessage.schemas) {
|
|
445
|
+
// Skip invalid schemas
|
|
446
|
+
if (!schema || !schema.name) {
|
|
447
|
+
logger.warn('Invalid schema in discovery payload', { schema });
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const agentKey = `${network}-${schema.name}`;
|
|
452
|
+
|
|
453
|
+
if (discoveryMessage.agentName !== agentName && !discoveredAgents[agentKey]) {
|
|
454
|
+
logger.info(`${agentName} discovered agent capability: ${discoveryMessage.agentName} with capability ${schema.name}`);
|
|
455
|
+
|
|
456
|
+
const handoffFunction = async (conversation, state, input) => {
|
|
457
|
+
try {
|
|
458
|
+
// Use withTimeout to ensure handoffs don't hang
|
|
459
|
+
return await withTimeout(
|
|
460
|
+
async () => {
|
|
461
|
+
try {
|
|
462
|
+
const message = new Message({
|
|
463
|
+
session: state,
|
|
464
|
+
content: input
|
|
465
|
+
});
|
|
466
|
+
// Exchange is set to direct for point-to-point communication
|
|
467
|
+
const req = await this.request(
|
|
468
|
+
'direct',
|
|
469
|
+
discoveryMessage.agentName,
|
|
470
|
+
message.serialize(),
|
|
471
|
+
{ timeout: TIMEOUT_TASK_REQUEST }
|
|
472
|
+
);
|
|
473
|
+
return req.string();
|
|
474
|
+
} catch (error) {
|
|
475
|
+
throw new HandoffError(
|
|
476
|
+
`Handoff to agent ${discoveryMessage.agentName} failed: ${error.message}`,
|
|
477
|
+
agentName,
|
|
478
|
+
discoveryMessage.agentName,
|
|
479
|
+
{ schemaName: schema.name }
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
TIMEOUT_TASK_REQUEST,
|
|
484
|
+
`handoff to ${discoveryMessage.agentName}`
|
|
485
|
+
);
|
|
486
|
+
} catch (error) {
|
|
487
|
+
logger.error(`Handoff error to ${discoveryMessage.agentName}`, {
|
|
488
|
+
error,
|
|
489
|
+
schema: schema.name
|
|
490
|
+
});
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
discoveredAgents[agentKey] = {
|
|
496
|
+
name: schema.name,
|
|
497
|
+
schema: schema,
|
|
498
|
+
function: handoffFunction
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
logger.error('Error processing discovery message', { error });
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Subscribe to discovery queue with binding to the exchange
|
|
508
|
+
await this.subscribe(
|
|
509
|
+
discoveryQueue,
|
|
510
|
+
discoveryExchange,
|
|
511
|
+
'#', // Listen to all routing keys
|
|
512
|
+
{ messageHandler }
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
logger.info(`Agent ${agentName} subscribed to discovery exchange ${discoveryExchange}`);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw new DiscoveryError(
|
|
518
|
+
`Failed to set up discovery subscription on exchange ${discoveryExchange}`,
|
|
519
|
+
{ agentName, exchange: discoveryExchange },
|
|
520
|
+
error
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Set up task handler for processing incoming requests
|
|
527
|
+
* @param {string} agentName - The agent name (used as the routing key)
|
|
528
|
+
* @param {Function} processingFunction - The function to process requests
|
|
529
|
+
* @returns {Promise<void>}
|
|
530
|
+
*/
|
|
531
|
+
async setupTaskHandler(agentName, processingFunction) {
|
|
532
|
+
const taskQueue = `tasks_${agentName}`;
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Process task message handler
|
|
536
|
+
const messageHandler = async (content, msg) => {
|
|
537
|
+
try {
|
|
538
|
+
// Parse and validate the payload
|
|
539
|
+
const payload = JSON.parse(content);
|
|
540
|
+
if (!payload || typeof payload !== 'object') {
|
|
541
|
+
throw new Error('Invalid payload: not a JSON object');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Process the message using the Message class
|
|
545
|
+
const message = new Message(payload);
|
|
546
|
+
|
|
547
|
+
// Process the task with timeout
|
|
548
|
+
const response = await withTimeout(
|
|
549
|
+
async () => processingFunction(message),
|
|
550
|
+
TIMEOUT_TASK_REQUEST * 2,
|
|
551
|
+
`task processing for ${agentName}`
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// Send response back if reply information is available
|
|
555
|
+
if (msg.properties.replyTo && msg.properties.correlationId) {
|
|
556
|
+
const replyContent = Buffer.from(response.serialize());
|
|
557
|
+
// await this.channel.sendToQueue(
|
|
558
|
+
// msg.properties.replyTo,
|
|
559
|
+
// replyContent,
|
|
560
|
+
// { correlationId: msg.properties.correlationId }
|
|
561
|
+
// );
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
logger.error("Error processing task", {
|
|
565
|
+
error,
|
|
566
|
+
agentName,
|
|
567
|
+
queue: taskQueue
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Send error response back if reply information is available
|
|
571
|
+
if (msg.properties.replyTo && msg.properties.correlationId) {
|
|
572
|
+
const errorContent = Buffer.from(JSON.stringify({
|
|
573
|
+
error: true,
|
|
574
|
+
message: error.message,
|
|
575
|
+
type: error.name || 'Error'
|
|
576
|
+
}));
|
|
577
|
+
// await this.channel.sendToQueue(
|
|
578
|
+
// msg.properties.replyTo,
|
|
579
|
+
// errorContent,
|
|
580
|
+
// { correlationId: msg.properties.correlationId }
|
|
581
|
+
// );
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Set up direct exchange for the agent
|
|
587
|
+
await this.ensureExchange('direct', 'direct');
|
|
588
|
+
|
|
589
|
+
// Subscribe to the task queue with binding to the direct exchange
|
|
590
|
+
await this.subscribe(
|
|
591
|
+
taskQueue,
|
|
592
|
+
'direct',
|
|
593
|
+
agentName,
|
|
594
|
+
{ messageHandler }
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
logger.info(`Agent ${agentName} subscribed for task handling`);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
throw new TransportError(
|
|
600
|
+
`Failed to set up task handler for ${agentName}: ${error.message}`,
|
|
601
|
+
this.transportType,
|
|
602
|
+
{ agentName }
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Creates a runtime for agent communication using RabbitMQ
|
|
609
|
+
* @param {string} namespace - The agent namespace
|
|
610
|
+
* @param {string} agentName - The agent name
|
|
611
|
+
* @param {Array} discoverySchemas - The agent capability schemas for discovery
|
|
612
|
+
* @param {Object} config - Additional configuration
|
|
613
|
+
* @returns {Promise<Object>} - The runtime { handleTask, discoveredAgents }
|
|
614
|
+
*/
|
|
615
|
+
async createRuntime(namespace, agentName, discoverySchemas, config) {
|
|
616
|
+
const discoveredAgents = {};
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
// Verify configuration
|
|
620
|
+
if (!config || !config.bindings || !config.bindings.discoveryTopic) {
|
|
621
|
+
throw new TransportError(
|
|
622
|
+
'Missing required RabbitMQ configuration: discoveryTopic',
|
|
623
|
+
this.transportType,
|
|
624
|
+
{ agentName }
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const discoveryExchange = config.bindings.discoveryTopic;
|
|
629
|
+
const acceptedNetworks = config.bindings.acceptedNetworks || [];
|
|
630
|
+
logger.info(`Agent ${agentName} initialized with discovery exchange ${discoveryExchange}`);
|
|
631
|
+
|
|
632
|
+
// Set up exchanges
|
|
633
|
+
await this.ensureExchange(discoveryExchange, 'topic');
|
|
634
|
+
|
|
635
|
+
// Step 1: Subscribe to discovery exchange
|
|
636
|
+
await this.setupDiscoverySubscription(discoveryExchange, namespace, agentName, discoveredAgents, acceptedNetworks);
|
|
637
|
+
|
|
638
|
+
// Step 2: Set up heartbeat using the exchange
|
|
639
|
+
// For RabbitMQ we use the setup heartbeat with exchange and agent as the routing key
|
|
640
|
+
this.intervals.push(setInterval(async () => {
|
|
641
|
+
try {
|
|
642
|
+
const discoveryMessage = new DiscoveryMessage(namespace, agentName, discoverySchemas);
|
|
643
|
+
await this.publish(discoveryExchange, agentName, discoveryMessage.serialize());
|
|
644
|
+
} catch (error) {
|
|
645
|
+
logger.error(`Failed to publish heartbeat for ${agentName}`, {
|
|
646
|
+
error,
|
|
647
|
+
transportType: this.transportType
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}, HEARTBEAT_INTERVAL));
|
|
651
|
+
|
|
652
|
+
// Step 3: Create task handler function
|
|
653
|
+
const handleTask = async (fn) => {
|
|
654
|
+
if (typeof fn !== 'function') {
|
|
655
|
+
throw new Error('Task handler must be a function');
|
|
656
|
+
}
|
|
657
|
+
await this.setupTaskHandler(agentName, fn);
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
return { handleTask, discoveredAgents };
|
|
661
|
+
} catch (error) {
|
|
662
|
+
// Enhance the error with context if it's not already a TransportError
|
|
663
|
+
if (!(error instanceof TransportError)) {
|
|
664
|
+
error = new TransportError(
|
|
665
|
+
`Failed to initialize RabbitMQ runtime: ${error.message}`,
|
|
666
|
+
this.transportType,
|
|
667
|
+
{ agentName }
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
logger.error("RabbitMQ runtime initialization failed", { error, agentName });
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Factory function to create a RabbitMQ transport instance
|
|
679
|
+
* @returns {RabbitMQTransport} - A RabbitMQ transport instance
|
|
680
|
+
*/
|
|
681
|
+
export function createRabbitMQTransport() {
|
|
682
|
+
return new RabbitMQTransport();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Adapter function to create a RabbitMQ-based runtime for agent communication
|
|
687
|
+
* @param {string} namespace - The agent namespace
|
|
688
|
+
* @param {string} agentName - The agent name
|
|
689
|
+
* @param {Array} ioInterfaces - The IO interfaces (only the first one is used)
|
|
690
|
+
* @param {Array} discoverySchemas - The agent capability schemas for discovery
|
|
691
|
+
* @returns {Promise<Object>} - The runtime { handleTask, discoveredAgents }
|
|
692
|
+
*/
|
|
693
|
+
export async function RabbitMQIOAgentRuntime(namespace, agentName, ioInterfaces, discoverySchemas) {
|
|
694
|
+
if (ioInterfaces.length > 1) {
|
|
695
|
+
throw new TransportError(
|
|
696
|
+
'Only one IO RabbitMQ interface is supported',
|
|
697
|
+
'RabbitMQ',
|
|
698
|
+
{ agentName, interfacesCount: ioInterfaces.length }
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (ioInterfaces.length === 0) {
|
|
703
|
+
logger.warn(`No RabbitMQ interfaces provided for agent ${agentName}, creating passive runtime`);
|
|
704
|
+
return { handleTask: async () => {}, discoveredAgents: {} };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const io = ioInterfaces[0];
|
|
708
|
+
const transport = createRabbitMQTransport();
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
// Connect to RabbitMQ with retry logic
|
|
712
|
+
logger.info(`Connecting to RabbitMQ for agent ${agentName}`);
|
|
713
|
+
await safeConnect(transport, io.config);
|
|
714
|
+
|
|
715
|
+
// Create runtime with the transport
|
|
716
|
+
return await transport.createRuntime(namespace, agentName, discoverySchemas, io.config);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
// Make sure to clean up if initialization fails
|
|
719
|
+
await transport.disconnect();
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|