agentnet 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Kafka 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
+ * Kafka implementation of the Transport interface
21
+ */
22
+ export class KafkaTransport extends Transport {
23
+ constructor() {
24
+ super('Kafka');
25
+ this.producer = null;
26
+ this.consumer = null;
27
+ this.admin = null;
28
+ this.requestMap = new Map(); // For request-response pattern
29
+ }
30
+
31
+ /**
32
+ * Connect to Kafka
33
+ * @param {Object} config - Kafka connection configuration
34
+ * @returns {Promise<any>} - The Kafka connection objects
35
+ */
36
+ async connect(config) {
37
+ if (this.connected) {
38
+ return { producer: this.producer, consumer: this.consumer };
39
+ }
40
+
41
+ try {
42
+ // Implementation will depend on the Kafka client library used
43
+ // For example, with KafkaJS:
44
+ // this.producer = kafka.producer();
45
+ // this.consumer = kafka.consumer({ groupId: config.groupId });
46
+ // this.admin = kafka.admin();
47
+
48
+ // await this.producer.connect();
49
+ // await this.consumer.connect();
50
+ // await this.admin.connect();
51
+
52
+ this.connected = true;
53
+ return { producer: this.producer, consumer: this.consumer };
54
+ } catch (error) {
55
+ throw new TransportError(
56
+ `Failed to connect to Kafka: ${error.message}`,
57
+ this.transportType,
58
+ { details: error.message }
59
+ );
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Disconnect from Kafka
65
+ * @returns {Promise<void>}
66
+ */
67
+ async disconnect() {
68
+ await super.disconnect();
69
+
70
+ try {
71
+ // Disconnect all clients
72
+ if (this.producer) await this.producer.disconnect();
73
+ if (this.consumer) await this.consumer.disconnect();
74
+ if (this.admin) await this.admin.disconnect();
75
+
76
+ this.producer = null;
77
+ this.consumer = null;
78
+ this.admin = null;
79
+ } catch (error) {
80
+ logger.warn('Error disconnecting from Kafka', { error });
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Publish a message to a Kafka topic
86
+ * @param {string} topic - The topic to publish to
87
+ * @param {string} message - The message to publish
88
+ * @returns {Promise<void>}
89
+ */
90
+ async publish(topic, message) {
91
+ if (!this.connected || !this.producer) {
92
+ throw new TransportError(
93
+ 'Cannot publish: not connected to Kafka',
94
+ this.transportType
95
+ );
96
+ }
97
+
98
+ try {
99
+ // Example with KafkaJS:
100
+ // await this.producer.send({
101
+ // topic,
102
+ // messages: [{ value: message }]
103
+ // });
104
+ } catch (error) {
105
+ throw new TransportError(
106
+ `Failed to publish to topic ${topic}: ${error.message}`,
107
+ this.transportType,
108
+ { topic }
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Subscribe to a Kafka topic
115
+ * @param {string} topic - The topic to subscribe to
116
+ * @param {Object} options - Subscription options
117
+ * @returns {Promise<any>} - The subscription identifier
118
+ */
119
+ async subscribe(topic, options = {}) {
120
+ if (!this.connected || !this.consumer) {
121
+ throw new TransportError(
122
+ 'Cannot subscribe: not connected to Kafka',
123
+ this.transportType
124
+ );
125
+ }
126
+
127
+ try {
128
+ // Example with KafkaJS:
129
+ // await this.consumer.subscribe({ topic, fromBeginning: false });
130
+ // Create a unique subscription ID
131
+ const subscriptionId = `sub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
132
+ return subscriptionId;
133
+ } catch (error) {
134
+ throw new TransportError(
135
+ `Failed to subscribe to topic ${topic}: ${error.message}`,
136
+ this.transportType,
137
+ { topic }
138
+ );
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Start consuming messages
144
+ * @param {Function} messageHandler - Function to handle incoming messages
145
+ * @returns {Promise<void>}
146
+ */
147
+ async startConsumer(messageHandler) {
148
+ if (!this.connected || !this.consumer) {
149
+ throw new TransportError(
150
+ 'Cannot start consumer: not connected to Kafka',
151
+ this.transportType
152
+ );
153
+ }
154
+
155
+ try {
156
+ // Example with KafkaJS:
157
+ // await this.consumer.run({
158
+ // eachMessage: async ({ topic, partition, message }) => {
159
+ // messageHandler(topic, message.value.toString());
160
+ // },
161
+ // });
162
+ } catch (error) {
163
+ throw new TransportError(
164
+ `Failed to start Kafka consumer: ${error.message}`,
165
+ this.transportType
166
+ );
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Send a request and wait for a response (implement request-response pattern on Kafka)
172
+ * @param {string} target - The target topic
173
+ * @param {string} message - The message to send
174
+ * @param {Object} options - Request options
175
+ * @returns {Promise<any>} - The response
176
+ */
177
+ async request(target, message, options = {}) {
178
+ if (!this.connected || !this.producer) {
179
+ throw new TransportError(
180
+ 'Cannot send request: not connected to Kafka',
181
+ this.transportType
182
+ );
183
+ }
184
+
185
+ const correlationId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
186
+ const replyTo = `${target}_replies`;
187
+ const timeout = options.timeout || TIMEOUT_TASK_REQUEST;
188
+
189
+ try {
190
+ // Create a promise that will be resolved when the response is received
191
+ const responsePromise = new Promise((resolve, reject) => {
192
+ // Set a timeout
193
+ const timeoutId = setTimeout(() => {
194
+ this.requestMap.delete(correlationId);
195
+ reject(new TimeoutError(`Request to ${target} timed out after ${timeout}ms`));
196
+ }, timeout);
197
+
198
+ // Store the resolver and timeout
199
+ this.requestMap.set(correlationId, {
200
+ resolve,
201
+ reject,
202
+ timeoutId
203
+ });
204
+ });
205
+
206
+ // Make sure we're subscribed to the reply topic
207
+ // const replySubscription = await this.subscribe(replyTo);
208
+
209
+ // Send the request with correlation ID and reply topic
210
+ // await this.producer.send({
211
+ // topic: target,
212
+ // messages: [{
213
+ // value: message,
214
+ // headers: {
215
+ // 'correlation-id': correlationId,
216
+ // 'reply-to': replyTo
217
+ // }
218
+ // }]
219
+ // });
220
+
221
+ // Wait for the response
222
+ return await responsePromise;
223
+ } catch (error) {
224
+ // Clean up
225
+ const requestData = this.requestMap.get(correlationId);
226
+ if (requestData) {
227
+ clearTimeout(requestData.timeoutId);
228
+ this.requestMap.delete(correlationId);
229
+ }
230
+
231
+ throw new TransportError(
232
+ `Request to ${target} failed: ${error.message}`,
233
+ this.transportType,
234
+ { target }
235
+ );
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Process incoming response messages
241
+ * @param {string} topic - The topic the message was received on
242
+ * @param {Object} message - The message object
243
+ */
244
+ processResponseMessage(topic, message) {
245
+ try {
246
+ const headers = message.headers || {};
247
+ const correlationId = headers['correlation-id'];
248
+
249
+ if (correlationId && this.requestMap.has(correlationId)) {
250
+ const { resolve, timeoutId } = this.requestMap.get(correlationId);
251
+ clearTimeout(timeoutId);
252
+ this.requestMap.delete(correlationId);
253
+ resolve({
254
+ string: () => message.value.toString()
255
+ });
256
+ }
257
+ } catch (error) {
258
+ logger.error('Error processing response message', { error, topic });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Set up discovery subscription
264
+ * This is a template implementation that would need to be adapted for Kafka's message model
265
+ */
266
+ async setupDiscoverySubscription(discoveryTopic, namespace, agentName, discoveredAgents, acceptedNetworks) {
267
+ try {
268
+ // Subscribe to discovery topic
269
+ await this.subscribe(discoveryTopic);
270
+ logger.info(`Agent ${agentName} subscribed to discovery topic ${discoveryTopic}`);
271
+
272
+ // Set up message handler for discovery messages
273
+ const messageHandler = async (topic, message) => {
274
+ if (topic !== discoveryTopic) return;
275
+
276
+ try {
277
+ // Parse and validate the discovery message
278
+ let discoveryMessage;
279
+ try {
280
+ discoveryMessage = DiscoveryMessage.fromString(message);
281
+ } catch (parseError) {
282
+ logger.warn('Invalid discovery message format', { error: parseError.message });
283
+ return;
284
+ }
285
+
286
+ // Process discovery message...
287
+ // The implementation would be similar to the NATS version but adapted for Kafka's async model
288
+ } catch (error) {
289
+ logger.error('Error processing discovery message', { error });
290
+ }
291
+ };
292
+
293
+ // Start listening for messages
294
+ // You would need to integrate this with your Kafka consumer implementation
295
+ } catch (error) {
296
+ throw new DiscoveryError(
297
+ `Failed to set up discovery subscription on topic ${discoveryTopic}`,
298
+ { agentName, topic: discoveryTopic },
299
+ error
300
+ );
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Set up task handler for processing incoming requests
306
+ * @param {string} agentName - The agent name (used as the topic)
307
+ * @param {Function} processingFunction - The function to process requests
308
+ * @returns {Promise<void>}
309
+ */
310
+ async setupTaskHandler(agentName, processingFunction) {
311
+ try {
312
+ // Subscribe to agent's topic
313
+ await this.subscribe(agentName);
314
+ logger.info(`Agent ${agentName} subscribed for task handling`);
315
+
316
+ // Set up message handler for task requests
317
+ const messageHandler = async (topic, message, headers) => {
318
+ if (topic !== agentName) return;
319
+
320
+ try {
321
+ // Parse and validate the payload
322
+ const payload = JSON.parse(message);
323
+ if (!payload || typeof payload !== 'object') {
324
+ throw new Error('Invalid payload: not a JSON object');
325
+ }
326
+
327
+ const replyTo = headers['reply-to'];
328
+ const correlationId = headers['correlation-id'];
329
+
330
+ // Process the message using the Message class
331
+ const msg = new Message(payload);
332
+
333
+ // Process the task with timeout
334
+ const response = await withTimeout(
335
+ async () => processingFunction(msg),
336
+ TIMEOUT_TASK_REQUEST * 2,
337
+ `task processing for ${agentName}`
338
+ );
339
+
340
+ // Send response back if reply information is available
341
+ if (replyTo && correlationId) {
342
+ await this.publish(replyTo, response.serialize(), {
343
+ headers: { 'correlation-id': correlationId }
344
+ });
345
+ }
346
+ } catch (error) {
347
+ logger.error("Error processing task", { error, agentName });
348
+
349
+ // Send error response back if reply information is available
350
+ if (replyTo && correlationId) {
351
+ await this.publish(replyTo, JSON.stringify({
352
+ error: true,
353
+ message: error.message,
354
+ type: error.name || 'Error'
355
+ }), {
356
+ headers: { 'correlation-id': correlationId }
357
+ });
358
+ }
359
+ }
360
+ };
361
+
362
+ // Start listening for messages
363
+ // You would need to integrate this with your Kafka consumer implementation
364
+ } catch (error) {
365
+ throw new TransportError(
366
+ `Failed to set up task handler for ${agentName}: ${error.message}`,
367
+ this.transportType,
368
+ { agentName }
369
+ );
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Creates a runtime for agent communication using Kafka
375
+ * @param {string} namespace - The agent namespace
376
+ * @param {string} agentName - The agent name
377
+ * @param {Array} discoverySchemas - The agent capability schemas for discovery
378
+ * @param {Object} config - Additional configuration
379
+ * @returns {Promise<Object>} - The runtime { handleTask, discoveredAgents }
380
+ */
381
+ async createRuntime(namespace, agentName, discoverySchemas, config) {
382
+ const discoveredAgents = {};
383
+
384
+ try {
385
+ // Verify configuration
386
+ if (!config || !config.bindings || !config.bindings.discoveryTopic) {
387
+ throw new TransportError(
388
+ 'Missing required Kafka configuration: discoveryTopic',
389
+ this.transportType,
390
+ { agentName }
391
+ );
392
+ }
393
+
394
+ const discoveryTopic = config.bindings.discoveryTopic;
395
+ const acceptedNetworks = config.bindings.acceptedNetworks || [];
396
+ logger.info(`Agent ${agentName} initialized with discovery topic ${discoveryTopic}`);
397
+
398
+ // Step 1: Subscribe to discovery topic
399
+ await this.setupDiscoverySubscription(discoveryTopic, namespace, agentName, discoveredAgents, acceptedNetworks);
400
+
401
+ // Step 2: Set up heartbeat
402
+ this.setupHeartbeat(discoveryTopic, namespace, agentName, discoverySchemas, HEARTBEAT_INTERVAL);
403
+
404
+ // Step 3: Create task handler function
405
+ const handleTask = async (fn) => {
406
+ if (typeof fn !== 'function') {
407
+ throw new Error('Task handler must be a function');
408
+ }
409
+ await this.setupTaskHandler(agentName, fn);
410
+ };
411
+
412
+ return { handleTask, discoveredAgents };
413
+ } catch (error) {
414
+ // Enhance the error with context if it's not already a TransportError
415
+ if (!(error instanceof TransportError)) {
416
+ error = new TransportError(
417
+ `Failed to initialize Kafka runtime: ${error.message}`,
418
+ this.transportType,
419
+ { agentName }
420
+ );
421
+ }
422
+
423
+ logger.error("Kafka runtime initialization failed", { error, agentName });
424
+ throw error;
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Factory function to create a Kafka transport instance
431
+ * @returns {KafkaTransport} - A Kafka transport instance
432
+ */
433
+ export function createKafkaTransport() {
434
+ return new KafkaTransport();
435
+ }
436
+
437
+ /**
438
+ * Adapter function to create a Kafka-based runtime for agent communication
439
+ * @param {string} namespace - The agent namespace
440
+ * @param {string} agentName - The agent name
441
+ * @param {Array} ioInterfaces - The IO interfaces (only the first one is used)
442
+ * @param {Array} discoverySchemas - The agent capability schemas for discovery
443
+ * @returns {Promise<Object>} - The runtime { handleTask, discoveredAgents }
444
+ */
445
+ export async function KafkaIOAgentRuntime(namespace, agentName, ioInterfaces, discoverySchemas) {
446
+ if (ioInterfaces.length > 1) {
447
+ throw new TransportError(
448
+ 'Only one IO Kafka interface is supported',
449
+ 'Kafka',
450
+ { agentName, interfacesCount: ioInterfaces.length }
451
+ );
452
+ }
453
+
454
+ if (ioInterfaces.length === 0) {
455
+ logger.warn(`No Kafka interfaces provided for agent ${agentName}, creating passive runtime`);
456
+ return { handleTask: async () => {}, discoveredAgents: {} };
457
+ }
458
+
459
+ const io = ioInterfaces[0];
460
+ const transport = createKafkaTransport();
461
+
462
+ try {
463
+ // Connect to Kafka with retry logic
464
+ logger.info(`Connecting to Kafka for agent ${agentName}`);
465
+ await safeConnect(transport, io.config);
466
+
467
+ // Create runtime with the transport
468
+ return await transport.createRuntime(namespace, agentName, discoverySchemas, io.config);
469
+ } catch (error) {
470
+ // Make sure to clean up if initialization fails
471
+ await transport.disconnect();
472
+ throw error;
473
+ }
474
+ }