@kadi.build/core 0.0.1-alpha.1 → 0.0.1-alpha.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.
- package/README.md +1145 -216
- package/examples/example-abilities/echo-js/README.md +131 -0
- package/examples/example-abilities/echo-js/agent.json +63 -0
- package/examples/example-abilities/echo-js/package.json +24 -0
- package/examples/example-abilities/echo-js/service.js +43 -0
- package/examples/example-abilities/hash-go/agent.json +53 -0
- package/examples/example-abilities/hash-go/cmd/hash_ability/main.go +340 -0
- package/examples/example-abilities/hash-go/go.mod +3 -0
- package/examples/example-agent/abilities/echo-js/0.0.1/README.md +131 -0
- package/examples/example-agent/abilities/echo-js/0.0.1/agent.json +63 -0
- package/examples/example-agent/abilities/echo-js/0.0.1/package-lock.json +93 -0
- package/examples/example-agent/abilities/echo-js/0.0.1/package.json +24 -0
- package/examples/example-agent/abilities/echo-js/0.0.1/service.js +41 -0
- package/examples/example-agent/abilities/hash-go/0.0.1/agent.json +53 -0
- package/examples/example-agent/abilities/hash-go/0.0.1/bin/hash_ability +0 -0
- package/examples/example-agent/abilities/hash-go/0.0.1/cmd/hash_ability/main.go +340 -0
- package/examples/example-agent/abilities/hash-go/0.0.1/go.mod +3 -0
- package/examples/example-agent/agent.json +39 -0
- package/examples/example-agent/index.js +102 -0
- package/examples/example-agent/package-lock.json +93 -0
- package/examples/example-agent/package.json +17 -0
- package/package.json +4 -2
- package/src/KadiAbility.js +478 -0
- package/src/index.js +65 -0
- package/src/loadAbility.js +1086 -0
- package/src/servers/BaseRpcServer.js +404 -0
- package/src/servers/BrokerRpcServer.js +776 -0
- package/src/servers/StdioRpcServer.js +360 -0
- package/src/transport/BrokerMessageBuilder.js +377 -0
- package/src/transport/IpcMessageBuilder.js +1229 -0
- package/src/utils/agentUtils.js +137 -0
- package/src/utils/commandUtils.js +64 -0
- package/src/utils/configUtils.js +72 -0
- package/src/utils/logger.js +161 -0
- package/src/utils/pathUtils.js +86 -0
- package/broker.js +0 -214
- package/index.js +0 -370
- package/ipc.js +0 -220
- package/ipcInterfaces/pythonAbilityIPC.py +0 -177
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { createComponentLogger } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base class for all RPC servers
|
|
6
|
+
*
|
|
7
|
+
* Provides common functionality and defines the interface that all
|
|
8
|
+
* concrete RPC servers must implement.
|
|
9
|
+
*/
|
|
10
|
+
export class BaseRpcServer extends EventEmitter {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new RPC server instance
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} options - Configuration options
|
|
15
|
+
* @param {string} options.protocol - The protocol this server handles
|
|
16
|
+
* @param {number} options.timeoutMs - Request timeout in milliseconds
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
this.protocol = options.protocol || 'unknown';
|
|
22
|
+
this.timeoutMs = options.timeoutMs || 15000;
|
|
23
|
+
this.isServing = false;
|
|
24
|
+
this.ability = null;
|
|
25
|
+
|
|
26
|
+
// Request tracking for timeouts and correlation
|
|
27
|
+
this.pendingRequests = new Map();
|
|
28
|
+
this.requestCounter = 0;
|
|
29
|
+
|
|
30
|
+
this.logger = createComponentLogger('BaseRpcServer');
|
|
31
|
+
this.logger.lifecycle(
|
|
32
|
+
'constructor',
|
|
33
|
+
`BaseRpcServer created with protocol: ${this.protocol}`
|
|
34
|
+
);
|
|
35
|
+
this.logger.trace('constructor', `Timeout: ${this.timeoutMs}ms`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Start serving the given ability
|
|
40
|
+
*
|
|
41
|
+
* This is the main entry point that concrete servers must implement.
|
|
42
|
+
* It should set up the transport, handle incoming requests, and keep
|
|
43
|
+
* the server running until explicitly stopped.
|
|
44
|
+
*
|
|
45
|
+
* @param {KadiAbility} ability - The ability instance to serve
|
|
46
|
+
* @returns {Promise<void>} - Resolves when server stops
|
|
47
|
+
*/
|
|
48
|
+
async serve(ability) {
|
|
49
|
+
this.logger.lifecycle('serve', 'Starting base server serve method');
|
|
50
|
+
this.logger.info('serve', `Ability: ${ability?.name || 'unnamed'}`);
|
|
51
|
+
throw new Error('BaseRpcServer.serve() must be implemented by subclasses');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Publish an event to connected clients/agents
|
|
56
|
+
*
|
|
57
|
+
* This method publishes fire-and-forget event notifications that flow
|
|
58
|
+
* from the ability to connected agents. The concrete implementation
|
|
59
|
+
* depends on the transport protocol:
|
|
60
|
+
*
|
|
61
|
+
* - Stdio: Sends JSON-RPC notification with __kadi_event method
|
|
62
|
+
* - Broker: Sends kadi.event message via WebSocket
|
|
63
|
+
* - Others: Implementation-specific
|
|
64
|
+
*
|
|
65
|
+
* Events are best-effort delivery with no acknowledgment expected.
|
|
66
|
+
* If the transport is disconnected, events may be dropped silently.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} eventName - Name of the event to publish
|
|
69
|
+
* @param {any} data - Event data payload (must be JSON-serializable)
|
|
70
|
+
*
|
|
71
|
+
* @abstract
|
|
72
|
+
* @throws {Error} If not implemented by subclass
|
|
73
|
+
*/
|
|
74
|
+
async publishEvent(eventName, data) {
|
|
75
|
+
this.logger.warn(
|
|
76
|
+
'publishEvent',
|
|
77
|
+
`BaseRpcServer.publishEvent() not implemented for protocol: ${this.protocol}`
|
|
78
|
+
);
|
|
79
|
+
throw new Error(
|
|
80
|
+
`publishEvent() must be implemented by ${this.protocol} RPC server`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Handle an incoming request
|
|
86
|
+
*
|
|
87
|
+
* This method contains the core request processing logic that is
|
|
88
|
+
* shared across all protocols. Concrete servers call this method
|
|
89
|
+
* after parsing their protocol-specific request format.
|
|
90
|
+
*
|
|
91
|
+
* @param {Object} request - Parsed request object
|
|
92
|
+
* @param {string|number} request.id - Request ID (may be null for notifications)
|
|
93
|
+
* @param {string} request.method - Method name to call
|
|
94
|
+
* @param {Object} request.params - Method parameters
|
|
95
|
+
* @returns {Object|null} - Response object, or null for notifications
|
|
96
|
+
*/
|
|
97
|
+
async handleRequest(request) {
|
|
98
|
+
const { id, method, params = {} } = request;
|
|
99
|
+
|
|
100
|
+
this.logger.request(id || 'notification', method, `Handling ${method}`);
|
|
101
|
+
this.logger.trace('request', `Params: ${JSON.stringify(params)}`);
|
|
102
|
+
|
|
103
|
+
this.emit('request', { id, method, params });
|
|
104
|
+
|
|
105
|
+
// Handle notifications (no response expected)
|
|
106
|
+
if (id === null || id === undefined) {
|
|
107
|
+
this.logger.trace('notification', `Processing notification: ${method}`);
|
|
108
|
+
try {
|
|
109
|
+
await this._executeMethod(method, params);
|
|
110
|
+
|
|
111
|
+
this.logger.trace('notification', `Notification ${method} completed`);
|
|
112
|
+
|
|
113
|
+
this.emit('response', { id, result: null });
|
|
114
|
+
return null; // No response for notifications
|
|
115
|
+
} catch (error) {
|
|
116
|
+
this.emit('response', { id, error: error.message });
|
|
117
|
+
// Still return null - notifications don't send error responses
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle regular requests (response expected)
|
|
123
|
+
try {
|
|
124
|
+
const result = await this._executeMethod(method, params);
|
|
125
|
+
this.logger.response(id, 'success', `Method ${method} completed`);
|
|
126
|
+
|
|
127
|
+
const response = this.createSuccessResponse(id, result);
|
|
128
|
+
this.emit('response', { id, result });
|
|
129
|
+
return response;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.logger.response(
|
|
132
|
+
id,
|
|
133
|
+
'error',
|
|
134
|
+
`Method ${method} failed: ${error.message}`
|
|
135
|
+
);
|
|
136
|
+
const response = this.createErrorResponse(id, -32603, error.message, {
|
|
137
|
+
stack: error.stack,
|
|
138
|
+
method
|
|
139
|
+
});
|
|
140
|
+
this.emit('response', { id, error: response.error });
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Execute a method on the ability
|
|
147
|
+
*
|
|
148
|
+
* @param {string} method - Method name
|
|
149
|
+
* @param {Object} params - Method parameters
|
|
150
|
+
* @returns {Promise<any>} - Method result
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
async _executeMethod(method, params) {
|
|
154
|
+
this.logger.methodCall('_executeMethod', `Executing method: ${method}`);
|
|
155
|
+
|
|
156
|
+
if (!this.ability) {
|
|
157
|
+
this.logger.error('execute', 'No ability instance available');
|
|
158
|
+
throw new Error('No ability instance available');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for built-in methods first
|
|
162
|
+
if (method === '__kadi_init') {
|
|
163
|
+
this.logger.trace('builtin', 'Executing __kadi_init');
|
|
164
|
+
return this._handleInit(params);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (method === '__kadi_discover') {
|
|
168
|
+
this.logger.trace('builtin', 'Executing __kadi_discover');
|
|
169
|
+
return this._handleDiscover(params);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Look up the method handler
|
|
173
|
+
const handler = this.ability.getMethodHandler(method);
|
|
174
|
+
if (!handler) {
|
|
175
|
+
this.logger.error('execute', `Method not found: ${method}`);
|
|
176
|
+
throw new Error(`Method not found: ${method}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.logger.trace(
|
|
180
|
+
'execute',
|
|
181
|
+
`Found handler for ${method}, executing with timeout ${this.timeoutMs}ms`
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Execute the handler with timeout
|
|
185
|
+
return await this._executeWithTimeout(handler, params);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Execute a handler with timeout protection
|
|
190
|
+
*
|
|
191
|
+
* @param {Function} handler - Method handler function
|
|
192
|
+
* @param {Object} params - Parameters to pass to handler
|
|
193
|
+
* @returns {Promise<any>} - Handler result
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
async _executeWithTimeout(handler, params) {
|
|
197
|
+
this.logger.trace(
|
|
198
|
+
'timeout',
|
|
199
|
+
`Starting execution with ${this.timeoutMs}ms timeout`
|
|
200
|
+
);
|
|
201
|
+
return new Promise(async (resolve, reject) => {
|
|
202
|
+
// Set up timeout
|
|
203
|
+
const timeoutId = setTimeout(() => {
|
|
204
|
+
this.logger.error(
|
|
205
|
+
'timeout',
|
|
206
|
+
`Method execution timed out after ${this.timeoutMs}ms`
|
|
207
|
+
);
|
|
208
|
+
reject(
|
|
209
|
+
new Error(`Method execution timed out after ${this.timeoutMs}ms`)
|
|
210
|
+
);
|
|
211
|
+
}, this.timeoutMs);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const result = await handler(params);
|
|
215
|
+
clearTimeout(timeoutId);
|
|
216
|
+
this.logger.trace('timeout', 'Method completed within timeout');
|
|
217
|
+
resolve(result);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
clearTimeout(timeoutId);
|
|
220
|
+
|
|
221
|
+
this.logger.error('timeout', `Method failed: ${error.message}`);
|
|
222
|
+
reject(error);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Handle built-in __kadi_init method
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} params - Init parameters
|
|
231
|
+
* @returns {Object} - Init response
|
|
232
|
+
* @private
|
|
233
|
+
*/
|
|
234
|
+
_handleInit(params) {
|
|
235
|
+
this.logger.methodCall('_handleInit', 'Processing init request');
|
|
236
|
+
|
|
237
|
+
const response = {
|
|
238
|
+
name: this.ability.name,
|
|
239
|
+
version: this.ability.version,
|
|
240
|
+
description: this.ability.description,
|
|
241
|
+
protocol: this.protocol,
|
|
242
|
+
functions: this._getFunctionDescriptions()
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
this.logger.success(
|
|
246
|
+
'_handleInit',
|
|
247
|
+
`Init complete for ${this.ability.name}@${this.ability.version}`
|
|
248
|
+
);
|
|
249
|
+
return response;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handle built-in __kadi_discover method
|
|
254
|
+
*
|
|
255
|
+
* @param {Object} params - Discover parameters
|
|
256
|
+
* @returns {Object} - Discover response
|
|
257
|
+
* @private
|
|
258
|
+
*/
|
|
259
|
+
_handleDiscover(params) {
|
|
260
|
+
this.logger.methodCall('_handleDiscover', 'Processing discover request');
|
|
261
|
+
|
|
262
|
+
const functions = this._getFunctionDescriptions();
|
|
263
|
+
|
|
264
|
+
this.logger.success(
|
|
265
|
+
'_handleDiscover',
|
|
266
|
+
`Discovered ${Object.keys(functions).length} functions`
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return { functions };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get function descriptions for discovery
|
|
274
|
+
*
|
|
275
|
+
* @returns {Object} - Map of function names to descriptions
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
_getFunctionDescriptions() {
|
|
279
|
+
this.logger.trace('discovery', 'Building function descriptions');
|
|
280
|
+
|
|
281
|
+
const functions = {};
|
|
282
|
+
|
|
283
|
+
for (const methodName of this.ability.getMethodNames()) {
|
|
284
|
+
const schema = this.ability.getMethodSchema(methodName);
|
|
285
|
+
|
|
286
|
+
functions[methodName] = {
|
|
287
|
+
description: schema?.description || `Handler for ${methodName}`,
|
|
288
|
+
inputSchema: schema?.inputSchema || { type: 'object' },
|
|
289
|
+
outputSchema: schema?.outputSchema || { type: 'object' }
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.logger.trace(
|
|
294
|
+
'discovery',
|
|
295
|
+
`Built descriptions for ${Object.keys(functions).length} functions`
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return functions;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Create a JSON-RPC success response
|
|
303
|
+
*
|
|
304
|
+
* @param {string|number} id - Request ID
|
|
305
|
+
* @param {any} result - Result value
|
|
306
|
+
* @returns {Object} - JSON-RPC response
|
|
307
|
+
*/
|
|
308
|
+
createSuccessResponse(id, result) {
|
|
309
|
+
return {
|
|
310
|
+
jsonrpc: '2.0',
|
|
311
|
+
id,
|
|
312
|
+
result
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create a JSON-RPC error response
|
|
318
|
+
*
|
|
319
|
+
* @param {string|number} id - Request ID
|
|
320
|
+
* @param {number} code - Error code
|
|
321
|
+
* @param {string} message - Error message
|
|
322
|
+
* @param {any} data - Additional error data
|
|
323
|
+
* @returns {Object} - JSON-RPC error response
|
|
324
|
+
*/
|
|
325
|
+
createErrorResponse(id, code, message, data = null) {
|
|
326
|
+
const error = { code, message };
|
|
327
|
+
if (data !== null) {
|
|
328
|
+
error.data = data;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
jsonrpc: '2.0',
|
|
333
|
+
id,
|
|
334
|
+
error
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Common error responses
|
|
340
|
+
*/
|
|
341
|
+
createMethodNotFoundResponse(id, method) {
|
|
342
|
+
this.logger.warn('response', `Method not found: ${method}`);
|
|
343
|
+
return this.createErrorResponse(id, -32601, `Method not found: ${method}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
createParseErrorResponse(id) {
|
|
347
|
+
this.logger.error('response', 'Parse error in request');
|
|
348
|
+
return this.createErrorResponse(id, -32700, 'Parse error');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
createInvalidRequestResponse(id) {
|
|
352
|
+
this.logger.error('response', 'Invalid request format');
|
|
353
|
+
return this.createErrorResponse(id, -32600, 'Invalid Request');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
createInternalErrorResponse(id, message, data) {
|
|
357
|
+
this.logger.error('response', `Internal error: ${message || 'Unknown'}`);
|
|
358
|
+
return this.createErrorResponse(
|
|
359
|
+
id,
|
|
360
|
+
-32603,
|
|
361
|
+
message || 'Internal error',
|
|
362
|
+
data
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Gracefully shutdown the server
|
|
368
|
+
*
|
|
369
|
+
* @param {string} reason - Reason for shutdown
|
|
370
|
+
*/
|
|
371
|
+
async shutdown(reason = 'unknown') {
|
|
372
|
+
this.isServing = false;
|
|
373
|
+
this.emit('stop', { reason });
|
|
374
|
+
|
|
375
|
+
// Cancel any pending requests
|
|
376
|
+
for (const [requestId, { reject }] of this.pendingRequests) {
|
|
377
|
+
reject(new Error(`Server shutdown: ${reason}`));
|
|
378
|
+
}
|
|
379
|
+
this.pendingRequests.clear();
|
|
380
|
+
|
|
381
|
+
// Give a moment for cleanup
|
|
382
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Log helper that respects stdio mode
|
|
387
|
+
*
|
|
388
|
+
* @param {...any} args - Arguments to log
|
|
389
|
+
*/
|
|
390
|
+
log(...args) {
|
|
391
|
+
this.logger.info('general', args.join(' '));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Error log helper
|
|
396
|
+
*
|
|
397
|
+
* @param {...any} args - Arguments to log
|
|
398
|
+
*/
|
|
399
|
+
logError(...args) {
|
|
400
|
+
this.logger.error('general', args.join(' '));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default BaseRpcServer;
|