@kadi.build/core 0.0.1-alpha.10 → 0.0.1-alpha.12

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 (71) hide show
  1. package/README.md +269 -1311
  2. package/dist/abilities/AbilityLoader.d.ts +26 -0
  3. package/dist/abilities/AbilityLoader.d.ts.map +1 -1
  4. package/dist/abilities/AbilityLoader.js +141 -18
  5. package/dist/abilities/AbilityLoader.js.map +1 -1
  6. package/dist/abilities/AbilityProxy.d.ts +33 -0
  7. package/dist/abilities/AbilityProxy.d.ts.map +1 -1
  8. package/dist/abilities/AbilityProxy.js +40 -0
  9. package/dist/abilities/AbilityProxy.js.map +1 -1
  10. package/dist/abilities/index.d.ts +1 -1
  11. package/dist/abilities/index.d.ts.map +1 -1
  12. package/dist/abilities/types.d.ts +67 -0
  13. package/dist/abilities/types.d.ts.map +1 -1
  14. package/dist/broker/BrokerProtocol.js +11 -11
  15. package/dist/broker/BrokerProtocol.js.map +1 -1
  16. package/dist/client/KadiClient.d.ts +191 -2
  17. package/dist/client/KadiClient.d.ts.map +1 -1
  18. package/dist/client/KadiClient.js +412 -2
  19. package/dist/client/KadiClient.js.map +1 -1
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +5 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/messages/index.d.ts +1 -1
  25. package/dist/messages/index.js +1 -1
  26. package/dist/messages/index.js.map +1 -1
  27. package/dist/schemas/index.d.ts +3 -0
  28. package/dist/schemas/index.d.ts.map +1 -1
  29. package/dist/schemas/index.js +2 -0
  30. package/dist/schemas/index.js.map +1 -1
  31. package/dist/schemas/zod-helpers.d.ts +129 -0
  32. package/dist/schemas/zod-helpers.d.ts.map +1 -0
  33. package/dist/schemas/zod-helpers.js +225 -0
  34. package/dist/schemas/zod-helpers.js.map +1 -0
  35. package/dist/schemas/zod-to-json-schema.d.ts +159 -0
  36. package/dist/schemas/zod-to-json-schema.d.ts.map +1 -0
  37. package/dist/schemas/zod-to-json-schema.js +154 -0
  38. package/dist/schemas/zod-to-json-schema.js.map +1 -0
  39. package/dist/transports/NativeTransport.d.ts +29 -0
  40. package/dist/transports/NativeTransport.d.ts.map +1 -1
  41. package/dist/transports/NativeTransport.js +98 -3
  42. package/dist/transports/NativeTransport.js.map +1 -1
  43. package/dist/transports/StdioTransport.d.ts +141 -63
  44. package/dist/transports/StdioTransport.d.ts.map +1 -1
  45. package/dist/transports/StdioTransport.js +309 -232
  46. package/dist/transports/StdioTransport.js.map +1 -1
  47. package/dist/types/broker.d.ts +0 -22
  48. package/dist/types/broker.d.ts.map +1 -1
  49. package/dist/types/broker.js +0 -27
  50. package/dist/types/broker.js.map +1 -1
  51. package/dist/types/index.d.ts +3 -1
  52. package/dist/types/index.d.ts.map +1 -1
  53. package/dist/types/index.js +1 -1
  54. package/dist/types/index.js.map +1 -1
  55. package/dist/types/zod-tools.d.ts +198 -0
  56. package/dist/types/zod-tools.d.ts.map +1 -0
  57. package/dist/types/zod-tools.js +14 -0
  58. package/dist/types/zod-tools.js.map +1 -0
  59. package/dist/utils/LockfileResolver.d.ts +108 -0
  60. package/dist/utils/LockfileResolver.d.ts.map +1 -0
  61. package/dist/utils/LockfileResolver.js +230 -0
  62. package/dist/utils/LockfileResolver.js.map +1 -0
  63. package/dist/utils/StdioMessageReader.d.ts +122 -0
  64. package/dist/utils/StdioMessageReader.d.ts.map +1 -0
  65. package/dist/utils/StdioMessageReader.js +209 -0
  66. package/dist/utils/StdioMessageReader.js.map +1 -0
  67. package/dist/utils/StdioMessageWriter.d.ts +104 -0
  68. package/dist/utils/StdioMessageWriter.d.ts.map +1 -0
  69. package/dist/utils/StdioMessageWriter.js +162 -0
  70. package/dist/utils/StdioMessageWriter.js.map +1 -0
  71. package/package.json +2 -1
