agentnet 0.0.1

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,379 @@
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
+ * Sets up discovery subscription to find other agents
21
+ */
22
+ async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discoveredAgents) {
23
+ let discoverySub;
24
+
25
+ try {
26
+ discoverySub = nc.subscribe(discoveryTopic);
27
+ logger.info(`Agent ${agentName} subscribed to discovery topic ${discoveryTopic}`);
28
+ } catch (error) {
29
+ throw new DiscoveryError(
30
+ `Failed to subscribe to discovery topic ${discoveryTopic}`,
31
+ { agentName, topic: discoveryTopic },
32
+ error
33
+ );
34
+ }
35
+
36
+ const handleDiscovery = async () => {
37
+ try {
38
+ for await (const m of discoverySub) {
39
+ try {
40
+ const payloadSetup = JSON.parse(m.string());
41
+
42
+ // Validate the payload structure
43
+ if (!payloadSetup.agentName || !Array.isArray(payloadSetup.schemas)) {
44
+ logger.warn('Invalid discovery payload received', { payload: payloadSetup });
45
+ continue;
46
+ }
47
+
48
+ for (const schema of payloadSetup.schemas) {
49
+ // Skip invalid schemas
50
+ if (!schema || !schema.name) {
51
+ logger.warn('Invalid schema in discovery payload', { schema });
52
+ continue;
53
+ }
54
+
55
+ const agentKey = `${payloadSetup.agentName}-${schema.name}`;
56
+
57
+ if (payloadSetup.agentName !== agentName && !discoveredAgents[agentKey]) {
58
+ logger.info(`${agentName} discovered agent capability: ${payloadSetup.agentName} with capability ${schema.name}`);
59
+
60
+ const handoffFunction = async (conversation, state, input) => {
61
+ try {
62
+ // Use withTimeout to ensure handoffs don't hang
63
+ return await withTimeout(
64
+ async () => {
65
+ try {
66
+ const response = await withRetry(
67
+ async () => {
68
+ const message = new Message({
69
+ session: state,
70
+ content: input
71
+ })
72
+ const req = await nc.request(
73
+ payloadSetup.agentName,
74
+ message.serialize(),
75
+ { timeout: TIMEOUT_TASK_REQUEST }
76
+ );
77
+ return req.string();
78
+ },
79
+ {
80
+ maxRetries: 2,
81
+ onRetry: ({ attempt }) => {
82
+ logger.warn(`Retrying handoff attempt ${attempt} to ${payloadSetup.agentName}`, {
83
+ schema: schema.name
84
+ });
85
+ }
86
+ }
87
+ );
88
+ return response;
89
+ } catch (error) {
90
+ throw new HandoffError(
91
+ `Handoff to agent ${payloadSetup.agentName} failed: ${error.message}`,
92
+ agentName,
93
+ payloadSetup.agentName,
94
+ { schemaName: schema.name }
95
+ );
96
+ }
97
+ },
98
+ TIMEOUT_TASK_REQUEST,
99
+ `handoff to ${payloadSetup.agentName}`
100
+ );
101
+ } catch (error) {
102
+ logger.error(`Handoff error to ${payloadSetup.agentName}`, {
103
+ error,
104
+ schema: schema.name
105
+ });
106
+ throw error;
107
+ }
108
+ };
109
+
110
+ discoveredAgents[agentKey] = {
111
+ name: schema.name,
112
+ schema: schema,
113
+ function: handoffFunction
114
+ };
115
+ }
116
+ }
117
+ } catch (error) {
118
+ logger.error('Error processing discovery message', { error });
119
+ }
120
+ }
121
+ } catch (error) {
122
+ logger.error("Discovery subscription error", { error });
123
+
124
+ // Attempt to resubscribe if the connection is still active
125
+ if (nc.isConnected()) {
126
+ logger.info('Attempting to resubscribe to discovery topic');
127
+ try {
128
+ discoverySub = nc.subscribe(discoveryTopic);
129
+ handleDiscovery(); // Restart the handling process
130
+ } catch (resubError) {
131
+ logger.error('Failed to resubscribe to discovery topic', { error: resubError });
132
+ }
133
+ }
134
+ }
135
+ };
136
+
137
+ // Start processing discovery messages
138
+ handleDiscovery();
139
+ }
140
+
141
+ /**
142
+ * Sets up a heartbeat to announce this agent's capabilities
143
+ */
144
+ function setupDiscoveryHeartbeat(nc, discoveryTopic, agentName, discoverySchemas) {
145
+ let consecutiveErrors = 0;
146
+
147
+ return setInterval(async () => {
148
+ try {
149
+ const heartbeatPayload = {
150
+ type: 'discovery',
151
+ agentName: agentName,
152
+ schemas: discoverySchemas
153
+ };
154
+
155
+ await nc.publish(discoveryTopic, JSON.stringify(heartbeatPayload));
156
+
157
+ // Reset error counter on success
158
+ if (consecutiveErrors > 0) {
159
+ logger.info(`Discovery heartbeat resumed for ${agentName}`);
160
+ consecutiveErrors = 0;
161
+ }
162
+ } catch (error) {
163
+ consecutiveErrors++;
164
+
165
+ // Log with increasing severity based on consecutive failures
166
+ if (consecutiveErrors > 5) {
167
+ logger.error(`Failed to publish discovery heartbeat (${consecutiveErrors} consecutive failures)`, {
168
+ error,
169
+ agentName,
170
+ topic: discoveryTopic
171
+ });
172
+ } else {
173
+ logger.warn(`Error publishing discovery heartbeat (attempt ${consecutiveErrors})`, {
174
+ error: error.message,
175
+ agentName
176
+ });
177
+ }
178
+ }
179
+ }, HEARTBEAT_INTERVAL);
180
+ }
181
+
182
+ /**
183
+ * Creates a task handler for incoming requests
184
+ */
185
+ async function createTaskHandler(nc, agentName, processingFunction) {
186
+ let taskSub;
187
+
188
+ try {
189
+ taskSub = nc.subscribe(agentName, { queue: agentName });
190
+ logger.info(`Agent ${agentName} subscribed for task handling`);
191
+ } catch (error) {
192
+ throw new TransportError(
193
+ `Failed to subscribe for task handling: ${error.message}`,
194
+ 'NATS',
195
+ { agentName }
196
+ );
197
+ }
198
+
199
+ try {
200
+ for await (const m of taskSub) {
201
+ let payload;
202
+
203
+ try {
204
+ // Parse and validate the payload
205
+ payload = m.json();
206
+ if (!payload || typeof payload !== 'object') {
207
+ throw new Error('Invalid payload: not a JSON object');
208
+ }
209
+ const message = new Message(payload)
210
+ const input = message.getContent()
211
+ const session = message.getSession()
212
+
213
+ logger.debug(`Received task request for ${agentName}`, {
214
+ inputPreview: typeof input === 'string'
215
+ ? input.substring(0, 100)
216
+ : 'Non-string input'
217
+ });
218
+
219
+ // Process the task with timeout
220
+ const response = await withTimeout(
221
+ async () => processingFunction(message),
222
+ TIMEOUT_TASK_REQUEST * 2, // Double the timeout for processing
223
+ `task processing for ${agentName}`
224
+ );
225
+
226
+ // Respond with the result
227
+ await m.respond(response.serialize());
228
+
229
+ logger.debug(`Completed task request for ${agentName}`);
230
+ } catch (error) {
231
+ logger.error("Error processing task", {
232
+ error,
233
+ agentName,
234
+ inputPreview: payload && input
235
+ ? (typeof input === 'string'
236
+ ? input.substring(0, 100)
237
+ : 'Non-string input')
238
+ : 'No input'
239
+ });
240
+
241
+ // Send error response back
242
+ try {
243
+ await m.respond(JSON.stringify({
244
+ error: true,
245
+ message: error.message,
246
+ type: error.name || 'Error'
247
+ }));
248
+ } catch (respondError) {
249
+ logger.error("Failed to send error response", { error: respondError });
250
+ }
251
+ }
252
+ }
253
+ } catch (error) {
254
+ logger.error("Task subscription error", { error, agentName });
255
+
256
+ // Attempt to resubscribe if the connection is still active
257
+ if (nc.isConnected()) {
258
+ logger.info('Attempting to resubscribe for task handling');
259
+ try {
260
+ const newTaskSub = nc.subscribe(agentName, { queue: agentName });
261
+ // Start a new processing loop
262
+ createTaskHandler(nc, agentName, processingFunction);
263
+ } catch (resubError) {
264
+ logger.error('Failed to resubscribe for task handling', { error: resubError });
265
+ throw new TransportError(
266
+ "Failed to resubscribe for task handling",
267
+ 'NATS',
268
+ { agentName, originalError: error.message, resubError: resubError.message }
269
+ );
270
+ }
271
+ } else {
272
+ throw new TransportError(
273
+ "NATS connection lost during task handling",
274
+ 'NATS',
275
+ { agentName }
276
+ );
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Safely connects to NATS with retry logic
283
+ */
284
+ async function safeConnect(instance, options = {}) {
285
+ const { maxRetries = MAX_RECONNECT_ATTEMPTS } = options;
286
+
287
+ return withRetry(
288
+ async () => {
289
+ try {
290
+ return await instance.connect();
291
+ } catch (error) {
292
+ throw new TransportError(
293
+ `Failed to connect to NATS: ${error.message}`,
294
+ 'NATS',
295
+ { details: error.message }
296
+ );
297
+ }
298
+ },
299
+ {
300
+ maxRetries,
301
+ baseDelayMs: 500,
302
+ onRetry: ({ attempt }) => {
303
+ logger.warn(`NATS connect attempt ${attempt}/${maxRetries} failed, retrying...`);
304
+ }
305
+ }
306
+ );
307
+ }
308
+
309
+ /**
310
+ * Creates a NATS-based runtime for agent communication
311
+ */
312
+ export async function NatsIOAgentRuntime(agentName, ioInterfaces, discoverySchemas) {
313
+ if (ioInterfaces.length > 1) {
314
+ throw new TransportError(
315
+ 'Only one IO Nats interface is supported',
316
+ 'NATS',
317
+ { agentName, interfacesCount: ioInterfaces.length }
318
+ );
319
+ }
320
+
321
+ if (ioInterfaces.length === 0) {
322
+ logger.warn(`No NATS interfaces provided for agent ${agentName}, creating passive runtime`);
323
+ return { handleTask: async () => {}, discoveredAgents: [] };
324
+ }
325
+
326
+ const io = ioInterfaces[0];
327
+ const intervals = [];
328
+ const discoveredAgents = {};
329
+
330
+ try {
331
+ // Connect to NATS with retry logic
332
+ logger.info(`Connecting to NATS for agent ${agentName}`);
333
+ const nc = await safeConnect(io.instance);
334
+
335
+ // Verify configuration
336
+ if (!io.config || !io.config.bindings || !io.config.bindings.discoveryTopic) {
337
+ throw new TransportError(
338
+ 'Missing required NATS configuration: discoveryTopic',
339
+ 'NATS',
340
+ { agentName }
341
+ );
342
+ }
343
+
344
+ const discoveryTopic = io.config.bindings.discoveryTopic;
345
+ logger.info(`Agent ${agentName} initialized with discovery topic ${discoveryTopic}`);
346
+
347
+ // Step 1. Subscribe to discovery topic
348
+ await setupDiscoverySubscription(nc, discoveryTopic, agentName, discoveredAgents);
349
+
350
+ // Step 2. Publish discovery heartbeat
351
+ const interval = setupDiscoveryHeartbeat(nc, discoveryTopic, agentName, discoverySchemas);
352
+ intervals.push(interval);
353
+
354
+ // Step 3. Create task handler
355
+ const handleTask = async (fn) => {
356
+ if (typeof fn !== 'function') {
357
+ throw new Error('Task handler must be a function');
358
+ }
359
+ await createTaskHandler(nc, agentName, fn);
360
+ };
361
+
362
+ return { handleTask, discoveredAgents };
363
+ } catch (error) {
364
+ // Clean up intervals if connection fails
365
+ intervals.forEach(clearInterval);
366
+
367
+ // Enhance the error with context if it's not already a TransportError
368
+ if (!(error instanceof TransportError)) {
369
+ error = new TransportError(
370
+ `Failed to initialize NATS runtime: ${error.message}`,
371
+ 'NATS',
372
+ { agentName }
373
+ );
374
+ }
375
+
376
+ logger.error("NATS runtime initialization failed", { error, agentName });
377
+ throw error;
378
+ }
379
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Base error class for all SmartAgent errors
3
+ */
4
+ export class SmartAgentError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'SmartAgentError';
8
+ Error.captureStackTrace(this, this.constructor);
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Configuration related errors
14
+ */
15
+ export class ConfigurationError extends SmartAgentError {
16
+ constructor(message, configContext = {}) {
17
+ super(message);
18
+ this.name = 'ConfigurationError';
19
+ this.configContext = configContext;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Tool execution related errors
25
+ */
26
+ export class ToolExecutionError extends SmartAgentError {
27
+ constructor(message, toolName, input, cause = null) {
28
+ super(message);
29
+ this.name = 'ToolExecutionError';
30
+ this.toolName = toolName;
31
+ this.input = input;
32
+ this.cause = cause;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Transport/IO related errors
38
+ */
39
+ export class TransportError extends SmartAgentError {
40
+ constructor(message, transportType, details = {}) {
41
+ super(message);
42
+ this.name = 'TransportError';
43
+ this.transportType = transportType;
44
+ this.details = details;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Errors related to LLM API calls
50
+ */
51
+ export class LLMError extends SmartAgentError {
52
+ constructor(message, provider, details = {}) {
53
+ super(message);
54
+ this.name = 'LLMError';
55
+ this.provider = provider;
56
+ this.details = details;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Errors related to agent discovery
62
+ */
63
+ export class DiscoveryError extends SmartAgentError {
64
+ constructor(message, discoveryContext = {}) {
65
+ super(message);
66
+ this.name = 'DiscoveryError';
67
+ this.discoveryContext = discoveryContext;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Errors related to agent handoffs
73
+ */
74
+ export class HandoffError extends SmartAgentError {
75
+ constructor(message, sourceAgent, targetAgent, payload = {}) {
76
+ super(message);
77
+ this.name = 'HandoffError';
78
+ this.sourceAgent = sourceAgent;
79
+ this.targetAgent = targetAgent;
80
+ this.payload = payload;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Errors during agent compilation
86
+ */
87
+ export class CompilationError extends SmartAgentError {
88
+ constructor(message, agentName, cause = null) {
89
+ super(message);
90
+ this.name = 'CompilationError';
91
+ this.agentName = agentName;
92
+ this.cause = cause;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Validation errors
98
+ */
99
+ export class ValidationError extends SmartAgentError {
100
+ constructor(message, errors = []) {
101
+ super(message);
102
+ this.name = 'ValidationError';
103
+ this.errors = errors;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Timeouts in agent operations
109
+ */
110
+ export class TimeoutError extends SmartAgentError {
111
+ constructor(message, operation, timeoutMs) {
112
+ super(message);
113
+ this.name = 'TimeoutError';
114
+ this.operation = operation;
115
+ this.timeoutMs = timeoutMs;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Helper to wrap async functions with timeout
121
+ * @param {Function} fn - Async function to execute
122
+ * @param {number} timeoutMs - Timeout in milliseconds
123
+ * @param {string} operationName - Name of the operation for error context
124
+ */
125
+ export async function withTimeout(fn, timeoutMs, operationName) {
126
+ let timeoutId;
127
+
128
+ const timeoutPromise = new Promise((_, reject) => {
129
+ timeoutId = setTimeout(() => {
130
+ reject(new TimeoutError(
131
+ `Operation '${operationName}' timed out after ${timeoutMs}ms`,
132
+ operationName,
133
+ timeoutMs
134
+ ));
135
+ }, timeoutMs);
136
+ });
137
+
138
+ try {
139
+ return await Promise.race([fn(), timeoutPromise]);
140
+ } finally {
141
+ clearTimeout(timeoutId);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Helper for retrying operations with exponential backoff
147
+ * @param {Function} fn - Async function to retry
148
+ * @param {Object} options - Retry options
149
+ */
150
+ export async function withRetry(fn, options = {}) {
151
+ const {
152
+ maxRetries = 3,
153
+ baseDelayMs = 300,
154
+ maxDelayMs = 5000,
155
+ retryableErrors = [TransportError, TimeoutError],
156
+ onRetry = () => {}
157
+ } = options;
158
+
159
+ let lastError;
160
+
161
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
162
+ try {
163
+ return await fn();
164
+ } catch (error) {
165
+ lastError = error;
166
+
167
+ const isRetryable = retryableErrors.some(ErrorClass =>
168
+ error instanceof ErrorClass
169
+ );
170
+
171
+ if (!isRetryable || attempt >= maxRetries) {
172
+ throw error;
173
+ }
174
+
175
+ // Calculate delay with exponential backoff and jitter
176
+ const delay = Math.min(
177
+ baseDelayMs * 2 ** attempt * (0.75 + Math.random() * 0.5),
178
+ maxDelayMs
179
+ );
180
+
181
+ // Call onRetry callback
182
+ onRetry({
183
+ error,
184
+ attempt: attempt + 1,
185
+ maxRetries,
186
+ delayMs: delay
187
+ });
188
+
189
+ // Wait before retrying
190
+ await new Promise(resolve => setTimeout(resolve, delay));
191
+ }
192
+ }
193
+
194
+ throw lastError;
195
+ }