@kadi.build/core 0.0.1-alpha.3 → 0.0.1-alpha.5

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 (128) hide show
  1. package/README.md +754 -606
  2. package/dist/KadiClient.d.ts +440 -0
  3. package/dist/KadiClient.d.ts.map +1 -0
  4. package/dist/KadiClient.js +1518 -0
  5. package/dist/KadiClient.js.map +1 -0
  6. package/dist/errors/error-codes.d.ts +215 -0
  7. package/dist/errors/error-codes.d.ts.map +1 -0
  8. package/dist/errors/error-codes.js +295 -0
  9. package/dist/errors/error-codes.js.map +1 -0
  10. package/dist/index.d.ts +15 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +24 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/loadAbility.d.ts +106 -0
  15. package/dist/loadAbility.d.ts.map +1 -0
  16. package/dist/loadAbility.js +376 -0
  17. package/dist/loadAbility.js.map +1 -0
  18. package/dist/messages/BrokerMessages.d.ts +84 -0
  19. package/dist/messages/BrokerMessages.d.ts.map +1 -0
  20. package/dist/messages/BrokerMessages.js +125 -0
  21. package/dist/messages/BrokerMessages.js.map +1 -0
  22. package/dist/messages/MessageBuilder.d.ts +83 -0
  23. package/dist/messages/MessageBuilder.d.ts.map +1 -0
  24. package/dist/messages/MessageBuilder.js +144 -0
  25. package/dist/messages/MessageBuilder.js.map +1 -0
  26. package/dist/schemas/events.schemas.d.ts +177 -0
  27. package/dist/schemas/events.schemas.d.ts.map +1 -0
  28. package/dist/schemas/events.schemas.js +265 -0
  29. package/dist/schemas/events.schemas.js.map +1 -0
  30. package/dist/schemas/index.d.ts +3 -0
  31. package/dist/schemas/index.d.ts.map +1 -0
  32. package/dist/schemas/index.js +4 -0
  33. package/dist/schemas/index.js.map +1 -0
  34. package/dist/schemas/kadi.schemas.d.ts +70 -0
  35. package/dist/schemas/kadi.schemas.d.ts.map +1 -0
  36. package/dist/schemas/kadi.schemas.js +120 -0
  37. package/dist/schemas/kadi.schemas.js.map +1 -0
  38. package/dist/transports/BrokerTransport.d.ts +106 -0
  39. package/dist/transports/BrokerTransport.d.ts.map +1 -0
  40. package/dist/transports/BrokerTransport.js +177 -0
  41. package/dist/transports/BrokerTransport.js.map +1 -0
  42. package/dist/transports/NativeTransport.d.ts +82 -0
  43. package/dist/transports/NativeTransport.d.ts.map +1 -0
  44. package/dist/transports/NativeTransport.js +263 -0
  45. package/dist/transports/NativeTransport.js.map +1 -0
  46. package/dist/transports/StdioTransport.d.ts +112 -0
  47. package/dist/transports/StdioTransport.d.ts.map +1 -0
  48. package/dist/transports/StdioTransport.js +445 -0
  49. package/dist/transports/StdioTransport.js.map +1 -0
  50. package/dist/transports/Transport.d.ts +93 -0
  51. package/dist/transports/Transport.d.ts.map +1 -0
  52. package/dist/transports/Transport.js +13 -0
  53. package/dist/transports/Transport.js.map +1 -0
  54. package/dist/types/broker.d.ts +31 -0
  55. package/dist/types/broker.d.ts.map +1 -0
  56. package/dist/types/broker.js +6 -0
  57. package/dist/types/broker.js.map +1 -0
  58. package/dist/types/core.d.ts +139 -0
  59. package/dist/types/core.d.ts.map +1 -0
  60. package/dist/types/core.js +26 -0
  61. package/dist/types/core.js.map +1 -0
  62. package/dist/types/events.d.ts +186 -0
  63. package/dist/types/events.d.ts.map +1 -0
  64. package/dist/types/events.js +16 -0
  65. package/dist/types/events.js.map +1 -0
  66. package/dist/types/index.d.ts +9 -0
  67. package/dist/types/index.d.ts.map +1 -0
  68. package/dist/types/index.js +13 -0
  69. package/dist/types/index.js.map +1 -0
  70. package/dist/types/protocol.d.ts +160 -0
  71. package/dist/types/protocol.d.ts.map +1 -0
  72. package/dist/types/protocol.js +5 -0
  73. package/dist/types/protocol.js.map +1 -0
  74. package/dist/utils/agentUtils.d.ts +187 -0
  75. package/dist/utils/agentUtils.d.ts.map +1 -0
  76. package/dist/utils/agentUtils.js +185 -0
  77. package/dist/utils/agentUtils.js.map +1 -0
  78. package/dist/utils/commandUtils.d.ts +45 -0
  79. package/dist/utils/commandUtils.d.ts.map +1 -0
  80. package/dist/utils/commandUtils.js +145 -0
  81. package/dist/utils/commandUtils.js.map +1 -0
  82. package/dist/utils/configUtils.d.ts +55 -0
  83. package/dist/utils/configUtils.d.ts.map +1 -0
  84. package/dist/utils/configUtils.js +100 -0
  85. package/dist/utils/configUtils.js.map +1 -0
  86. package/dist/utils/logger.d.ts +59 -0
  87. package/dist/utils/logger.d.ts.map +1 -0
  88. package/dist/utils/logger.js +122 -0
  89. package/dist/utils/logger.js.map +1 -0
  90. package/dist/utils/pathUtils.d.ts +48 -0
  91. package/dist/utils/pathUtils.d.ts.map +1 -0
  92. package/dist/utils/pathUtils.js +128 -0
  93. package/dist/utils/pathUtils.js.map +1 -0
  94. package/package.json +56 -5
  95. package/agent.json +0 -18
  96. package/examples/example-abilities/echo-js/README.md +0 -131
  97. package/examples/example-abilities/echo-js/agent.json +0 -63
  98. package/examples/example-abilities/echo-js/package.json +0 -24
  99. package/examples/example-abilities/echo-js/service.js +0 -43
  100. package/examples/example-abilities/hash-go/agent.json +0 -53
  101. package/examples/example-abilities/hash-go/cmd/hash_ability/main.go +0 -340
  102. package/examples/example-abilities/hash-go/go.mod +0 -3
  103. package/examples/example-agent/abilities/echo-js/0.0.1/README.md +0 -131
  104. package/examples/example-agent/abilities/echo-js/0.0.1/agent.json +0 -63
  105. package/examples/example-agent/abilities/echo-js/0.0.1/package-lock.json +0 -93
  106. package/examples/example-agent/abilities/echo-js/0.0.1/package.json +0 -24
  107. package/examples/example-agent/abilities/echo-js/0.0.1/service.js +0 -41
  108. package/examples/example-agent/abilities/hash-go/0.0.1/agent.json +0 -53
  109. package/examples/example-agent/abilities/hash-go/0.0.1/bin/hash_ability +0 -0
  110. package/examples/example-agent/abilities/hash-go/0.0.1/cmd/hash_ability/main.go +0 -340
  111. package/examples/example-agent/abilities/hash-go/0.0.1/go.mod +0 -3
  112. package/examples/example-agent/agent.json +0 -39
  113. package/examples/example-agent/index.js +0 -102
  114. package/examples/example-agent/package-lock.json +0 -93
  115. package/examples/example-agent/package.json +0 -17
  116. package/src/KadiAbility.js +0 -478
  117. package/src/index.js +0 -65
  118. package/src/loadAbility.js +0 -1086
  119. package/src/servers/BaseRpcServer.js +0 -404
  120. package/src/servers/BrokerRpcServer.js +0 -776
  121. package/src/servers/StdioRpcServer.js +0 -360
  122. package/src/transport/BrokerMessageBuilder.js +0 -377
  123. package/src/transport/IpcMessageBuilder.js +0 -1229
  124. package/src/utils/agentUtils.js +0 -137
  125. package/src/utils/commandUtils.js +0 -64
  126. package/src/utils/configUtils.js +0 -72
  127. package/src/utils/logger.js +0 -161
  128. package/src/utils/pathUtils.js +0 -86
