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,212 @@
1
+ import { logger } from './logger.js';
2
+ import { ConfigurationError } from '../errors/index.js';
3
+
4
+ /**
5
+ * Map of API versions with their compatibility information
6
+ */
7
+ export const API_VERSIONS = {
8
+ // Smartagent API versions
9
+ 'smartagent.io/v1alpha1': {
10
+ isSupported: true,
11
+ isStable: false,
12
+ minEngineVersion: '0.5.0',
13
+ features: ['basic', 'discovery', 'handoffs']
14
+ },
15
+ // Agentnet API versions
16
+ 'agentnet.io/v1alpha1': {
17
+ isSupported: true,
18
+ isStable: false,
19
+ minEngineVersion: '0.8.0',
20
+ features: ['basic', 'discovery', 'handoffs', 'advanced-routing']
21
+ }
22
+ };
23
+
24
+ /**
25
+ * Default API version to use when none is specified
26
+ */
27
+ export const DEFAULT_API_VERSION = 'smartagent.io/v1alpha1';
28
+
29
+ /**
30
+ * Latest stable API version
31
+ */
32
+ export const LATEST_STABLE_VERSION = 'agentnet.io/v1alpha1';
33
+
34
+ /**
35
+ * Version upgrade paths map
36
+ * Defines valid upgrade paths between versions
37
+ */
38
+ export const VERSION_UPGRADE_PATHS = {
39
+ 'smartagent.io/v1alpha1': ['agentnet.io/v1alpha1']
40
+ };
41
+
42
+ /**
43
+ * Check if an API version is supported
44
+ * @param {string} version - The API version to check
45
+ * @returns {boolean} True if the version is supported
46
+ */
47
+ export function isVersionSupported(version) {
48
+ return API_VERSIONS[version]?.isSupported === true;
49
+ }
50
+
51
+ /**
52
+ * Check if an API version is stable (not subject to breaking changes)
53
+ * @param {string} version - The API version to check
54
+ * @returns {boolean} True if the version is stable
55
+ */
56
+ export function isVersionStable(version) {
57
+ return API_VERSIONS[version]?.isStable === true;
58
+ }
59
+
60
+ /**
61
+ * Checks if a feature is supported in given API version
62
+ * @param {string} version - The API version to check
63
+ * @param {string} feature - The feature to check
64
+ * @returns {boolean} True if the feature is supported in this version
65
+ */
66
+ export function isFeatureSupported(version, feature) {
67
+ return API_VERSIONS[version]?.features?.includes(feature) === true;
68
+ }
69
+
70
+ /**
71
+ * Validates an agent definition's API version
72
+ * @param {object} definition - Agent definition with apiVersion
73
+ * @param {string} currentEngineVersion - Current version of the agent engine
74
+ * @returns {object} Validated version information
75
+ * @throws {ConfigurationError} If version is unsupported
76
+ */
77
+ export function validateApiVersion(definition, currentEngineVersion = '1.0.0') {
78
+ const apiVersion = definition.apiVersion || DEFAULT_API_VERSION;
79
+ const agentName = definition.metadata?.name || 'Unnamed Agent';
80
+
81
+ // Check if version exists
82
+ const versionInfo = API_VERSIONS[apiVersion];
83
+ if (!versionInfo) {
84
+ const supportedVersions = Object.keys(API_VERSIONS).join(', ');
85
+ throw new ConfigurationError(
86
+ `Unsupported apiVersion '${apiVersion}' for agent '${agentName}'. Supported versions: ${supportedVersions}`,
87
+ { apiVersion, agentName }
88
+ );
89
+ }
90
+
91
+ // Check if version is supported
92
+ if (!versionInfo.isSupported) {
93
+ throw new ConfigurationError(
94
+ `API version '${apiVersion}' is deprecated for agent '${agentName}'. Please update to a supported version.`,
95
+ { apiVersion, agentName }
96
+ );
97
+ }
98
+
99
+ // Check engine compatibility (simple version check for now)
100
+ if (versionInfo.minEngineVersion && versionInfo.minEngineVersion > currentEngineVersion) {
101
+ logger.warn(
102
+ `Agent '${agentName}' uses apiVersion '${apiVersion}' which requires engine version ${versionInfo.minEngineVersion} or higher. Current: ${currentEngineVersion}`,
103
+ { apiVersion, agentName, minEngineVersion: versionInfo.minEngineVersion, currentEngineVersion }
104
+ );
105
+ }
106
+
107
+ // Log stability warning for non-stable versions
108
+ if (!versionInfo.isStable) {
109
+ logger.warn(
110
+ `Agent '${agentName}' uses apiVersion '${apiVersion}' which is not marked as stable and may have breaking changes in future releases.`,
111
+ { apiVersion, agentName }
112
+ );
113
+ }
114
+
115
+ return {
116
+ version: apiVersion,
117
+ info: versionInfo
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Get normalized version of API version
123
+ * This extracts just the version part, removing the domain prefix
124
+ * @param {string} apiVersion - Full API version string (e.g., 'agentnet.io/v1alpha1')
125
+ * @returns {string} Normalized version (e.g., 'v1alpha1')
126
+ */
127
+ export function getNormalizedVersion(apiVersion) {
128
+ const parts = apiVersion.split('/');
129
+ return parts.length > 1 ? parts[1] : apiVersion;
130
+ }
131
+
132
+ /**
133
+ * Checks if an upgrade path exists between versions
134
+ * @param {string} fromVersion - Source version
135
+ * @param {string} toVersion - Target version
136
+ * @returns {boolean} Whether a direct upgrade path exists
137
+ */
138
+ export function canUpgradeVersion(fromVersion, toVersion) {
139
+ if (fromVersion === toVersion) return true;
140
+ return VERSION_UPGRADE_PATHS[fromVersion]?.includes(toVersion) === true;
141
+ }
142
+
143
+ /**
144
+ * Get possible upgrade paths for a version
145
+ * @param {string} fromVersion - Current version
146
+ * @returns {string[]} Available upgrade targets
147
+ */
148
+ export function getUpgradeOptions(fromVersion) {
149
+ return VERSION_UPGRADE_PATHS[fromVersion] || [];
150
+ }
151
+
152
+ /**
153
+ * Migration helpers for each version transformation
154
+ */
155
+ const VERSION_MIGRATIONS = {
156
+ // Migration from smartagent.io/v1alpha1 to agentnet.io/v1alpha1
157
+ 'smartagent.io/v1alpha1->agentnet.io/v1alpha1': (definition) => {
158
+ // Deep clone the definition to avoid modifying the original
159
+ const newDef = JSON.parse(JSON.stringify(definition));
160
+
161
+ // Update the apiVersion
162
+ newDef.apiVersion = 'agentnet.io/v1alpha1';
163
+
164
+ // Handle specific field migrations
165
+ // Example: rename or restructure fields as needed
166
+
167
+ // Log the migration
168
+ logger.info(`Migrated agent definition from smartagent.io/v1alpha1 to agentnet.io/v1alpha1`, {
169
+ agentName: newDef.metadata?.name
170
+ });
171
+
172
+ return newDef;
173
+ }
174
+ };
175
+
176
+ /**
177
+ * Migrate an agent definition from one version to another
178
+ * @param {object} definition - Agent definition to migrate
179
+ * @param {string} targetVersion - Target API version
180
+ * @returns {object} Migrated definition
181
+ * @throws {ConfigurationError} If migration path doesn't exist
182
+ */
183
+ export function migrateDefinition(definition, targetVersion) {
184
+ const sourceVersion = definition.apiVersion || DEFAULT_API_VERSION;
185
+
186
+ // If already at target version, return as is
187
+ if (sourceVersion === targetVersion) {
188
+ return definition;
189
+ }
190
+
191
+ // Check if direct upgrade path exists
192
+ if (!canUpgradeVersion(sourceVersion, targetVersion)) {
193
+ throw new ConfigurationError(
194
+ `No direct migration path from ${sourceVersion} to ${targetVersion}`,
195
+ { sourceVersion, targetVersion }
196
+ );
197
+ }
198
+
199
+ // Get the migration function
200
+ const migrationKey = `${sourceVersion}->${targetVersion}`;
201
+ const migrationFn = VERSION_MIGRATIONS[migrationKey];
202
+
203
+ if (!migrationFn) {
204
+ throw new ConfigurationError(
205
+ `Migration path exists but no implementation found for ${migrationKey}`,
206
+ { sourceVersion, targetVersion }
207
+ );
208
+ }
209
+
210
+ // Apply the migration
211
+ return migrationFn(definition);
212
+ }
@@ -1,379 +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
- * 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
- }