agentnet 0.0.2 → 0.0.4
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.
- package/README.md +69 -1
- package/examples/simple/simple.js +2 -2
- package/jest.config.js +1 -0
- package/package.json +5 -2
- package/src/agent/agent-loader.js +75 -12
- package/src/agent/agent.js +4 -2
- package/src/agent/runtime.js +7 -4
- package/src/llm/base.js +131 -0
- package/src/llm/gemini.js +137 -122
- package/src/llm/gpt.js +131 -109
- package/src/tests/agent.test.js +350 -0
- package/src/tools/migrate-version.js +250 -0
- package/src/transport/README.md +123 -0
- package/src/transport/base.js +237 -0
- package/src/transport/index.js +89 -0
- package/src/transport/kafka.js +474 -0
- package/src/transport/nats.js +521 -0
- package/src/transport/rabbitmq.js +722 -0
- package/src/transport/redis.js +532 -0
- package/src/utils/version.js +212 -0
- package/src/agent/runtimes/nats.js +0 -506
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { jest } from '@jest/globals'
|
|
2
|
+
|
|
3
|
+
import { Agent } from '../agent/agent';
|
|
4
|
+
import { ConfigurationError, CompilationError } from '../errors';
|
|
5
|
+
|
|
6
|
+
// Mock the AgentRuntime module
|
|
7
|
+
jest.mock('../agent/runtime.js', () => ({
|
|
8
|
+
AgentRuntime: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
// Import the mocked AgentRuntime to allow inspection (e.g. toHaveBeenCalledWith)
|
|
11
|
+
import { AgentRuntime } from '../agent/runtime.js';
|
|
12
|
+
|
|
13
|
+
describe('Agent Core Functionality', () => {
|
|
14
|
+
let agentBuilder;
|
|
15
|
+
let mockLlmApi;
|
|
16
|
+
let mockStoreInstance;
|
|
17
|
+
let mockIoInstance;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
// Reset mocks before each test
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
|
|
23
|
+
agentBuilder = Agent();
|
|
24
|
+
mockLlmApi = {
|
|
25
|
+
getClient: jest.fn().mockResolvedValue({}),
|
|
26
|
+
callModel: jest.fn().mockResolvedValue('llm_response'),
|
|
27
|
+
// Adding mock prompt and onResponse as AgentRuntime might expect them if not deeply mocked
|
|
28
|
+
prompt: jest.fn(),
|
|
29
|
+
onResponse: jest.fn(),
|
|
30
|
+
};
|
|
31
|
+
mockStoreInstance = {
|
|
32
|
+
connect: jest.fn().mockResolvedValue(true),
|
|
33
|
+
// Add other methods if your store interactions become more complex in tests
|
|
34
|
+
};
|
|
35
|
+
mockIoInstance = {
|
|
36
|
+
type: 'TestIO',
|
|
37
|
+
// Mock other IO methods if needed by AgentRuntime or specific IO logic
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Agent Factory & Defaults', () => {
|
|
42
|
+
it('should create a new agent builder with default metadata', () => {
|
|
43
|
+
expect(agentBuilder._config.metadata).toEqual({
|
|
44
|
+
name: 'default',
|
|
45
|
+
namespace: 'default',
|
|
46
|
+
description: 'A default agent',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should create a new agent builder with default runner config', () => {
|
|
51
|
+
expect(agentBuilder._config.runner).toEqual({
|
|
52
|
+
maxRuns: 10,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should create a new agent builder with default hooks', () => {
|
|
57
|
+
expect(typeof agentBuilder._config.on.prompt).toBe('function');
|
|
58
|
+
expect(typeof agentBuilder._config.on.response).toBe('function');
|
|
59
|
+
// Test the default behavior of hooks
|
|
60
|
+
const testInput = "test input";
|
|
61
|
+
expect(agentBuilder._config.on.prompt({}, testInput)).resolves.toBe(testInput);
|
|
62
|
+
const testResult = "test result";
|
|
63
|
+
expect(agentBuilder._config.on.response({}, [], testResult)).resolves.toBe(testResult);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('setMetadata(metadata)', () => {
|
|
68
|
+
it('should allow setting valid metadata', () => {
|
|
69
|
+
const metadata = { name: 'testAgent', namespace: 'testSpace', description: 'A test agent' };
|
|
70
|
+
agentBuilder.setMetadata(metadata);
|
|
71
|
+
expect(agentBuilder._config.metadata).toEqual(metadata);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should merge with existing metadata, overwriting common fields', () => {
|
|
75
|
+
agentBuilder.setMetadata({ name: 'initialName', customField: 'initialValue' });
|
|
76
|
+
agentBuilder.setMetadata({ name: 'newName', description: 'newDescription' });
|
|
77
|
+
expect(agentBuilder._config.metadata).toEqual({
|
|
78
|
+
name: 'newName',
|
|
79
|
+
namespace: 'default', // From initial defaults if not overridden
|
|
80
|
+
description: 'newDescription',
|
|
81
|
+
customField: 'initialValue'
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw ConfigurationError if metadata is null', () => {
|
|
86
|
+
expect(() => agentBuilder.setMetadata(null)).toThrow(ConfigurationError);
|
|
87
|
+
expect(() => agentBuilder.setMetadata(null)).toThrow('Metadata is required');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Validations for name and namespace during compile are more prominent,
|
|
91
|
+
// but direct setters could also enforce this, though current code doesn't.
|
|
92
|
+
// If direct enforcement in setMetadata is desired, add tests here.
|
|
93
|
+
// For now, these are primarily tested via compile's validation.
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('withLLM(llmApi, llmConfig)', () => {
|
|
97
|
+
it('should configure LLM with valid API and config', () => {
|
|
98
|
+
const llmConfig = { model: 'test-model', temperature: 0.7 };
|
|
99
|
+
agentBuilder.withLLM(mockLlmApi, llmConfig);
|
|
100
|
+
expect(agentBuilder._config.llm.api).toBe(mockLlmApi);
|
|
101
|
+
expect(agentBuilder._config.llm.config).toEqual(llmConfig);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should throw ConfigurationError if llmApi is null', () => {
|
|
105
|
+
expect(() => agentBuilder.withLLM(null, {})).toThrow(ConfigurationError);
|
|
106
|
+
expect(() => agentBuilder.withLLM(null, {})).toThrow('LLM API is required');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should throw ConfigurationError if llmApi is not an object (during compile)', async () => {
|
|
110
|
+
agentBuilder.withLLM("notAnObject", {});
|
|
111
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("LLM API must be a valid object"));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should throw ConfigurationError if llmApi is missing getClient (during compile)', async () => {
|
|
115
|
+
const invalidApi = { ...mockLlmApi };
|
|
116
|
+
delete invalidApi.getClient;
|
|
117
|
+
agentBuilder.withLLM(invalidApi, {});
|
|
118
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("LLM API must have a getClient method"));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should throw ConfigurationError if llmApi is missing callModel (during compile)', async () => {
|
|
122
|
+
const invalidApi = { ...mockLlmApi };
|
|
123
|
+
delete invalidApi.callModel;
|
|
124
|
+
agentBuilder.withLLM(invalidApi, {});
|
|
125
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("LLM API must have a callModel method"));
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('withStore(storeInstance, storeConfig)', () => {
|
|
130
|
+
it('should configure store with valid instance and config', () => {
|
|
131
|
+
const storeConfig = { type: 'test-store' };
|
|
132
|
+
agentBuilder.withStore(mockStoreInstance, storeConfig);
|
|
133
|
+
expect(agentBuilder._config.store.instance).toBe(mockStoreInstance);
|
|
134
|
+
expect(agentBuilder._config.store.config).toEqual(storeConfig);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should throw ConfigurationError if storeInstance is null', () => {
|
|
138
|
+
expect(() => agentBuilder.withStore(null, {})).toThrow(ConfigurationError);
|
|
139
|
+
expect(() => agentBuilder.withStore(null, {})).toThrow('Store instance is required');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should throw ConfigurationError if storeInstance is not an object (during compile)', async () => {
|
|
143
|
+
agentBuilder.withLLM(mockLlmApi, {}); // Need LLM for compilation
|
|
144
|
+
agentBuilder.withStore("notAnObject", {});
|
|
145
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Store instance must be a valid object"));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should throw ConfigurationError if storeInstance is missing connect (during compile)', async () => {
|
|
149
|
+
const invalidStore = {}; // Missing connect
|
|
150
|
+
agentBuilder.withLLM(mockLlmApi, {});
|
|
151
|
+
agentBuilder.withStore(invalidStore, {});
|
|
152
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Store instance must have a connect method"));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('addIO(instance, ioConfig)', () => {
|
|
157
|
+
it('should add IO interface with valid instance and config', () => {
|
|
158
|
+
const ioConfig = { setting: 'test-setting' };
|
|
159
|
+
agentBuilder.addIO(mockIoInstance, ioConfig);
|
|
160
|
+
expect(agentBuilder._config.io[0].type).toBe(mockIoInstance.type);
|
|
161
|
+
expect(agentBuilder._config.io[0].instance).toBe(mockIoInstance);
|
|
162
|
+
expect(agentBuilder._config.io[0].config).toEqual(ioConfig);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should throw ConfigurationError if instance is null', () => {
|
|
166
|
+
expect(() => agentBuilder.addIO(null, {})).toThrow(ConfigurationError);
|
|
167
|
+
expect(() => agentBuilder.addIO(null, {})).toThrow('IO instance must have a type');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should throw ConfigurationError if instance.type is missing', () => {
|
|
171
|
+
expect(() => agentBuilder.addIO({}, {})).toThrow(ConfigurationError);
|
|
172
|
+
expect(() => agentBuilder.addIO({}, {})).toThrow('IO instance must have a type');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Further IO validation (missing instance/config on compile) is tested in compile section
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('addToolSchema(schema)', () => {
|
|
179
|
+
it('should add a valid tool schema', () => {
|
|
180
|
+
const toolSchema = { name: 'testTool', description: 'A tool for testing' };
|
|
181
|
+
agentBuilder.addToolSchema(toolSchema);
|
|
182
|
+
expect(agentBuilder._config.toolsSchemas['testTool']).toEqual(toolSchema);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should throw ConfigurationError if schema is null', () => {
|
|
186
|
+
expect(() => agentBuilder.addToolSchema(null)).toThrow(ConfigurationError);
|
|
187
|
+
expect(() => agentBuilder.addToolSchema(null)).toThrow('Tool schema must have a name');
|
|
188
|
+
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should throw ConfigurationError if schema.name is missing', () => {
|
|
192
|
+
expect(() => agentBuilder.addToolSchema({ description: ' nameless tool' })).toThrow(ConfigurationError);
|
|
193
|
+
expect(() => agentBuilder.addToolSchema({ description: ' nameless tool' })).toThrow('Tool schema must have a name');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('addDiscoverySchema(schema)', () => {
|
|
198
|
+
it('should add a valid discovery schema', () => {
|
|
199
|
+
const discoverySchema = { name: 'testDiscovery', description: 'For discovering things' };
|
|
200
|
+
agentBuilder.addDiscoverySchema(discoverySchema);
|
|
201
|
+
expect(agentBuilder._config.discoverySchemas).toContainEqual(discoverySchema);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should throw ConfigurationError if schema is null', () => {
|
|
205
|
+
expect(() => agentBuilder.addDiscoverySchema(null)).toThrow(ConfigurationError);
|
|
206
|
+
expect(() => agentBuilder.addDiscoverySchema(null)).toThrow('Discovery schema is required');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('on(eventName, handler)', () => {
|
|
211
|
+
it('should register custom prompt and response handlers', () => {
|
|
212
|
+
const mockPromptFn = jest.fn();
|
|
213
|
+
const mockResponseFn = jest.fn();
|
|
214
|
+
agentBuilder.on('prompt', mockPromptFn);
|
|
215
|
+
agentBuilder.on('response', mockResponseFn);
|
|
216
|
+
expect(agentBuilder._config.on.prompt).toBe(mockPromptFn);
|
|
217
|
+
expect(agentBuilder._config.on.response).toBe(mockResponseFn);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should throw ConfigurationError if handler is not a function', () => {
|
|
221
|
+
expect(() => agentBuilder.on('prompt', 'not-a-function')).toThrow(ConfigurationError);
|
|
222
|
+
expect(() => agentBuilder.on('prompt', 'not-a-function')).toThrow('Event handler for prompt must be a function');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('getToolsSchemas()', () => {
|
|
227
|
+
it('should return a copy of tool schemas', () => {
|
|
228
|
+
const toolSchema1 = { name: 'tool1', description: 'Tool one' };
|
|
229
|
+
const toolSchema2 = { name: 'tool2', description: 'Tool two' };
|
|
230
|
+
agentBuilder.addToolSchema(toolSchema1);
|
|
231
|
+
agentBuilder.addToolSchema(toolSchema2);
|
|
232
|
+
|
|
233
|
+
const retrievedSchemas = agentBuilder.getToolsSchemas();
|
|
234
|
+
expect(retrievedSchemas).toEqual({ tool1: toolSchema1, tool2: toolSchema2 });
|
|
235
|
+
// Ensure it's a copy
|
|
236
|
+
retrievedSchemas.tool1.description = "modified";
|
|
237
|
+
expect(agentBuilder._config.toolsSchemas.tool1.description).toBe("Tool one");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should return an empty object if no tools are added', () => {
|
|
241
|
+
expect(agentBuilder.getToolsSchemas()).toEqual({});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('compile()', () => {
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
// Minimum valid config for most compile tests
|
|
248
|
+
agentBuilder.withLLM(mockLlmApi, { model: 'test-model' });
|
|
249
|
+
agentBuilder.setMetadata({ name: 'compileAgent', namespace: 'compileSpace' });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should successfully compile with minimal valid configuration', async () => {
|
|
253
|
+
AgentRuntime.mockResolvedValue({ query: jest.fn() });
|
|
254
|
+
const compiledAgent = await agentBuilder.compile();
|
|
255
|
+
expect(AgentRuntime).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(AgentRuntime).toHaveBeenCalledWith(agentBuilder._config); // Check if called with the correct config
|
|
257
|
+
expect(compiledAgent).toBeDefined();
|
|
258
|
+
expect(typeof compiledAgent.query).toBe('function');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should throw ConfigurationError if metadata.name is empty during compile', async () => {
|
|
262
|
+
agentBuilder.setMetadata({ name: '', namespace: 'test' });
|
|
263
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Agent name cannot be empty"));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should throw ConfigurationError if metadata.namespace is empty during compile', async () => {
|
|
267
|
+
agentBuilder.setMetadata({ name: 'test', namespace: ' ' }); // Whitespace
|
|
268
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Agent namespace cannot be empty"));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should throw ConfigurationError if LLM is not configured', async () => {
|
|
272
|
+
const freshAgent = Agent(); // No LLM
|
|
273
|
+
freshAgent.setMetadata({ name: 'noLlmAgent', namespace: 'test' });
|
|
274
|
+
await expect(freshAgent.compile()).rejects.toThrow(ConfigurationError);
|
|
275
|
+
// The schema validation might throw a generic "is required" or a more specific one based on schema order
|
|
276
|
+
// For AGENT_CONFIG_SCHEMA, 'llm' is required.
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should throw ConfigurationError if an added IO interface has no instance (during compile)', async () => {
|
|
280
|
+
agentBuilder._config.io.push({ type: 'BadIO', config: {} /* no instance */});
|
|
281
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("IO interface BadIO at index 0 has no instance"));
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should throw ConfigurationError if an added IO interface has no config (during compile)', async () => {
|
|
285
|
+
agentBuilder._config.io.push({ type: 'BadIO', instance: mockIoInstance /* no config */});
|
|
286
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("IO interface BadIO at index 0 has no configuration"));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should throw ConfigurationError if a tool schema is invalid (e.g., name missing, checked during compile)', async () => {
|
|
290
|
+
// Note: addToolSchema checks this, but validateConfiguration re-checks.
|
|
291
|
+
// This test ensures validateConfiguration's check works.
|
|
292
|
+
agentBuilder._config.toolsSchemas['badTool'] = { description: "I am bad" }; // No name in schema value
|
|
293
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Tool schema must have a name"));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should throw ConfigurationError if runner.maxRuns is not a positive number', async () => {
|
|
297
|
+
agentBuilder._config.runner.maxRuns = 0;
|
|
298
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("runner.maxRuns must be greater than 0"));
|
|
299
|
+
agentBuilder._config.runner.maxRuns = -1;
|
|
300
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("runner.maxRuns must be greater than 0"));
|
|
301
|
+
agentBuilder._config.runner.maxRuns = 'not a number';
|
|
302
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Invalid type for runner.maxRuns in agent_config: expected number, got string"));
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should throw ConfigurationError if an event handler is not a function', async () => {
|
|
306
|
+
agentBuilder._config.on.prompt = "not a function";
|
|
307
|
+
await expect(agentBuilder.compile()).rejects.toThrow(new ConfigurationError("Event handler for 'prompt' must be a function"));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should throw CompilationError if AgentRuntime throws an error', async () => {
|
|
311
|
+
const runtimeError = new Error('Runtime failed!');
|
|
312
|
+
AgentRuntime.mockRejectedValue(runtimeError);
|
|
313
|
+
await expect(agentBuilder.compile()).rejects.toThrow(CompilationError);
|
|
314
|
+
await expect(agentBuilder.compile()).rejects.toThrow(`Failed to compile agent ${agentBuilder._config.metadata.name}: ${runtimeError.message}`);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should pass full configuration to AgentRuntime', async () => {
|
|
318
|
+
const toolSchema = { name: 'myTool', parameters: {} };
|
|
319
|
+
const discoverySchema = { name: 'myDiscovery' };
|
|
320
|
+
const ioConfig = { network: 'testNet' };
|
|
321
|
+
const storeConfig = { db: 'testDb' };
|
|
322
|
+
const promptHook = jest.fn();
|
|
323
|
+
const responseHook = jest.fn();
|
|
324
|
+
|
|
325
|
+
agentBuilder
|
|
326
|
+
.addToolSchema(toolSchema)
|
|
327
|
+
.addDiscoverySchema(discoverySchema)
|
|
328
|
+
.addIO(mockIoInstance, ioConfig)
|
|
329
|
+
.withStore(mockStoreInstance, storeConfig)
|
|
330
|
+
.on('prompt', promptHook)
|
|
331
|
+
.on('response', responseHook);
|
|
332
|
+
|
|
333
|
+
AgentRuntime.mockResolvedValue({ query: jest.fn() });
|
|
334
|
+
await agentBuilder.compile();
|
|
335
|
+
|
|
336
|
+
expect(AgentRuntime).toHaveBeenCalledWith(
|
|
337
|
+
expect.objectContaining({
|
|
338
|
+
metadata: agentBuilder._config.metadata,
|
|
339
|
+
llm: agentBuilder._config.llm,
|
|
340
|
+
store: agentBuilder._config.store,
|
|
341
|
+
io: expect.arrayContaining([expect.objectContaining({ type: mockIoInstance.type, instance: mockIoInstance, config: ioConfig })]),
|
|
342
|
+
toolsSchemas: expect.objectContaining({ 'myTool': toolSchema }),
|
|
343
|
+
discoverySchemas: expect.arrayContaining([discoverySchema]),
|
|
344
|
+
on: expect.objectContaining({ prompt: promptHook, response: responseHook }),
|
|
345
|
+
runner: agentBuilder._config.runner
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { parse, stringify } from 'yaml';
|
|
6
|
+
import { migrateDefinition, validateApiVersion, API_VERSIONS, LATEST_STABLE_VERSION } from '../utils/version.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Print command usage instructions
|
|
11
|
+
*/
|
|
12
|
+
function printUsage() {
|
|
13
|
+
console.log(`
|
|
14
|
+
Agent Definition Version Migration Utility
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
node migrate-version.js <input-file> [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--output <file> Output file (default: adds '-migrated' to input filename)
|
|
21
|
+
--version <ver> Target API version (default: ${LATEST_STABLE_VERSION})
|
|
22
|
+
--check Only check if migration is needed, don't perform it
|
|
23
|
+
--quiet Suppress informational output
|
|
24
|
+
--help Show this help message
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
node migrate-version.js ./agents.yaml
|
|
28
|
+
node migrate-version.js ./agents.yaml --version agentnet.io/v1alpha1
|
|
29
|
+
node migrate-version.js ./agents.yaml --output ./agents-new.yaml
|
|
30
|
+
node migrate-version.js ./agents.yaml --check
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse command line arguments
|
|
36
|
+
* @returns {Object} Parsed arguments
|
|
37
|
+
*/
|
|
38
|
+
function parseArgs() {
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const result = {
|
|
41
|
+
inputFile: null,
|
|
42
|
+
outputFile: null,
|
|
43
|
+
targetVersion: LATEST_STABLE_VERSION,
|
|
44
|
+
checkOnly: false,
|
|
45
|
+
quiet: false,
|
|
46
|
+
help: false
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < args.length; i++) {
|
|
50
|
+
const arg = args[i];
|
|
51
|
+
|
|
52
|
+
if (arg === '--help' || arg === '-h') {
|
|
53
|
+
result.help = true;
|
|
54
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
55
|
+
result.outputFile = args[++i];
|
|
56
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
57
|
+
result.targetVersion = args[++i];
|
|
58
|
+
} else if (arg === '--check' || arg === '-c') {
|
|
59
|
+
result.checkOnly = true;
|
|
60
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
61
|
+
result.quiet = true;
|
|
62
|
+
} else if (!result.inputFile) {
|
|
63
|
+
result.inputFile = arg;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get default output filename based on input filename
|
|
72
|
+
* @param {string} inputFile - Input file path
|
|
73
|
+
* @param {string} targetVersion - Target API version
|
|
74
|
+
* @returns {string} Default output file path
|
|
75
|
+
*/
|
|
76
|
+
function getDefaultOutputFile(inputFile, targetVersion) {
|
|
77
|
+
const parsedPath = path.parse(inputFile);
|
|
78
|
+
const versionSuffix = targetVersion.replace(/\//g, '-');
|
|
79
|
+
return path.join(
|
|
80
|
+
parsedPath.dir,
|
|
81
|
+
`${parsedPath.name}-${versionSuffix}${parsedPath.ext}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Log a message if not in quiet mode
|
|
87
|
+
* @param {string} message - Message to log
|
|
88
|
+
* @param {boolean} isError - Whether this is an error message
|
|
89
|
+
* @param {boolean} quiet - Whether quiet mode is enabled
|
|
90
|
+
*/
|
|
91
|
+
function log(message, isError = false, quiet = false) {
|
|
92
|
+
if (isError || !quiet) {
|
|
93
|
+
console.log(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Process and migrate a YAML file containing agent definitions
|
|
99
|
+
* @param {string} content - YAML content
|
|
100
|
+
* @param {string} targetVersion - Target API version
|
|
101
|
+
* @param {boolean} checkOnly - Only check, don't modify
|
|
102
|
+
* @returns {Object} Migration results including modified content
|
|
103
|
+
*/
|
|
104
|
+
function processYamlFile(content, targetVersion, checkOnly) {
|
|
105
|
+
// Split the YAML content by document separator
|
|
106
|
+
const documents = content.split(/^---$/m)
|
|
107
|
+
.map(s => s.trim())
|
|
108
|
+
.filter(s => s);
|
|
109
|
+
|
|
110
|
+
const result = {
|
|
111
|
+
migrated: false,
|
|
112
|
+
migratedCount: 0,
|
|
113
|
+
alreadyUpToDateCount: 0,
|
|
114
|
+
failedCount: 0,
|
|
115
|
+
needsMigration: false,
|
|
116
|
+
updatedContent: null,
|
|
117
|
+
failures: []
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Process each document
|
|
121
|
+
const processedDocs = [];
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < documents.length; i++) {
|
|
124
|
+
try {
|
|
125
|
+
const docContent = documents[i];
|
|
126
|
+
const doc = parse(docContent);
|
|
127
|
+
|
|
128
|
+
// Skip non-agent definitions
|
|
129
|
+
if (!doc || doc.kind !== 'AgentDefinition') {
|
|
130
|
+
processedDocs.push(docContent);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if migration is needed
|
|
135
|
+
const currentVersion = doc.apiVersion || 'smartagent.io/v1alpha1';
|
|
136
|
+
const agentName = doc.metadata?.name || `[Document ${i+1}]`;
|
|
137
|
+
|
|
138
|
+
if (currentVersion === targetVersion) {
|
|
139
|
+
result.alreadyUpToDateCount++;
|
|
140
|
+
log(`Agent "${agentName}" is already at version ${targetVersion}`);
|
|
141
|
+
processedDocs.push(docContent);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
result.needsMigration = true;
|
|
146
|
+
|
|
147
|
+
// If only checking, skip migration
|
|
148
|
+
if (checkOnly) {
|
|
149
|
+
processedDocs.push(docContent);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Perform migration
|
|
154
|
+
const migratedDoc = migrateDefinition(doc, targetVersion);
|
|
155
|
+
result.migratedCount++;
|
|
156
|
+
result.migrated = true;
|
|
157
|
+
|
|
158
|
+
// Convert back to YAML and add to processed docs
|
|
159
|
+
processedDocs.push(stringify(migratedDoc));
|
|
160
|
+
|
|
161
|
+
log(`Successfully migrated agent "${agentName}" from ${currentVersion} to ${targetVersion}`);
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
result.failedCount++;
|
|
165
|
+
result.failures.push({
|
|
166
|
+
documentIndex: i,
|
|
167
|
+
error: error.message
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Keep original document on failure
|
|
171
|
+
processedDocs.push(documents[i]);
|
|
172
|
+
|
|
173
|
+
log(`Error processing document ${i+1}: ${error.message}`, true);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Combine the processed documents back into a single YAML file
|
|
178
|
+
result.updatedContent = processedDocs.join('\n---\n');
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Main function
|
|
185
|
+
*/
|
|
186
|
+
async function main() {
|
|
187
|
+
const args = parseArgs();
|
|
188
|
+
|
|
189
|
+
// Show help if requested or no input file
|
|
190
|
+
if (args.help || !args.inputFile) {
|
|
191
|
+
printUsage();
|
|
192
|
+
process.exit(args.help ? 0 : 1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Validate target version
|
|
196
|
+
if (!API_VERSIONS[args.targetVersion]) {
|
|
197
|
+
log(`Error: Unsupported target version: ${args.targetVersion}`, true);
|
|
198
|
+
log(`Supported versions: ${Object.keys(API_VERSIONS).join(', ')}`, true);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Set default output file if not specified
|
|
204
|
+
if (!args.outputFile && !args.checkOnly) {
|
|
205
|
+
args.outputFile = getDefaultOutputFile(args.inputFile, args.targetVersion);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Read input file
|
|
209
|
+
const content = fs.readFileSync(args.inputFile, 'utf8');
|
|
210
|
+
|
|
211
|
+
// Process the file
|
|
212
|
+
const result = processYamlFile(content, args.targetVersion, args.checkOnly);
|
|
213
|
+
|
|
214
|
+
// Output results
|
|
215
|
+
if (result.needsMigration) {
|
|
216
|
+
if (args.checkOnly) {
|
|
217
|
+
log(`Migration needed: ${result.alreadyUpToDateCount} up-to-date, ${result.failedCount + documents.length - result.alreadyUpToDateCount} need migration`);
|
|
218
|
+
process.exit(10); // Special exit code indicating migration needed
|
|
219
|
+
} else if (result.migrated) {
|
|
220
|
+
// Write the output file
|
|
221
|
+
fs.writeFileSync(args.outputFile, result.updatedContent);
|
|
222
|
+
|
|
223
|
+
log(`
|
|
224
|
+
Migration completed:
|
|
225
|
+
- ${result.migratedCount} agent definitions migrated
|
|
226
|
+
- ${result.alreadyUpToDateCount} already up-to-date
|
|
227
|
+
- ${result.failedCount} failures
|
|
228
|
+
- Output written to: ${args.outputFile}
|
|
229
|
+
`);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
log(`No migration needed. All agent definitions are already at version ${args.targetVersion}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Exit with error if any migrations failed
|
|
236
|
+
if (result.failedCount > 0) {
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
} catch (error) {
|
|
241
|
+
log(`Error: ${error.message}`, true);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Run the main function
|
|
247
|
+
main().catch(error => {
|
|
248
|
+
console.error('Unhandled error:', error);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
});
|