agentnet 0.0.2 → 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,521 @@
1
+ /**
2
+ * NATS 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
+ const MAX_RECONNECT_ATTEMPTS = 5;
19
+
20
+ /**
21
+ * NATS implementation of the Transport interface
22
+ */
23
+ export class NatsTransport extends Transport {
24
+ constructor() {
25
+ super('NATS');
26
+ this.connection = null;
27
+ }
28
+
29
+ /**
30
+ * Connect to NATS
31
+ * @param {Object} config - NATS connection configuration
32
+ * @returns {Promise<any>} - The NATS connection instance
33
+ */
34
+ async connect(config) {
35
+ if (this.connected && this.connection) {
36
+ return this.connection;
37
+ }
38
+
39
+ try {
40
+ this.connection = await config.instance.connect();
41
+ this.connected = true;
42
+ return this.connection;
43
+ } catch (error) {
44
+ throw new TransportError(
45
+ `Failed to connect to NATS: ${error.message}`,
46
+ this.transportType,
47
+ { details: error.message }
48
+ );
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Disconnect from NATS
54
+ * @returns {Promise<void>}
55
+ */
56
+ async disconnect() {
57
+ await super.disconnect();
58
+
59
+ if (this.connection) {
60
+ try {
61
+ await this.connection.drain();
62
+ this.connection = null;
63
+ } catch (error) {
64
+ logger.warn('Error disconnecting from NATS', { error });
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Publish a message to a NATS topic
71
+ * @param {string} topic - The topic to publish to
72
+ * @param {string} message - The message to publish
73
+ * @returns {Promise<void>}
74
+ */
75
+ async publish(topic, message) {
76
+ if (!this.connected || !this.connection) {
77
+ throw new TransportError(
78
+ 'Cannot publish: not connected to NATS',
79
+ this.transportType
80
+ );
81
+ }
82
+
83
+ try {
84
+ await this.connection.publish(topic, message);
85
+ } catch (error) {
86
+ throw new TransportError(
87
+ `Failed to publish to topic ${topic}: ${error.message}`,
88
+ this.transportType,
89
+ { topic }
90
+ );
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Subscribe to a NATS topic
96
+ * @param {string} topic - The topic to subscribe to
97
+ * @param {Object} options - Subscription options
98
+ * @returns {Promise<any>} - The subscription instance
99
+ */
100
+ async subscribe(topic, options = {}) {
101
+ if (!this.connected || !this.connection) {
102
+ throw new TransportError(
103
+ 'Cannot subscribe: not connected to NATS',
104
+ this.transportType
105
+ );
106
+ }
107
+
108
+ try {
109
+ return this.connection.subscribe(topic, options);
110
+ } catch (error) {
111
+ throw new TransportError(
112
+ `Failed to subscribe to topic ${topic}: ${error.message}`,
113
+ this.transportType,
114
+ { topic }
115
+ );
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Send a request and wait for a response
121
+ * @param {string} target - The target to send the request to
122
+ * @param {string} message - The message to send
123
+ * @param {Object} options - Request options
124
+ * @returns {Promise<any>} - The response
125
+ */
126
+ async request(target, message, options = {}) {
127
+ if (!this.connected || !this.connection) {
128
+ throw new TransportError(
129
+ 'Cannot send request: not connected to NATS',
130
+ this.transportType
131
+ );
132
+ }
133
+
134
+ try {
135
+ return await this.connection.request(target, message, options);
136
+ } catch (error) {
137
+ throw new TransportError(
138
+ `Request to ${target} failed: ${error.message}`,
139
+ this.transportType,
140
+ { target }
141
+ );
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Set up discovery subscription
147
+ * @param {string} discoveryTopic - The topic for discovery messages
148
+ * @param {string} namespace - The agent namespace
149
+ * @param {string} agentName - The agent name
150
+ * @param {Object} discoveredAgents - Map to store discovered agents
151
+ * @param {Array} acceptedNetworks - List of accepted networks
152
+ * @returns {Promise<void>}
153
+ */
154
+ async setupDiscoverySubscription(discoveryTopic, namespace, agentName, discoveredAgents, acceptedNetworks) {
155
+ let discoverySub;
156
+
157
+ try {
158
+ discoverySub = await this.subscribe(discoveryTopic);
159
+ logger.info(`Agent ${agentName} subscribed to discovery topic ${discoveryTopic}`);
160
+ } catch (error) {
161
+ throw new DiscoveryError(
162
+ `Failed to subscribe to discovery topic ${discoveryTopic}`,
163
+ { agentName, topic: discoveryTopic },
164
+ error
165
+ );
166
+ }
167
+
168
+ const handleDiscovery = async () => {
169
+ try {
170
+ let nonAcceptedNetworks = {};
171
+ for await (const m of discoverySub) {
172
+ try {
173
+ // Attempt to parse and validate the discovery message
174
+ let discoveryMessage;
175
+ try {
176
+ discoveryMessage = DiscoveryMessage.fromString(m.string());
177
+ } catch (parseError) {
178
+ logger.warn('Invalid discovery message format', { error: parseError.message });
179
+ continue;
180
+ }
181
+
182
+ const network = discoveryMessage.network;
183
+ const networkNamespace = network.split(".")[0];
184
+ const networkName = network.split(".")[1];
185
+
186
+ // Skip self
187
+ if (network === `${namespace}.${agentName}`) {
188
+ continue;
189
+ }
190
+
191
+ // Skip if already processed
192
+ if (nonAcceptedNetworks[network] === true) {
193
+ continue;
194
+ }
195
+
196
+ let isAccepted = false;
197
+ for (const acceptedNetwork of acceptedNetworks) {
198
+ const acceptedNetworkNamespace = acceptedNetwork.split(".")[0];
199
+ const acceptedNetworkName = acceptedNetwork.split(".")[1];
200
+
201
+ if (acceptedNetworkNamespace === networkNamespace && acceptedNetworkName === networkName) {
202
+ isAccepted = true;
203
+ continue;
204
+ }
205
+ // Check for wildcard patterns in accepted networks
206
+
207
+ if (acceptedNetworkNamespace === '*' && acceptedNetworkName === '*') {
208
+ // Both namespace and name are wildcards, accept any network
209
+ logger.debug(`Agent ${agentName} accepting network ${network} due to wildcard pattern *.*`);
210
+ isAccepted = true;
211
+ continue;
212
+ }
213
+
214
+ if (acceptedNetworkNamespace === '*' && acceptedNetworkName === networkName) {
215
+ // Namespace is wildcard, but name matches
216
+ logger.debug(`Agent ${agentName} accepting network ${network} due to wildcard pattern *.${networkName}`);
217
+ isAccepted = true;
218
+ continue;
219
+ }
220
+
221
+ if (acceptedNetworkNamespace === networkNamespace && acceptedNetworkName === '*') {
222
+ // Name is wildcard, but namespace matches
223
+ logger.debug(`Agent ${agentName} accepting network ${network} due to wildcard pattern ${networkNamespace}.*`);
224
+ isAccepted = true;
225
+ continue;
226
+ }
227
+ }
228
+
229
+ // Skip if not accepted
230
+ if (!isAccepted) {
231
+ logger.warn(`Agent ${agentName} does not accept network ${network}`);
232
+ nonAcceptedNetworks[network] = true;
233
+ continue;
234
+ }
235
+
236
+ // Process the schemas from the discovery message
237
+ for (const schema of discoveryMessage.schemas) {
238
+ // Skip invalid schemas
239
+ if (!schema || !schema.name) {
240
+ logger.warn('Invalid schema in discovery payload', { schema });
241
+ continue;
242
+ }
243
+
244
+ const agentKey = `${network}-${schema.name}`;
245
+
246
+ if (discoveryMessage.agentName !== agentName && !discoveredAgents[agentKey]) {
247
+ logger.info(`${agentName} discovered agent capability: ${discoveryMessage.agentName} with capability ${schema.name}`);
248
+
249
+ const handoffFunction = async (conversation, state, input) => {
250
+ try {
251
+ // Use withTimeout to ensure handoffs don't hang
252
+ return await withTimeout(
253
+ async () => {
254
+ try {
255
+ const message = new Message({
256
+ session: state,
257
+ content: input
258
+ });
259
+ const req = await this.request(
260
+ discoveryMessage.agentName,
261
+ message.serialize(),
262
+ { timeout: TIMEOUT_TASK_REQUEST }
263
+ );
264
+ return req.string();
265
+ } catch (error) {
266
+ throw new HandoffError(
267
+ `Handoff to agent ${discoveryMessage.agentName} failed: ${error.message}`,
268
+ agentName,
269
+ discoveryMessage.agentName,
270
+ { schemaName: schema.name }
271
+ );
272
+ }
273
+ },
274
+ TIMEOUT_TASK_REQUEST,
275
+ `handoff to ${discoveryMessage.agentName}`
276
+ );
277
+ } catch (error) {
278
+ logger.error(`Handoff error to ${discoveryMessage.agentName}`, {
279
+ error,
280
+ schema: schema.name
281
+ });
282
+ throw error;
283
+ }
284
+ };
285
+
286
+ discoveredAgents[agentKey] = {
287
+ name: schema.name,
288
+ schema: schema,
289
+ function: handoffFunction
290
+ };
291
+ }
292
+ }
293
+ } catch (error) {
294
+ logger.error('Error processing discovery message', { error });
295
+ }
296
+ }
297
+ } catch (error) {
298
+ logger.error("Discovery subscription error", { error });
299
+
300
+ // Attempt to resubscribe if the connection is still active
301
+ if (this.connected) {
302
+ logger.info('Attempting to resubscribe to discovery topic');
303
+ try {
304
+ discoverySub = await this.subscribe(discoveryTopic);
305
+ handleDiscovery(); // Restart the handling process
306
+ } catch (resubError) {
307
+ logger.error('Failed to resubscribe to discovery topic', { error: resubError });
308
+ }
309
+ }
310
+ }
311
+ };
312
+
313
+ // Start processing discovery messages
314
+ handleDiscovery();
315
+ }
316
+
317
+ /**
318
+ * Set up task handler for processing incoming requests
319
+ * @param {string} agentName - The agent name
320
+ * @param {Function} processingFunction - The function to process requests
321
+ * @returns {Promise<void>}
322
+ */
323
+ async setupTaskHandler(agentName, processingFunction) {
324
+ let taskSub;
325
+
326
+ try {
327
+ taskSub = await this.subscribe(agentName, { queue: agentName });
328
+ logger.info(`Agent ${agentName} subscribed for task handling`);
329
+ } catch (error) {
330
+ throw new TransportError(
331
+ `Failed to subscribe for task handling: ${error.message}`,
332
+ this.transportType,
333
+ { agentName }
334
+ );
335
+ }
336
+
337
+ try {
338
+ for await (const m of taskSub) {
339
+ let payload;
340
+
341
+ try {
342
+ // Parse and validate the payload
343
+ payload = m.json();
344
+ if (!payload || typeof payload !== 'object') {
345
+ throw new Error('Invalid payload: not a JSON object');
346
+ }
347
+
348
+ const message = new Message(payload);
349
+ const input = message.getContent();
350
+ const session = message.getSession();
351
+
352
+ logger.debug(`Received task request for ${agentName}`, {
353
+ inputPreview: typeof input === 'string'
354
+ ? input.substring(0, 100)
355
+ : 'Non-string input'
356
+ });
357
+
358
+ // Process the task with timeout
359
+ const response = await withTimeout(
360
+ async () => processingFunction(message),
361
+ TIMEOUT_TASK_REQUEST * 2, // Double the timeout for processing
362
+ `task processing for ${agentName}`
363
+ );
364
+
365
+ // Respond with the result
366
+ await m.respond(response.serialize());
367
+
368
+ logger.debug(`Completed task request for ${agentName}`);
369
+ } catch (error) {
370
+ logger.error("Error processing task", {
371
+ error,
372
+ agentName,
373
+ inputPreview: payload && payload.content
374
+ ? (typeof payload.content === 'string'
375
+ ? payload.content.substring(0, 100)
376
+ : 'Non-string input')
377
+ : 'No input'
378
+ });
379
+
380
+ // Send error response back
381
+ try {
382
+ await m.respond(JSON.stringify({
383
+ error: true,
384
+ message: error.message,
385
+ type: error.name || 'Error'
386
+ }));
387
+ } catch (respondError) {
388
+ logger.error("Failed to send error response", { error: respondError });
389
+ }
390
+ }
391
+ }
392
+ } catch (error) {
393
+ logger.error("Task subscription error", { error, agentName });
394
+
395
+ // Attempt to resubscribe if the connection is still active
396
+ if (this.connected) {
397
+ logger.info('Attempting to resubscribe for task handling');
398
+ try {
399
+ await this.setupTaskHandler(agentName, processingFunction);
400
+ } catch (resubError) {
401
+ logger.error('Failed to resubscribe for task handling', { error: resubError });
402
+ throw new TransportError(
403
+ "Failed to resubscribe for task handling",
404
+ this.transportType,
405
+ { agentName, originalError: error.message, resubError: resubError.message }
406
+ );
407
+ }
408
+ } else {
409
+ throw new TransportError(
410
+ "NATS connection lost during task handling",
411
+ this.transportType,
412
+ { agentName }
413
+ );
414
+ }
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Creates a runtime for agent communication using NATS
420
+ * @param {string} namespace - The agent namespace
421
+ * @param {string} agentName - The agent name
422
+ * @param {Array} discoverySchemas - The agent capability schemas for discovery
423
+ * @param {Object} config - Additional configuration
424
+ * @returns {Promise<Object>} - The runtime { handleTask, discoveredAgents }
425
+ */
426
+ async createRuntime(namespace, agentName, discoverySchemas, config) {
427
+ const discoveredAgents = {};
428
+
429
+ try {
430
+ // Verify configuration
431
+ if (!config || !config.bindings || !config.bindings.discoveryTopic) {
432
+ throw new TransportError(
433
+ 'Missing required NATS configuration: discoveryTopic',
434
+ this.transportType,
435
+ { agentName }
436
+ );
437
+ }
438
+
439
+ const discoveryTopic = config.bindings.discoveryTopic;
440
+ const acceptedNetworks = config.bindings.acceptedNetworks || [];
441
+ logger.info(`Agent ${agentName} initialized with discovery topic ${discoveryTopic}`);
442
+
443
+ // Step 1: Subscribe to discovery topic
444
+ await this.setupDiscoverySubscription(discoveryTopic, namespace, agentName, discoveredAgents, acceptedNetworks);
445
+
446
+ // Step 2: Set up heartbeat
447
+ this.setupHeartbeat(discoveryTopic, namespace, agentName, discoverySchemas, HEARTBEAT_INTERVAL);
448
+
449
+ // Step 3: Create task handler function
450
+ const handleTask = async (fn) => {
451
+ if (typeof fn !== 'function') {
452
+ throw new Error('Task handler must be a function');
453
+ }
454
+ await this.setupTaskHandler(agentName, fn);
455
+ };
456
+
457
+ return { handleTask, discoveredAgents };
458
+ } catch (error) {
459
+ // Enhance the error with context if it's not already a TransportError
460
+ if (!(error instanceof TransportError)) {
461
+ error = new TransportError(
462
+ `Failed to initialize NATS runtime: ${error.message}`,
463
+ this.transportType,
464
+ { agentName }
465
+ );
466
+ }
467
+
468
+ logger.error("NATS runtime initialization failed", { error, agentName });
469
+ throw error;
470
+ }
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Factory function to create a NATS transport instance
476
+ * @param {Object} config - Configuration for the NATS transport
477
+ * @returns {NatsTransport} - A configured NATS transport instance
478
+ */
479
+ export function createNatsTransport() {
480
+ return new NatsTransport();
481
+ }
482
+
483
+ /**
484
+ * Adapter function to create a NATS-based runtime for agent communication
485
+ * Maintains compatibility with the original NatsIOAgentRuntime function
486
+ * @param {string} namespace - The agent namespace
487
+ * @param {string} agentName - The agent name
488
+ * @param {Array} ioInterfaces - The IO interfaces (only the first one is used)
489
+ * @param {Array} discoverySchemas - The agent capability schemas for discovery
490
+ * @returns {Promise<Object>} - The runtime { handleTask, discoveredAgents }
491
+ */
492
+ export async function NatsIOAgentRuntime(namespace, agentName, ioInterfaces, discoverySchemas) {
493
+ if (ioInterfaces.length > 1) {
494
+ throw new TransportError(
495
+ 'Only one IO Nats interface is supported',
496
+ 'NATS',
497
+ { agentName, interfacesCount: ioInterfaces.length }
498
+ );
499
+ }
500
+
501
+ if (ioInterfaces.length === 0) {
502
+ logger.warn(`No NATS interfaces provided for agent ${agentName}, creating passive runtime`);
503
+ return { handleTask: async () => {}, discoveredAgents: {} };
504
+ }
505
+
506
+ const io = ioInterfaces[0];
507
+ const transport = createNatsTransport();
508
+
509
+ try {
510
+ // Connect to NATS with retry logic
511
+ logger.info(`Connecting to NATS for agent ${agentName}`);
512
+ await safeConnect(transport, { instance: io.instance });
513
+
514
+ // Create runtime with the transport
515
+ return await transport.createRuntime(namespace, agentName, discoverySchemas, io.config);
516
+ } catch (error) {
517
+ // Make sure to clean up if initialization fails
518
+ await transport.disconnect();
519
+ throw error;
520
+ }
521
+ }