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.
@@ -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
+ }