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