@@ -1,1229 +0,0 @@
1
- // IpcMessageBuilder.js
2
- // ESM module
3
- // A unified message builder for Kadi ability communication supporting both
4
- // client-side (sending requests) and server-side (handling requests) operations.
5
- // Supports multiple transport modes: stdio (LSP-framed), broker (WebSocket), and custom.
6
-
7
- import { WebSocket } from 'ws';
8
- import { Broker, IdFactory } from './BrokerMessageBuilder.js';
9
- import crypto from 'node:crypto';
10
- //
11
- // CLIENT SIDE USAGE:
12
- // import { Ipc } from './IpcMessageBuilder.js';
13
- //
14
- // // Option A: bind once
15
- // const IPC = Ipc.with(rpc);
16
- // const initRes = await IPC.init({ api: '1.0' });
17
- // const added = await IPC.call('add', { a: 1, b: 2 });
18
- //
19
- // // Option B: inline (no binding)
20
- // await Ipc.init({ api: '1.0' }, rpc);
21
- // const sum = await Ipc.call('add', { a: 1, b: 2 }, rpc);
22
- //
23
- // SERVER SIDE USAGE:
24
- // import { IpcServer, StdioTransport } from './IpcMessageBuilder.js';
25
- //
26
- // const server = IpcServer.create()
27
- // .method('add', async ({ a, b }) => ({ result: a + b }))
28
- // .transport(new StdioTransport())
29
- // .serve();
30
- //
31
- // ──────────────────────────────────────────────────────────────────────────────
32
-
33
- import { EventEmitter } from 'events';
34
- import { Readable, Writable } from 'stream';
35
-
36
- // Frame protocol constants - single source of truth
37
- const FRAME_HEADERS = {
38
- CONTENT_LENGTH: 'Kadi-Content-Length',
39
- CONTENT_TYPE: 'Content-Type'
40
- };
41
-
42
- const FRAME_VALUES = {
43
- CONTENT_TYPE_VALUE: 'application/kadi-jsonrpc; charset=utf-8',
44
- HEADER_DELIMITER: '\r\n\r\n'
45
- };
46
-
47
- /** Central wire method map (override via configureMethods). */
48
- const methodNames = {
49
- init: '__kadi_init',
50
- discover: '__kadi_discover'
51
- };
52
-
53
- // ──────────────────────────────────────────────────────────────────────────────
54
- // TRANSPORT LAYER CLASSES (extracted from KadiAbility)
55
- // ──────────────────────────────────────────────────────────────────────────────
56
-
57
- /**
58
- * Frame reader for LSP-style stdio communication with corruption recovery
59
- */
60
- export class StdioFrameReader {
61
- constructor(input = process.stdin, options = {}) {
62
- this.input = input;
63
- this.buffer = Buffer.alloc(0);
64
- this.maxBufferSize = options.maxBufferSize || 8 * 1024 * 1024; // 8MB default
65
-
66
- // Pre-compile our delimiter buffers for efficiency
67
- this.HEADER_DELIM = Buffer.from('\r\n\r\n');
68
- this.CONTENT_LENGTH_MARKER = Buffer.from('Kadi-Content-Length:');
69
- }
70
-
71
- /**
72
- * Set up event-driven message processing
73
- */
74
- onMessage(callback) {
75
- this.input.on('data', (chunk) => {
76
- this.buffer = Buffer.concat([this.buffer, chunk]);
77
-
78
- if (this.buffer.length > this.maxBufferSize) {
79
- this._trimBuffer();
80
- }
81
-
82
- // Try to extract messages from the buffer
83
- let result;
84
- while ((result = this._extractMessage())) {
85
- callback(result);
86
- }
87
- });
88
-
89
- this.input.on('end', () => {
90
- // Process any remaining messages in buffer
91
- let result;
92
- while ((result = this._extractMessage())) {
93
- callback(result);
94
- }
95
- });
96
- }
97
-
98
- _extractMessage() {
99
- const contentLengthPos = this.buffer.indexOf(this.CONTENT_LENGTH_MARKER);
100
- if (contentLengthPos === -1) return null;
101
-
102
- if (contentLengthPos > 0) {
103
- const garbage = this.buffer.subarray(0, contentLengthPos);
104
- if (garbage.length > 100) {
105
- const preview =
106
- garbage.length > 50
107
- ? garbage.subarray(0, 50).toString('utf8').replace(/\n/g, '\\n') +
108
- '...'
109
- : garbage.toString('utf8').replace(/\n/g, '\\n');
110
- console.error(
111
- `[StdioFrameReader] Skipping ${garbage.length} bytes of garbage: "${preview}"`
112
- );
113
- }
114
- this.buffer = this.buffer.subarray(contentLengthPos);
115
- }
116
-
117
- const headerEndPos = this.buffer.indexOf(this.HEADER_DELIM);
118
- if (headerEndPos === -1) return null;
119
-
120
- const headerBytes = this.buffer.subarray(0, headerEndPos);
121
- const headerText = headerBytes.toString('ascii');
122
- const headers = this._parseHeaders(headerText);
123
-
124
- const contentLength = parseInt(
125
- headers[FRAME_HEADERS.CONTENT_LENGTH] || '0',
126
- 10
127
- );
128
- if (!Number.isFinite(contentLength) || contentLength < 0) {
129
- console.error(
130
- `[StdioFrameReader] Invalid Content-Length: "${headers[FRAME_HEADERS.CONTENT_LENGTH]}" - discarding frame`
131
- );
132
- this.buffer = this.buffer.subarray(
133
- headerEndPos + this.HEADER_DELIM.length
134
- );
135
- return {
136
- success: false,
137
- error: 'INVALID_CONTENT_LENGTH',
138
- message: `Invalid Content-Length: "${headers[FRAME_HEADERS.CONTENT_LENGTH]}"`,
139
- corruption_type: 'WRONG_LENGTH',
140
- content_length: headers[FRAME_HEADERS.CONTENT_LENGTH],
141
- raw: headerBytes.subarray(0, 100)
142
- };
143
- }
144
-
145
- const bodyStart = headerEndPos + this.HEADER_DELIM.length;
146
- const bodyEnd = bodyStart + contentLength;
147
-
148
- if (this.buffer.length < bodyEnd) return null; // Wait for more body bytes
149
-
150
- const bodyBytes = this.buffer.subarray(bodyStart, bodyEnd);
151
- this.buffer = this.buffer.subarray(bodyEnd);
152
-
153
- try {
154
- const data = JSON.parse(bodyBytes.toString('utf8'));
155
- return {
156
- success: true,
157
- data
158
- };
159
- } catch (err) {
160
- console.error(
161
- `[StdioFrameReader] Failed to parse JSON body: ${err.message}`
162
- );
163
- console.error(
164
- `[StdioFrameReader] Body preview: ${bodyBytes.subarray(0, 100).toString('utf8')}...`
165
- );
166
- return {
167
- success: false,
168
- error: 'JSON_PARSE_FAILED',
169
- message: err.message,
170
- corruption_type: 'INVALID_JSON',
171
- content_length: parseInt(
172
- headers[FRAME_HEADERS.CONTENT_LENGTH] || '0',
173
- 10
174
- ),
175
- actual_length: bodyBytes.length,
176
- raw: bodyBytes.subarray(0, 200) // First 200 bytes for debugging
177
- };
178
- }
179
- }
180
-
181
- _parseHeaders(headerText) {
182
- const headers = {};
183
- const lines = headerText.split('\r\n');
184
- for (const line of lines) {
185
- const [name, value] = line.split(/:\s*/);
186
- if (name && value) {
187
- headers[name] = value;
188
- }
189
- }
190
- return headers;
191
- }
192
-
193
- _trimBuffer() {
194
- console.warn(
195
- `[StdioFrameReader] Buffer size (${this.buffer.length}) exceeds max (${this.maxBufferSize}), trimming...`
196
- );
197
- const lastMarkerPos = this.buffer.lastIndexOf(this.CONTENT_LENGTH_MARKER);
198
- if (lastMarkerPos > this.maxBufferSize / 2) {
199
- const keepFrom = Math.max(lastMarkerPos - 1024, 0);
200
- this.buffer = this.buffer.subarray(keepFrom);
201
- console.warn(
202
- `[StdioFrameReader] Trimmed to ${this.buffer.length} bytes, preserved from last frame marker`
203
- );
204
- } else {
205
- const keepBytes = Math.floor(this.maxBufferSize / 2);
206
- this.buffer = this.buffer.subarray(this.buffer.length - keepBytes);
207
- console.warn(
208
- `[StdioFrameReader] Emergency trim: kept last ${keepBytes} bytes`
209
- );
210
- }
211
- }
212
- }
213
-
214
- /**
215
- * Frame writer for LSP-style stdio communication
216
- */
217
- export class StdioFrameWriter {
218
- constructor(output = process.stdout) {
219
- this.output = output;
220
- }
221
-
222
- async write(message) {
223
- const body = Buffer.from(JSON.stringify(message), 'utf-8');
224
- const header = `${FRAME_HEADERS.CONTENT_LENGTH}: ${body.length}\r\n${FRAME_HEADERS.CONTENT_TYPE}: ${FRAME_VALUES.CONTENT_TYPE_VALUE}\r\n\r\n`;
225
-
226
- // Make the write atomic by combining header and body into a single buffer
227
- const fullFrame = Buffer.concat([Buffer.from(header, 'ascii'), body]);
228
-
229
- await this.writeBuffer(fullFrame);
230
- }
231
-
232
- writeBuffer(buffer) {
233
- return new Promise((resolve, reject) => {
234
- this.output.write(buffer, (err) => {
235
- if (err) reject(err);
236
- else resolve();
237
- });
238
- });
239
- }
240
- }
241
-
242
- /** Allow consumers to override one or more method strings. */
243
- export function configureMethods(overrides = {}) {
244
- for (const [k, v] of Object.entries(overrides)) {
245
- if (!(k in methodNames)) throw new Error(`Unknown method key: ${k}`);
246
- if (typeof v !== 'string' || !v.length)
247
- throw new Error(`Bad method string for ${k}`);
248
- methodNames[k] = v;
249
- }
250
- return { ...methodNames };
251
- }
252
-
253
- /**
254
- * Lightweight, chainable message builder that *doesn't* manage ids.
255
- * It mirrors your transport, which already assigns ids on `rpc.call`.
256
- */
257
- class MsgBuilder {
258
- /** @param {string} method */
259
- constructor(method) {
260
- this._method = method;
261
- this._params = undefined;
262
- this._isNotification = false;
263
- }
264
-
265
- /** Attach/override params. */
266
- params(obj) {
267
- this._params = obj ?? undefined;
268
- return this;
269
- }
270
-
271
- /** Mark as a notification (fire‑and‑forget). */
272
- asNotification(on = true) {
273
- this._isNotification = !!on;
274
- return this;
275
- }
276
-
277
- /** Plain call tuple for your `rpc.call(method, params)` */
278
- forCall() {
279
- return { method: this._method, params: this._params };
280
- }
281
-
282
- /** Plain notify tuple for your `rpc.notify(method, params)` */
283
- forNotify() {
284
- return { method: this._method, params: this._params };
285
- }
286
-
287
- /** Convenience: send with your existing rpc. */
288
- send(rpc) {
289
- const { method, params } = this.forCall();
290
- return rpc.call(method, params);
291
- }
292
- notifyWith(rpc) {
293
- const { method, params } = this.forNotify();
294
- return rpc.notify(method, params);
295
- }
296
-
297
- /** Optional: build a raw JSON-RPC object (supply your own id). */
298
- toJsonRpc(id) {
299
- const obj = { jsonrpc: '2.0', method: this._method };
300
- if (this._params !== undefined) obj.params = this._params;
301
- if (!this._isNotification) obj.id = id;
302
- return obj;
303
- }
304
- /** Convenience: full LSP frame (headers + body) as a string. */
305
- toJsonRpcFrame(id) {
306
- const body = JSON.stringify(this.toJsonRpc(id));
307
- return (
308
- `Kadi-Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n` + body
309
- );
310
- }
311
- }
312
-
313
- // ──────────────────────────────────────────────────────────────────────────────
314
- // TRANSPORT ABSTRACTION
315
- // ──────────────────────────────────────────────────────────────────────────────
316
-
317
- /**
318
- * Base transport interface that all transports must implement
319
- */
320
- export class BaseTransport extends EventEmitter {
321
- /**
322
- * Start listening for incoming messages
323
- * @returns {AsyncIterator} Async iterator of incoming messages
324
- */
325
- async *listen() {
326
- throw new Error('Transport must implement listen()');
327
- }
328
-
329
- /**
330
- * Send a message
331
- * @param {Object} message - JSON-RPC message to send
332
- */
333
- async send(message) {
334
- throw new Error('Transport must implement send()');
335
- }
336
-
337
- /**
338
- * Close the transport
339
- */
340
- async close() {
341
- // Default implementation - transports can override
342
- }
343
- }
344
-
345
- /**
346
- * Stdio transport using LSP framing
347
- */
348
- export class StdioTransport extends BaseTransport {
349
- constructor(options = {}) {
350
- super();
351
- this.reader = new StdioFrameReader(options.stdin, options);
352
- this.writer = new StdioFrameWriter(options.stdout);
353
- this._messageQueue = [];
354
- this._listeners = [];
355
- }
356
-
357
- async *listen() {
358
- // Set up the message handler
359
- this.reader.onMessage((result) => {
360
- if (!result.success) {
361
- // Frame parsing failed - emit error and skip
362
- this.emit(
363
- 'error',
364
- new Error(`Frame corruption: ${result.error} - ${result.message}`)
365
- );
366
- return;
367
- }
368
-
369
- this._messageQueue.push(result.data);
370
- this._notifyListeners();
371
- });
372
-
373
- // Yield messages as they arrive
374
- while (true) {
375
- if (this._messageQueue.length > 0) {
376
- yield this._messageQueue.shift();
377
- } else {
378
- // Wait for the next message
379
- await new Promise((resolve) => {
380
- this._listeners.push(resolve);
381
- });
382
- }
383
- }
384
- }
385
-
386
- _notifyListeners() {
387
- const listeners = this._listeners.splice(0);
388
- listeners.forEach((resolve) => resolve());
389
- }
390
-
391
- async send(message) {
392
- await this.writer.write(message);
393
- }
394
-
395
- /**
396
- * Start serving requests using this transport
397
- *
398
- * Creates an internal IpcServer instance and delegates serving to it.
399
- * This makes StdioTransport self-contained like BrokerTransport.
400
- *
401
- * @param {Object} ability - KadiAbility instance with handlers and metadata
402
- */
403
- async serve(ability) {
404
- if (!ability) {
405
- throw new Error('StdioTransport.serve() requires an ability instance');
406
- }
407
-
408
- // Create an internal IpcServer with the ability's metadata
409
- const server = new IpcServer({
410
- name: ability.name,
411
- version: ability.version,
412
- description: ability.description
413
- });
414
-
415
- // Copy all handlers from the ability to the server
416
- for (const [methodName, handler] of ability._handlers) {
417
- server.method(methodName, handler);
418
- }
419
-
420
- // Set this transport and start serving
421
- server.transport(this);
422
-
423
- // Forward events from server to ability
424
- server.on('start', (data) => ability.emit('start', data));
425
- server.on('error', (error) => ability.emit('error', error));
426
- server.on('stop', () => ability.emit('stop'));
427
- server.on('request', (data) => ability.emit('request', data));
428
- server.on('response', (data) => ability.emit('response', data));
429
-
430
- await server.serve();
431
- }
432
- }
433
-
434
- /**
435
- * Broker transport using native broker protocol
436
- *
437
- * This transport connects abilities to the Kadi broker via WebSocket
438
- * and speaks the broker's native protocol directly (agent.message/ability.result),
439
- * bypassing JSON-RPC translation entirely.
440
- */
441
- export class BrokerTransport extends BaseTransport {
442
- constructor(options = {}) {
443
- super(); // Call BaseTransport constructor
444
-
445
- this.brokerUrl = options.brokerUrl || 'ws://localhost:8080';
446
- this.abilityName = options.abilityName || 'unnamed-ability';
447
- this.abilityVersion = options.abilityVersion || '1.0.0';
448
- this.description = options.description || 'A Kadi ability';
449
- this.ability = options.ability || null; // Reference to KadiAbility instance
450
-
451
- this._ws = null;
452
- this._isConnected = false;
453
- this._idFactory = new IdFactory();
454
- this._isServing = false;
455
- this._requestMetadata = new Map(); // Store broker metadata by request ID
456
- }
457
-
458
- /**
459
- * Start serving - connects to broker and handles incoming tool calls directly
460
- * This method preserves the original working behavior while making BrokerTransport
461
- * inherit from BaseTransport for consistency.
462
- *
463
- * @param {Object} ability - KadiAbility instance with handlers and metadata
464
- */
465
- async serve(ability) {
466
- // If ability is passed, use it; otherwise fall back to constructor option
467
- if (ability) {
468
- this.ability = ability;
469
- }
470
- console.error('[BrokerTransport] Starting broker service...');
471
-
472
- // Connect to broker and complete handshake
473
- await this._connect();
474
- await this._handshake();
475
-
476
- this._isServing = true;
477
-
478
- // Set up message handler for incoming ability calls
479
- this._ws.on('message', async (data) => {
480
- try {
481
- const message = JSON.parse(data.toString());
482
- console.error(
483
- `[BrokerTransport] Received message:`,
484
- JSON.stringify(message, null, 2)
485
- );
486
-
487
- // Handle incoming tool calls directly using the ability reference
488
- if (message.method === 'agent.message' && message.params) {
489
- console.error(
490
- '[BrokerTransport] Detected agent.message, handling tool call...'
491
- );
492
- await this._handleToolCallDirect(message);
493
- } else if (message.method === 'ability.result') {
494
- // This would be responses to tools we called - not needed for basic abilities
495
- console.error(
496
- '[BrokerTransport] Received ability.result (not handling)'
497
- );
498
- } else {
499
- console.error(
500
- '[BrokerTransport] Message not handled:',
501
- message.method
502
- );
503
- }
504
- } catch (err) {
505
- console.error(
506
- '[BrokerTransport] Failed to parse message:',
507
- err.message
508
- );
509
- }
510
- });
511
-
512
- // Keep the connection alive
513
- this._startHeartbeat();
514
-
515
- console.error(
516
- '[BrokerTransport] Broker service started, ready to receive tool calls'
517
- );
518
-
519
- // Keep the process alive
520
- return new Promise((resolve, reject) => {
521
- this._ws.on('close', () => {
522
- this._isServing = false;
523
- console.error('[BrokerTransport] Connection closed');
524
- resolve();
525
- });
526
-
527
- this._ws.on('error', (err) => {
528
- this._isServing = false;
529
- console.error('[BrokerTransport] WebSocket error:', err);
530
- reject(err);
531
- });
532
- });
533
- }
534
-
535
- /**
536
- * Listen for incoming broker messages and yield them as JSON-RPC requests
537
- *
538
- * This method connects to the broker, performs handshake, and then yields
539
- * incoming tool calls as standard JSON-RPC requests that can be handled
540
- * by the IpcServer.serve() loop.
541
- */
542
- async *listen() {
543
- // Ensure we're connected and registered
544
- if (!this._isConnected) {
545
- await this._connect();
546
- await this._handshake();
547
- }
548
-
549
- console.error('[BrokerTransport] Starting message listening...');
550
-
551
- // Create a queue for incoming messages
552
- const messageQueue = [];
553
- const listeners = [];
554
-
555
- // Set up message handler for incoming ability calls
556
- this._ws.on('message', async (data) => {
557
- try {
558
- const message = JSON.parse(data.toString());
559
-
560
- // Handle incoming tool calls - convert to JSON-RPC format
561
- if (message.method === 'agent.message' && message.params) {
562
- const { toolName, args, requestId, from } = message.params;
563
-
564
- console.error(`[BrokerTransport] Received tool call: ${toolName}`);
565
-
566
- // Store broker metadata for response handling
567
- this._requestMetadata.set(requestId, { requestId, from });
568
-
569
- // Convert broker message to JSON-RPC request format
570
- const jsonRpcRequest = {
571
- jsonrpc: '2.0',
572
- id: requestId, // Use broker's requestId as JSON-RPC id
573
- method: toolName,
574
- params: args || {}
575
- };
576
-
577
- messageQueue.push(jsonRpcRequest);
578
- this._notifyListeners(listeners);
579
- }
580
- // Handle other broker messages (ping responses, etc.)
581
- else if (message.method === 'ability.result') {
582
- console.error(
583
- '[BrokerTransport] Received ability.result (not handling in listen)'
584
- );
585
- } else {
586
- console.error(
587
- '[BrokerTransport] Message not handled:',
588
- message.method
589
- );
590
- }
591
- } catch (err) {
592
- console.error(
593
- '[BrokerTransport] Failed to parse message:',
594
- err.message
595
- );
596
- }
597
- });
598
-
599
- // Keep the connection alive
600
- this._startHeartbeat();
601
-
602
- console.error('[BrokerTransport] Ready to receive tool calls via listen()');
603
-
604
- // Yield messages as they arrive
605
- while (true) {
606
- if (messageQueue.length > 0) {
607
- yield messageQueue.shift();
608
- } else {
609
- // Wait for the next message
610
- await new Promise((resolve) => {
611
- listeners.push(resolve);
612
- });
613
- }
614
- }
615
- }
616
-
617
- /**
618
- * Send response back to broker using the broker protocol
619
- *
620
- * Converts JSON-RPC response back to broker protocol format and sends it.
621
- * Uses the broker metadata stored in the original request to route the response.
622
- */
623
- async send(message) {
624
- if (!this._ws || !this._isConnected) {
625
- throw new Error('BrokerTransport: WebSocket not connected');
626
- }
627
-
628
- // If this is a JSON-RPC response, convert it to broker protocol
629
- if (message.jsonrpc === '2.0' && message.id !== undefined) {
630
- // Get the broker metadata for this request
631
- const brokerMeta = this._requestMetadata.get(message.id);
632
- if (!brokerMeta) {
633
- console.error(
634
- `[BrokerTransport] No broker metadata found for request ${message.id}`
635
- );
636
- return;
637
- }
638
-
639
- // Clean up the metadata after use
640
- this._requestMetadata.delete(message.id);
641
-
642
- let brokerResponse;
643
-
644
- if (message.error) {
645
- // Error response
646
- brokerResponse = {
647
- jsonrpc: '2.0',
648
- method: 'ability.result',
649
- params: {
650
- requestId: brokerMeta.requestId,
651
- toSessionId: brokerMeta.from,
652
- error: message.error.message || 'Unknown error'
653
- }
654
- };
655
- } else {
656
- // Success response
657
- brokerResponse = {
658
- jsonrpc: '2.0',
659
- method: 'ability.result',
660
- params: {
661
- requestId: brokerMeta.requestId,
662
- toSessionId: brokerMeta.from,
663
- result: message.result
664
- }
665
- };
666
- }
667
-
668
- console.error(
669
- `[BrokerTransport] Sending broker response:`,
670
- JSON.stringify(brokerResponse, null, 2)
671
- );
672
-
673
- this._ws.send(JSON.stringify(brokerResponse));
674
- } else {
675
- // Fallback: send the message directly as JSON-RPC over WebSocket
676
- this._ws.send(JSON.stringify(message));
677
- }
678
- }
679
-
680
- /**
681
- * Start heartbeat to keep connection alive
682
- */
683
- _startHeartbeat() {
684
- setInterval(() => {
685
- if (this._isConnected && this._ws.readyState === 1) {
686
- this._ws.send(
687
- JSON.stringify({
688
- jsonrpc: '2.0',
689
- method: 'kadi.ping'
690
- })
691
- );
692
- }
693
- }, 25000); // 25 seconds as per agentA.js
694
- }
695
-
696
- /**
697
- * Close the broker connection
698
- */
699
- async close() {
700
- if (this._ws) {
701
- this._isConnected = false;
702
- this._isServing = false;
703
- this._ws.close();
704
- this._ws = null;
705
- }
706
- }
707
-
708
- /**
709
- * Handle incoming tool calls from the broker using the original direct approach
710
- * This preserves the working behavior while keeping the new listen()/send() methods available
711
- */
712
- async _handleToolCallDirect(message) {
713
- const { toolName, args, requestId, from } = message.params;
714
-
715
- console.error(`[BrokerTransport] Handling tool call: ${toolName}`);
716
-
717
- try {
718
- // Get the handler directly from the ability
719
- if (!this.ability || !this.ability._handlers) {
720
- throw new Error('No ability or handlers available');
721
- }
722
-
723
- const handler = this.ability._handlers.get(toolName);
724
- if (!handler) {
725
- throw new Error(`Tool '${toolName}' not found`);
726
- }
727
-
728
- // Call the handler directly with the arguments
729
- console.error(
730
- `[BrokerTransport] Calling handler for ${toolName} with args:`,
731
- args
732
- );
733
- const result = await handler(args || {});
734
- console.error(`[BrokerTransport] Handler result:`, result);
735
-
736
- // Send the result back to the broker using native broker protocol
737
- const response = {
738
- jsonrpc: '2.0',
739
- method: 'ability.result',
740
- params: {
741
- requestId,
742
- toSessionId: from,
743
- result
744
- }
745
- };
746
-
747
- console.error(
748
- `[BrokerTransport] Sending response:`,
749
- JSON.stringify(response, null, 2)
750
- );
751
- this._ws.send(JSON.stringify(response));
752
- } catch (error) {
753
- console.error(`[BrokerTransport] Error handling tool call:`, error);
754
-
755
- // Send error response
756
- const errorResponse = {
757
- jsonrpc: '2.0',
758
- method: 'ability.result',
759
- params: {
760
- requestId,
761
- toSessionId: from,
762
- error: error.message
763
- }
764
- };
765
-
766
- this._ws.send(JSON.stringify(errorResponse));
767
- }
768
- }
769
-
770
- /**
771
- * Helper function to notify listeners waiting for messages
772
- */
773
- _notifyListeners(listeners) {
774
- const listenersToNotify = listeners.splice(0);
775
- listenersToNotify.forEach((resolve) => resolve());
776
- }
777
-
778
- async _connect() {
779
- return new Promise((resolve, reject) => {
780
- this._ws = new WebSocket(this.brokerUrl);
781
-
782
- this._ws.on('open', () => {
783
- this._isConnected = true;
784
- console.error(
785
- `[BrokerTransport] Connected to broker at ${this.brokerUrl}`
786
- );
787
- resolve();
788
- });
789
-
790
- this._ws.on('error', (err) => {
791
- console.error('[BrokerTransport] WebSocket error:', err.message || err);
792
- console.error(
793
- '[BrokerTransport] Failed to connect to broker at:',
794
- this.brokerUrl
795
- );
796
- reject(
797
- new Error(
798
- `Failed to connect to broker at ${this.brokerUrl}: ${err.message || err}`
799
- )
800
- );
801
- });
802
-
803
- this._ws.on('close', () => {
804
- this._isConnected = false;
805
- console.error('[BrokerTransport] Disconnected from broker');
806
- });
807
- });
808
- }
809
-
810
- async _handshake() {
811
- // Step 1: Send hello as agent (abilities connect as agents in the current broker)
812
- const hello = Broker.hello({ role: 'agent' })
813
- .id(this._idFactory.next())
814
- .build();
815
- this._ws.send(JSON.stringify(hello));
816
-
817
- // Wait for hello response with nonce
818
- const helloResponse = await this._waitForResponse(hello.id);
819
- if (!helloResponse.result || !helloResponse.result.nonce) {
820
- throw new Error('Invalid hello response from broker');
821
- }
822
-
823
- const nonce = helloResponse.result.nonce;
824
- console.error(`[BrokerTransport] Received nonce: ${nonce}`);
825
-
826
- // Step 2: Generate ephemeral keys and authenticate
827
- const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
828
- const publicKeyBase64 = publicKey
829
- .export({ format: 'der', type: 'spki' })
830
- .toString('base64');
831
-
832
- const signature = crypto
833
- .sign(null, Buffer.from(nonce), privateKey)
834
- .toString('base64');
835
-
836
- const authenticate = Broker.authenticate({
837
- publicKeyBase64Der: publicKeyBase64,
838
- privateKey, // This will be used to generate signature
839
- nonce,
840
- wantNewId: true
841
- })
842
- .id(this._idFactory.next())
843
- .build();
844
-
845
- console.error(`[BrokerTransport] Sending authentication...`);
846
- this._ws.send(JSON.stringify(authenticate));
847
-
848
- // Wait for authentication response
849
- const authResponse = await this._waitForResponse(authenticate.id);
850
- if (!authResponse.result || !authResponse.result.agentId) {
851
- throw new Error(
852
- `Authentication failed: ${JSON.stringify(authResponse.error)}`
853
- );
854
- }
855
-
856
- console.error(
857
- `[BrokerTransport] Authenticated as agent: ${authResponse.result.agentId}`
858
- );
859
-
860
- // Step 3: Register capabilities (extract tools from ability)
861
- if (
862
- this.ability &&
863
- typeof this.ability.extractToolsForBroker === 'function'
864
- ) {
865
- try {
866
- console.error('[BrokerTransport] Extracting tools from ability...');
867
- const tools = await this.ability.extractToolsForBroker();
868
- console.error(
869
- `[BrokerTransport] Extracted tools:`,
870
- JSON.stringify(tools, null, 2)
871
- );
872
-
873
- if (tools.length > 0) {
874
- const registerCapabilities = Broker.registerCapabilities({
875
- displayName: this.abilityName,
876
- tools,
877
- mailboxMode: 'persistent',
878
- scopes: ['global']
879
- })
880
- .id(this._idFactory.next())
881
- .build();
882
-
883
- console.error(
884
- `[BrokerTransport] Sending registration:`,
885
- JSON.stringify(registerCapabilities, null, 2)
886
- );
887
- this._ws.send(JSON.stringify(registerCapabilities));
888
-
889
- // Wait for registration response
890
- const registrationResponse = await this._waitForResponse(
891
- registerCapabilities.id
892
- );
893
- console.error(
894
- `[BrokerTransport] Registration response:`,
895
- JSON.stringify(registrationResponse, null, 2)
896
- );
897
-
898
- console.error(
899
- `[BrokerTransport] Registered ${tools.length} capabilities with broker`
900
- );
901
- } else {
902
- console.error(
903
- '[BrokerTransport] No tools found, skipping capability registration'
904
- );
905
- }
906
- } catch (err) {
907
- console.error(
908
- `[BrokerTransport] Failed to register capabilities: ${err.message}`
909
- );
910
- console.error(`[BrokerTransport] Error details:`, err);
911
- throw err;
912
- }
913
- } else {
914
- console.error(
915
- '[BrokerTransport] No ability reference or extractToolsForBroker method not found'
916
- );
917
- }
918
-
919
- console.error(
920
- '[BrokerTransport] Handshake completed, ready to receive calls'
921
- );
922
-
923
- // Note: Authentication step would go here in a production system
924
- // For now, abilities connect without explicit authentication
925
- }
926
-
927
- async _waitForResponse(messageId, timeoutMs = 5000) {
928
- return new Promise((resolve, reject) => {
929
- const timeout = setTimeout(() => {
930
- reject(
931
- new Error(`Timeout waiting for response to message ${messageId}`)
932
- );
933
- }, timeoutMs);
934
-
935
- const handler = (data) => {
936
- try {
937
- const message = JSON.parse(data.toString());
938
- if (message.id === messageId) {
939
- clearTimeout(timeout);
940
- this._ws.off('message', handler);
941
- resolve(message);
942
- }
943
- } catch (err) {
944
- // Ignore parsing errors for non-matching messages
945
- }
946
- };
947
-
948
- this._ws.on('message', handler);
949
- });
950
- }
951
-
952
- _notifyListeners() {
953
- const listeners = this._listeners.splice(0);
954
- listeners.forEach((resolve) => resolve());
955
- }
956
- }
957
-
958
- // ──────────────────────────────────────────────────────────────────────────────
959
- // SERVER-SIDE COMPONENTS
960
- // ──────────────────────────────────────────────────────────────────────────────
961
-
962
- /**
963
- * Server-side message builders for responses
964
- */
965
- export const IpcResponse = {
966
- /** Build a success response */
967
- success(id, result) {
968
- return { jsonrpc: '2.0', id, result };
969
- },
970
-
971
- /** Build an error response */
972
- error(id, code, message, data = null) {
973
- const error = { code, message };
974
- if (data !== null) error.data = data;
975
- return { jsonrpc: '2.0', id, error };
976
- },
977
-
978
- /** Common error responses */
979
- methodNotFound(id, method) {
980
- return this.error(id, -32601, `Method not found: ${method}`);
981
- },
982
-
983
- parseError(id) {
984
- return this.error(id, -32700, 'Parse error');
985
- },
986
-
987
- internalError(id, message, data) {
988
- return this.error(id, -32000, message || 'Internal error', data);
989
- }
990
- };
991
-
992
- /**
993
- * Simple ability server that can work with any transport
994
- */
995
- export class IpcServer extends EventEmitter {
996
- constructor(options = {}) {
997
- super();
998
- this.name = options.name || 'unnamed-ability';
999
- this.version = options.version || '1.0.0';
1000
- this.description = options.description || '';
1001
- this._handlers = new Map();
1002
- this._transport = null;
1003
-
1004
- // Register built-in discovery methods
1005
- this._registerBuiltins();
1006
- }
1007
-
1008
- /**
1009
- * Register a method handler
1010
- */
1011
- method(name, handler) {
1012
- if (typeof name === 'object') {
1013
- // Batch registration
1014
- for (const [methodName, methodHandler] of Object.entries(name)) {
1015
- if (typeof methodHandler !== 'function') {
1016
- throw new TypeError(
1017
- `Handler for method "${methodName}" must be a function`
1018
- );
1019
- }
1020
- this._handlers.set(methodName, methodHandler);
1021
- }
1022
- } else {
1023
- if (typeof handler !== 'function') {
1024
- throw new TypeError(`Handler for method "${name}" must be a function`);
1025
- }
1026
- this._handlers.set(name, handler);
1027
- }
1028
- return this; // Chainable
1029
- }
1030
-
1031
- /**
1032
- * Set the transport for this server
1033
- */
1034
- transport(transport) {
1035
- this._transport = transport;
1036
- return this;
1037
- }
1038
-
1039
- /**
1040
- * Start serving requests
1041
- */
1042
- async serve() {
1043
- if (!this._transport) {
1044
- throw new Error(
1045
- 'No transport configured. Use .transport(transport) first.'
1046
- );
1047
- }
1048
-
1049
- this.emit('start', {
1050
- name: this.name,
1051
- version: this.version,
1052
- methods: Array.from(this._handlers.keys()).filter(
1053
- (m) => !m.startsWith('__kadi_')
1054
- )
1055
- });
1056
-
1057
- try {
1058
- for await (const request of this._transport.listen()) {
1059
- await this._handleRequest(request);
1060
- }
1061
- } catch (error) {
1062
- this.emit('error', error);
1063
- throw error;
1064
- } finally {
1065
- this.emit('stop');
1066
- }
1067
- }
1068
-
1069
- /**
1070
- * Handle a single request
1071
- */
1072
- async _handleRequest(request) {
1073
- try {
1074
- const { id, method, params = {} } = request;
1075
-
1076
- this.emit('request', { id, method, params });
1077
-
1078
- const handler = this._handlers.get(method);
1079
- if (!handler) {
1080
- const response = IpcResponse.methodNotFound(id, method);
1081
- await this._transport.send(response);
1082
- this.emit('response', { id, error: response.error });
1083
- return;
1084
- }
1085
-
1086
- try {
1087
- const result = await handler(params);
1088
- const response = IpcResponse.success(id, result);
1089
- await this._transport.send(response);
1090
- this.emit('response', { id, result });
1091
- } catch (handlerError) {
1092
- const response = IpcResponse.internalError(
1093
- id,
1094
- handlerError.message,
1095
- handlerError.stack
1096
- );
1097
- await this._transport.send(response);
1098
- this.emit('response', { id, error: response.error });
1099
- this.emit('handler-error', { method, error: handlerError });
1100
- }
1101
- } catch (protocolError) {
1102
- this.emit('protocol-error', protocolError);
1103
-
1104
- if (request?.id) {
1105
- try {
1106
- const response = IpcResponse.parseError(request.id);
1107
- await this._transport.send(response);
1108
- } catch {
1109
- // Ignore write errors - connection might be broken
1110
- }
1111
- }
1112
- }
1113
- }
1114
-
1115
- /**
1116
- * Register built-in discovery methods
1117
- */
1118
- _registerBuiltins() {
1119
- this._handlers.set('__kadi_init', async () => ({
1120
- name: this.name,
1121
- version: this.version,
1122
- description: this.description,
1123
- functions: this._getFunctionDescriptions()
1124
- }));
1125
-
1126
- this._handlers.set('__kadi_discover', async () => ({
1127
- functions: this._getFunctionDescriptions()
1128
- }));
1129
- }
1130
-
1131
- /**
1132
- * Get function descriptions for discovery
1133
- */
1134
- _getFunctionDescriptions() {
1135
- const functions = {};
1136
- for (const [name, handler] of this._handlers) {
1137
- if (name.startsWith('__kadi_')) continue;
1138
-
1139
- functions[name] = {
1140
- description: handler.description || `Handler for ${name}`,
1141
- inputSchema: handler.inputSchema || { type: 'object' },
1142
- outputSchema: handler.outputSchema || { type: 'object' }
1143
- };
1144
- }
1145
- return functions;
1146
- }
1147
-
1148
- /**
1149
- * Add metadata to a handler function
1150
- */
1151
- describe(handler, metadata) {
1152
- Object.assign(handler, metadata);
1153
- return handler;
1154
- }
1155
-
1156
- /**
1157
- * Static factory method
1158
- */
1159
- static create(options) {
1160
- return new IpcServer(options);
1161
- }
1162
- }
1163
-
1164
- // ──────────────────────────────────────────────────────────────────────────────
1165
- // CLIENT-SIDE COMPONENTS
1166
- // ──────────────────────────────────────────────────────────────────────────────
1167
- export const Ipc = {
1168
- /** __kadi_init — Initialize the child process context. */
1169
- init(params = {}, rpc) {
1170
- const b = new MsgBuilder(methodNames.init).params(params);
1171
- return rpc ? b.send(rpc) : b;
1172
- },
1173
-
1174
- /** __kadi_discover — Ask the child to enumerate capabilities. */
1175
- discover(params = {}, rpc) {
1176
- const b = new MsgBuilder(methodNames.discover).params(params);
1177
- return rpc ? b.send(rpc) : b;
1178
- },
1179
-
1180
- /** Call any ability method explicitly. */
1181
- call(method, params = {}, rpc) {
1182
- if (!method) throw new Error('Ipc.call: method is required');
1183
- const b = new MsgBuilder(String(method)).params(params);
1184
- return rpc ? b.send(rpc) : b;
1185
- },
1186
-
1187
- /** Notification (fire‑and‑forget). */
1188
- notify(method, params = {}, rpc) {
1189
- if (!method) throw new Error('Ipc.notify: method is required');
1190
- const b = new MsgBuilder(String(method))
1191
- .params(params)
1192
- .asNotification(true);
1193
- return rpc ? b.notifyWith(rpc) : b;
1194
- },
1195
-
1196
- /** Bind your rpc once and get call‑through functions. */
1197
- with(rpc) {
1198
- return {
1199
- init: (params = {}) => Ipc.init(params, rpc),
1200
- discover: (params = {}) => Ipc.discover(params, rpc),
1201
- call: (method, params = {}) => Ipc.call(method, params, rpc),
1202
- notify: (method, params = {}) => Ipc.notify(method, params, rpc)
1203
- };
1204
- }
1205
- };
1206
-
1207
- const IpcMessageBuilder = {
1208
- // Client-side
1209
- MsgBuilder,
1210
- Ipc,
1211
-
1212
- // Server-side
1213
- IpcServer,
1214
- IpcResponse,
1215
-
1216
- // Transport layer
1217
- BaseTransport,
1218
- StdioTransport,
1219
- BrokerTransport,
1220
- StdioFrameReader,
1221
- StdioFrameWriter,
1222
-
1223
- // Configuration
1224
- configureMethods,
1225
- _methodNames: methodNames
1226
- };
1227
-
1228
- export default IpcMessageBuilder;
1229
- export { FRAME_HEADERS, FRAME_VALUES };