@kadi.build/core 0.0.1-alpha.2 → 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.
Files changed (39) hide show
  1. package/README.md +1145 -216
  2. package/examples/example-abilities/echo-js/README.md +131 -0
  3. package/examples/example-abilities/echo-js/agent.json +63 -0
  4. package/examples/example-abilities/echo-js/package.json +24 -0
  5. package/examples/example-abilities/echo-js/service.js +43 -0
  6. package/examples/example-abilities/hash-go/agent.json +53 -0
  7. package/examples/example-abilities/hash-go/cmd/hash_ability/main.go +340 -0
  8. package/examples/example-abilities/hash-go/go.mod +3 -0
  9. package/examples/example-agent/abilities/echo-js/0.0.1/README.md +131 -0
  10. package/examples/example-agent/abilities/echo-js/0.0.1/agent.json +63 -0
  11. package/examples/example-agent/abilities/echo-js/0.0.1/package-lock.json +93 -0
  12. package/examples/example-agent/abilities/echo-js/0.0.1/package.json +24 -0
  13. package/examples/example-agent/abilities/echo-js/0.0.1/service.js +41 -0
  14. package/examples/example-agent/abilities/hash-go/0.0.1/agent.json +53 -0
  15. package/examples/example-agent/abilities/hash-go/0.0.1/bin/hash_ability +0 -0
  16. package/examples/example-agent/abilities/hash-go/0.0.1/cmd/hash_ability/main.go +340 -0
  17. package/examples/example-agent/abilities/hash-go/0.0.1/go.mod +3 -0
  18. package/examples/example-agent/agent.json +39 -0
  19. package/examples/example-agent/index.js +102 -0
  20. package/examples/example-agent/package-lock.json +93 -0
  21. package/examples/example-agent/package.json +17 -0
  22. package/package.json +4 -2
  23. package/src/KadiAbility.js +478 -0
  24. package/src/index.js +65 -0
  25. package/src/loadAbility.js +1086 -0
  26. package/src/servers/BaseRpcServer.js +404 -0
  27. package/src/servers/BrokerRpcServer.js +776 -0
  28. package/src/servers/StdioRpcServer.js +360 -0
  29. package/src/transport/BrokerMessageBuilder.js +377 -0
  30. package/src/transport/IpcMessageBuilder.js +1229 -0
  31. package/src/utils/agentUtils.js +137 -0
  32. package/src/utils/commandUtils.js +64 -0
  33. package/src/utils/configUtils.js +72 -0
  34. package/src/utils/logger.js +161 -0
  35. package/src/utils/pathUtils.js +86 -0
  36. package/broker.js +0 -214
  37. package/index.js +0 -382
  38. package/ipc.js +0 -220
  39. 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;