@@ -1,36 +1,42 @@
1
1
  /**
2
2
  * Stdio Transport
3
3
  *
4
- * Communicates with abilities running as child processes via stdin/stdout.
5
- * Enables using abilities written in any language that supports JSON over stdio.
4
+ * Spawns abilities as child processes and communicates via stdin/stdout
5
+ * using Content-Length framed JSON-RPC messages (LSP-style).
6
6
  *
7
7
  * @module transports/StdioTransport
8
8
  */
9
9
  import { EventEmitter } from 'events';
10
10
  import { spawn } from 'child_process';
11
- import { StdioMessageType } from '../types/index.js';
12
11
  import { KadiError, ErrorCode } from '../types/index.js';
13
- /**
14
- * Message ID counter
15
- */
16
- let messageIdCounter = 0;
12
+ import { StdioMessageReader } from '../utils/StdioMessageReader.js';
13
+ import { StdioMessageWriter } from '../utils/StdioMessageWriter.js';
17
14
  /**
18
15
  * Stdio Transport
19
16
  *
20
- * Spawns a child process and communicates via JSON messages over stdin/stdout.
21
- * Uses length-prefixed framing for reliable message boundaries.
17
+ * Spawns an ability as a child process and communicates via JSON-RPC
18
+ * over stdin/stdout using Content-Length framing (LSP-style).
19
+ *
20
+ * **Features**:
21
+ * - Spawns child process with configurable command/args
22
+ * - Content-Length framed messages (reliable even with logs)
23
+ * - Captures stderr separately (ability logs)
24
+ * - Request/response matching via JSON-RPC ids
25
+ * - Timeout handling per request
26
+ * - Graceful shutdown with SIGTERM
22
27
  *
23
28
  * @example
24
29
  * ```typescript
25
30
  * const transport = new StdioTransport({
26
- * abilityName: 'python-calculator',
27
- * abilityPath: '/path/to/calculator',
28
- * command: 'python3',
29
- * args: ['main.py']
31
+ * abilityName: 'calculator',
32
+ * command: 'node',
33
+ * args: ['./abilities/calculator/index.js']
30
34
  * });
31
35
  *
32
36
  * await transport.connect();
33
- * const result = await transport.invoke('calculate', { expr: '2 + 2' });
37
+ * const result = await transport.invoke('add', { a: 5, b: 3 });
38
+ * console.log(result); // { result: 8 }
39
+ * await transport.disconnect();
34
40
  * ```
35
41
  */
