@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,776 +0,0 @@
1
- import { BaseRpcServer } from './BaseRpcServer.js';
2
- import { Broker, IdFactory } from '../transport/BrokerMessageBuilder.js';
3
- import { WebSocket } from 'ws';
4
- import crypto from 'node:crypto';
5
- import { createComponentLogger } from '../utils/logger.js';
6
- /**
7
- * RPC Server for broker transport using WebSocket
8
- *
9
- * This server connects abilities to the Kadi broker via WebSocket and speaks
10
- * the broker's native protocol directly (agent.message/ability.result),
11
- * providing distributed ability execution across the network.
12
- */
13
- export class BrokerRpcServer extends BaseRpcServer {
14
- /**
15
- * Create a new BrokerRpcServer instance
16
- *
17
- * @param {Object} options - Configuration options
18
- * @param {string} options.brokerUrl - Broker WebSocket URL
19
- * @param {string} options.serviceName - Service name for broker registration
20
- * @param {number} options.connectTimeoutMs - Connection timeout
21
- * @param {number} options.heartbeatIntervalMs - Heartbeat interval
22
- */
23
- constructor(options = {}) {
24
- super({ ...options, protocol: 'broker' });
25
-
26
- this.brokerUrl =
27
- options.brokerUrl || process.env.KADI_BROKER_URL || 'ws://localhost:8080';
28
- this.serviceName =
29
- options.serviceName || process.env.KADI_SERVICE_NAME || 'unnamed-ability';
30
- this.connectTimeoutMs = options.connectTimeoutMs || 10000;
31
- this.heartbeatIntervalMs = options.heartbeatIntervalMs || 25000;
32
-
33
- this.logger = createComponentLogger('BrokerRpcServer');
34
- this.logger.lifecycle('constructor', 'BrokerRpcServer initialized');
35
- this.logger.trace('constructor', `Broker URL: ${this.brokerUrl}`);
36
- this.logger.trace('constructor', `Service name: ${this.serviceName}`);
37
- this.logger.trace(
38
- 'constructor',
39
- `Connect timeout: ${this.connectTimeoutMs}ms`
40
- );
41
-
42
- this._ws = null;
43
- this._isConnected = false;
44
- this._idFactory = new IdFactory();
45
- this._requestMetadata = new Map(); // Store broker metadata by request ID
46
- this._heartbeatTimer = null;
47
- this._reconnectAttempts = 0;
48
- this._maxReconnectAttempts = options.maxReconnectAttempts || 5;
49
- this._reconnectDelayMs = options.reconnectDelayMs || 1000;
50
-
51
- this.ability = null;
52
- this.resolve = null;
53
- this.reject = null;
54
- }
55
-
56
- /**
57
- * Start serving the ability via broker
58
- *
59
- * @param {KadiAbility} ability - The ability instance to serve
60
- * @returns {Promise<void>} - Promise that resolves when server stops
61
- */
62
- async serve(ability) {
63
- if (this.isServing) {
64
- this.logger.error('serve', 'BrokerRpcServer is already serving');
65
- throw new Error('BrokerRpcServer is already serving');
66
- }
67
-
68
- this.ability = ability;
69
- this.isServing = true;
70
-
71
- this.logger.lifecycle(
72
- 'serve',
73
- `Starting ${ability.name || 'unnamed ability'} on broker`
74
- );
75
- this.logger.info('serve', `Broker URL: ${this.brokerUrl}`);
76
- this.logger.info('serve', `Service name: ${this.serviceName}`);
77
- this.logger.info(
78
- 'serve',
79
- `Available methods: ${ability.getMethodNames().join(', ')}`
80
- );
81
-
82
- try {
83
- // Connect to broker and complete handshake
84
- await this._connect();
85
- await this._handshake();
86
-
87
- // Emit start event
88
- this.emit('start', {
89
- name: ability.name,
90
- version: ability.version,
91
- methods: ability.getMethodNames()
92
- });
93
-
94
- this.logger.success(
95
- 'serve',
96
- 'Broker service started, ready to receive tool calls'
97
- );
98
-
99
- // Keep the process alive and handle messages
100
- return new Promise((resolve, reject) => {
101
- this.resolve = resolve;
102
- this.reject = reject;
103
- });
104
- } catch (error) {
105
- this.isServing = false;
106
- this.logger.error(
107
- 'serve',
108
- `Failed to start broker service: ${error.message}`
109
- );
110
- this.emit('error', error);
111
- throw error;
112
- }
113
- }
114
-
115
- /**
116
- * Publish an event to connected agents via the broker
117
- *
118
- * Events are sent as 'kadi.event' messages through the WebSocket connection.
119
- * The broker will route these to subscribed agents.
120
- *
121
- * @param {string} eventName - Name of the event to publish
122
- * @param {any} data - Event data payload (must be JSON-serializable)
123
- */
124
- async publishEvent(eventName, data = {}) {
125
- this.logger.trace(
126
- 'publishEvent',
127
- `Publishing event via broker: ${eventName}`
128
- );
129
-
130
- if (!this._ws || !this._isConnected) {
131
- this.logger.warn(
132
- 'publishEvent',
133
- `Cannot publish event ${eventName}: not connected to broker`
134
- );
135
- return;
136
- }
137
-
138
- try {
139
- // Send event as a broker notification (no id field)
140
- const eventMessage = {
141
- jsonrpc: '2.0',
142
- method: 'kadi.event',
143
- params: {
144
- eventName,
145
- eventData: data,
146
- timestamp: Date.now(),
147
- from: this.serviceName // Include source for routing
148
- }
149
- };
150
-
151
- this._ws.send(JSON.stringify(eventMessage));
152
-
153
- this.logger.trace('publishEvent', `Event ${eventName} sent to broker`);
154
- } catch (error) {
155
- // Events are best-effort - log but don't throw
156
- this.logger.warn(
157
- 'publishEvent',
158
- `Failed to publish event ${eventName}: ${error.message}`
159
- );
160
- }
161
- }
162
-
163
- /**
164
- * Handle requests in broker-native format
165
- *
166
- * This implementation handles incoming tool calls from the broker and
167
- * converts them to internal method calls. Broker requests come as
168
- * agent.message notifications with tool execution parameters.
169
- *
170
- * @param {Object} message - The broker message object
171
- */
172
- async _handleBrokerMessage(message) {
173
- try {
174
- this.logger.trace(
175
- 'message',
176
- `Received broker message: ${message.method}`
177
- );
178
- this.logger.trace('message-detail', JSON.stringify(message, null, 2));
179
-
180
- // Handle incoming tool calls
181
- if (message.method === 'agent.message' && message.params) {
182
- await this._handleToolCall(message);
183
- }
184
- // Handle ability results (responses to our tool calls)
185
- else if (message.method === 'ability.result') {
186
- this.logger.trace('message', 'Received ability.result (not handling)');
187
- }
188
- // Handle other broker messages
189
- else {
190
- this.logger.warn(
191
- 'message',
192
- `Unhandled broker message: ${message.method}`
193
- );
194
- this.logger.trace('message-detail', JSON.stringify(message, null, 2));
195
- }
196
- } catch (error) {
197
- this.logger.error(
198
- 'message',
199
- `Failed to handle broker message: ${error.message}`
200
- );
201
- this.logError('Failed to handle broker message:', error);
202
- this.emit('error', error);
203
- }
204
- }
205
-
206
- /**
207
- * Handle incoming tool calls from the broker
208
- *
209
- * @param {Object} message - Broker message containing tool call
210
- */
211
- async _handleToolCall(message) {
212
- const { toolName, args, requestId, from } = message.params;
213
-
214
- this.logger.request(requestId, toolName, 'Handling tool call');
215
- this.logger.trace(
216
- 'broker-tool-call',
217
- `From: ${from}, Args: ${JSON.stringify(args)}`
218
- );
219
-
220
- try {
221
- // Convert broker message to internal request format
222
- const request = {
223
- id: requestId,
224
- method: toolName,
225
- params: args || {}
226
- };
227
-
228
- // Store broker metadata for response routing
229
- this._requestMetadata.set(requestId, { requestId, from });
230
-
231
- // Process the request using base class logic
232
- const response = await this.handleRequest(request);
233
-
234
- // Send response back to broker
235
- await this._sendBrokerResponse(requestId, from, response);
236
- } catch (error) {
237
- this.logger.error(
238
- 'tool-call',
239
- `Error handling tool call ${toolName}: ${error.message}`
240
- );
241
-
242
- this.logError(`Error handling tool call ${toolName}:`, error);
243
-
244
- // Send error response to broker
245
- await this._sendBrokerError(requestId, from, error.message);
246
- }
247
- }
248
-
249
- /**
250
- * Send a successful result back to the broker
251
- *
252
- * @param {string} requestId - Original request ID
253
- * @param {string} toSessionId - Target session ID
254
- * @param {Object} response - Response object
255
- */
256
- async _sendBrokerResponse(requestId, toSessionId, response) {
257
- if (!response) {
258
- // Notification - no response needed
259
- this.logger.trace(
260
- 'response',
261
- `No response needed for notification ${requestId}`
262
- );
263
- return;
264
- }
265
-
266
- const brokerResponse = {
267
- jsonrpc: '2.0',
268
- method: 'ability.result',
269
- params: {
270
- requestId,
271
- toSessionId,
272
- result: response.result
273
- }
274
- };
275
-
276
- this.logger.response(requestId, 'success', 'Sending broker response');
277
- this.logger.trace(
278
- 'response-detail',
279
- JSON.stringify(brokerResponse, null, 2)
280
- );
281
-
282
- this._ws.send(JSON.stringify(brokerResponse));
283
-
284
- // Clean up metadata
285
- this._requestMetadata.delete(requestId);
286
- }
287
-
288
- /**
289
- * Send an error result back to the broker
290
- *
291
- * @param {string} requestId - Original request ID
292
- * @param {string} toSessionId - Target session ID
293
- * @param {string} errorMessage - Error message
294
- */
295
- async _sendBrokerError(requestId, toSessionId, errorMessage) {
296
- const brokerResponse = {
297
- jsonrpc: '2.0',
298
- method: 'ability.result',
299
- params: {
300
- requestId,
301
- toSessionId,
302
- error: errorMessage
303
- }
304
- };
305
-
306
- this.logger.response(
307
- requestId,
308
- 'error',
309
- `Sending broker error: ${errorMessage}`
310
- );
311
- this.logger.trace(
312
- 'broker-response-detail',
313
- JSON.stringify(brokerResponse, null, 2)
314
- );
315
-
316
- this._ws.send(JSON.stringify(brokerResponse));
317
-
318
- // Clean up metadata
319
- this._requestMetadata.delete(requestId);
320
- }
321
-
322
- /**
323
- * Connect to the broker WebSocket
324
- */
325
- async _connect() {
326
- this.logger.info('connect', `Connecting to broker at ${this.brokerUrl}`);
327
-
328
- return new Promise((resolve, reject) => {
329
- const timeout = setTimeout(() => {
330
- this.logger.error(
331
- 'connect',
332
- `Connection timeout after ${this.connectTimeoutMs}ms`
333
- );
334
-
335
- reject(
336
- new Error(`Connection timeout after ${this.connectTimeoutMs}ms`)
337
- );
338
- }, this.connectTimeoutMs);
339
-
340
- this._ws = new WebSocket(this.brokerUrl);
341
-
342
- this._ws.on('open', () => {
343
- clearTimeout(timeout);
344
- this._isConnected = true;
345
- this._reconnectAttempts = 0;
346
- this.logger.success(
347
- 'connect',
348
- `Connected to broker at ${this.brokerUrl}`
349
- );
350
- resolve();
351
- });
352
-
353
- this._ws.on('error', (error) => {
354
- clearTimeout(timeout);
355
- this.logger.error(
356
- 'connect',
357
- `WebSocket connection error: ${error.message || error}`
358
- );
359
- this.logError('WebSocket connection error:', error.message);
360
-
361
- reject(new Error(`Failed to connect to broker: ${error.message}`));
362
- });
363
-
364
- this._ws.on('close', (code, reason) => {
365
- this._isConnected = false;
366
- this.logger.warn(
367
- 'connect',
368
- `Disconnected from broker (${code}): ${reason}`
369
- );
370
-
371
- if (
372
- this.isServing &&
373
- this._reconnectAttempts < this._maxReconnectAttempts
374
- ) {
375
- this._attemptReconnect();
376
- } else {
377
- this.shutdown('broker_disconnected');
378
- }
379
- });
380
-
381
- this._ws.on('message', (data) => {
382
- try {
383
- const message = JSON.parse(data.toString());
384
- this._handleBrokerMessage(message);
385
- } catch (error) {
386
- this.logError('Failed to parse broker message:', error);
387
- }
388
- });
389
- });
390
- }
391
-
392
- /**
393
- * Attempt to reconnect to the broker
394
- */
395
- async _attemptReconnect() {
396
- this._reconnectAttempts++;
397
- const delay =
398
- this._reconnectDelayMs * Math.pow(2, this._reconnectAttempts - 1);
399
-
400
- this.logger.info(
401
- 'reconnect',
402
- `Attempting to reconnect (${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms`
403
- );
404
-
405
- setTimeout(async () => {
406
- try {
407
- await this._connect();
408
- await this._handshake();
409
- this.logger.success('reconnect', 'Successfully reconnected to broker');
410
- } catch (error) {
411
- this.logger.error('reconnect', `Reconnection failed: ${error.message}`);
412
- if (this._reconnectAttempts >= this._maxReconnectAttempts) {
413
- this.shutdown('max_reconnect_attempts_reached');
414
- }
415
- }
416
- }, delay);
417
- }
418
-
419
- /**
420
- * Complete the broker handshake
421
- */
422
- async _handshake() {
423
- this.logger.info('handshake', 'Starting broker handshake');
424
-
425
- // Step 1: Send hello as agent
426
- const hello = Broker.hello({ role: 'agent' })
427
- .id(this._idFactory.next())
428
- .build();
429
- this._ws.send(JSON.stringify(hello));
430
-
431
- // Wait for hello response with nonce
432
- const helloResponse = await this._waitForResponse(hello.id);
433
- if (!helloResponse.result || !helloResponse.result.nonce) {
434
- this.logger.trace('handshake', 'Received nonce from broker');
435
-
436
- throw new Error('Invalid hello response from broker');
437
- }
438
-
439
- const nonce = helloResponse.result.nonce;
440
- this.log(`Received nonce from broker`);
441
-
442
- // Step 2: Generate ephemeral keys and authenticate
443
- const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
444
- const publicKeyBase64 = publicKey
445
- .export({ format: 'der', type: 'spki' })
446
- .toString('base64');
447
-
448
- const authenticate = Broker.authenticate({
449
- publicKeyBase64Der: publicKeyBase64,
450
- privateKey,
451
- nonce,
452
- wantNewId: true
453
- })
454
- .id(this._idFactory.next())
455
- .build();
456
-
457
- this.logger.trace('handshake', 'Sending authentication to broker');
458
- this._ws.send(JSON.stringify(authenticate));
459
-
460
- // Wait for authentication response
461
- const authResponse = await this._waitForResponse(authenticate.id);
462
- if (!authResponse.result || !authResponse.result.agentId) {
463
- this.logger.error(
464
- 'handshake',
465
- `Authentication failed: ${JSON.stringify(authResponse.error)}`
466
- );
467
-
468
- throw new Error(
469
- `Authentication failed: ${JSON.stringify(authResponse.error)}`
470
- );
471
- }
472
-
473
- this.logger.success(
474
- 'handshake',
475
- `Authenticated as agent: ${authResponse.result.agentId}`
476
- );
477
-
478
- // Step 3: Register capabilities
479
- await this._registerCapabilities();
480
-
481
- // Step 4: Start heartbeat
482
- this._startHeartbeat();
483
-
484
- this.logger.success('handshake', 'Handshake completed successfully');
485
- }
486
-
487
- /**
488
- * Register capabilities with the broker
489
- */
490
- async _registerCapabilities() {
491
- if (
492
- !this.ability ||
493
- typeof this.ability.extractToolsForBroker !== 'function'
494
- ) {
495
- this.logger.warn(
496
- 'capabilities',
497
- 'No ability reference or extractToolsForBroker method not found'
498
- );
499
-
500
- return;
501
- }
502
-
503
- try {
504
- this.logger.info('capabilities', 'Extracting tools from ability');
505
- const tools = await this.ability.extractToolsForBroker();
506
- // this.log(`Extracted tools:`, JSON.stringify(tools, null, 2));
507
-
508
- if (tools.length > 0) {
509
- const registerCapabilities = Broker.registerCapabilities({
510
- displayName: this.serviceName,
511
- tools,
512
- mailboxMode: 'persistent',
513
- scopes: [this.ability.scope] // We want to register the tools in the same scope as the agent
514
- })
515
- .id(this._idFactory.next())
516
- .build();
517
-
518
- this._ws.send(JSON.stringify(registerCapabilities));
519
-
520
- // Wait for registration response
521
- const registrationResponse = await this._waitForResponse(
522
- registerCapabilities.id
523
- );
524
- this.logger.trace(
525
- 'capabilities',
526
- `Registration response: ${JSON.stringify(registrationResponse, null, 2)}`
527
- );
528
- this.logger.success(
529
- 'capabilities',
530
- `Registered ${tools.length} capabilities with broker`
531
- );
532
- } else {
533
- this.logger.warn(
534
- 'capabilities',
535
- 'No tools found, skipping capability registration'
536
- );
537
- }
538
- } catch (error) {
539
- this.logger.error(
540
- 'capabilities',
541
- `Failed to register capabilities: ${error.message}`
542
- );
543
- throw error;
544
- }
545
- }
546
-
547
- /**
548
- * Start heartbeat to keep connection alive
549
- */
550
- _startHeartbeat() {
551
- this.logger.trace(
552
- 'heartbeat',
553
- `Starting heartbeat every ${this.heartbeatIntervalMs}ms`
554
- );
555
-
556
- this._heartbeatTimer = setInterval(() => {
557
- if (this._isConnected && this._ws.readyState === WebSocket.OPEN) {
558
- if (this.logger.enabled) {
559
- this.logger.trace('heartbeat', 'Sending ping to broker');
560
- }
561
- this._ws.send(
562
- JSON.stringify({
563
- jsonrpc: '2.0',
564
- method: 'kadi.ping'
565
- })
566
- );
567
- }
568
- }, this.heartbeatIntervalMs);
569
- }
570
-
571
- /**
572
- * Wait for a response to a specific message
573
- *
574
- * @param {string|number} messageId - Message ID to wait for
575
- * @param {number} timeoutMs - Timeout in milliseconds
576
- * @returns {Promise<Object>} - Response message
577
- */
578
- async _waitForResponse(messageId, timeoutMs = 5000) {
579
- this.logger.trace(
580
- 'wait-response',
581
- `Waiting for response to message ${messageId} (timeout: ${timeoutMs}ms)`
582
- );
583
-
584
- return new Promise((resolve, reject) => {
585
- const timeout = setTimeout(() => {
586
- this._ws.off('message', handler);
587
- this.logger.error(
588
- 'wait-response',
589
- `Timeout waiting for response to message ${messageId}`
590
- );
591
-
592
- reject(
593
- new Error(`Timeout waiting for response to message ${messageId}`)
594
- );
595
- }, timeoutMs);
596
-
597
- const handler = (data) => {
598
- try {
599
- const message = JSON.parse(data.toString());
600
- if (message.id === messageId) {
601
- clearTimeout(timeout);
602
- this._ws.off('message', handler);
603
- this.logger.trace(
604
- 'wait-response',
605
- `Received response for message ${messageId}`
606
- );
607
-
608
- resolve(message);
609
- }
610
- } catch (error) {
611
- // Ignore parsing errors for non-matching messages
612
- }
613
- };
614
-
615
- this._ws.on('message', handler);
616
- });
617
- }
618
-
619
- /**
620
- * Gracefully shutdown the broker server
621
- *
622
- * @param {string} reason - Reason for shutdown
623
- */
624
- async shutdown(reason = 'unknown') {
625
- this.logger.lifecycle(
626
- 'shutdown',
627
- `Shutting down broker server, reason: ${reason}`
628
- );
629
-
630
- await super.shutdown(reason);
631
-
632
- // Stop heartbeat
633
- if (this._heartbeatTimer) {
634
- this.logger.trace('shutdown', 'Stopping heartbeat timer');
635
-
636
- clearInterval(this._heartbeatTimer);
637
- this._heartbeatTimer = null;
638
- }
639
-
640
- // Close WebSocket connection
641
- if (this._ws) {
642
- this._isConnected = false;
643
- this.logger.trace('shutdown', 'Closing WebSocket connection');
644
-
645
- this._ws.close();
646
- this._ws = null;
647
- }
648
-
649
- // Clear request metadata
650
- this._requestMetadata.clear();
651
-
652
- // Resolve the serve promise
653
- if (this.resolve) {
654
- this.logger.trace('shutdown', 'Resolving serve promise');
655
- this.resolve();
656
- }
657
- }
658
-
659
- /**
660
- * Send a message to the broker
661
- *
662
- * @param {Object} message - Message to send
663
- */
664
- async sendMessage(message) {
665
- if (!this._ws || !this._isConnected) {
666
- throw new Error('Not connected to broker');
667
- }
668
-
669
- this._ws.send(JSON.stringify(message));
670
- }
671
-
672
- /**
673
- * Call another ability through the broker
674
- *
675
- * @param {string} toolName - Name of the tool/ability to call
676
- * @param {Object} args - Arguments to pass
677
- * @returns {Promise<any>} - Result from the ability
678
- */
679
- async callAbility(toolName, args = {}) {
680
- this.logger.info('call-ability', `Calling ability: ${toolName}`);
681
- this.logger.trace('call-ability', `Args: ${JSON.stringify(args)}`);
682
-
683
- const callMessage = Broker.callAbility({ toolName, args })
684
- .id(this._idFactory.next())
685
- .build();
686
-
687
- const ack = await this._sendAndWaitForResponse(callMessage);
688
- if (ack.error) {
689
- this.logger.error(
690
- 'call-ability',
691
- `Call failed: ${ack.error.message || 'agent.callAbility failed'}`
692
- );
693
-
694
- throw new Error(ack.error.message || 'agent.callAbility failed');
695
- }
696
-
697
- const expectedRequestId = ack.result?.requestId;
698
- this.logger.trace(
699
- 'call-ability',
700
- `Waiting for ability result, requestId: ${expectedRequestId}`
701
- );
702
-
703
- return new Promise((resolve, reject) => {
704
- const timeout = setTimeout(() => {
705
- this._ws.off('message', handler);
706
- this.logger.error(
707
- 'call-ability',
708
- `Ability call timeout for ${toolName}`
709
- );
710
-
711
- reject(new Error('Ability call timeout'));
712
- }, this.timeoutMs);
713
-
714
- const handler = (data) => {
715
- try {
716
- const message = JSON.parse(data.toString());
717
- if (message.method === 'ability.result') {
718
- const { requestId, result, error } = message.params || {};
719
- if (requestId === expectedRequestId) {
720
- clearTimeout(timeout);
721
- this._ws.off('message', handler);
722
- if (error) {
723
- this.logger.error(
724
- 'call-ability',
725
- `Ability ${toolName} returned error: ${error.message || error}`
726
- );
727
-
728
- reject(new Error(error.message || 'Ability error'));
729
- } else {
730
- this.logger.success(
731
- 'call-ability',
732
- `Ability ${toolName} completed successfully`
733
- );
734
-
735
- resolve(result);
736
- }
737
- }
738
- }
739
- } catch (parseError) {
740
- // Ignore parsing errors
741
- }
742
- };
743
-
744
- this._ws.on('message', handler);
745
- });
746
- }
747
-
748
- /**
749
- * Send a message and wait for its response
750
- *
751
- * @param {Object} message - Message to send
752
- * @returns {Promise<Object>} - Response message
753
- */
754
- async _sendAndWaitForResponse(message) {
755
- this.logger.trace(
756
- 'send-wait',
757
- `Sending message and waiting for response: ${message.method || 'unknown'}`
758
- );
759
-
760
- this._ws.send(JSON.stringify(message));
761
- const response = await this._waitForResponse(message.id);
762
-
763
- if (response.error) {
764
- this.logger.warn(
765
- 'send-wait',
766
- `Response contained error: ${JSON.stringify(response.error)}`
767
- );
768
- } else {
769
- this.logger.trace('send-wait', 'Response received successfully');
770
- }
771
-
772
- return response;
773
- }
774
- }
775
-
776
- export default BrokerRpcServer;