@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.
- package/README.md +269 -1311
- package/dist/abilities/AbilityLoader.d.ts +26 -0
- package/dist/abilities/AbilityLoader.d.ts.map +1 -1
- package/dist/abilities/AbilityLoader.js +141 -18
- package/dist/abilities/AbilityLoader.js.map +1 -1
- package/dist/abilities/AbilityProxy.d.ts +33 -0
- package/dist/abilities/AbilityProxy.d.ts.map +1 -1
- package/dist/abilities/AbilityProxy.js +40 -0
- package/dist/abilities/AbilityProxy.js.map +1 -1
- package/dist/abilities/index.d.ts +1 -1
- package/dist/abilities/index.d.ts.map +1 -1
- package/dist/abilities/types.d.ts +67 -0
- package/dist/abilities/types.d.ts.map +1 -1
- package/dist/broker/BrokerProtocol.js +11 -11
- package/dist/broker/BrokerProtocol.js.map +1 -1
- package/dist/client/KadiClient.d.ts +191 -2
- package/dist/client/KadiClient.d.ts.map +1 -1
- package/dist/client/KadiClient.js +412 -2
- package/dist/client/KadiClient.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/messages/index.d.ts +1 -1
- package/dist/messages/index.js +1 -1
- package/dist/messages/index.js.map +1 -1
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/zod-helpers.d.ts +129 -0
- package/dist/schemas/zod-helpers.d.ts.map +1 -0
- package/dist/schemas/zod-helpers.js +225 -0
- package/dist/schemas/zod-helpers.js.map +1 -0
- package/dist/schemas/zod-to-json-schema.d.ts +159 -0
- package/dist/schemas/zod-to-json-schema.d.ts.map +1 -0
- package/dist/schemas/zod-to-json-schema.js +154 -0
- package/dist/schemas/zod-to-json-schema.js.map +1 -0
- package/dist/transports/NativeTransport.d.ts +29 -0
- package/dist/transports/NativeTransport.d.ts.map +1 -1
- package/dist/transports/NativeTransport.js +98 -3
- package/dist/transports/NativeTransport.js.map +1 -1
- package/dist/transports/StdioTransport.d.ts +141 -63
- package/dist/transports/StdioTransport.d.ts.map +1 -1
- package/dist/transports/StdioTransport.js +309 -232
- package/dist/transports/StdioTransport.js.map +1 -1
- package/dist/types/broker.d.ts +0 -22
- package/dist/types/broker.d.ts.map +1 -1
- package/dist/types/broker.js +0 -27
- package/dist/types/broker.js.map +1 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/zod-tools.d.ts +198 -0
- package/dist/types/zod-tools.d.ts.map +1 -0
- package/dist/types/zod-tools.js +14 -0
- package/dist/types/zod-tools.js.map +1 -0
- package/dist/utils/LockfileResolver.d.ts +108 -0
- package/dist/utils/LockfileResolver.d.ts.map +1 -0
- package/dist/utils/LockfileResolver.js +230 -0
- package/dist/utils/LockfileResolver.js.map +1 -0
- package/dist/utils/StdioMessageReader.d.ts +122 -0
- package/dist/utils/StdioMessageReader.d.ts.map +1 -0
- package/dist/utils/StdioMessageReader.js +209 -0
- package/dist/utils/StdioMessageReader.js.map +1 -0
- package/dist/utils/StdioMessageWriter.d.ts +104 -0
- package/dist/utils/StdioMessageWriter.d.ts.map +1 -0
- package/dist/utils/StdioMessageWriter.js +162 -0
- package/dist/utils/StdioMessageWriter.js.map +1 -0
- package/package.json +2 -1
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stdio Transport
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
|
21
|
-
*
|
|
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: '
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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('
|
|
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
|
|
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
|
-
*
|
|
80
|
+
* Agent.json representation (from readAgentJson)
|
|
55
81
|
*/
|
|
56
|
-
|
|
82
|
+
agentJson;
|
|
57
83
|
/**
|
|
58
|
-
*
|
|
84
|
+
* Request ID counter
|
|
59
85
|
*/
|
|
60
|
-
|
|
86
|
+
requestIdCounter = 0;
|
|
61
87
|
/**
|
|
62
|
-
*
|
|
88
|
+
* Pending requests (waiting for responses)
|
|
63
89
|
*/
|
|
64
|
-
|
|
90
|
+
pendingRequests = new Map();
|
|
65
91
|
/**
|
|
66
92
|
* Create a new StdioTransport
|
|
67
93
|
*
|
|
68
|
-
* @param 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
|
|
111
|
+
* Spawns the child process, sets up stdio communication,
|
|
112
|
+
* and performs discovery via readAgentJson.
|
|
87
113
|
*
|
|
88
|
-
* @throws {KadiError} If
|
|
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
|
|
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
|
-
//
|
|
111
|
-
this.
|
|
112
|
-
|
|
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.
|
|
115
|
-
this.
|
|
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
|
-
//
|
|
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', (
|
|
120
|
-
this.
|
|
153
|
+
this.process.stdout.on('data', (chunk) => {
|
|
154
|
+
this.reader?.onData(chunk);
|
|
121
155
|
});
|
|
122
156
|
}
|
|
123
|
-
//
|
|
157
|
+
// Capture stderr
|
|
124
158
|
if (this.process.stderr) {
|
|
125
|
-
this.process.stderr.on('data', (
|
|
126
|
-
|
|
159
|
+
this.process.stderr.on('data', (chunk) => {
|
|
160
|
+
this.stderrBuffer = Buffer.concat([this.stderrBuffer, chunk]);
|
|
127
161
|
});
|
|
128
162
|
}
|
|
129
|
-
//
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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(
|
|
177
|
-
reject(new KadiError(`
|
|
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
|
-
|
|
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
|
-
*
|
|
376
|
+
* Read agent.json representation
|
|
377
|
+
*
|
|
378
|
+
* Returns the agent.json representation retrieved during connection.
|
|
212
379
|
*
|
|
213
|
-
*
|
|
380
|
+
* @returns agent.json representation or undefined
|
|
214
381
|
*
|
|
215
382
|
* @example
|
|
216
383
|
* ```typescript
|
|
217
|
-
*
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
this.cleanup();
|
|
225
|
-
this.emit('disconnected');
|
|
391
|
+
readAgentJson() {
|
|
392
|
+
return this.agentJson;
|
|
226
393
|
}
|
|
227
394
|
/**
|
|
228
|
-
*
|
|
395
|
+
* Get captured stderr output
|
|
229
396
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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
|
-
*
|
|
400
|
+
* @returns Stderr content as string
|
|
279
401
|
*
|
|
280
|
-
* @
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* const stderr = transport.getStderr();
|
|
405
|
+
* console.log('Ability logs:', stderr);
|
|
406
|
+
* ```
|
|
281
407
|
*/
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
*
|
|
412
|
+
* Disconnect from the ability
|
|
301
413
|
*
|
|
302
|
-
*
|
|
414
|
+
* Kills the child process with SIGTERM and cleans up resources.
|
|
303
415
|
*
|
|
304
|
-
* @
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
//
|
|
362
|
-
|
|
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
|
|
428
|
+
for (const pending of this.pendingRequests.values()) {
|
|
391
429
|
clearTimeout(pending.timeout);
|
|
392
|
-
pending.reject(new KadiError('
|
|
430
|
+
pending.reject(new KadiError('Transport disconnected', ErrorCode.TRANSPORT_NOT_CONNECTED, 503, { abilityName: this.options.abilityName }));
|
|
393
431
|
}
|
|
394
432
|
this.pendingRequests.clear();
|
|
395
|
-
//
|
|
433
|
+
// Cleanup reader/writer
|
|
434
|
+
this.reader?.destroy();
|
|
435
|
+
this.writer?.destroy();
|
|
436
|
+
// Kill child process
|
|
396
437
|
if (this.process) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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.
|
|
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
|