36
42
  export class StdioTransport extends EventEmitter {
@@ -41,7 +47,27 @@ export class StdioTransport extends EventEmitter {
41
47
  /**
42
48
  * Child process instance
43
49
  */
44
- process = null;
50
+ process;
51
+ /**
52
+ * Message reader for stdout
53
+ */
54
+ reader;
55
+ /**
56
+ * Message writer for stdin
57
+ */
58
+ writer;
59
+ /**
60
+ * Stderr buffer
61
+ */
62
+ stderrBuffer = Buffer.alloc(0);
63
+ /**
64
+ * Whether transport is connected
65
+ */
66
+ connected = false;
67
+ /**
68
+ * Whether we're intentionally disconnecting
69
+ */
70
+ disconnecting = false;
45
71
  /**
46
72
  * Discovered methods
47
73
  */
@@ -51,46 +77,45 @@ export class StdioTransport extends EventEmitter {
51
77
  */
52
78
  schemas = new Map();
53
79
  /**
54
- * Whether transport is connected
80
+ * Agent.json representation (from readAgentJson)
55
81
  */
56
- connected = false;
82
+ agentJson;
57
83
  /**
58
- * Pending requests waiting for responses
84
+ * Request ID counter
59
85
  */
60
- pendingRequests = new Map();
86
+ requestIdCounter = 0;
61
87
  /**
62
- * Buffer for incomplete messages
88
+ * Pending requests (waiting for responses)
63
89
  */
64
- buffer = Buffer.alloc(0);
90
+ pendingRequests = new Map();
65
91
  /**
66
92
  * Create a new StdioTransport
67
93
  *
68
- * @param options - Stdio transport options
94
+ * @param options - Transport options
69
95
  */
70
96
  constructor(options) {
71
97
  super();
72
98
  this.options = {
73
- command: options.command ?? 'node',
74
- args: options.args ?? ['index.js'],
75
- env: options.env ?? {},
76
- cwd: options.cwd ?? options.abilityPath,
77
- timeout: options.timeout ?? 30000,
78
- abilityPath: options.abilityPath,
79
99
  abilityName: options.abilityName,
100
+ command: options.command,
101
+ args: options.args,
102
+ cwd: options.cwd,
103
+ env: options.env,
104
+ timeout: options.timeout ?? 30000,
80
105
  abilityVersion: options.abilityVersion ?? '1.0.0'
81
106
  };
82
107
  }
83
108
  /**
84
109
  * Connect to the ability
85
110
  *
86
- * Spawns the child process and performs method discovery.
111
+ * Spawns the child process, sets up stdio communication,
112
+ * and performs discovery via readAgentJson.
87
113
  *
88
- * @throws {KadiError} If process fails to start or discovery fails
114
+ * @throws {KadiError} If spawn fails or discovery fails
89
115
  *
90
116
  * @example
91
117
  * ```typescript
92
118
  * await transport.connect();
93
- * console.log('Process started');
94
119
  * ```
95
120
  */
96
121
  async connect() {
@@ -98,44 +123,105 @@ export class StdioTransport extends EventEmitter {
98
123
  return;
99
124
  }
100
125
  try {
101
- // Spawn the child process
102
- this.process = spawn(this.options.command, this.options.args, {
126
+ // Spawn child process
127
+ this.process = spawn(this.options.command, this.options.args ?? [], {
128
+ stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
103
129
  cwd: this.options.cwd,
104
130
  env: {
105
131
  ...process.env,
106
132
  ...this.options.env
107
- },
108
- stdio: ['pipe', 'pipe', 'pipe']
133
+ }
109
134
  });
110
- // Set up error handling
111
- this.process.on('error', (error) => {
112
- this.handleProcessError(error);
135
+ // Setup message reader for stdout
136
+ this.reader = new StdioMessageReader();
137
+ this.reader.on('message', (message) => {
138
+ this.handleMessage(message);
113
139
  });
114
- this.process.on('exit', (code, signal) => {
115
- this.handleProcessExit(code, signal);
140
+ this.reader.on('error', (error) => {
141
+ this.emit('error', new KadiError(`Failed to parse message from ability: ${error.message}`, ErrorCode.STDIO_FRAMING_ERROR, 500, {
142
+ abilityName: this.options.abilityName,
143
+ error: error.message
144
+ }));
116
145
  });
117
- // Set up stdout handling (receives messages)
146
+ // Setup message writer for stdin
147
+ if (!this.process.stdin) {
148
+ throw new KadiError('Child process stdin is not available', ErrorCode.STDIO_PROCESS_FAILED, 500, { abilityName: this.options.abilityName });
149
+ }
150
+ this.writer = new StdioMessageWriter(this.process.stdin);
151
+ // Capture stdout data
118
152
  if (this.process.stdout) {
119
- this.process.stdout.on('data', (data) => {
120
- this.handleData(data);
153
+ this.process.stdout.on('data', (chunk) => {
154
+ this.reader?.onData(chunk);
121
155
  });
122
156
  }
123
- // Set up stderr handling (for debugging)
157
+ // Capture stderr
124
158
  if (this.process.stderr) {
125
- this.process.stderr.on('data', (data) => {
126
- console.error(`[${this.options.abilityName}] stderr:`, data.toString());
159
+ this.process.stderr.on('data', (chunk) => {
160
+ this.stderrBuffer = Buffer.concat([this.stderrBuffer, chunk]);
127
161
  });
128
162
  }
129
- // Perform discovery
130
- await this.discoverMethods();
163
+ // Handle process exit
164
+ this.process.on('exit', (code, signal) => {
165
+ // Only treat as error if:
166
+ // 1. We're not intentionally disconnecting AND
167
+ // 2. Exit code is non-zero (error exit)
168
+ //
169
+ // Note: We don't treat code=0 as error even if connected, because
170
+ // the process may exit cleanly during cleanup
171
+ const isUnexpectedExit = !this.disconnecting && code !== 0;
172
+ if (isUnexpectedExit) {
173
+ const error = new KadiError(`Ability process exited unexpectedly (code: ${code}, signal: ${signal})`, ErrorCode.STDIO_PROCESS_EXITED, 500, {
174
+ abilityName: this.options.abilityName,
175
+ exitCode: code,
176
+ signal,
177
+ stderr: this.getStderr()
178
+ });
179
+ // Reject all pending requests
180
+ for (const pending of this.pendingRequests.values()) {
181
+ clearTimeout(pending.timeout);
182
+ pending.reject(error);
183
+ }
184
+ this.pendingRequests.clear();
185
+ this.emit('error', error);
186
+ }
187
+ this.connected = false;
188
+ });
189
+ // Handle process errors
190
+ this.process.on('error', (error) => {
191
+ const kadiError = new KadiError(`Failed to spawn ability process: ${error.message}`, ErrorCode.STDIO_PROCESS_FAILED, 500, {
192
+ abilityName: this.options.abilityName,
193
+ command: this.options.command,
194
+ args: this.options.args,
195
+ error: error.message
196
+ });
197
+ this.emit('error', kadiError);
198
+ throw kadiError;
199
+ });
200
+ // Perform discovery via readAgentJson
201
+ this.agentJson = await this.sendRequest('readAgentJson', {});
202
+ // Extract methods from agent.json
203
+ this.methods = this.agentJson.tools.map(tool => tool.name);
204
+ // Store schemas
205
+ for (const tool of this.agentJson.tools) {
206
+ if (tool.inputSchema || tool.outputSchema) {
207
+ this.schemas.set(tool.name, {
208
+ inputSchema: tool.inputSchema,
209
+ outputSchema: tool.outputSchema,
210
+ description: tool.description
211
+ });
212
+ }
213
+ }
131
214
  this.connected = true;
132
215
  this.emit('connected');
133
216
  }
134
217
  catch (error) {
135
- this.cleanup();
218
+ // Cleanup on connection failure
219
+ if (this.process) {
220
+ this.process.kill('SIGTERM');
221
+ }
136
222
  const kadiError = error instanceof KadiError
137
223
  ? error
138
- : new KadiError(`Failed to start process: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.STDIO_PROCESS_FAILED, 500, {
224
+ : new KadiError(`Failed to connect to ability: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.ABILITY_LOAD_FAILED, 500, {
139
225
  abilityName: this.options.abilityName,
140
226
  command: this.options.command,
141
227
  args: this.options.args,
@@ -159,41 +245,112 @@ export class StdioTransport extends EventEmitter {
159
245
  *
160
246
  * @example
161
247
  * ```typescript
162
- * const result = await transport.invoke('add', { a: 5, b: 3 });
248
+ * const result = await transport.invoke<{ a: number; b: number }, { result: number }>(
249
+ * 'add',
250
+ * { a: 5, b: 3 }
251
+ * );
163
252
  * ```
164
253
  */
165
254
  async invoke(method, params) {
166
- if (!this.connected || !this.process) {
255
+ if (!this.connected) {
167
256
  throw new KadiError('Transport not connected', ErrorCode.TRANSPORT_NOT_CONNECTED, 503, { abilityName: this.options.abilityName });
168
257
  }
169
258
  // Check if method exists
170
259
  if (!this.methods.includes(method)) {
171
260
  throw new KadiError(`Method '${method}' not found on ability '${this.options.abilityName}'`, ErrorCode.ABILITY_METHOD_NOT_FOUND, 404, { abilityName: this.options.abilityName, method, availableMethods: this.methods });
172
261
  }
173
- const messageId = ++messageIdCounter;
174
- return new Promise((resolve, reject) => {
262
+ // Send invoke request via JSON-RPC
263
+ const result = await this.sendRequest('invoke', {
264
+ toolName: method,
265
+ toolInput: params
266
+ });
267
+ return result;
268
+ }
269
+ /**
270
+ * Send a JSON-RPC request and wait for response
271
+ *
272
+ * @param method - JSON-RPC method name
273
+ * @param params - Request parameters
274
+ * @returns Promise resolving to response result
275
+ *
276
+ * @throws {KadiError} If request fails or times out
277
+ */
278
+ async sendRequest(method, params) {
279
+ if (!this.writer) {
280
+ throw new KadiError('Writer not initialized', ErrorCode.TRANSPORT_SEND_FAILED, 500, { abilityName: this.options.abilityName });
281
+ }
282
+ // Generate unique request ID
283
+ const id = ++this.requestIdCounter;
284
+ // Create promise for response
285
+ const responsePromise = new Promise((resolve, reject) => {
286
+ // Setup timeout
175
287
  const timeout = setTimeout(() => {
176
- this.pendingRequests.delete(messageId);
177
- reject(new KadiError(`Method invocation timeout after ${this.options.timeout}ms`, ErrorCode.TOOL_TIMEOUT, 408, { abilityName: this.options.abilityName, method, timeout: this.options.timeout }));
288
+ this.pendingRequests.delete(id);
289
+ reject(new KadiError(`Request timeout after ${this.options.timeout}ms`, ErrorCode.TOOL_TIMEOUT, 408, {
290
+ abilityName: this.options.abilityName,
291
+ method,
292
+ timeout: this.options.timeout
293
+ }));
178
294
  }, this.options.timeout);
179
- this.pendingRequests.set(messageId, {
295
+ // Store pending request (cast resolve to match unknown type)
296
+ this.pendingRequests.set(id, {
180
297
  resolve: resolve,
181
298
  reject,
182
299
  timeout
183
300
  });
184
- // Send invoke message
185
- this.sendMessage({
186
- type: StdioMessageType.INVOKE,
187
- id: messageId,
188
- method,
189
- params
190
- });
191
301
  });
302
+ // Send JSON-RPC request
303
+ await this.writer.write({
304
+ jsonrpc: '2.0',
305
+ id,
306
+ method,
307
+ params
308
+ });
309
+ return responsePromise;
310
+ }
311
+ /**
312
+ * Handle incoming JSON-RPC message
313
+ *
314
+ * Matches responses to pending requests via id.
315
+ *
316
+ * @param message - Incoming message
317
+ */
318
+ handleMessage(message) {
319
+ const { id, result, error } = message;
320
+ if (typeof id !== 'number') {
321
+ // Not a response (or malformed)
322
+ return;
323
+ }
324
+ const pending = this.pendingRequests.get(id);
325
+ if (!pending) {
326
+ // Response for unknown request (may have timed out)
327
+ return;
328
+ }
329
+ // Clear timeout
330
+ clearTimeout(pending.timeout);
331
+ this.pendingRequests.delete(id);
332
+ // Resolve or reject based on response
333
+ if (error) {
334
+ const errorObj = error;
335
+ pending.reject(new KadiError(errorObj.message || 'Request failed', ErrorCode.TOOL_INVOCATION_FAILED, errorObj.code || 500, {
336
+ abilityName: this.options.abilityName,
337
+ ...(errorObj.data && typeof errorObj.data === 'object' ? errorObj.data : {})
338
+ }));
339
+ }
340
+ else {
341
+ pending.resolve(result);
342
+ }
192
343
  }
193
344
  /**
194
345
  * Get list of available methods
195
346
  *
196
347
  * @returns Array of method names
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * const methods = transport.getMethods();
352
+ * console.log('Available methods:', methods);
353
+ * ```
197
354
  */
198
355
  getMethods() {
199
356
  return [...this.methods];
@@ -203,208 +360,128 @@ export class StdioTransport extends EventEmitter {
203
360
  *
204
361
  * @param method - Method name
205
362
  * @returns Method schema or undefined
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * const schema = transport.getMethodSchema('add');
367
+ * if (schema) {
368
+ * console.log('Input schema:', schema.inputSchema);
369
+ * }
370
+ * ```
206
371
  */
207
372
  getMethodSchema(method) {
208
373
  return this.schemas.get(method);
209
374
  }
210
375
  /**
211
- * Disconnect from the ability
376
+ * Read agent.json representation
377
+ *
378
+ * Returns the agent.json representation retrieved during connection.
212
379
  *
213
- * Terminates the child process and cleans up resources.
380
+ * @returns agent.json representation or undefined
214
381
  *
215
382
  * @example
216
383
  * ```typescript
217
- * await transport.disconnect();
384
+ * const agentJson = transport.readAgentJson();
385
+ * if (agentJson) {
386
+ * console.log('Ability:', agentJson.name, agentJson.version);
387
+ * console.log('Tools:', agentJson.tools.map(t => t.name));
388
+ * }
218
389
  * ```
219
390
  */
220
- async disconnect() {
221
- if (!this.connected) {
222
- return;
223
- }
224
- this.cleanup();
225
- this.emit('disconnected');
391
+ readAgentJson() {
392
+ return this.agentJson;
226
393
  }
227
394
  /**
228
- * Publish an event from the ability
395
+ * Get captured stderr output
229
396
  *
230
- * @param eventName - Event name
231
- * @param data - Event data
232
- */
233
- publishEvent(eventName, data) {
234
- this.emit('event', {
235
- eventName,
236
- data,
237
- timestamp: Date.now(),
238
- source: this.options.abilityName
239
- });
240
- }
241
- /**
242
- * Discover methods by sending discovery request
243
- */
244
- async discoverMethods() {
245
- if (!this.process) {
246
- throw new KadiError('Process not started', ErrorCode.STDIO_PROCESS_FAILED, 500, { abilityName: this.options.abilityName });
247
- }
248
- const messageId = ++messageIdCounter;
249
- return new Promise((resolve, reject) => {
250
- const timeout = setTimeout(() => {
251
- this.pendingRequests.delete(messageId);
252
- reject(new KadiError('Discovery timeout', ErrorCode.CONNECTION_TIMEOUT, 408, { abilityName: this.options.abilityName }));
253
- }, this.options.timeout);
254
- this.pendingRequests.set(messageId, {
255
- resolve: (result) => {
256
- const discovery = result;
257
- this.methods = discovery.methods;
258
- if (discovery.schemas) {
259
- for (const [method, schema] of Object.entries(discovery.schemas)) {
260
- this.schemas.set(method, schema);
261
- }
262
- }
263
- resolve();
264
- },
265
- reject,
266
- timeout
267
- });
268
- // Send discovery message
269
- this.sendMessage({
270
- type: StdioMessageType.DISCOVER,
271
- id: messageId
272
- });
273
- });
274
- }
275
- /**
276
- * Send a message to the child process
397
+ * Returns all stderr output captured from the child process.
398
+ * Useful for debugging or viewing ability logs.
277
399
  *
278
- * Uses length-prefixed framing: [length:4 bytes][json message]
400
+ * @returns Stderr content as string
279
401
  *
280
- * @param message - Message to send
402
+ * @example
403
+ * ```typescript
404
+ * const stderr = transport.getStderr();
405
+ * console.log('Ability logs:', stderr);
406
+ * ```
281
407
  */
282
- sendMessage(message) {
283
- if (!this.process || !this.process.stdin) {
284
- throw new KadiError('Process stdin not available', ErrorCode.TRANSPORT_SEND_FAILED, 500, { abilityName: this.options.abilityName });
285
- }
286
- try {
287
- const json = JSON.stringify(message);
288
- const messageBuffer = Buffer.from(json, 'utf-8');
289
- const lengthBuffer = Buffer.allocUnsafe(4);
290
- lengthBuffer.writeUInt32BE(messageBuffer.length, 0);
291
- // Write length prefix + message
292
- this.process.stdin.write(lengthBuffer);
293
- this.process.stdin.write(messageBuffer);
294
- }
295
- catch (error) {
296
- throw new KadiError(`Failed to send message: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.TRANSPORT_SEND_FAILED, 500, { abilityName: this.options.abilityName });
297
- }
408
+ getStderr() {
409
+ return this.stderrBuffer.toString('utf8');
298
410
  }
299
411
  /**
300
- * Handle incoming data from stdout
412
+ * Disconnect from the ability
301
413
  *
302
- * Implements length-prefixed framing to extract complete messages.
414
+ * Kills the child process with SIGTERM and cleans up resources.
303
415
  *
304
- * @param data - Raw data from stdout
305
- */
306
- handleData(data) {
307
- // Append to buffer
308
- this.buffer = Buffer.concat([this.buffer, data]);
309
- // Process complete messages
310
- while (this.buffer.length >= 4) {
311
- // Read message length (4 bytes, big-endian)
312
- const messageLength = this.buffer.readUInt32BE(0);
313
- // Check if we have the complete message
314
- if (this.buffer.length < 4 + messageLength) {
315
- break; // Wait for more data
316
- }
317
- // Extract message
318
- const messageBuffer = this.buffer.subarray(4, 4 + messageLength);
319
- this.buffer = this.buffer.subarray(4 + messageLength);
320
- // Parse and handle message
321
- try {
322
- const message = JSON.parse(messageBuffer.toString('utf-8'));
323
- this.handleMessage(message);
324
- }
325
- catch (error) {
326
- this.emit('error', new KadiError(`Failed to parse message: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.STDIO_FRAMING_ERROR, 400, { abilityName: this.options.abilityName }));
327
- }
328
- }
329
- }
330
- /**
331
- * Handle a parsed message
332
- *
333
- * @param message - Parsed message object
416
+ * @example
417
+ * ```typescript
418
+ * await transport.disconnect();
419
+ * ```
334
420
  */
335
- handleMessage(message) {
336
- // Handle result messages
337
- if (message.type === StdioMessageType.RESULT && message.id !== undefined) {
338
- const pending = this.pendingRequests.get(message.id);
339
- if (pending) {
340
- clearTimeout(pending.timeout);
341
- this.pendingRequests.delete(message.id);
342
- pending.resolve(message.result);
343
- }
344
- return;
345
- }
346
- // Handle error messages
347
- if (message.type === StdioMessageType.ERROR && message.id !== undefined) {
348
- const pending = this.pendingRequests.get(message.id);
349
- if (pending) {
350
- clearTimeout(pending.timeout);
351
- this.pendingRequests.delete(message.id);
352
- pending.reject(new KadiError(String(message.error ?? 'Unknown error'), ErrorCode.TOOL_INVOCATION_FAILED, 500, { abilityName: this.options.abilityName }));
353
- }
354
- return;
355
- }
356
- // Handle event messages
357
- if (message.type === StdioMessageType.EVENT) {
358
- this.emit('event', message.data);
421
+ async disconnect() {
422
+ if (!this.connected) {
359
423
  return;
360
424
  }
361
- // Unknown message type
362
- console.warn(`Unknown message type: ${message.type}`, message);
363
- }
364
- /**
365
- * Handle process error
366
- *
367
- * @param error - Error from child process
368
- */
369
- handleProcessError(error) {
370
- this.emit('error', new KadiError(`Process error: ${error.message}`, ErrorCode.STDIO_PROCESS_FAILED, 500, { abilityName: this.options.abilityName, originalError: error.message }));
371
- this.cleanup();
372
- }
373
- /**
374
- * Handle process exit
375
- *
376
- * @param code - Exit code
377
- * @param signal - Exit signal
378
- */
379
- handleProcessExit(code, signal) {
380
- if (this.connected) {
381
- this.emit('error', new KadiError(`Process exited unexpectedly`, ErrorCode.STDIO_PROCESS_EXITED, 500, { abilityName: this.options.abilityName, exitCode: code, signal }));
382
- }
383
- this.cleanup();
384
- }
385
- /**
386
- * Cleanup resources
387
- */
388
- cleanup() {
425
+ // Mark as intentionally disconnecting
426
+ this.disconnecting = true;
389
427
  // Reject all pending requests
390
- for (const [_id, pending] of this.pendingRequests) {
428
+ for (const pending of this.pendingRequests.values()) {
391
429
  clearTimeout(pending.timeout);
392
- pending.reject(new KadiError('Process terminated', ErrorCode.STDIO_PROCESS_EXITED, 503, { abilityName: this.options.abilityName }));
430
+ pending.reject(new KadiError('Transport disconnected', ErrorCode.TRANSPORT_NOT_CONNECTED, 503, { abilityName: this.options.abilityName }));
393
431
  }
394
432
  this.pendingRequests.clear();
395
- // Kill process if running
433
+ // Cleanup reader/writer
434
+ this.reader?.destroy();
435
+ this.writer?.destroy();
436
+ // Kill child process
396
437
  if (this.process) {
397
- if (!this.process.killed) {
398
- this.process.kill();
399
- }
400
- this.process.removeAllListeners();
401
- this.process = null;
438
+ this.process.kill('SIGTERM');
439
+ // Wait briefly for graceful exit
440
+ await new Promise((resolve) => {
441
+ if (!this.process) {
442
+ resolve();
443
+ return;
444
+ }
445
+ const timeout = setTimeout(() => {
446
+ // Force kill if not exited
447
+ this.process?.kill('SIGKILL');
448
+ resolve();
449
+ }, 5000);
450
+ this.process.once('exit', () => {
451
+ clearTimeout(timeout);
452
+ resolve();
453
+ });
454
+ });
402
455
  }
403
456
  // Clear state
404
- this.buffer = Buffer.alloc(0);
457
+ this.process = undefined;
458
+ this.reader = undefined;
459
+ this.writer = undefined;
405
460
  this.methods = [];
406
461
  this.schemas.clear();
462
+ this.agentJson = undefined;
407
463
  this.connected = false;
464
+ this.disconnecting = false;
465
+ this.emit('disconnected');
466
+ }
467
+ /**
468
+ * Publish an event from the ability
469
+ *
470
+ * @param eventName - Event name
471
+ * @param data - Event data
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * transport.publishEvent('calculation.completed', { result: 42 });
476
+ * ```
477
+ */
478
+ publishEvent(eventName, data) {
479
+ this.emit('event', {
480
+ eventName,
481
+ data,
482
+ timestamp: Date.now(),
483
+ source: this.options.abilityName
484
+ });
408
485
  }
409
486
  }
410
487
  //# sourceMappingURL=StdioTransport.js.map