@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,360 @@
|
|
|
1
|
+
import { BaseRpcServer } from './BaseRpcServer.js';
|
|
2
|
+
import {
|
|
3
|
+
StdioFrameReader,
|
|
4
|
+
StdioFrameWriter
|
|
5
|
+
} from '../transport/IpcMessageBuilder.js';
|
|
6
|
+
import { createComponentLogger } from '../utils/logger.js';
|
|
7
|
+
/**
|
|
8
|
+
* RPC Server for stdio transport using LSP-style framing
|
|
9
|
+
*
|
|
10
|
+
* This server handles JSON-RPC requests over stdio using Language Server Protocol
|
|
11
|
+
* style framing (Content-Length headers + JSON body). It's designed for process-to-process
|
|
12
|
+
* communication where the parent process spawns this ability as a child process.
|
|
13
|
+
*/
|
|
14
|
+
export class StdioRpcServer extends BaseRpcServer {
|
|
15
|
+
/**
|
|
16
|
+
* Create a new StdioRpcServer instance
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options - Configuration options
|
|
19
|
+
* @param {number} options.maxBufferSize - Maximum buffer size for frame reader
|
|
20
|
+
* @param {Readable} options.stdin - Input stream (default: process.stdin)
|
|
21
|
+
* @param {Writable} options.stdout - Output stream (default: process.stdout)
|
|
22
|
+
*/
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
super({ ...options, protocol: 'stdio' });
|
|
25
|
+
|
|
26
|
+
this.frameReader = new StdioFrameReader(options.stdin || process.stdin, {
|
|
27
|
+
maxBufferSize: options.maxBufferSize || 8 * 1024 * 1024
|
|
28
|
+
});
|
|
29
|
+
this.frameWriter = new StdioFrameWriter(options.stdout || process.stdout);
|
|
30
|
+
|
|
31
|
+
this.logger = createComponentLogger('StdioRpcServer');
|
|
32
|
+
this.logger.lifecycle('constructor', 'StdioRpcServer initialized');
|
|
33
|
+
this.logger.trace(
|
|
34
|
+
'constructor',
|
|
35
|
+
`Max buffer size: ${options.maxBufferSize || 8 * 1024 * 1024} bytes`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
this.ability = null;
|
|
39
|
+
this.resolve = null;
|
|
40
|
+
this.reject = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start serving the ability over stdio
|
|
45
|
+
*
|
|
46
|
+
* @param {KadiAbility} ability - The ability instance to serve
|
|
47
|
+
* @returns {Promise<void>} - Promise that resolves when server stops
|
|
48
|
+
*/
|
|
49
|
+
async serve(ability) {
|
|
50
|
+
if (this.isServing) {
|
|
51
|
+
this.logger.error('serve', 'StdioRpcServer is already serving');
|
|
52
|
+
throw new Error('StdioRpcServer is already serving');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.ability = ability;
|
|
56
|
+
this.isServing = true;
|
|
57
|
+
|
|
58
|
+
this.logger.lifecycle(
|
|
59
|
+
'serve',
|
|
60
|
+
`Starting ${ability.name || 'unnamed ability'} on stdio`
|
|
61
|
+
);
|
|
62
|
+
this.logger.info(
|
|
63
|
+
'serve',
|
|
64
|
+
`Available methods: ${ability.getMethodNames().join(', ')}`
|
|
65
|
+
);
|
|
66
|
+
this.logger.trace('serve', 'Setting up frame reader and process handlers');
|
|
67
|
+
|
|
68
|
+
// Emit start event
|
|
69
|
+
this.emit('start', {
|
|
70
|
+
name: ability.name,
|
|
71
|
+
version: ability.version,
|
|
72
|
+
methods: ability.getMethodNames()
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Set up frame reading with message handling
|
|
76
|
+
this.frameReader.onMessage(async (frameResult) => {
|
|
77
|
+
if (!frameResult.success) {
|
|
78
|
+
this.logger.error('frame', `Frame corruption: ${frameResult.error}`);
|
|
79
|
+
this.logger.trace(
|
|
80
|
+
'frame',
|
|
81
|
+
`Corruption type: ${frameResult.corruption_type}, message: ${frameResult.message}`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Emit error event but don't crash - frame corruption can be recovered
|
|
85
|
+
this.emit('error', new Error(`Frame corruption: ${frameResult.error}`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
this.logger.trace(
|
|
91
|
+
'request',
|
|
92
|
+
`Processing JSON-RPC request: ${frameResult.data?.method}`
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Process the JSON-RPC request
|
|
96
|
+
const response = await this.handleRequest(frameResult.data);
|
|
97
|
+
|
|
98
|
+
// Write response if it's not a notification
|
|
99
|
+
if (response !== null) {
|
|
100
|
+
this.logger.trace(
|
|
101
|
+
'response',
|
|
102
|
+
`Sending response for request ID: ${frameResult.data?.id}`
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await this.frameWriter.write(response);
|
|
106
|
+
} else {
|
|
107
|
+
this.logger.trace(
|
|
108
|
+
'notification',
|
|
109
|
+
`Notification processed: ${frameResult.data?.method}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.error(
|
|
114
|
+
'request',
|
|
115
|
+
`Fatal error processing request: ${error.message}`
|
|
116
|
+
);
|
|
117
|
+
this.logger.trace('request', `Error stack: ${error.stack}`);
|
|
118
|
+
|
|
119
|
+
this.logError('Fatal error processing request:', error);
|
|
120
|
+
|
|
121
|
+
// Try to send an error response if we can extract an ID
|
|
122
|
+
const requestId = frameResult.data?.id;
|
|
123
|
+
if (requestId !== undefined) {
|
|
124
|
+
try {
|
|
125
|
+
const errorResponse = this.createInternalErrorResponse(
|
|
126
|
+
requestId,
|
|
127
|
+
'Internal error',
|
|
128
|
+
{ error: error.message }
|
|
129
|
+
);
|
|
130
|
+
await this.frameWriter.write(errorResponse);
|
|
131
|
+
} catch (writeError) {
|
|
132
|
+
this.logError('Failed to send error response:', writeError);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Emit error event
|
|
137
|
+
this.emit('error', error);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Handle process termination gracefully
|
|
142
|
+
const signalHandler = (signal) => {
|
|
143
|
+
this.logger.lifecycle(
|
|
144
|
+
'signal',
|
|
145
|
+
`Received ${signal}, shutting down gracefully`
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
this.log(`Received ${signal}, shutting down gracefully`);
|
|
149
|
+
this.shutdown(signal);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
process.on('SIGINT', signalHandler);
|
|
153
|
+
process.on('SIGTERM', signalHandler);
|
|
154
|
+
|
|
155
|
+
// Keep the process alive and handle stdio events
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
this.resolve = resolve;
|
|
158
|
+
this.reject = reject;
|
|
159
|
+
|
|
160
|
+
// Handle stdin end - parent process closed the pipe
|
|
161
|
+
process.stdin.on('end', () => {
|
|
162
|
+
this.logger.lifecycle('stdin', 'stdin closed, shutting down');
|
|
163
|
+
this.log('stdin closed, shutting down');
|
|
164
|
+
this.shutdown('stdin_end');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Handle stdin errors
|
|
168
|
+
process.stdin.on('error', (error) => {
|
|
169
|
+
this.logger.error('stdin', `stdin error: ${error.message}`);
|
|
170
|
+
this.logError('stdin error:', error);
|
|
171
|
+
this.reject(error);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Handle stdout errors (broken pipe, etc.)
|
|
175
|
+
process.stdout.on('error', (error) => {
|
|
176
|
+
this.logger.error('stdout', `stdout error: ${error.message}`);
|
|
177
|
+
this.logError('stdout error:', error);
|
|
178
|
+
this.reject(error);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle requests in stdio-native format
|
|
185
|
+
*
|
|
186
|
+
* This implementation applies stdio-specific validation and error handling.
|
|
187
|
+
* Stdio requests come as JSON-RPC over LSP framing and often need more
|
|
188
|
+
* human-readable error messages since they might be displayed to developers.
|
|
189
|
+
*
|
|
190
|
+
* @param {Object} request - The JSON-RPC request object
|
|
191
|
+
* @returns {Object|null} - JSON-RPC response or null for notifications
|
|
192
|
+
*/
|
|
193
|
+
async handleRequest(request) {
|
|
194
|
+
this.logger.trace('validation', 'Validating JSON-RPC request format');
|
|
195
|
+
|
|
196
|
+
// Validate JSON-RPC format
|
|
197
|
+
if (!request || typeof request !== 'object') {
|
|
198
|
+
this.logger.warn('validation', 'Invalid request: not an object');
|
|
199
|
+
return this.createInvalidRequestResponse(null);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for required jsonrpc field
|
|
203
|
+
if (request.jsonrpc !== '2.0') {
|
|
204
|
+
this.logger.warn(
|
|
205
|
+
'validation',
|
|
206
|
+
`Invalid JSON-RPC version: ${request.jsonrpc}`
|
|
207
|
+
);
|
|
208
|
+
return this.createInvalidRequestResponse(request.id || null);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for required method field
|
|
212
|
+
if (typeof request.method !== 'string') {
|
|
213
|
+
this.logger.warn('validation', 'Invalid request: method is not a string');
|
|
214
|
+
return this.createInvalidRequestResponse(request.id || null);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Validate params if present
|
|
218
|
+
if (
|
|
219
|
+
request.params !== undefined &&
|
|
220
|
+
typeof request.params !== 'object' &&
|
|
221
|
+
!Array.isArray(request.params)
|
|
222
|
+
) {
|
|
223
|
+
this.logger.warn(
|
|
224
|
+
'validation',
|
|
225
|
+
'Invalid request: params is not an object or array'
|
|
226
|
+
);
|
|
227
|
+
return this.createInvalidRequestResponse(request.id || null);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.logger.trace('validation', 'JSON-RPC request validation passed');
|
|
231
|
+
|
|
232
|
+
// Use base class request handling
|
|
233
|
+
return await super.handleRequest(request);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Gracefully shutdown the stdio server
|
|
238
|
+
*
|
|
239
|
+
* @param {string} reason - Reason for shutdown
|
|
240
|
+
*/
|
|
241
|
+
async shutdown(reason = 'unknown') {
|
|
242
|
+
this.logger.lifecycle(
|
|
243
|
+
'shutdown',
|
|
244
|
+
`Shutting down stdio server, reason: ${reason}`
|
|
245
|
+
);
|
|
246
|
+
await super.shutdown(reason);
|
|
247
|
+
|
|
248
|
+
// Resolve the serve promise to allow clean exit
|
|
249
|
+
if (this.resolve) {
|
|
250
|
+
this.logger.trace('shutdown', 'Resolving serve promise for clean exit');
|
|
251
|
+
this.resolve();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Send a notification to the parent process
|
|
257
|
+
*
|
|
258
|
+
* This is useful for sending status updates or logs that don't require a response
|
|
259
|
+
*
|
|
260
|
+
* @param {string} method - Notification method name
|
|
261
|
+
* @param {Object} params - Notification parameters
|
|
262
|
+
*/
|
|
263
|
+
async sendNotification(method, params = {}) {
|
|
264
|
+
this.logger.trace('notification', `Sending notification: ${method}`);
|
|
265
|
+
|
|
266
|
+
const notification = {
|
|
267
|
+
jsonrpc: '2.0',
|
|
268
|
+
method,
|
|
269
|
+
params
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await this.frameWriter.write(notification);
|
|
274
|
+
this.logger.trace(
|
|
275
|
+
'notification',
|
|
276
|
+
`Notification ${method} sent successfully`
|
|
277
|
+
);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.logger.error(
|
|
280
|
+
'notification',
|
|
281
|
+
`Failed to send notification ${method}: ${error.message}`
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
this.logError('Failed to send notification:', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Publish an event to the parent process via stdio
|
|
290
|
+
*
|
|
291
|
+
* Events are sent as JSON-RPC notifications with the special method '__kadi_event'.
|
|
292
|
+
* The parent process (agent) will receive these and emit them on the ability's
|
|
293
|
+
* events EventEmitter.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} eventName - Name of the event to publish
|
|
296
|
+
* @param {any} data - Event data payload (must be JSON-serializable)
|
|
297
|
+
*/
|
|
298
|
+
async publishEvent(eventName, data = {}) {
|
|
299
|
+
this.logger.trace(
|
|
300
|
+
'publishEvent',
|
|
301
|
+
`Publishing event via stdio: ${eventName}`
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// Send as a JSON-RPC notification with special method name
|
|
306
|
+
// No 'id' field means it's fire-and-forget
|
|
307
|
+
const eventNotification = {
|
|
308
|
+
jsonrpc: '2.0',
|
|
309
|
+
method: '__kadi_event',
|
|
310
|
+
params: {
|
|
311
|
+
eventName,
|
|
312
|
+
data,
|
|
313
|
+
timestamp: Date.now() // Add timestamp for event ordering
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await this.frameWriter.write(eventNotification);
|
|
318
|
+
|
|
319
|
+
this.logger.trace('publishEvent', `Event ${eventName} sent successfully`);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
// Events are best-effort - log but don't throw
|
|
322
|
+
this.logger.warn(
|
|
323
|
+
'publishEvent',
|
|
324
|
+
`Failed to publish event ${eventName}: ${error.message}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Send a status update notification
|
|
331
|
+
*
|
|
332
|
+
* @param {string} status - Status message
|
|
333
|
+
* @param {Object} data - Additional status data
|
|
334
|
+
*/
|
|
335
|
+
async sendStatus(status, data = {}) {
|
|
336
|
+
await this.sendNotification('__kadi_status', { status, ...data });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Send a progress notification
|
|
341
|
+
*
|
|
342
|
+
* @param {number} current - Current progress value
|
|
343
|
+
* @param {number} total - Total progress value
|
|
344
|
+
* @param {string} message - Progress message
|
|
345
|
+
*/
|
|
346
|
+
async sendProgress(current, total, message = '') {
|
|
347
|
+
this.logger.trace(
|
|
348
|
+
'progress',
|
|
349
|
+
`Sending progress: ${current}/${total} - ${message}`
|
|
350
|
+
);
|
|
351
|
+
await this.sendNotification('__kadi_progress', {
|
|
352
|
+
current,
|
|
353
|
+
total,
|
|
354
|
+
percentage: Math.round((current / total) * 100),
|
|
355
|
+
message
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default StdioRpcServer;
|