@kadi.build/core 0.0.1-alpha.2 → 0.0.1-alpha.4

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 (99) hide show
  1. package/README.md +1122 -216
  2. package/dist/KadiClient.d.ts +303 -0
  3. package/dist/KadiClient.d.ts.map +1 -0
  4. package/dist/KadiClient.js +1162 -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 +65 -0
  15. package/dist/loadAbility.d.ts.map +1 -0
  16. package/dist/loadAbility.js +335 -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 +127 -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 +96 -0
  39. package/dist/transports/BrokerTransport.d.ts.map +1 -0
  40. package/dist/transports/BrokerTransport.js +145 -0
  41. package/dist/transports/BrokerTransport.js.map +1 -0
  42. package/dist/transports/NativeTransport.d.ts +92 -0
  43. package/dist/transports/NativeTransport.d.ts.map +1 -0
  44. package/dist/transports/NativeTransport.js +221 -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 +440 -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 +138 -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 +102 -0
  75. package/dist/utils/agentUtils.d.ts.map +1 -0
  76. package/dist/utils/agentUtils.js +166 -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 +58 -5
  95. package/agent.json +0 -18
  96. package/broker.js +0 -214
  97. package/index.js +0 -382
  98. package/ipc.js +0 -220
  99. package/ipcInterfaces/pythonAbilityIPC.py +0 -177
@@ -0,0 +1,1162 @@
1
+ import { EventEmitter } from 'events';
2
+ import WebSocket from 'ws';
3
+ import crypto from 'node:crypto';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { StdioTransport } from './transports/StdioTransport.js';
7
+ import { createComponentLogger } from './utils/logger.js';
8
+ import { IdFactory, KadiMessages } from './messages/BrokerMessages.js';
9
+ import { loadAbility } from './loadAbility.js';
10
+ // Internal event transport constants
11
+ // These are NOT user events - they're internal mechanisms for transporting events between protocols
12
+ /**
13
+ * ABILITY_EVENT_TRANSPORT - Internal event used to transport events from loaded abilities
14
+ * to the main client. When an ability (loaded via native/stdio) publishes an event like
15
+ * 'echo.test-event', we wrap it in this transport event to move it from the ability
16
+ * context to the main client context, where it can be unwrapped and delivered to
17
+ * user subscriptions.
18
+ *
19
+ * Think of it like an envelope: The actual event ('echo.test-event') is the letter,
20
+ * and ABILITY_EVENT_TRANSPORT is the envelope we put it in for delivery.
21
+ */
22
+ const ABILITY_EVENT_TRANSPORT = '__KADI_INTERNAL_ABILITY_EVENT_TRANSPORT__';
23
+ /**
24
+ * ProtocolHandlerFactory - Creates the right handler for each communication protocol
25
+ */
26
+ class ProtocolHandlerFactory {
27
+ static create(protocol, options = {}) {
28
+ switch (protocol?.toLowerCase()) {
29
+ case 'native':
30
+ // No handler needed - direct method calls
31
+ return undefined;
32
+ case 'stdio':
33
+ // Use StdioTransport for child process communication
34
+ return new StdioTransport({
35
+ abilityName: options.name || 'unnamed-client',
36
+ abilityVersion: options.version || '1.0.0',
37
+ abilityDir: process.cwd(),
38
+ startCmd: 'node index.js',
39
+ manifest: options
40
+ });
41
+ case 'broker':
42
+ // For broker protocol, we handle it directly in KadiClient
43
+ // No separate handler needed as KadiClient has built-in broker support
44
+ return undefined;
45
+ default:
46
+ return undefined;
47
+ }
48
+ }
49
+ }
50
+ /**
51
+ * KadiClient - Unified client for KADI protocol
52
+ *
53
+ * This class combines the functionality of KadiAgent and KadiAbility,
54
+ * providing a single interface for:
55
+ *
56
+ * 1. Registering tools that can be called by others
57
+ * 2. Calling remote tools on other services
58
+ * 3. Supporting multiple protocols (native, stdio, broker)
59
+ * 4. Connecting to broker with configurable role (agent, ability, service)
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * // Create as an ability
64
+ * const service = new KadiClient({
65
+ * name: 'math-service',
66
+ * role: 'ability',
67
+ * protocol: 'broker'
68
+ * });
69
+ *
70
+ * // Register tools
71
+ * service.registerTool('add', async ({a, b}) => ({result: a + b}));
72
+ *
73
+ * // With schema
74
+ * service.tool('multiply', async ({a, b}) => ({result: a * b}));
75
+ *
76
+ * // Start serving
77
+ * await service.serve();
78
+ * ```
79
+ */
80
+ export class KadiClient extends EventEmitter {
81
+ name;
82
+ version;
83
+ description;
84
+ role;
85
+ protocol;
86
+ scope;
87
+ networks;
88
+ brokerUrls;
89
+ abilityAgentJSON;
90
+ logger;
91
+ protocolHandler;
92
+ toolHandlers = new Map();
93
+ _abilities = new Map();
94
+ _brokerConnections = [];
95
+ _isConnected = false;
96
+ _agentId = '';
97
+ _idFactory;
98
+ _pendingResponses = new Map();
99
+ _pendingToolCalls = new Map();
100
+ _eventSubscriptions = new Map();
101
+ constructor(options = {}) {
102
+ super();
103
+ this.logger = createComponentLogger(`KadiClient:${options.name || 'unnamed'}`);
104
+ // Store configuration
105
+ this.name = options.name || 'unnamed-client';
106
+ this.version = options.version || '1.0.0';
107
+ this.description = options.description || '';
108
+ this.role = options.role || 'ability';
109
+ this.protocol =
110
+ options.protocol || process.env.KADI_PROTOCOL || 'native';
111
+ this.scope = options.scope || 'global';
112
+ this.networks = options.networks || ['global'];
113
+ this.brokerUrls =
114
+ options.brokerUrls || (options.brokerUrl ? [options.brokerUrl] : []);
115
+ this.abilityAgentJSON = options.abilityAgentJSON;
116
+ // Initialize ID factory
117
+ this._idFactory = new IdFactory();
118
+ this.logger.lifecycle('constructor', `Creating client: ${this.name}@${this.version} (role: ${this.role}, protocol: ${this.protocol})`);
119
+ // Create protocol handler for stdio (broker is handled internally)
120
+ if (this.protocol !== 'broker') {
121
+ this.protocolHandler = ProtocolHandlerFactory.create(this.protocol, options);
122
+ // Forward events from protocol handler
123
+ if (this.protocolHandler) {
124
+ this.protocolHandler.on('start', (data) => this.emit('start', { ...data, protocol: this.protocol }));
125
+ this.protocolHandler.on('error', (error) => this.emit('error', error));
126
+ this.protocolHandler.on('stop', () => this.emit('stop'));
127
+ this.protocolHandler.on('request', (data) => this.emit('request', data));
128
+ this.protocolHandler.on('response', (data) => this.emit('response', data));
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Register a tool for this service
134
+ *
135
+ * @param name - Tool name
136
+ * @param handler - Handler function
137
+ * @param schema - Optional schema
138
+ * @returns This instance for chaining
139
+ */
140
+ registerTool(name, handler, schema) {
141
+ if (typeof name !== 'string') {
142
+ throw new TypeError('Tool name must be a string');
143
+ }
144
+ if (typeof handler !== 'function') {
145
+ throw new TypeError('Tool handler must be a function');
146
+ }
147
+ this.logger.trace('registerTool', `Registering tool: ${name}`);
148
+ this.toolHandlers.set(name, {
149
+ name,
150
+ handler,
151
+ schema
152
+ });
153
+ return this;
154
+ }
155
+ /**
156
+ * Get all registered tool names
157
+ */
158
+ getToolNames() {
159
+ return Array.from(this.toolHandlers.keys()).filter((name) => !name.startsWith('__kadi_'));
160
+ }
161
+ /**
162
+ * Get all registered tools (agent compatibility)
163
+ */
164
+ getTools() {
165
+ return this.getToolNames();
166
+ }
167
+ /**
168
+ * Check if a tool is registered
169
+ */
170
+ hasTool(name) {
171
+ return this.toolHandlers.has(name);
172
+ }
173
+ /**
174
+ * Get tool handler
175
+ */
176
+ getToolHandler(name) {
177
+ const tool = this.toolHandlers.get(name);
178
+ return tool?.handler;
179
+ }
180
+ /**
181
+ * Get tool schema
182
+ */
183
+ getToolSchema(name) {
184
+ const tool = this.toolHandlers.get(name);
185
+ return tool?.schema;
186
+ }
187
+ /**
188
+ * Publish an event
189
+ */
190
+ publishEvent(eventName, data = {}) {
191
+ if (typeof eventName !== 'string' || !eventName) {
192
+ throw new TypeError('Event name must be a non-empty string');
193
+ }
194
+ this.logger.trace('publishEvent', `Publishing event: ${eventName}, protocol: ${this.protocol}, data: ${JSON.stringify(data)}`);
195
+ // For native protocol, emit directly using internal transport event
196
+ if (this.protocol === 'native') {
197
+ this.logger.debug('publishEvent', `Emitting ${ABILITY_EVENT_TRANSPORT} for native protocol`);
198
+ // Wrap the user event in our transport envelope
199
+ this.emit(ABILITY_EVENT_TRANSPORT, { name: eventName, data });
200
+ return;
201
+ }
202
+ // For stdio, delegate to handler
203
+ if (this.protocol === 'stdio') {
204
+ this.logger.trace('publishEvent', `Checking stdio handler: ${!!this.protocolHandler}, hasPublishEvent: ${typeof this.protocolHandler?.publishEvent}`);
205
+ if (this.protocolHandler &&
206
+ typeof this.protocolHandler.publishEvent === 'function') {
207
+ this.logger.debug('publishEvent', 'Delegating to stdio protocolHandler');
208
+ this.protocolHandler.publishEvent(eventName, data);
209
+ }
210
+ else {
211
+ this.logger.warn('publishEvent', `No protocolHandler available for stdio protocol - handler: ${!!this.protocolHandler}`);
212
+ }
213
+ return;
214
+ }
215
+ // For broker, send event message
216
+ if (this.protocol === 'broker' && this._brokerConnections.length > 0) {
217
+ this.logger.debug('publishEvent', `Publishing broker event: ${eventName}`);
218
+ const broker = this._brokerConnections[0];
219
+ if (broker.ws && broker.ws.readyState === WebSocket.OPEN) {
220
+ const eventMessage = {
221
+ jsonrpc: '2.0',
222
+ method: KadiMessages.EVENT_PUBLISH,
223
+ params: {
224
+ channel: eventName, // RabbitMQ calls this "routing key"
225
+ data
226
+ }
227
+ };
228
+ this.logger.trace('publishEvent', `Sending broker event message: ${JSON.stringify(eventMessage)}`);
229
+ broker.ws.send(JSON.stringify(eventMessage));
230
+ }
231
+ else {
232
+ this.logger.warn('publishEvent', 'Broker connection not ready');
233
+ }
234
+ }
235
+ else if (this.protocol === 'broker') {
236
+ this.logger.warn('publishEvent', 'No broker connections available');
237
+ }
238
+ }
239
+ /**
240
+ * Connect to brokers (for event subscription and/or broker protocol)
241
+ */
242
+ async connectToBrokers(urls) {
243
+ // Allow any protocol to connect to brokers for event subscription
244
+ // The protocol only determines how this client itself serves, not what it can connect to
245
+ const brokerUrls = urls || [
246
+ process.env.KADI_BROKER_URL || 'ws://localhost:8080'
247
+ ];
248
+ this.logger.info('connectToBrokers', `Connecting to ${brokerUrls.length} broker(s)`);
249
+ for (const url of brokerUrls) {
250
+ await this.connectToBroker(url);
251
+ }
252
+ this._isConnected = true;
253
+ this.emit('connected');
254
+ }
255
+ /**
256
+ * Connect to a single broker
257
+ */
258
+ async connectToBroker(url) {
259
+ this.logger.info('connectToBroker', `Connecting to broker at ${url}`);
260
+ const ws = new WebSocket(url);
261
+ const connection = {
262
+ url,
263
+ ws,
264
+ isAuthenticated: false,
265
+ agentId: '' // Use agentId instead of sessionId
266
+ };
267
+ return new Promise((resolve, reject) => {
268
+ const timeout = setTimeout(() => {
269
+ reject(new Error(`Connection timeout to ${url}`));
270
+ }, 10000);
271
+ ws.on('open', async () => {
272
+ clearTimeout(timeout);
273
+ this.logger.debug('connectToBroker', `WebSocket connected to ${url}`);
274
+ try {
275
+ // Perform handshake
276
+ await this.performHandshake(connection);
277
+ this._brokerConnections.push(connection);
278
+ resolve();
279
+ }
280
+ catch (error) {
281
+ reject(error);
282
+ }
283
+ });
284
+ ws.on('error', (error) => {
285
+ clearTimeout(timeout);
286
+ this.logger.error('connectToBroker', `WebSocket error: ${error.message}`);
287
+ reject(error);
288
+ });
289
+ ws.on('message', (data) => {
290
+ this.handleBrokerMessage(connection, data.toString());
291
+ });
292
+ ws.on('close', () => {
293
+ this.logger.info('connectToBroker', `Disconnected from ${url}`);
294
+ // Clean up heartbeat timer
295
+ if (connection.heartbeatTimer) {
296
+ clearInterval(connection.heartbeatTimer);
297
+ connection.heartbeatTimer = undefined;
298
+ this.logger.debug('connectToBroker', 'Heartbeat timer cleaned up');
299
+ }
300
+ const index = this._brokerConnections.indexOf(connection);
301
+ if (index !== -1) {
302
+ this._brokerConnections.splice(index, 1);
303
+ }
304
+ });
305
+ });
306
+ }
307
+ /**
308
+ * Perform KADI protocol handshake
309
+ */
310
+ async performHandshake(connection) {
311
+ this.logger.debug('performHandshake', 'Starting KADI protocol handshake');
312
+ // Generate ephemeral keys
313
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
314
+ // Step 1: Send hello
315
+ const helloMsg = {
316
+ jsonrpc: '2.0',
317
+ method: KadiMessages.SESSION_HELLO,
318
+ params: {
319
+ role: this.role,
320
+ version: this.version
321
+ },
322
+ id: this._idFactory.next()
323
+ };
324
+ const helloResponse = await this.sendRequest(connection, helloMsg);
325
+ const helloResult = helloResponse.result;
326
+ const nonce = helloResult?.nonce;
327
+ if (!nonce)
328
+ throw new Error('No nonce received');
329
+ // Store the heartbeat interval for later use
330
+ const heartbeatIntervalSec = helloResult?.heartbeatIntervalSec || 0;
331
+ // Step 2: Authenticate
332
+ const signature = crypto
333
+ .sign(null, Buffer.from(nonce), privateKey)
334
+ .toString('base64');
335
+ const authMsg = {
336
+ jsonrpc: '2.0',
337
+ method: KadiMessages.SESSION_AUTHENTICATE,
338
+ params: {
339
+ publicKey: publicKey
340
+ .export({ format: 'der', type: 'spki' })
341
+ .toString('base64'),
342
+ signature,
343
+ nonce,
344
+ wantNewId: true
345
+ },
346
+ id: this._idFactory.next()
347
+ };
348
+ const authResponse = await this.sendRequest(connection, authMsg);
349
+ const authResult = authResponse.result;
350
+ this._agentId = authResult?.agentId;
351
+ connection.isAuthenticated = true;
352
+ // Step 3: Register capabilities
353
+ await this.registerCapabilities(connection);
354
+ // Step 4: Start heartbeat if required
355
+ if (heartbeatIntervalSec > 0) {
356
+ this.startHeartbeat(connection, heartbeatIntervalSec);
357
+ }
358
+ this.logger.info('performHandshake', `Authenticated as ${this._agentId}`);
359
+ }
360
+ /**
361
+ * Start heartbeat to keep broker connection alive
362
+ * Sends ping messages at the specified interval to prevent timeout
363
+ */
364
+ startHeartbeat(connection, intervalSec) {
365
+ this.logger.debug('startHeartbeat', `Starting heartbeat with ${intervalSec}s interval`);
366
+ // Clear any existing heartbeat timer
367
+ if (connection.heartbeatTimer) {
368
+ clearInterval(connection.heartbeatTimer);
369
+ }
370
+ // Send ping at the specified interval
371
+ connection.heartbeatTimer = setInterval(() => {
372
+ if (connection.ws.readyState === WebSocket.OPEN) {
373
+ const pingMsg = {
374
+ jsonrpc: '2.0',
375
+ method: KadiMessages.SESSION_PING,
376
+ params: {}
377
+ };
378
+ const message = JSON.stringify(pingMsg);
379
+ connection.ws.send(message);
380
+ this.logger.trace('heartbeat', 'Sent ping to broker');
381
+ }
382
+ else {
383
+ // Connection closed, stop heartbeat
384
+ if (connection.heartbeatTimer) {
385
+ clearInterval(connection.heartbeatTimer);
386
+ connection.heartbeatTimer = undefined;
387
+ }
388
+ this.logger.debug('heartbeat', 'Connection closed, stopping heartbeat');
389
+ }
390
+ }, intervalSec * 1000);
391
+ }
392
+ /**
393
+ * Register capabilities with broker
394
+ */
395
+ async registerCapabilities(connection) {
396
+ const tools = await this.extractToolsForBroker();
397
+ const params = {
398
+ displayName: this.name,
399
+ networks: this.networks,
400
+ tools: tools
401
+ };
402
+ const registerMsg = {
403
+ jsonrpc: '2.0',
404
+ method: KadiMessages.AGENT_REGISTER,
405
+ params,
406
+ id: this._idFactory.next()
407
+ };
408
+ await this.sendRequest(connection, registerMsg);
409
+ this.logger.info('registerCapabilities', `Registered ${tools.length} tools`);
410
+ }
411
+ /**
412
+ * Send request to broker and wait for response
413
+ */
414
+ sendRequest(connection, message) {
415
+ return new Promise((resolve, reject) => {
416
+ const id = message.id;
417
+ // ! DO NOT USE CONSOLE.LOG use the the logger
418
+ console.log(`[KadiClient] 📤 SENDING REQUEST:`, JSON.stringify(message, null, 2));
419
+ const timeout = setTimeout(() => {
420
+ // ! DO NOT USE CONSOLE.LOG use the the logger
421
+ console.log(`[KadiClient] ⏰ TIMEOUT for request ${id} (${message.method})`);
422
+ this._pendingResponses.delete(id);
423
+ reject(new Error(`Request timeout: ${message.method}`));
424
+ }, 30000);
425
+ this._pendingResponses.set(id, {
426
+ resolve: resolve,
427
+ reject,
428
+ timer: timeout
429
+ });
430
+ // ! DO NOT USE CONSOLE.LOG use the the logger
431
+ console.log(`[KadiClient] 📝 STORED pending response for ID: ${id}`);
432
+ connection.ws.send(JSON.stringify(message));
433
+ });
434
+ }
435
+ /**
436
+ * Handle incoming broker messages
437
+ */
438
+ handleBrokerMessage(connection, data) {
439
+ try {
440
+ const message = JSON.parse(data);
441
+ this.logger?.trace('handleBrokerMessage', `Received message: ${JSON.stringify(message)}`);
442
+ // Handle responses to our requests
443
+ if ('id' in message &&
444
+ message.id !== null &&
445
+ this._pendingResponses.has(message.id)) {
446
+ this.logger?.trace('handleBrokerMessage', `Found pending response for ID: ${message.id}`);
447
+ const pending = this._pendingResponses.get(message.id);
448
+ clearTimeout(pending.timer);
449
+ this._pendingResponses.delete(message.id);
450
+ if ('error' in message && message.error) {
451
+ this.logger?.debug('handleBrokerMessage', `Error response: ${message.error.message}`);
452
+ pending.reject(new Error(`${message.error.code}: ${message.error.message}`));
453
+ }
454
+ else {
455
+ this.logger?.trace('handleBrokerMessage', `Success response for ID: ${message.id}`);
456
+ pending.resolve(message);
457
+ }
458
+ return;
459
+ }
460
+ else if ('id' in message && message.id !== null) {
461
+ // Only log if there's an ID but we don't recognize it (unexpected response)
462
+ this.logger?.warn('handleBrokerMessage', `Received response with unrecognized ID: ${message.id}`);
463
+ }
464
+ // For notifications (no ID), this is normal - no need to log
465
+ // Handle incoming method calls (both agent.invoke and kadi.ability.call)
466
+ // Handle incoming tool invocation requests
467
+ // Using ABILITY_INVOKE for all tool invocations (broker-to-agent and client-to-client)
468
+ if ('method' in message &&
469
+ message.method === KadiMessages.ABILITY_INVOKE) {
470
+ this.handleToolInvocation(connection, message);
471
+ return;
472
+ }
473
+ // Handle tool invocation results
474
+ if ('method' in message &&
475
+ message.method === KadiMessages.ABILITY_RESULT) {
476
+ this.handleToolResult(message);
477
+ return;
478
+ }
479
+ // Handle other notifications
480
+ if ('method' in message && !('id' in message)) {
481
+ this.emit('notification', message);
482
+ // Also emit for our unified event system
483
+ this.emit('broker:message', message);
484
+ }
485
+ }
486
+ catch (error) {
487
+ this.logger.error('handleBrokerMessage', `Failed to parse message: ${error}`);
488
+ }
489
+ }
490
+ /**
491
+ * Handle tool invocation result from broker
492
+ */
493
+ handleToolResult(message) {
494
+ const { requestId, result, error, toSessionId } = message.params;
495
+ // Check if this result is for us
496
+ if (toSessionId !== this._agentId) {
497
+ return; // Not for us
498
+ }
499
+ // Find pending tool call
500
+ if (this._pendingToolCalls.has(requestId)) {
501
+ const pending = this._pendingToolCalls.get(requestId);
502
+ clearTimeout(pending.timer);
503
+ this._pendingToolCalls.delete(requestId);
504
+ if (error) {
505
+ pending.reject(new Error(`Tool call failed: ${error.message || error}`));
506
+ }
507
+ else {
508
+ pending.resolve(result);
509
+ }
510
+ }
511
+ }
512
+ /**
513
+ * Handle incoming tool invocation request
514
+ */
515
+ async handleToolInvocation(connection, request) {
516
+ const { requestId, from, toolName, toolInput } = request.params;
517
+ const method = this.toolHandlers.get(toolName);
518
+ if (!method) {
519
+ // Send kadi.ability.result with error
520
+ const errorMessage = {
521
+ jsonrpc: '2.0',
522
+ method: KadiMessages.ABILITY_RESULT,
523
+ params: {
524
+ requestId,
525
+ toSessionId: from,
526
+ error: {
527
+ code: 'TOOL_NOT_FOUND',
528
+ message: `Tool ${toolName} not found`
529
+ }
530
+ }
531
+ };
532
+ connection.ws.send(JSON.stringify(errorMessage));
533
+ return;
534
+ }
535
+ try {
536
+ console.log(`[KadiClient] 🔧 EXECUTING TOOL: ${toolName} with params:`, toolInput);
537
+ const result = await method.handler(toolInput);
538
+ console.log(`[KadiClient] ✅ TOOL RESULT for ${toolName}:`, result);
539
+ // Send kadi.ability.result with success result
540
+ const resultMessage = {
541
+ jsonrpc: '2.0',
542
+ method: KadiMessages.ABILITY_RESULT,
543
+ params: {
544
+ requestId,
545
+ toSessionId: from,
546
+ result
547
+ }
548
+ };
549
+ console.log(`[KadiClient] 📤 SENDING RESULT:`, JSON.stringify(resultMessage, null, 2));
550
+ connection.ws.send(JSON.stringify(resultMessage));
551
+ }
552
+ catch (error) {
553
+ console.log(`[KadiClient] ❌ TOOL ERROR for ${toolName}:`, error);
554
+ // Send kadi.ability.result with error
555
+ const errorMessage = {
556
+ jsonrpc: '2.0',
557
+ method: KadiMessages.ABILITY_RESULT,
558
+ params: {
559
+ requestId,
560
+ toSessionId: from,
561
+ error: {
562
+ code: 'EXECUTION_ERROR',
563
+ message: error.message || 'Tool execution failed'
564
+ }
565
+ }
566
+ };
567
+ connection.ws.send(JSON.stringify(errorMessage));
568
+ }
569
+ }
570
+ /**
571
+ * Extract tool definitions for broker registration
572
+ */
573
+ async extractToolsForBroker() {
574
+ const tools = [];
575
+ // Try to load from agent.json
576
+ const agentJsonExports = await this._loadAgentJsonExports();
577
+ for (const [name, method] of this.toolHandlers) {
578
+ if (name.startsWith('__kadi_'))
579
+ continue;
580
+ let schema = method.schema;
581
+ // Check agent.json if no inline schema
582
+ if (!schema) {
583
+ const exportSchema = agentJsonExports.find((exp) => exp.name === name);
584
+ if (exportSchema) {
585
+ schema = {
586
+ description: exportSchema.description,
587
+ inputSchema: exportSchema.inputSchema,
588
+ outputSchema: exportSchema.outputSchema
589
+ };
590
+ }
591
+ }
592
+ if (!schema) {
593
+ this.logger.warn('extractToolsForBroker', `No schema for method '${name}', skipping`);
594
+ continue;
595
+ }
596
+ tools.push({
597
+ name,
598
+ description: schema.description || `Execute ${name}`,
599
+ inputSchema: schema.inputSchema || { type: 'object' },
600
+ outputSchema: schema.outputSchema || { type: 'object' } // Default to empty object schema
601
+ });
602
+ }
603
+ return tools;
604
+ }
605
+ /**
606
+ * Start serving (main entry point)
607
+ *
608
+ * @param options - Optional serve options
609
+ */
610
+ async serve(options = {}) {
611
+ this.logger.lifecycle('serve', `Starting to serve: ${this.name}`);
612
+ // Handle protocol override
613
+ if (options.mode && options.mode !== this.protocol) {
614
+ this.logger.info('serve', `Protocol override: ${this.protocol} -> ${options.mode}`);
615
+ this.protocol = options.mode;
616
+ // Recreate handler if needed
617
+ if (this.protocol !== 'broker') {
618
+ this.protocolHandler = ProtocolHandlerFactory.create(this.protocol, {
619
+ name: this.name,
620
+ version: this.version,
621
+ description: this.description,
622
+ scope: this.scope
623
+ });
624
+ }
625
+ }
626
+ try {
627
+ switch (this.protocol) {
628
+ case 'native':
629
+ // No serving needed - methods called directly
630
+ this.logger.info('serve', `Serving ${this.name} in native mode`);
631
+ this.emit('start', {
632
+ protocol: this.protocol,
633
+ tools: this.getToolNames()
634
+ });
635
+ // Keep process alive
636
+ return new Promise(() => { });
637
+ case 'stdio':
638
+ // For stdio serving, delegate to StdioTransport's serve method
639
+ // This handles JSON-RPC over stdin/stdout when running as a child process
640
+ if (this.protocolHandler) {
641
+ await this.protocolHandler.serve(this);
642
+ this.logger.info('serve', `Serving ${this.name} via stdio`);
643
+ }
644
+ else {
645
+ // Create a StdioTransport for serving if one doesn't exist
646
+ const transport = new StdioTransport({
647
+ abilityName: this.name,
648
+ abilityVersion: this.version || '1.0.0',
649
+ abilityDir: process.cwd(),
650
+ startCmd: '' // Not needed for server mode
651
+ });
652
+ this.protocolHandler = transport;
653
+ await transport.serve(this);
654
+ }
655
+ break;
656
+ case 'broker':
657
+ // Connect to broker and register
658
+ await this.connectToBrokers();
659
+ this.logger.info('serve', `Serving ${this.name} via broker`);
660
+ // Keep process alive
661
+ return new Promise(() => { });
662
+ default:
663
+ throw new Error(`Unknown protocol: ${this.protocol}`);
664
+ }
665
+ }
666
+ catch (error) {
667
+ this.logger.error('serve', `Failed to start serving: ${error.message}`);
668
+ this.emit('error', error);
669
+ throw error;
670
+ }
671
+ }
672
+ /**
673
+ * Start the client (alias for serve for agent compatibility)
674
+ */
675
+ async start() {
676
+ return this.serve();
677
+ }
678
+ /**
679
+ * Discover tools available from a remote agent
680
+ *
681
+ * This function queries the broker to find what tools
682
+ * a specific remote agent provides.
683
+ *
684
+ * @param targetAgent - The name of the agent to discover tools from
685
+ * @returns Array of tool names available from the target agent
686
+ */
687
+ async discoverRemoteTools(targetAgent) {
688
+ if (this.protocol !== 'broker' || this._brokerConnections.length === 0) {
689
+ throw new Error('Must be connected to broker to discover remote methods');
690
+ }
691
+ const broker = this._brokerConnections[0];
692
+ // Use ABILITY_LIST to discover available tools from the target agent
693
+ const request = {
694
+ jsonrpc: '2.0',
695
+ method: KadiMessages.ABILITY_LIST,
696
+ params: {
697
+ targetAgent,
698
+ networks: this.networks
699
+ },
700
+ id: this._idFactory.next()
701
+ };
702
+ try {
703
+ const response = await this.sendRequest(broker, request);
704
+ const result = response.result;
705
+ // Handle different response formats
706
+ if (result.methods) {
707
+ return result.methods;
708
+ }
709
+ else if (result.tools) {
710
+ return result.tools.map((tool) => tool.name);
711
+ }
712
+ return [];
713
+ }
714
+ catch (error) {
715
+ this.logger.error('discoverRemoteTools', `Failed to discover tools from ${targetAgent}: ${error}`);
716
+ // Return empty array on error - the agent might not be connected
717
+ return [];
718
+ }
719
+ }
720
+ /**
721
+ * Call a tool on a remote agent via the broker
722
+ *
723
+ * This function sends an RPC call through the broker to invoke
724
+ * a specific tool on a remote agent.
725
+ *
726
+ * @param targetAgent - The name of the agent that has the tool
727
+ * @param toolName - The tool name to invoke
728
+ * @param params - The parameters to pass to the tool
729
+ * @returns The result from the remote tool invocation
730
+ */
731
+ async callTool(targetAgent, toolName, params) {
732
+ if (this.protocol !== 'broker' || this._brokerConnections.length === 0) {
733
+ throw new Error('Must be connected to broker to call remote tools');
734
+ }
735
+ const broker = this._brokerConnections[0];
736
+ // Use ABILITY_INVOKE to call a remote agent's tool
737
+ const request = {
738
+ jsonrpc: '2.0',
739
+ method: KadiMessages.ABILITY_INVOKE,
740
+ params: {
741
+ targetAgent, // Which agent to call
742
+ toolName: toolName,
743
+ toolInput: params
744
+ },
745
+ id: this._idFactory.next()
746
+ };
747
+ // First, send the request and get the "pending" response
748
+ const pendingResponse = await this.sendRequest(broker, request);
749
+ const pendingResult = pendingResponse.result;
750
+ // Check if it's a pending response with requestId
751
+ if (pendingResult?.type === 'pending' && pendingResult?.requestId) {
752
+ // Wait for the actual result via kadi.ability.result
753
+ return new Promise((resolve, reject) => {
754
+ const timeout = setTimeout(() => {
755
+ this._pendingToolCalls.delete(pendingResult.requestId);
756
+ reject(new Error(`Tool call timeout: ${toolName}`));
757
+ }, 30000);
758
+ this._pendingToolCalls.set(pendingResult.requestId, {
759
+ resolve: resolve,
760
+ reject,
761
+ timer: timeout
762
+ });
763
+ });
764
+ }
765
+ else {
766
+ // Immediate response (shouldn't happen for tool calls, but handle anyway)
767
+ return pendingResult;
768
+ }
769
+ }
770
+ /**
771
+ * Load an external ability
772
+ */
773
+ async loadAbility(nameOrPath, protocol = 'native', options = {}) {
774
+ const ability = await loadAbility(nameOrPath, protocol, options);
775
+ this._abilities.set(ability.name, ability);
776
+ // For native/stdio protocols, connect ability events to this client's event system
777
+ if (protocol === 'native' || protocol === 'stdio') {
778
+ this.logger.debug('loadAbility', `Connecting ability events for ${protocol} protocol`);
779
+ // Forward events from the loaded ability to this client
780
+ this.logger.trace('loadAbility', 'Setting up ability.on(event) listener');
781
+ /**
782
+ * Forward events from the loaded ability to the main client's event system.
783
+ * The AbilityProxy emits events when it receives them from the transport,
784
+ * and we need to re-emit them using our internal transport mechanism
785
+ * so they can be delivered to user subscriptions.
786
+ *
787
+ * Note: We only need to listen to ability.on('event') - the ability.events
788
+ * is a legacy EventEmitter that mirrors the same events, so listening to
789
+ * both would cause duplicates.
790
+ */
791
+ ability.on('event', (eventData) => {
792
+ this.logger.trace('loadAbility', `Received event from ability: ${JSON.stringify(eventData)}`);
793
+ const eventName = eventData.eventName || eventData.name;
794
+ if (eventName) {
795
+ this.logger.debug('loadAbility', `Forwarding event from ability: ${eventName}`);
796
+ // Wrap the event in our internal transport envelope for delivery
797
+ this.emit(ABILITY_EVENT_TRANSPORT, {
798
+ eventName,
799
+ data: eventData.data
800
+ });
801
+ }
802
+ });
803
+ }
804
+ return ability;
805
+ }
806
+ /**
807
+ * Disconnect from brokers
808
+ */
809
+ async disconnect() {
810
+ for (const connection of this._brokerConnections) {
811
+ // Clean up heartbeat timer before closing
812
+ if (connection.heartbeatTimer) {
813
+ clearInterval(connection.heartbeatTimer);
814
+ connection.heartbeatTimer = undefined;
815
+ }
816
+ if (connection.ws) {
817
+ connection.ws.close();
818
+ }
819
+ }
820
+ this._brokerConnections.length = 0;
821
+ this._isConnected = false;
822
+ this.emit('disconnected');
823
+ }
824
+ /**
825
+ * Helper to load agent.json exports
826
+ */
827
+ async _loadAgentJsonExports() {
828
+ try {
829
+ const agentJsonPath = await this._findAgentJson();
830
+ const agentJson = JSON.parse(fs.readFileSync(agentJsonPath, 'utf8'));
831
+ return agentJson.exports || [];
832
+ }
833
+ catch (err) {
834
+ return [];
835
+ }
836
+ }
837
+ /**
838
+ * Find agent.json file
839
+ */
840
+ async _findAgentJson() {
841
+ const possiblePaths = [
842
+ this.abilityAgentJSON,
843
+ path.join(process.cwd(), 'agent.json'),
844
+ path.join(path.dirname(process.argv[1]), 'agent.json'),
845
+ path.join(process.cwd(), '..', 'agent.json'),
846
+ path.join(process.cwd(), '..', '..', 'agent.json')
847
+ ].filter(Boolean);
848
+ for (const agentPath of possiblePaths) {
849
+ if (fs.existsSync(agentPath)) {
850
+ return agentPath;
851
+ }
852
+ }
853
+ throw new Error('agent.json not found');
854
+ }
855
+ /**
856
+ * Subscribe to events with unified API across all protocols
857
+ *
858
+ * @param pattern Event pattern - exact string for native/stdio, wildcards supported for broker
859
+ * @param callback Function to call when event is received
860
+ * @returns Unsubscribe function
861
+ */
862
+ subscribeToEvent(pattern, callback) {
863
+ this.logger.debug('subscribeToEvent', `Subscribing to pattern: ${pattern}`);
864
+ // Validate pattern - no colons allowed (RabbitMQ conflict)
865
+ if (pattern.includes(':')) {
866
+ throw new Error(`Colons (:) not allowed in event patterns due to RabbitMQ routing conflicts. Use dots (.) instead.`);
867
+ }
868
+ // Validate pattern based on protocol
869
+ if (this.protocol !== 'broker' && pattern.includes('*')) {
870
+ throw new Error(`Wildcard patterns not supported in ${this.protocol} protocol. Use exact event names.`);
871
+ }
872
+ // Store subscription
873
+ if (!this._eventSubscriptions.has(pattern)) {
874
+ this._eventSubscriptions.set(pattern, []);
875
+ }
876
+ this._eventSubscriptions.get(pattern).push(callback);
877
+ // Protocol-specific subscription setup
878
+ this._setupProtocolEventSubscription(pattern);
879
+ // Return unsubscribe function
880
+ return () => this.unsubscribeFromEvent(pattern, callback);
881
+ }
882
+ /**
883
+ * Subscribe to multiple events at once
884
+ *
885
+ * @param patterns Array of event patterns
886
+ * @param callback Function to call when any event is received (receives eventName and data)
887
+ * @returns Unsubscribe function that removes all subscriptions
888
+ */
889
+ subscribeToEvents(patterns, callback) {
890
+ const unsubscribeFunctions = [];
891
+ for (const pattern of patterns) {
892
+ const unsubscribe = this.subscribeToEvent(pattern, (data) => {
893
+ callback(pattern, data);
894
+ });
895
+ unsubscribeFunctions.push(unsubscribe);
896
+ }
897
+ // Return function that unsubscribes from all patterns
898
+ return () => {
899
+ unsubscribeFunctions.forEach((fn) => fn());
900
+ };
901
+ }
902
+ /**
903
+ * Unsubscribe from event pattern
904
+ *
905
+ * @param pattern Event pattern to unsubscribe from
906
+ * @param callback Optional specific callback to remove (if not provided, removes all)
907
+ */
908
+ unsubscribeFromEvent(pattern, callback) {
909
+ const callbacks = this._eventSubscriptions.get(pattern);
910
+ if (!callbacks)
911
+ return;
912
+ if (callback) {
913
+ // Remove specific callback
914
+ const index = callbacks.indexOf(callback);
915
+ if (index > -1) {
916
+ callbacks.splice(index, 1);
917
+ }
918
+ }
919
+ else {
920
+ // Remove all callbacks for this pattern
921
+ callbacks.length = 0;
922
+ }
923
+ // Clean up empty subscription
924
+ if (callbacks.length === 0) {
925
+ this._eventSubscriptions.delete(pattern);
926
+ this._cleanupProtocolEventSubscription(pattern);
927
+ }
928
+ }
929
+ /**
930
+ * Subscribe to an event only once
931
+ *
932
+ * @param pattern Event pattern
933
+ * @param callback Function to call when event is received (auto-unsubscribes after first call)
934
+ */
935
+ onceEvent(pattern, callback) {
936
+ const unsubscribe = this.subscribeToEvent(pattern, (data) => {
937
+ callback(data);
938
+ unsubscribe();
939
+ });
940
+ }
941
+ /**
942
+ * Setup universal event subscription that works regardless of client protocol
943
+ *
944
+ * @private
945
+ * @param pattern Event pattern to subscribe to
946
+ */
947
+ /**
948
+ * Marker to track if we've already set up the event transport infrastructure
949
+ */
950
+ _eventTransportSetup = false;
951
+ /**
952
+ * Setup the event transport infrastructure ONCE, then subscribe to specific patterns
953
+ * TODO: I do not understand this function very well - better step by step comments
954
+ * would be nice.
955
+ *
956
+ * @private
957
+ * @param pattern Event pattern to subscribe to (e.g., 'echo.test-event')
958
+ */
959
+ _setupProtocolEventSubscription(pattern) {
960
+ this.logger.debug('subscribeToEvent', `Subscribing to pattern: ${pattern}`);
961
+ // Set up the transport infrastructure only once
962
+ if (!this._eventTransportSetup) {
963
+ this._eventTransportSetup = true;
964
+ this.logger.debug('subscribeToEvent', 'Setting up event transport infrastructure (first time only)');
965
+ /**
966
+ * Set up listener for events from native/stdio loaded abilities.
967
+ * This listens for our internal transport event and unwraps the actual
968
+ * user events for delivery to subscriptions.
969
+ */
970
+ this.logger.trace('subscribeToEvent', `Setting up ${ABILITY_EVENT_TRANSPORT} listener`);
971
+ this.on(ABILITY_EVENT_TRANSPORT, (envelope) => {
972
+ this.logger.trace('subscribeToEvent', `Received ${ABILITY_EVENT_TRANSPORT}: ${envelope.eventName}`);
973
+ // Check all registered patterns to see which subscriptions match this event
974
+ for (const [registeredPattern] of this._eventSubscriptions) {
975
+ if (this._matchesPattern(envelope.eventName, registeredPattern)) {
976
+ this.logger.debug('subscribeToEvent', `Pattern matched (${envelope.eventName} vs ${registeredPattern}), dispatching event`);
977
+ this._dispatchEvent(registeredPattern, envelope.data);
978
+ }
979
+ }
980
+ });
981
+ /**
982
+ * Set up listener for stdio transport events if we have a stdio handler.
983
+ * This is for when THIS client is running in stdio mode and receiving events.
984
+ */
985
+ if (this.protocolHandler &&
986
+ typeof this.protocolHandler.on === 'function') {
987
+ this.logger.debug('subscribeToEvent', 'Setting up stdio transport event listener');
988
+ this.protocolHandler.on('event', (event) => {
989
+ if (event.name) {
990
+ this.logger.trace('subscribeToEvent', `Received stdio transport event: ${event.name}`);
991
+ // Check all registered patterns
992
+ for (const [registeredPattern] of this._eventSubscriptions) {
993
+ if (this._matchesPattern(event.name, registeredPattern)) {
994
+ this.logger.debug('subscribeToEvent', `Stdio event matched pattern: ${event.name} vs ${registeredPattern}`);
995
+ this._dispatchEvent(registeredPattern, event.data);
996
+ }
997
+ }
998
+ }
999
+ });
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Subscribe to broker events for this specific pattern.
1004
+ * Broker subscriptions are per-pattern, unlike the transport listeners above.
1005
+ */
1006
+ if (this.brokerUrls && this.brokerUrls.length > 0) {
1007
+ if (this._brokerConnections.length === 0) {
1008
+ this.logger.debug('subscribeToEvent', 'Connecting to broker for event subscription');
1009
+ this.connectToBrokers()
1010
+ .then(() => {
1011
+ this.logger.debug('subscribeToEvent', `Subscribing to broker event: ${pattern}`);
1012
+ this._subscribeToBrokerEvent(pattern);
1013
+ })
1014
+ .catch((error) => {
1015
+ this.logger.warn('subscribeToEvent', `Failed to connect to broker for events: ${error.message}`);
1016
+ });
1017
+ }
1018
+ else {
1019
+ this.logger.debug('subscribeToEvent', `Subscribing to broker event: ${pattern}`);
1020
+ this._subscribeToBrokerEvent(pattern);
1021
+ }
1022
+ }
1023
+ }
1024
+ /**
1025
+ * Subscribe to broker events
1026
+ *
1027
+ * @private
1028
+ * @param pattern Event pattern to subscribe to
1029
+ */
1030
+ async _subscribeToBrokerEvent(pattern) {
1031
+ if (this._brokerConnections.length === 0) {
1032
+ throw new Error('Not connected to any broker. Call connectToBrokers() first.');
1033
+ }
1034
+ const broker = this._brokerConnections[0];
1035
+ if (!broker.ws || broker.ws.readyState !== WebSocket.OPEN) {
1036
+ throw new Error('Broker connection not ready');
1037
+ }
1038
+ try {
1039
+ const subscribeMessage = {
1040
+ jsonrpc: '2.0',
1041
+ method: KadiMessages.EVENT_SUBSCRIBE,
1042
+ params: {
1043
+ channels: [pattern],
1044
+ networkId: this.networks[0] || 'global'
1045
+ },
1046
+ id: this._idFactory.next()
1047
+ };
1048
+ broker.ws.send(JSON.stringify(subscribeMessage));
1049
+ this.logger.debug('_subscribeToBrokerEvent', `Sent subscription for: ${pattern}`);
1050
+ // Setup EVENT_DELIVERY handler if not already done
1051
+ if (!this._brokerEventHandlerSetup) {
1052
+ this._setupBrokerEventHandler();
1053
+ }
1054
+ }
1055
+ catch (error) {
1056
+ this.logger.error('_subscribeToBrokerEvent', `Failed to subscribe to ${pattern}:`, error);
1057
+ throw new Error(`Failed to subscribe to broker event '${pattern}': ${error instanceof Error ? error.message : error}`);
1058
+ }
1059
+ }
1060
+ /**
1061
+ * Setup broker event delivery handler
1062
+ *
1063
+ * @private
1064
+ */
1065
+ _brokerEventHandlerSetup = false;
1066
+ _setupBrokerEventHandler() {
1067
+ if (this._brokerEventHandlerSetup)
1068
+ return;
1069
+ // Handle broker messages for EVENT_DELIVERY
1070
+ this.on('broker:message', (message) => {
1071
+ if (message.method === KadiMessages.EVENT_DELIVERY) {
1072
+ const eventData = message.params;
1073
+ this.logger.debug('_setupBrokerEventHandler', `Received event on channel: ${eventData.channel}`);
1074
+ // Dispatch to matching subscribers
1075
+ for (const [pattern, callbacks] of this._eventSubscriptions.entries()) {
1076
+ if (this._matchesPattern(eventData.channel, pattern)) {
1077
+ callbacks.forEach((callback) => callback(eventData.data));
1078
+ }
1079
+ }
1080
+ // Also emit for backward compatibility
1081
+ this.emit('event', eventData);
1082
+ }
1083
+ });
1084
+ this._brokerEventHandlerSetup = true;
1085
+ }
1086
+ /**
1087
+ * Clean up protocol-specific event subscription
1088
+ *
1089
+ * @private
1090
+ * @param pattern Event pattern to clean up
1091
+ */
1092
+ _cleanupProtocolEventSubscription(pattern) {
1093
+ if (this.protocol === 'broker') {
1094
+ // Send unsubscribe message to broker
1095
+ const broker = this._brokerConnections[0];
1096
+ if (broker?.ws && broker.ws.readyState === WebSocket.OPEN) {
1097
+ const unsubscribeMessage = {
1098
+ jsonrpc: '2.0',
1099
+ method: KadiMessages.EVENT_UNSUBSCRIBE,
1100
+ params: {
1101
+ channels: [pattern],
1102
+ networkId: this.networks[0] || 'global'
1103
+ }
1104
+ };
1105
+ broker.ws.send(JSON.stringify(unsubscribeMessage));
1106
+ this.logger.debug('_cleanupProtocolEventSubscription', `Unsubscribed from: ${pattern}`);
1107
+ }
1108
+ }
1109
+ }
1110
+ /**
1111
+ * Check if event name matches pattern
1112
+ *
1113
+ * @private
1114
+ * @param eventName Event name to check
1115
+ * @param pattern Pattern to match against (supports wildcards for broker protocol)
1116
+ * @returns True if matches
1117
+ */
1118
+ _matchesPattern(eventName, pattern) {
1119
+ // Exact match for all protocols
1120
+ if (eventName === pattern)
1121
+ return true;
1122
+ // Wildcard matching only for broker protocol
1123
+ if (this.protocol === 'broker' && pattern.includes('*')) {
1124
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
1125
+ return regex.test(eventName);
1126
+ }
1127
+ return false;
1128
+ }
1129
+ /**
1130
+ * Dispatch event to subscribers
1131
+ * TODO: I do not understand this function
1132
+ *
1133
+ * @private
1134
+ * @param pattern Pattern that matched
1135
+ * @param data Event data
1136
+ */
1137
+ _dispatchEvent(pattern, data) {
1138
+ const callbacks = this._eventSubscriptions.get(pattern);
1139
+ if (callbacks) {
1140
+ callbacks.forEach((callback) => {
1141
+ try {
1142
+ callback(data);
1143
+ }
1144
+ catch (error) {
1145
+ this.logger.error('_dispatchEvent', `Error in event callback for ${pattern}:`, error);
1146
+ }
1147
+ });
1148
+ }
1149
+ }
1150
+ // Getters for compatibility
1151
+ get isConnected() {
1152
+ return this._isConnected;
1153
+ }
1154
+ get agentId() {
1155
+ return this._agentId;
1156
+ }
1157
+ get broker() {
1158
+ return this._brokerConnections[0];
1159
+ }
1160
+ }
1161
+ export default KadiClient;
1162
+ //# sourceMappingURL=KadiClient.js.map