@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
@@ -0,0 +1,1518 @@
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
+ import { getAgentJSON } from './utils/agentUtils.js';
11
+ // Internal event transport constants
12
+ // These are NOT user events - they're internal mechanisms for transporting events between protocols
13
+ /**
14
+ * ABILITY_EVENT_TRANSPORT - Internal event used to transport events from loaded abilities
15
+ * to the main client. When an ability (loaded via native/stdio) publishes an event like
16
+ * 'echo.test-event', we wrap it in this transport event to move it from the ability
17
+ * context to the main client context, where it can be unwrapped and delivered to
18
+ * user subscriptions.
19
+ *
20
+ * Think of it like an envelope: The actual event ('echo.test-event') is the letter,
21
+ * and ABILITY_EVENT_TRANSPORT is the envelope we put it in for delivery.
22
+ */
23
+ const ABILITY_EVENT_TRANSPORT = '__KADI_INTERNAL_ABILITY_EVENT_TRANSPORT__';
24
+ /**
25
+ * ProtocolHandlerFactory - Creates the right handler for each communication protocol
26
+ */
27
+ class ProtocolHandlerFactory {
28
+ static create(protocol, options = {}) {
29
+ switch (protocol?.toLowerCase()) {
30
+ case 'native':
31
+ // No handler needed - direct method calls
32
+ return undefined;
33
+ case 'stdio':
34
+ // Use StdioTransport for child process communication
35
+ return new StdioTransport({
36
+ abilityName: options.name || 'unnamed-client',
37
+ abilityVersion: options.version || '1.0.0',
38
+ abilityDir: process.cwd(),
39
+ startCmd: 'node index.js',
40
+ manifest: options
41
+ });
42
+ case 'broker':
43
+ // For broker protocol, we handle it directly in KadiClient
44
+ // No separate handler needed as KadiClient has built-in broker support
45
+ return undefined;
46
+ default:
47
+ return undefined;
48
+ }
49
+ }
50
+ }
51
+ /**
52
+ * KadiClient - Unified client for KADI protocol
53
+ *
54
+ * This class combines the functionality of KadiAgent and KadiAbility,
55
+ * providing a single interface for:
56
+ *
57
+ * 1. Registering tools that can be called by others
58
+ * 2. Calling remote tools on other services
59
+ * 3. Supporting multiple protocols (native, stdio, broker)
60
+ * 4. Connecting to broker with configurable role (agent, ability, service)
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // Create as an ability
65
+ * const service = new KadiClient({
66
+ * name: 'math-service',
67
+ * role: 'ability',
68
+ * protocol: 'broker'
69
+ * });
70
+ *
71
+ * // Register tools
72
+ * service.registerTool('add', async ({a, b}) => ({result: a + b}));
73
+ *
74
+ * // With schema
75
+ * service.tool('multiply', async ({a, b}) => ({result: a * b}));
76
+ *
77
+ * // Start serving
78
+ * await service.serve();
79
+ * ```
80
+ */
81
+ export class KadiClient extends EventEmitter {
82
+ name;
83
+ version;
84
+ description;
85
+ role;
86
+ protocol;
87
+ network;
88
+ networks;
89
+ brokers;
90
+ defaultBroker;
91
+ abilityAgentJSON;
92
+ logger;
93
+ protocolHandler;
94
+ toolHandlers = new Map();
95
+ _abilities = new Map();
96
+ _brokerConnections = [];
97
+ _isConnected = false;
98
+ _agentId = '';
99
+ _idFactory;
100
+ _pendingResponses = new Map();
101
+ _pendingToolCalls = new Map();
102
+ _currentBroker; // The currently active broker name
103
+ /**
104
+ * Resolver function for the native protocol serve promise.
105
+ * When serve() is called with native protocol, it creates a promise to keep
106
+ * the process alive. This resolver allows us to resolve that promise during
107
+ * disconnect(), enabling clean shutdown of native abilities.
108
+ */
109
+ _nativeServePromiseResolve;
110
+ /**
111
+ * Stores all event subscriptions as a Map of pattern → array of callback functions
112
+ * Example structure:
113
+ * {
114
+ * 'user.login' => [handleLogin, logLoginEvent], // 2 functions listening to user.login
115
+ * 'payment.*' => [processPayment], // 1 function listening to all payment events
116
+ * 'system.shutdown' => [saveState, cleanupResources] // 2 functions for shutdown
117
+ * }
118
+ * When an event arrives, we check which patterns match and call all their callbacks
119
+ */
120
+ _eventSubscriptions = new Map();
121
+ constructor(options = {}) {
122
+ super();
123
+ this.logger = createComponentLogger(`KadiClient:${options.name || 'unnamed'}`);
124
+ // Store configuration
125
+ this.name = options.name || 'unnamed-client';
126
+ this.version = options.version || '1.0.0';
127
+ this.description = options.description || '';
128
+ this.role = options.role || 'ability';
129
+ this.protocol =
130
+ options.protocol || process.env.KADI_PROTOCOL || 'native';
131
+ this.network = options.network || options.scope || 'global'; // Support legacy 'scope' parameter
132
+ this.networks = options.networks || ['global'];
133
+ // Broker integration logic with agent.json fallback
134
+ this.brokers = this.resolveBrokerConfiguration(options);
135
+ this.defaultBroker = this.resolveDefaultBroker(options);
136
+ this._currentBroker = this.defaultBroker; // Initialize current broker to default
137
+ this.abilityAgentJSON = options.abilityAgentJSON;
138
+ // Initialize ID factory
139
+ this._idFactory = new IdFactory();
140
+ this.logger.lifecycle('constructor', `Creating client: ${this.name}@${this.version} (role: ${this.role}, protocol: ${this.protocol})`);
141
+ this.logger.debug('constructor', `Resolved ${Object.keys(this.brokers).length} brokers, default: ${this.defaultBroker}`);
142
+ // Create protocol handler for stdio (broker is handled internally)
143
+ if (this.protocol !== 'broker') {
144
+ this.protocolHandler = ProtocolHandlerFactory.create(this.protocol, options);
145
+ // Forward events from protocol handler
146
+ if (this.protocolHandler) {
147
+ this.protocolHandler.on('start', (data) => this.emit('start', { ...data, protocol: this.protocol }));
148
+ this.protocolHandler.on('error', (error) => this.emit('error', error));
149
+ this.protocolHandler.on('stop', () => this.emit('stop'));
150
+ this.protocolHandler.on('request', (data) => this.emit('request', data));
151
+ this.protocolHandler.on('response', (data) => this.emit('response', data));
152
+ }
153
+ }
154
+ }
155
+ /**
156
+ * Get the currently active broker name
157
+ */
158
+ get currentBroker() {
159
+ return this._currentBroker;
160
+ }
161
+ /**
162
+ * Set the currently active broker
163
+ * @param brokerName The name of the broker to use
164
+ * @throws Error if the broker name doesn't exist in configuration
165
+ */
166
+ setCurrentBroker(brokerName) {
167
+ if (!this.brokers[brokerName]) {
168
+ const availableBrokers = Object.keys(this.brokers);
169
+ throw new Error(`Broker '${brokerName}' not found in configuration.\n` +
170
+ `Available brokers: ${availableBrokers.join(', ') || 'none'}`);
171
+ }
172
+ this._currentBroker = brokerName;
173
+ this.logger.info('setCurrentBroker', `Switched current broker to: ${brokerName}`);
174
+ }
175
+ /**
176
+ * Get the current broker's connection (if connected)
177
+ */
178
+ getCurrentBrokerConnection() {
179
+ if (!this._currentBroker) {
180
+ throw new Error('No current broker set');
181
+ }
182
+ const brokerUrl = this.brokers[this._currentBroker];
183
+ const connection = this._brokerConnections.find((c) => c.url === brokerUrl);
184
+ if (!connection) {
185
+ throw new Error(`Not connected to broker '${this._currentBroker}' (${brokerUrl}). ` +
186
+ `Call connectToBrokers() first.`);
187
+ }
188
+ return connection;
189
+ }
190
+ /**
191
+ * Resolve broker configuration with agent.json integration
192
+ * Precedence: Code brokers > agent.json brokers > environment defaults
193
+ */
194
+ resolveBrokerConfiguration(options) {
195
+ this.logger.debug('resolveBrokerConfiguration', 'Entering broker resolution');
196
+ // If brokers specified in code, use them (override behavior)
197
+ if (options.brokers && Object.keys(options.brokers).length > 0) {
198
+ this.logger.info('resolveBrokerConfiguration', `Using ${Object.keys(options.brokers).length} brokers from code configuration`);
199
+ this.logger.debug('resolveBrokerConfiguration', `Code brokers: ${Object.keys(options.brokers).join(', ')}`);
200
+ return options.brokers;
201
+ }
202
+ // Try to load brokers from agent.json
203
+ try {
204
+ const agentJson = getAgentJSON();
205
+ if (agentJson?.brokers && Object.keys(agentJson.brokers).length > 0) {
206
+ this.logger.info('resolveBrokerConfiguration', `Using ${Object.keys(agentJson.brokers).length} brokers from agent.json`);
207
+ this.logger.debug('resolveBrokerConfiguration', `agent.json brokers: ${Object.keys(agentJson.brokers).join(', ')}`);
208
+ return agentJson.brokers;
209
+ }
210
+ else {
211
+ this.logger.debug('resolveBrokerConfiguration', 'No brokers found in agent.json');
212
+ }
213
+ }
214
+ catch (error) {
215
+ this.logger.warn('resolveBrokerConfiguration', `Failed to load agent.json: ${error instanceof Error ? error.message : error}`);
216
+ }
217
+ // Fallback to environment/default
218
+ const fallbackUrl = process.env.KADI_BROKER_URL || 'ws://localhost:8080';
219
+ const fallbackBrokers = { default: fallbackUrl };
220
+ this.logger.info('resolveBrokerConfiguration', `Using fallback broker configuration: default -> ${fallbackUrl}`);
221
+ this.logger.debug('resolveBrokerConfiguration', 'Exiting broker resolution with fallback');
222
+ return fallbackBrokers;
223
+ }
224
+ /**
225
+ * Resolve default broker with fallback logic
226
+ * Priority: Explicit defaultBroker > agent.json defaultBroker > 'prod' key > first broker key
227
+ */
228
+ resolveDefaultBroker(options) {
229
+ this.logger.debug('resolveDefaultBroker', 'Entering default broker resolution');
230
+ const brokerKeys = Object.keys(this.brokers);
231
+ // If no brokers available, no default possible
232
+ if (brokerKeys.length === 0) {
233
+ this.logger.warn('resolveDefaultBroker', 'No brokers available, cannot determine default');
234
+ return undefined;
235
+ }
236
+ // Check explicit defaultBroker from code
237
+ if (options.defaultBroker) {
238
+ if (this.brokers[options.defaultBroker]) {
239
+ this.logger.info('resolveDefaultBroker', `Using explicit default broker from code: ${options.defaultBroker}`);
240
+ return options.defaultBroker;
241
+ }
242
+ else {
243
+ this.logger.warn('resolveDefaultBroker', `Specified default broker '${options.defaultBroker}' not found in broker list`);
244
+ }
245
+ }
246
+ // Check defaultBroker from agent.json
247
+ try {
248
+ const agentJson = getAgentJSON();
249
+ if (agentJson?.defaultBroker && this.brokers[agentJson.defaultBroker]) {
250
+ this.logger.info('resolveDefaultBroker', `Using default broker from agent.json: ${agentJson.defaultBroker}`);
251
+ return agentJson.defaultBroker;
252
+ }
253
+ }
254
+ catch (error) {
255
+ this.logger.debug('resolveDefaultBroker', `Could not check agent.json for default broker: ${error instanceof Error ? error.message : error}`);
256
+ }
257
+ // Fallback to 'prod' if it exists
258
+ if (this.brokers['prod']) {
259
+ this.logger.info('resolveDefaultBroker', 'Using fallback default broker: prod');
260
+ return 'prod';
261
+ }
262
+ // Final fallback: first broker in the list
263
+ const firstBroker = brokerKeys[0];
264
+ this.logger.info('resolveDefaultBroker', `Using first available broker as default: ${firstBroker}`);
265
+ this.logger.debug('resolveDefaultBroker', 'Exiting default broker resolution');
266
+ return firstBroker;
267
+ }
268
+ /**
269
+ * Register a tool for this service
270
+ *
271
+ * @param name - Tool name
272
+ * @param handler - Handler function
273
+ * @param schema - Optional schema
274
+ * @returns This instance for chaining
275
+ */
276
+ registerTool(name, handler, schema) {
277
+ if (typeof name !== 'string') {
278
+ throw new TypeError('Tool name must be a string');
279
+ }
280
+ if (typeof handler !== 'function') {
281
+ throw new TypeError('Tool handler must be a function');
282
+ }
283
+ this.logger.trace('registerTool', `Registering tool: ${name}`);
284
+ this.toolHandlers.set(name, {
285
+ name,
286
+ handler,
287
+ schema
288
+ });
289
+ return this;
290
+ }
291
+ /**
292
+ * Get all registered tool names
293
+ */
294
+ getToolNames() {
295
+ return Array.from(this.toolHandlers.keys()).filter((name) => !name.startsWith('__kadi_'));
296
+ }
297
+ /**
298
+ * Get all registered tools (agent compatibility)
299
+ */
300
+ getTools() {
301
+ return this.getToolNames();
302
+ }
303
+ /**
304
+ * Check if a tool is registered
305
+ */
306
+ hasTool(name) {
307
+ return this.toolHandlers.has(name);
308
+ }
309
+ /**
310
+ * Get tool handler
311
+ */
312
+ getToolHandler(name) {
313
+ const tool = this.toolHandlers.get(name);
314
+ return tool?.handler;
315
+ }
316
+ /**
317
+ * Get tool schema
318
+ */
319
+ getToolSchema(name) {
320
+ const tool = this.toolHandlers.get(name);
321
+ return tool?.schema;
322
+ }
323
+ /**
324
+ * Publish an event
325
+ */
326
+ publishEvent(eventName, data = {}) {
327
+ if (typeof eventName !== 'string' || !eventName) {
328
+ throw new TypeError('Event name must be a non-empty string');
329
+ }
330
+ this.logger.trace('publishEvent', `Publishing event: ${eventName}, protocol: ${this.protocol}, data: ${JSON.stringify(data)}`);
331
+ // For native protocol, emit directly using internal transport event
332
+ if (this.protocol === 'native') {
333
+ this.logger.debug('publishEvent', `Emitting ${ABILITY_EVENT_TRANSPORT} for native protocol`);
334
+ // Wrap the user event in our transport envelope
335
+ this.emit(ABILITY_EVENT_TRANSPORT, { name: eventName, data });
336
+ return;
337
+ }
338
+ // For stdio, delegate to handler
339
+ if (this.protocol === 'stdio') {
340
+ this.logger.trace('publishEvent', `Checking stdio handler: ${!!this.protocolHandler}, hasPublishEvent: ${typeof this.protocolHandler?.publishEvent}`);
341
+ if (this.protocolHandler &&
342
+ typeof this.protocolHandler.publishEvent === 'function') {
343
+ this.logger.debug('publishEvent', 'Delegating to stdio protocolHandler');
344
+ this.protocolHandler.publishEvent(eventName, data);
345
+ }
346
+ else {
347
+ this.logger.warn('publishEvent', `No protocolHandler available for stdio protocol - handler: ${!!this.protocolHandler}`);
348
+ }
349
+ return;
350
+ }
351
+ // For broker, send event message
352
+ // TODO: To make things consistent with the above, we can create a
353
+ // TODO: PublishEvent function inside BrokerTransport. Another reason
354
+ // TODO: why i think we should do this is because the BrokerTransport
355
+ // TODO: Implements the Transport interface which (optionally) has
356
+ // TODO: a publishEvent function
357
+ if (this.protocol === 'broker' && this._brokerConnections.length > 0) {
358
+ this.logger.debug('publishEvent', `Publishing broker event: ${eventName}`);
359
+ // TODO: Instead of picking the first broker, we should pick the
360
+ // TODO: the currently active broker name. See: "_currentBroker?: string;"
361
+ const broker = this._brokerConnections[0];
362
+ if (broker.ws && broker.ws.readyState === WebSocket.OPEN) {
363
+ const eventMessage = {
364
+ jsonrpc: '2.0',
365
+ method: KadiMessages.EVENT_PUBLISH,
366
+ params: {
367
+ channel: eventName, // RabbitMQ calls this a "routing key"
368
+ data
369
+ }
370
+ };
371
+ this.logger.trace('publishEvent', `Sending broker event message: ${JSON.stringify(eventMessage)}`);
372
+ broker.ws.send(JSON.stringify(eventMessage));
373
+ }
374
+ else {
375
+ this.logger.warn('publishEvent', 'Broker connection not ready');
376
+ }
377
+ }
378
+ else if (this.protocol === 'broker') {
379
+ this.logger.warn('publishEvent', 'No broker connections available');
380
+ }
381
+ }
382
+ /**
383
+ * Connect to all configured brokers (for event subscription and/or broker protocol)
384
+ * Always connects to ALL brokers defined in this.brokers for maximum redundancy
385
+ */
386
+ async connectToBrokers() {
387
+ this.logger.debug('connectToBrokers', 'Entering broker connection process');
388
+ // Check if already connected to avoid duplicate connections
389
+ if (this._brokerConnections.length > 0) {
390
+ this.logger.debug('connectToBrokers', 'Already connected to brokers, skipping');
391
+ return;
392
+ }
393
+ // Allow any protocol to connect to brokers for event subscription
394
+ // The protocol only determines how this client itself serves, not what it can connect to
395
+ const brokerNames = Object.keys(this.brokers);
396
+ const brokerUrls = Object.values(this.brokers);
397
+ if (brokerUrls.length === 0) {
398
+ const fallbackUrl = process.env.KADI_BROKER_URL || 'ws://localhost:8080';
399
+ this.logger.warn('connectToBrokers', `No brokers configured, using fallback: default -> ${fallbackUrl}`);
400
+ await this.connectToBroker(fallbackUrl, 'default');
401
+ this._isConnected = true;
402
+ this.emit('connected');
403
+ this.logger.debug('connectToBrokers', 'Exiting broker connection process with fallback');
404
+ return;
405
+ }
406
+ this.logger.info('connectToBrokers', `Connecting to ${brokerUrls.length} configured broker(s): ${brokerNames.join(', ')}`);
407
+ const connectionPromises = brokerNames.map(async (brokerName, index) => {
408
+ const url = brokerUrls[index];
409
+ try {
410
+ await this.connectToBroker(url, brokerName);
411
+ this.logger.info('connectToBrokers', `✅ Successfully connected to broker: ${brokerName} (${url})`);
412
+ return { brokerName, url, success: true, error: null };
413
+ }
414
+ catch (error) {
415
+ const errorMessage = error instanceof Error ? error.message : String(error);
416
+ this.logger.warn('connectToBrokers', `❌ Failed to connect to broker ${brokerName} (${url}): ${errorMessage}`);
417
+ return { brokerName, url, success: false, error: errorMessage };
418
+ }
419
+ });
420
+ const results = await Promise.allSettled(connectionPromises);
421
+ const connectionResults = results.map((result, index) => result.status === 'fulfilled'
422
+ ? result.value
423
+ : { url: brokerUrls[index], success: false, error: 'Promise rejected' });
424
+ const successfulConnections = connectionResults.filter((result) => result.success);
425
+ const failedConnections = connectionResults.filter((result) => !result.success);
426
+ if (successfulConnections.length === 0) {
427
+ const errorMessage = `Failed to connect to any brokers. Attempted: ${brokerUrls.join(', ')}`;
428
+ this.logger.error('connectToBrokers', errorMessage);
429
+ throw new Error(errorMessage);
430
+ }
431
+ this.logger.info('connectToBrokers', `Connected to ${successfulConnections.length}/${brokerUrls.length} brokers`);
432
+ if (failedConnections.length > 0) {
433
+ this.logger.warn('connectToBrokers', `Some brokers failed to connect: ${failedConnections.map((f) => f.url).join(', ')}`);
434
+ }
435
+ this._isConnected = true;
436
+ this.emit('connected');
437
+ }
438
+ /**
439
+ * Check if connected to a specific broker URL
440
+ */
441
+ isConnectedToBroker(url) {
442
+ return this._brokerConnections.some((conn) => conn.url === url);
443
+ }
444
+ /**
445
+ * Connect to a single broker
446
+ */
447
+ async connectToBroker(url, brokerName) {
448
+ const displayName = brokerName ? `${brokerName} (${url})` : url;
449
+ this.logger.info('connectToBroker', `Connecting to broker: ${displayName}`);
450
+ const ws = new WebSocket(url);
451
+ const connection = {
452
+ url,
453
+ brokerName,
454
+ ws,
455
+ isAuthenticated: false,
456
+ agentId: '' // Use agentId instead of sessionId
457
+ };
458
+ return new Promise((resolve, reject) => {
459
+ const timeout = setTimeout(() => {
460
+ reject(new Error(`Connection timeout to ${url}`));
461
+ }, 10000);
462
+ ws.on('open', async () => {
463
+ clearTimeout(timeout);
464
+ this.logger.debug('connectToBroker', `WebSocket connected to ${url}`);
465
+ try {
466
+ // Perform handshake
467
+ await this.performHandshake(connection);
468
+ this._brokerConnections.push(connection);
469
+ resolve();
470
+ }
471
+ catch (error) {
472
+ reject(error);
473
+ }
474
+ });
475
+ ws.on('error', (error) => {
476
+ clearTimeout(timeout);
477
+ this.logger.error('connectToBroker', `WebSocket error: ${error.message}`);
478
+ reject(error);
479
+ });
480
+ ws.on('message', (data) => {
481
+ this.handleBrokerMessage(connection, data.toString());
482
+ });
483
+ ws.on('close', () => {
484
+ this.logger.info('connectToBroker', `WebSocket close event for ${brokerName} (${url})`);
485
+ // Clean up heartbeat timer
486
+ if (connection.heartbeatTimer) {
487
+ this.logger.debug('connectToBroker', `Cleaning up heartbeat timer in close event for ${brokerName}`);
488
+ clearInterval(connection.heartbeatTimer);
489
+ connection.heartbeatTimer = undefined;
490
+ this.logger.debug('connectToBroker', 'Heartbeat timer cleaned up');
491
+ }
492
+ const index = this._brokerConnections.indexOf(connection);
493
+ if (index !== -1) {
494
+ this._brokerConnections.splice(index, 1);
495
+ }
496
+ });
497
+ });
498
+ }
499
+ /**
500
+ * Perform KADI protocol handshake
501
+ */
502
+ async performHandshake(connection) {
503
+ this.logger.debug('performHandshake', 'Starting KADI protocol handshake');
504
+ // Generate ephemeral keys
505
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
506
+ // Step 1: Send hello
507
+ const helloMsg = {
508
+ jsonrpc: '2.0',
509
+ method: KadiMessages.SESSION_HELLO,
510
+ params: {
511
+ role: this.role,
512
+ version: this.version
513
+ },
514
+ id: this._idFactory.next()
515
+ };
516
+ const helloResponse = await this.sendRequest(connection, helloMsg);
517
+ const helloResult = helloResponse.result;
518
+ const nonce = helloResult?.nonce;
519
+ if (!nonce)
520
+ throw new Error('No nonce received');
521
+ // Store the heartbeat interval for later use
522
+ const heartbeatIntervalSec = helloResult?.heartbeatIntervalSec || 0;
523
+ // Step 2: Authenticate
524
+ const signature = crypto
525
+ .sign(null, Buffer.from(nonce), privateKey)
526
+ .toString('base64');
527
+ const authMsg = {
528
+ jsonrpc: '2.0',
529
+ method: KadiMessages.SESSION_AUTHENTICATE,
530
+ params: {
531
+ publicKey: publicKey
532
+ .export({ format: 'der', type: 'spki' })
533
+ .toString('base64'),
534
+ signature,
535
+ nonce,
536
+ wantNewId: true
537
+ },
538
+ id: this._idFactory.next()
539
+ };
540
+ const authResponse = await this.sendRequest(connection, authMsg);
541
+ const authResult = authResponse.result;
542
+ this._agentId = authResult?.agentId;
543
+ connection.isAuthenticated = true;
544
+ // Step 3: Register capabilities
545
+ await this.registerCapabilities(connection);
546
+ // Step 4: Start heartbeat if required
547
+ if (heartbeatIntervalSec > 0) {
548
+ this.startHeartbeat(connection, heartbeatIntervalSec);
549
+ }
550
+ this.logger.info('performHandshake', `Authenticated as ${this._agentId}`);
551
+ }
552
+ /**
553
+ * Start heartbeat to keep broker connection alive
554
+ * Sends ping messages at the specified interval to prevent timeout
555
+ */
556
+ startHeartbeat(connection, intervalSec) {
557
+ this.logger.debug('startHeartbeat', `Starting heartbeat with ${intervalSec}s interval`);
558
+ // Clear any existing heartbeat timer
559
+ if (connection.heartbeatTimer) {
560
+ clearInterval(connection.heartbeatTimer);
561
+ }
562
+ // Send ping at the specified interval
563
+ connection.heartbeatTimer = setInterval(() => {
564
+ if (connection.ws.readyState === WebSocket.OPEN) {
565
+ const pingMsg = {
566
+ jsonrpc: '2.0',
567
+ method: KadiMessages.SESSION_PING,
568
+ params: {}
569
+ };
570
+ const message = JSON.stringify(pingMsg);
571
+ connection.ws.send(message);
572
+ this.logger.trace('heartbeat', 'Sent ping to broker');
573
+ }
574
+ else {
575
+ // Connection closed, stop heartbeat
576
+ if (connection.heartbeatTimer) {
577
+ clearInterval(connection.heartbeatTimer);
578
+ connection.heartbeatTimer = undefined;
579
+ }
580
+ this.logger.debug('heartbeat', 'Connection closed, stopping heartbeat');
581
+ }
582
+ }, intervalSec * 1000);
583
+ }
584
+ /**
585
+ * Register capabilities with broker
586
+ */
587
+ async registerCapabilities(connection) {
588
+ const tools = await this.extractToolsForBroker();
589
+ const params = {
590
+ displayName: this.name,
591
+ networks: this.networks,
592
+ tools: tools
593
+ };
594
+ const registerMsg = {
595
+ jsonrpc: '2.0',
596
+ method: KadiMessages.AGENT_REGISTER,
597
+ params,
598
+ id: this._idFactory.next()
599
+ };
600
+ await this.sendRequest(connection, registerMsg);
601
+ this.logger.info('registerCapabilities', `Registered ${tools.length} tools`);
602
+ }
603
+ /**
604
+ * Send request to broker and wait for response
605
+ */
606
+ sendRequest(connection, message) {
607
+ return new Promise((resolve, reject) => {
608
+ const id = message.id;
609
+ this.logger.trace('sendBrokerRequest', `Sending ${message.method} request (ID: ${id})`);
610
+ const timeout = setTimeout(() => {
611
+ this.logger.warn('sendBrokerRequest', `Request timeout for ${message.method} (ID: ${id})`);
612
+ this._pendingResponses.delete(id);
613
+ reject(new Error(`Request timeout: ${message.method}`));
614
+ }, 30000);
615
+ this._pendingResponses.set(id, {
616
+ resolve: resolve,
617
+ reject,
618
+ timer: timeout
619
+ });
620
+ this.logger.trace('sendBrokerRequest', `Stored pending response for ${message.method} (ID: ${id})`);
621
+ connection.ws.send(JSON.stringify(message));
622
+ });
623
+ }
624
+ /**
625
+ * Handle incoming broker messages
626
+ */
627
+ handleBrokerMessage(connection, data) {
628
+ try {
629
+ const message = JSON.parse(data);
630
+ this.logger?.trace('handleBrokerMessage', `Received message: ${JSON.stringify(message)}`);
631
+ // Handle responses to our requests
632
+ if ('id' in message &&
633
+ message.id !== null &&
634
+ this._pendingResponses.has(message.id)) {
635
+ this.logger?.trace('handleBrokerMessage', `Found pending response for ID: ${message.id}`);
636
+ const pending = this._pendingResponses.get(message.id);
637
+ clearTimeout(pending.timer);
638
+ this._pendingResponses.delete(message.id);
639
+ if ('error' in message && message.error) {
640
+ this.logger?.debug('handleBrokerMessage', `Error response: ${message.error.message}`);
641
+ pending.reject(new Error(`${message.error.code}: ${message.error.message}`));
642
+ }
643
+ else {
644
+ this.logger?.trace('handleBrokerMessage', `Success response for ID: ${message.id}`);
645
+ pending.resolve(message);
646
+ }
647
+ return;
648
+ }
649
+ else if ('id' in message && message.id !== null) {
650
+ // Only log if there's an ID but we don't recognize it (unexpected response)
651
+ this.logger?.warn('handleBrokerMessage', `Received response with unrecognized ID: ${message.id}`);
652
+ }
653
+ // For notifications (no ID), this is normal - no need to log
654
+ // Handle incoming method calls (both agent.invoke and kadi.ability.call)
655
+ // Handle incoming tool invocation requests
656
+ // Using ABILITY_INVOKE for all tool invocations (broker-to-agent and client-to-client)
657
+ if ('method' in message &&
658
+ message.method === KadiMessages.ABILITY_INVOKE) {
659
+ this.handleToolInvocation(connection, message);
660
+ return;
661
+ }
662
+ // Handle tool invocation results
663
+ if ('method' in message &&
664
+ message.method === KadiMessages.ABILITY_RESULT) {
665
+ this.handleToolResult(message);
666
+ return;
667
+ }
668
+ // Handle other notifications
669
+ if ('method' in message && !('id' in message)) {
670
+ this.emit('notification', message);
671
+ // Also emit for our unified event system
672
+ this.emit('broker:message', message);
673
+ }
674
+ }
675
+ catch (error) {
676
+ this.logger.error('handleBrokerMessage', `Failed to parse message: ${error}`);
677
+ }
678
+ }
679
+ /**
680
+ * Handle tool invocation result from broker
681
+ */
682
+ handleToolResult(message) {
683
+ const { requestId, result, error, toSessionId } = message.params;
684
+ // Check if this result is for us
685
+ if (toSessionId !== this._agentId) {
686
+ return; // Not for us
687
+ }
688
+ // Find pending tool call
689
+ if (this._pendingToolCalls.has(requestId)) {
690
+ const pending = this._pendingToolCalls.get(requestId);
691
+ clearTimeout(pending.timer);
692
+ this._pendingToolCalls.delete(requestId);
693
+ if (error) {
694
+ pending.reject(new Error(`Tool call failed: ${error.message || error}`));
695
+ }
696
+ else {
697
+ pending.resolve(result);
698
+ }
699
+ }
700
+ }
701
+ /**
702
+ * Handle incoming tool invocation request
703
+ */
704
+ async handleToolInvocation(connection, request) {
705
+ const { requestId, from, toolName, toolInput } = request.params;
706
+ const method = this.toolHandlers.get(toolName);
707
+ if (!method) {
708
+ // Send kadi.ability.result with error
709
+ const errorMessage = {
710
+ jsonrpc: '2.0',
711
+ method: KadiMessages.ABILITY_RESULT,
712
+ params: {
713
+ requestId,
714
+ toSessionId: from,
715
+ error: {
716
+ code: 'TOOL_NOT_FOUND',
717
+ message: `Tool ${toolName} not found`
718
+ }
719
+ }
720
+ };
721
+ connection.ws.send(JSON.stringify(errorMessage));
722
+ return;
723
+ }
724
+ try {
725
+ this.logger.debug('handleToolInvocation', `Executing tool: ${toolName}`);
726
+ const result = await method.handler(toolInput);
727
+ this.logger.debug('handleToolInvocation', `Tool ${toolName} completed successfully`);
728
+ // Send kadi.ability.result with success result
729
+ const resultMessage = {
730
+ jsonrpc: '2.0',
731
+ method: KadiMessages.ABILITY_RESULT,
732
+ params: {
733
+ requestId,
734
+ toSessionId: from,
735
+ result
736
+ }
737
+ };
738
+ this.logger.trace('handleToolInvocation', `Sending result for tool: ${toolName}`);
739
+ connection.ws.send(JSON.stringify(resultMessage));
740
+ }
741
+ catch (error) {
742
+ this.logger.error('handleToolInvocation', `Tool ${toolName} failed: ${error.message}`);
743
+ // Send kadi.ability.result with error
744
+ const errorMessage = {
745
+ jsonrpc: '2.0',
746
+ method: KadiMessages.ABILITY_RESULT,
747
+ params: {
748
+ requestId,
749
+ toSessionId: from,
750
+ error: {
751
+ code: 'EXECUTION_ERROR',
752
+ message: error.message || 'Tool execution failed'
753
+ }
754
+ }
755
+ };
756
+ connection.ws.send(JSON.stringify(errorMessage));
757
+ }
758
+ }
759
+ /**
760
+ * Extract tool definitions for broker registration
761
+ */
762
+ async extractToolsForBroker() {
763
+ const tools = [];
764
+ // Try to load from agent.json
765
+ const agentJsonExports = await this._loadAgentJsonExports();
766
+ for (const [name, method] of this.toolHandlers) {
767
+ if (name.startsWith('__kadi_'))
768
+ continue;
769
+ let schema = method.schema;
770
+ // Check agent.json if no inline schema
771
+ if (!schema) {
772
+ const exportSchema = agentJsonExports.find((exp) => exp.name === name);
773
+ if (exportSchema) {
774
+ schema = {
775
+ description: exportSchema.description,
776
+ inputSchema: exportSchema.inputSchema,
777
+ outputSchema: exportSchema.outputSchema
778
+ };
779
+ }
780
+ }
781
+ if (!schema) {
782
+ this.logger.warn('extractToolsForBroker', `No schema for method '${name}', skipping`);
783
+ continue;
784
+ }
785
+ tools.push({
786
+ name,
787
+ description: schema.description || `Execute ${name}`,
788
+ inputSchema: schema.inputSchema || { type: 'object' },
789
+ outputSchema: schema.outputSchema || { type: 'object' } // Default to empty object schema
790
+ });
791
+ }
792
+ return tools;
793
+ }
794
+ /**
795
+ * Start serving (main entry point)
796
+ *
797
+ * @param options - Optional serve options
798
+ */
799
+ async serve(options = {}) {
800
+ this.logger.lifecycle('serve', `Starting to serve: ${this.name}`);
801
+ // Handle protocol override
802
+ if (options.mode && options.mode !== this.protocol) {
803
+ this.logger.info('serve', `Protocol override: ${this.protocol} -> ${options.mode}`);
804
+ this.protocol = options.mode;
805
+ // Recreate handler if needed
806
+ if (this.protocol !== 'broker') {
807
+ this.protocolHandler = ProtocolHandlerFactory.create(this.protocol, {
808
+ name: this.name,
809
+ version: this.version,
810
+ description: this.description,
811
+ network: this.network
812
+ });
813
+ }
814
+ }
815
+ try {
816
+ switch (this.protocol) {
817
+ case 'native':
818
+ // No serving needed - methods called directly
819
+ this.logger.info('serve', `Serving ${this.name} in native mode`);
820
+ this.emit('start', {
821
+ protocol: this.protocol,
822
+ tools: this.getToolNames()
823
+ });
824
+ // Keep process alive with a cancelable promise.
825
+ // When abilities are loaded natively, they still call serve() which
826
+ // creates this promise. We store the resolver so disconnect() can
827
+ // resolve it later, allowing the process to exit cleanly.
828
+ // Without this, the unresolved promise would keep the Node.js event
829
+ // loop running forever, preventing graceful shutdown.
830
+ return new Promise((resolve) => {
831
+ this._nativeServePromiseResolve = resolve;
832
+ });
833
+ case 'stdio':
834
+ // For stdio serving, delegate to StdioTransport's serve method
835
+ // This handles JSON-RPC over stdin/stdout when running as a child process
836
+ if (this.protocolHandler) {
837
+ await this.protocolHandler.serve(this);
838
+ this.logger.info('serve', `Serving ${this.name} via stdio`);
839
+ }
840
+ else {
841
+ // Create a StdioTransport for serving if one doesn't exist
842
+ const transport = new StdioTransport({
843
+ abilityName: this.name,
844
+ abilityVersion: this.version || '1.0.0',
845
+ abilityDir: process.cwd(),
846
+ startCmd: '' // Not needed for server mode
847
+ });
848
+ this.protocolHandler = transport;
849
+ await transport.serve(this);
850
+ }
851
+ break;
852
+ case 'broker':
853
+ // Connect to broker and register
854
+ await this.connectToBrokers();
855
+ this.logger.info('serve', `Serving ${this.name} via broker`);
856
+ // Keep process alive
857
+ return new Promise(() => { });
858
+ default:
859
+ throw new Error(`Unknown protocol: ${this.protocol}`);
860
+ }
861
+ }
862
+ catch (error) {
863
+ this.logger.error('serve', `Failed to start serving: ${error.message}`);
864
+ this.emit('error', error);
865
+ throw error;
866
+ }
867
+ }
868
+ /**
869
+ * Start the client (alias for serve for agent compatibility)
870
+ */
871
+ async start() {
872
+ return this.serve();
873
+ }
874
+ /**
875
+ * Discover tools available from a remote agent
876
+ *
877
+ * This function queries the broker to find what tools
878
+ * a specific remote agent provides.
879
+ *
880
+ * @param targetAgent - The name of the agent to discover tools from
881
+ * @returns Array of tool names available from the target agent
882
+ */
883
+ async discoverRemoteTools(targetAgent) {
884
+ if (this.protocol !== 'broker' || this._brokerConnections.length === 0) {
885
+ throw new Error('Must be connected to broker to discover remote methods');
886
+ }
887
+ const broker = this._brokerConnections[0];
888
+ // Use ABILITY_LIST to discover available tools from the target agent
889
+ const request = {
890
+ jsonrpc: '2.0',
891
+ method: KadiMessages.ABILITY_LIST,
892
+ params: {
893
+ targetAgent,
894
+ networks: this.networks
895
+ },
896
+ id: this._idFactory.next()
897
+ };
898
+ try {
899
+ const response = await this.sendRequest(broker, request);
900
+ const result = response.result;
901
+ // Handle different response formats
902
+ if (result.methods) {
903
+ return result.methods;
904
+ }
905
+ else if (result.tools) {
906
+ return result.tools.map((tool) => tool.name);
907
+ }
908
+ return [];
909
+ }
910
+ catch (error) {
911
+ this.logger.error('discoverRemoteTools', `Failed to discover tools from ${targetAgent}: ${error}`);
912
+ // Return empty array on error - the agent might not be connected
913
+ return [];
914
+ }
915
+ }
916
+ /**
917
+ * Call a tool on a remote agent via the broker
918
+ *
919
+ * This function sends an RPC call through the broker to invoke
920
+ * a specific tool on a remote agent.
921
+ *
922
+ * @param targetAgent - The name of the agent that has the tool
923
+ * @param toolName - The tool name to invoke
924
+ * @param params - The parameters to pass to the tool
925
+ * @returns The result from the remote tool invocation
926
+ */
927
+ async callTool(targetAgent, toolName, params) {
928
+ if (this._brokerConnections.length === 0) {
929
+ throw new Error('Must be connected to broker to call remote tools');
930
+ }
931
+ const broker = this._brokerConnections[0];
932
+ // Use ABILITY_INVOKE to call a remote agent's tool
933
+ const request = {
934
+ jsonrpc: '2.0',
935
+ method: KadiMessages.ABILITY_INVOKE,
936
+ params: {
937
+ targetAgent, // Which agent to call
938
+ toolName: toolName,
939
+ toolInput: params
940
+ },
941
+ id: this._idFactory.next()
942
+ };
943
+ // First, send the request and get the "pending" response
944
+ const pendingResponse = await this.sendRequest(broker, request);
945
+ const pendingResult = pendingResponse.result;
946
+ // Check if it's a pending response with requestId
947
+ if (pendingResult?.type === 'pending' && pendingResult?.requestId) {
948
+ // Wait for the actual result via kadi.ability.result
949
+ return new Promise((resolve, reject) => {
950
+ const timeout = setTimeout(() => {
951
+ this._pendingToolCalls.delete(pendingResult.requestId);
952
+ reject(new Error(`Tool call timeout: ${toolName}`));
953
+ }, 30000);
954
+ this._pendingToolCalls.set(pendingResult.requestId, {
955
+ resolve: resolve,
956
+ reject,
957
+ timer: timeout
958
+ });
959
+ });
960
+ }
961
+ else {
962
+ // Immediate response (shouldn't happen for tool calls, but handle anyway)
963
+ return pendingResult;
964
+ }
965
+ }
966
+ /**
967
+ * Send a request directly to the broker
968
+ * Uses the current broker or the specified broker
969
+ *
970
+ * @param method The RPC method to call (e.g., 'kadi.ability.list')
971
+ * @param params The parameters for the method
972
+ * @param brokerName Optional broker name to use (overrides current broker)
973
+ * @returns The response from the broker
974
+ */
975
+ async sendBrokerRequest(method, params, brokerName) {
976
+ // If specific broker requested, temporarily use it
977
+ const originalBroker = this._currentBroker;
978
+ if (brokerName) {
979
+ this.setCurrentBroker(brokerName);
980
+ }
981
+ try {
982
+ const connection = this.getCurrentBrokerConnection();
983
+ const request = {
984
+ jsonrpc: '2.0',
985
+ method,
986
+ params,
987
+ id: this._idFactory.next()
988
+ };
989
+ const response = await this.sendRequest(connection, request);
990
+ if ('error' in response && response.error) {
991
+ throw new Error(`Broker request failed: ${response.error.message || 'Unknown error'}`);
992
+ }
993
+ return response.result;
994
+ }
995
+ finally {
996
+ // Restore original broker if we changed it
997
+ if (brokerName && originalBroker) {
998
+ this._currentBroker = originalBroker;
999
+ }
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Load an external ability and make its methods available for calling
1004
+ *
1005
+ * This is the KadiClient's convenient wrapper for loading abilities. It handles
1006
+ * broker resolution from your client config and delegates the actual loading
1007
+ * to the standalone loadAbility() function.
1008
+ *
1009
+ * Note: This method delegates to the standalone loadAbility() function after
1010
+ * resolving broker configurations. If you need more control or don't have a
1011
+ * KadiClient instance, you can use the standalone loadAbility() function directly.
1012
+ *
1013
+ * @param nameOrPath - Which ability to load. Can be:
1014
+ * - "ability-name" - loads from your installed abilities
1015
+ * - "/path/to/ability" - loads from a folder path
1016
+ *
1017
+ * @param protocol - How to connect to the ability:
1018
+ * - 'native': Load directly into this process (fastest, same language only)
1019
+ * - 'stdio': Spawn as child process, communicate via stdin/stdout (any language)
1020
+ * - 'broker': Connect via broker (ability runs anywhere, most flexible)
1021
+ *
1022
+ * @param options - Configuration for how to load the ability:
1023
+ *
1024
+ * broker: Which named broker to use (from your KadiClient config). Only for 'broker' protocol.
1025
+ * Example: 'prod', 'local', 'staging'
1026
+ * Uses your current/default broker if not specified.
1027
+ *
1028
+ * brokerUrl: Direct broker WebSocket URL. Only for 'broker' protocol.
1029
+ * Example: 'ws://localhost:8080', 'wss://broker.company.com'
1030
+ * Overrides the 'broker' option if provided.
1031
+ *
1032
+ * spawnAbility: Whether to start the ability first. Only for 'broker' protocol.
1033
+ * - true: Start the ability process AND connect to it (useful for development)
1034
+ * - false (default): Just connect to an already-running ability
1035
+ * Most production setups use false since abilities run independently.
1036
+ *
1037
+ * networks: Which KADI networks to search for the ability. Only for 'broker' protocol.
1038
+ * Example: ['global', 'team-alpha', 'dev-environment']
1039
+ * search specific networks to find them. Defaults to ['global'] if not specified.
1040
+ *
1041
+ * @returns A proxy object that lets you call the ability's methods directly.
1042
+ * Example: ability.processData({input: "hello"}) calls the ability's processData method.
1043
+ *
1044
+ * @example
1045
+ * // Load a JavaScript ability in the same process (fastest)
1046
+ * const mathLib = await client.loadAbility('math-utils', 'native');
1047
+ * const result = await mathLib.add({a: 5, b: 3}); // Returns 8
1048
+ *
1049
+ * @example
1050
+ * // Load a Go/Python/Rust ability via child process
1051
+ * const imageProcessor = await client.loadAbility('image-resizer', 'stdio');
1052
+ * const thumbnail = await imageProcessor.resize({image: buffer, size: '100x100'});
1053
+ *
1054
+ * @example
1055
+ * // Connect to an ability running on a remote broker (using named broker from config)
1056
+ * const aiService = await client.loadAbility('gpt-analyzer', 'broker', {
1057
+ * broker: 'prod', // Use the 'prod' broker from KadiClient config
1058
+ * networks: ['ai-services', 'global'] // Look in these networks
1059
+ * });
1060
+ * const analysis = await aiService.analyze({text: "Hello world"});
1061
+ */
1062
+ async loadAbility(nameOrPath, protocol = 'native', options = {}) {
1063
+ // For broker protocol, resolve broker name to URL
1064
+ let resolvedOptions = { ...options };
1065
+ if (protocol === 'broker') {
1066
+ // If brokerUrl not provided, resolve from broker name or use current/default
1067
+ if (!options.brokerUrl) {
1068
+ const brokerName = options.broker || this._currentBroker || this.defaultBroker;
1069
+ if (!brokerName) {
1070
+ throw new Error(`No broker specified. Either provide 'broker' option or set a default broker.`);
1071
+ }
1072
+ if (!this.brokers[brokerName]) {
1073
+ const availableBrokers = Object.keys(this.brokers);
1074
+ throw new Error(`Broker '${brokerName}' not found in configuration.\n` +
1075
+ `Available brokers: ${availableBrokers.join(', ') || 'none'}\n` +
1076
+ `Add broker to KadiClient config or agent.json.`);
1077
+ }
1078
+ // If a specific broker was requested, update current broker
1079
+ if (options.broker) {
1080
+ this.setCurrentBroker(brokerName);
1081
+ }
1082
+ resolvedOptions.brokerUrl = this.brokers[brokerName];
1083
+ this.logger.debug('loadAbility', `Resolved broker '${brokerName}' to URL: ${resolvedOptions.brokerUrl}`);
1084
+ }
1085
+ }
1086
+ // Pass the client instance for broker protocol to reuse connection
1087
+ const optionsWithClient = protocol === 'broker'
1088
+ ? { ...resolvedOptions, existingClient: this }
1089
+ : resolvedOptions;
1090
+ const ability = await loadAbility(nameOrPath, protocol, optionsWithClient);
1091
+ this._abilities.set(ability.name, ability);
1092
+ // For native/stdio protocols, connect ability events to this client's event system
1093
+ if (protocol === 'native' || protocol === 'stdio') {
1094
+ this.logger.debug('loadAbility', `Connecting ability events for ${protocol} protocol`);
1095
+ // Forward events from the loaded ability to this client
1096
+ this.logger.trace('loadAbility', 'Setting up ability.on(event) listener');
1097
+ /**
1098
+ * Forward events from the loaded ability to the main client's event system.
1099
+ * The AbilityProxy emits events when it receives them from the transport,
1100
+ * and we need to re-emit them using our internal transport mechanism
1101
+ * so they can be delivered to user subscriptions.
1102
+ *
1103
+ * Note: We only need to listen to ability.on('event') - the ability.events
1104
+ * is a legacy EventEmitter that mirrors the same events, so listening to
1105
+ * both would cause duplicates.
1106
+ */
1107
+ ability.on('event', (eventData) => {
1108
+ this.logger.trace('loadAbility', `Received event from ability: ${JSON.stringify(eventData)}`);
1109
+ const eventName = eventData.eventName || eventData.name;
1110
+ if (eventName) {
1111
+ this.logger.debug('loadAbility', `Forwarding event from ability: ${eventName}`);
1112
+ // Wrap the event in our internal transport envelope for delivery
1113
+ this.emit(ABILITY_EVENT_TRANSPORT, {
1114
+ eventName,
1115
+ data: eventData.data
1116
+ });
1117
+ }
1118
+ });
1119
+ }
1120
+ return ability;
1121
+ }
1122
+ /**
1123
+ * Disconnect from brokers and cleanup resources
1124
+ */
1125
+ async disconnect() {
1126
+ this.logger.info('disconnect', `Starting disconnect for ${this.name}`);
1127
+ // Resolve the native serve promise if it exists
1128
+ // This is critical for native protocol abilities to shut down cleanly.
1129
+ // When an ability calls serve() in native mode, it creates a promise
1130
+ // that keeps the process alive. By resolving it here, we allow the
1131
+ // Node.js event loop to exit naturally.
1132
+ if (this._nativeServePromiseResolve) {
1133
+ this.logger.debug('disconnect', 'Resolving native serve promise to allow clean shutdown');
1134
+ this._nativeServePromiseResolve();
1135
+ this._nativeServePromiseResolve = undefined;
1136
+ }
1137
+ // Disconnect from all broker connections (if any)
1138
+ this.logger.info('disconnect', `Disconnecting from ${this._brokerConnections.length} broker connections`);
1139
+ for (const connection of this._brokerConnections) {
1140
+ // Clean up heartbeat timer before closing
1141
+ if (connection.heartbeatTimer) {
1142
+ this.logger.debug('disconnect', `Clearing heartbeat timer for ${connection.brokerName}`);
1143
+ clearInterval(connection.heartbeatTimer);
1144
+ connection.heartbeatTimer = undefined;
1145
+ }
1146
+ if (connection.ws) {
1147
+ this.logger.debug('disconnect', `Closing WebSocket for ${connection.brokerName}`);
1148
+ connection.ws.close();
1149
+ }
1150
+ }
1151
+ this._brokerConnections.length = 0;
1152
+ this._isConnected = false;
1153
+ // Remove all event listeners
1154
+ this.logger.debug('disconnect', 'Removing all internal event listeners');
1155
+ this.removeAllListeners();
1156
+ this.logger.info('disconnect', `Disconnect complete for ${this.name}`);
1157
+ this.emit('disconnected');
1158
+ }
1159
+ /**
1160
+ * Helper to load agent.json exports
1161
+ */
1162
+ async _loadAgentJsonExports() {
1163
+ try {
1164
+ const agentJsonPath = await this._findAgentJson();
1165
+ const agentJson = JSON.parse(fs.readFileSync(agentJsonPath, 'utf8'));
1166
+ return agentJson.exports || [];
1167
+ }
1168
+ catch (err) {
1169
+ return [];
1170
+ }
1171
+ }
1172
+ /**
1173
+ * Find agent.json file
1174
+ * TODO: Not sure, but maybe this function can be moved to pathUtils.ts?
1175
+ */
1176
+ async _findAgentJson() {
1177
+ const possiblePaths = [
1178
+ this.abilityAgentJSON,
1179
+ path.join(process.cwd(), 'agent.json'),
1180
+ path.join(path.dirname(process.argv[1]), 'agent.json'),
1181
+ path.join(process.cwd(), '..', 'agent.json'),
1182
+ path.join(process.cwd(), '..', '..', 'agent.json')
1183
+ ].filter(Boolean);
1184
+ for (const agentPath of possiblePaths) {
1185
+ if (fs.existsSync(agentPath)) {
1186
+ return agentPath;
1187
+ }
1188
+ }
1189
+ throw new Error('agent.json not found');
1190
+ }
1191
+ /**
1192
+ * Subscribe to events with unified API across all protocols
1193
+ *
1194
+ * @param pattern Event pattern - exact string for native/stdio, wildcards supported for broker
1195
+ * @param callback Function to call when event is received
1196
+ * @returns Unsubscribe function
1197
+ */
1198
+ subscribeToEvent(pattern, callback) {
1199
+ this.logger.debug('subscribeToEvent', `Subscribing to pattern: ${pattern}`);
1200
+ // Validate pattern - no colons allowed (RabbitMQ conflict)
1201
+ if (pattern.includes(':')) {
1202
+ throw new Error(`Colons (:) not allowed in event patterns due to RabbitMQ routing conflicts. Use dots (.) instead.`);
1203
+ }
1204
+ // Validate pattern based on protocol
1205
+ if (this.protocol !== 'broker' && pattern.includes('*')) {
1206
+ throw new Error(`Wildcard patterns not supported in ${this.protocol} protocol. Use exact event names.`);
1207
+ }
1208
+ // Store subscription
1209
+ if (!this._eventSubscriptions.has(pattern)) {
1210
+ this._eventSubscriptions.set(pattern, []);
1211
+ }
1212
+ this._eventSubscriptions.get(pattern).push(callback);
1213
+ // Protocol-specific subscription setup
1214
+ this._setupProtocolEventSubscription(pattern);
1215
+ // Return unsubscribe function
1216
+ return () => this.unsubscribeFromEvent(pattern, callback);
1217
+ }
1218
+ /**
1219
+ * Subscribe to multiple events at once
1220
+ *
1221
+ * @param patterns Array of event patterns
1222
+ * @param callback Function to call when any event is received (receives eventName and data)
1223
+ * @returns Unsubscribe function that removes all subscriptions
1224
+ */
1225
+ subscribeToEvents(patterns, callback) {
1226
+ const unsubscribeFunctions = [];
1227
+ for (const pattern of patterns) {
1228
+ const unsubscribe = this.subscribeToEvent(pattern, (data) => {
1229
+ callback(pattern, data);
1230
+ });
1231
+ unsubscribeFunctions.push(unsubscribe);
1232
+ }
1233
+ // Return function that unsubscribes from all patterns
1234
+ return () => {
1235
+ unsubscribeFunctions.forEach((fn) => fn());
1236
+ };
1237
+ }
1238
+ /**
1239
+ * Unsubscribe from event pattern
1240
+ *
1241
+ * @param pattern Event pattern to unsubscribe from
1242
+ * @param callback Optional specific callback to remove (if not provided, removes all)
1243
+ */
1244
+ unsubscribeFromEvent(pattern, callback) {
1245
+ const callbacks = this._eventSubscriptions.get(pattern);
1246
+ if (!callbacks)
1247
+ return;
1248
+ if (callback) {
1249
+ // Remove specific callback
1250
+ const index = callbacks.indexOf(callback);
1251
+ if (index > -1) {
1252
+ callbacks.splice(index, 1);
1253
+ }
1254
+ }
1255
+ else {
1256
+ // Remove all callbacks for this pattern
1257
+ callbacks.length = 0;
1258
+ }
1259
+ // Clean up empty subscription
1260
+ if (callbacks.length === 0) {
1261
+ this._eventSubscriptions.delete(pattern);
1262
+ this._cleanupProtocolEventSubscription(pattern);
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Subscribe to an event only once
1267
+ *
1268
+ * @param pattern Event pattern
1269
+ * @param callback Function to call when event is received (auto-unsubscribes after first call)
1270
+ */
1271
+ onceEvent(pattern, callback) {
1272
+ const unsubscribe = this.subscribeToEvent(pattern, (data) => {
1273
+ callback(data);
1274
+ unsubscribe();
1275
+ });
1276
+ }
1277
+ /**
1278
+ * Setup universal event subscription that works regardless of client protocol
1279
+ *
1280
+ * @private
1281
+ * @param pattern Event pattern to subscribe to
1282
+ */
1283
+ /**
1284
+ * Marker to track if we've already set up the event transport infrastructure
1285
+ */
1286
+ _eventTransportSetup = false;
1287
+ /**
1288
+ * Setup the event transport infrastructure ONCE, then subscribe to specific patterns
1289
+ * TODO: I do not understand this function very well - better step by step comments
1290
+ * would be nice.
1291
+ *
1292
+ * @private
1293
+ * @param pattern Event pattern to subscribe to (e.g., 'echo.test-event')
1294
+ */
1295
+ _setupProtocolEventSubscription(pattern) {
1296
+ this.logger.debug('subscribeToEvent', `Subscribing to pattern: ${pattern}`);
1297
+ // Set up the transport infrastructure only once
1298
+ if (!this._eventTransportSetup) {
1299
+ this._eventTransportSetup = true;
1300
+ this.logger.debug('subscribeToEvent', 'Setting up event transport infrastructure (first time only)');
1301
+ /**
1302
+ * Set up listener for events from native/stdio loaded abilities.
1303
+ * This listens for our internal transport event and unwraps the actual
1304
+ * user events for delivery to subscriptions.
1305
+ */
1306
+ this.logger.trace('subscribeToEvent', `Setting up ${ABILITY_EVENT_TRANSPORT} listener`);
1307
+ this.on(ABILITY_EVENT_TRANSPORT, (envelope) => {
1308
+ this.logger.trace('subscribeToEvent', `Received ${ABILITY_EVENT_TRANSPORT}: ${envelope.eventName}`);
1309
+ // Check all registered patterns to see which subscriptions match this event
1310
+ // _eventSubscriptions is a Map like: {'user.login' => [callbacks], 'payment.*' => [callbacks]}
1311
+ // We loop through each pattern to see if the incoming event matches
1312
+ for (const [registeredPattern] of this._eventSubscriptions) {
1313
+ // Example: If event is 'user.login' and pattern is 'user.*', this matches!
1314
+ if (this._matchesPattern(envelope.eventName, registeredPattern)) {
1315
+ this.logger.debug('subscribeToEvent', `Pattern matched (${envelope.eventName} vs ${registeredPattern}), dispatching event`);
1316
+ // Found a match! Call all callbacks registered for this pattern
1317
+ this._dispatchEvent(registeredPattern, envelope.data);
1318
+ }
1319
+ }
1320
+ });
1321
+ /**
1322
+ * Set up listener for stdio transport events if we have a stdio handler.
1323
+ * This is for when THIS client is running in stdio mode and receiving events.
1324
+ */
1325
+ if (this.protocolHandler &&
1326
+ typeof this.protocolHandler.on === 'function') {
1327
+ this.logger.debug('subscribeToEvent', 'Setting up stdio transport event listener');
1328
+ this.protocolHandler.on('event', (event) => {
1329
+ if (event.name) {
1330
+ this.logger.trace('subscribeToEvent', `Received stdio transport event: ${event.name}`);
1331
+ // Check all registered patterns
1332
+ for (const [registeredPattern] of this._eventSubscriptions) {
1333
+ if (this._matchesPattern(event.name, registeredPattern)) {
1334
+ this.logger.debug('subscribeToEvent', `Stdio event matched pattern: ${event.name} vs ${registeredPattern}`);
1335
+ this._dispatchEvent(registeredPattern, event.data);
1336
+ }
1337
+ }
1338
+ }
1339
+ });
1340
+ }
1341
+ }
1342
+ /**
1343
+ * Subscribe to broker events for this specific pattern.
1344
+ * Broker subscriptions are per-pattern, unlike the transport listeners above.
1345
+ * Only connect to broker if we're actually using the broker protocol.
1346
+ */
1347
+ if (this.protocol === 'broker' &&
1348
+ this.brokers &&
1349
+ Object.keys(this.brokers).length > 0) {
1350
+ if (this._brokerConnections.length === 0) {
1351
+ this.logger.debug('subscribeToEvent', 'Connecting to broker for event subscription');
1352
+ this.connectToBrokers()
1353
+ .then(() => {
1354
+ this.logger.debug('subscribeToEvent', `Subscribing to broker event: ${pattern}`);
1355
+ this._subscribeToBrokerEvent(pattern);
1356
+ })
1357
+ .catch((error) => {
1358
+ this.logger.warn('subscribeToEvent', `Failed to connect to broker for events: ${error.message}`);
1359
+ });
1360
+ }
1361
+ else {
1362
+ this.logger.debug('subscribeToEvent', `Subscribing to broker event: ${pattern}`);
1363
+ this._subscribeToBrokerEvent(pattern);
1364
+ }
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Subscribe to broker events
1369
+ *
1370
+ * @private
1371
+ * @param pattern Event pattern to subscribe to
1372
+ */
1373
+ async _subscribeToBrokerEvent(pattern) {
1374
+ if (this._brokerConnections.length === 0) {
1375
+ throw new Error('Not connected to any broker. Call connectToBrokers() first.');
1376
+ }
1377
+ const broker = this._brokerConnections[0];
1378
+ if (!broker.ws || broker.ws.readyState !== WebSocket.OPEN) {
1379
+ throw new Error('Broker connection not ready');
1380
+ }
1381
+ try {
1382
+ const subscribeMessage = {
1383
+ jsonrpc: '2.0',
1384
+ method: KadiMessages.EVENT_SUBSCRIBE,
1385
+ params: {
1386
+ channels: [pattern],
1387
+ networkId: this.networks[0] || 'global'
1388
+ },
1389
+ id: this._idFactory.next()
1390
+ };
1391
+ broker.ws.send(JSON.stringify(subscribeMessage));
1392
+ this.logger.debug('_subscribeToBrokerEvent', `Sent subscription for: ${pattern}`);
1393
+ // Setup EVENT_DELIVERY handler if not already done
1394
+ if (!this._brokerEventHandlerSetup) {
1395
+ this._setupBrokerEventHandler();
1396
+ }
1397
+ }
1398
+ catch (error) {
1399
+ this.logger.error('_subscribeToBrokerEvent', `Failed to subscribe to ${pattern}:`, error);
1400
+ throw new Error(`Failed to subscribe to broker event '${pattern}': ${error instanceof Error ? error.message : error}`);
1401
+ }
1402
+ }
1403
+ /**
1404
+ * Setup broker event delivery handler
1405
+ *
1406
+ * @private
1407
+ */
1408
+ _brokerEventHandlerSetup = false;
1409
+ _setupBrokerEventHandler() {
1410
+ if (this._brokerEventHandlerSetup)
1411
+ return;
1412
+ // Handle broker messages for EVENT_DELIVERY
1413
+ this.on('broker:message', (message) => {
1414
+ if (message.method === KadiMessages.EVENT_DELIVERY) {
1415
+ const eventData = message.params;
1416
+ this.logger.debug('_setupBrokerEventHandler', `Received event on channel: ${eventData.channel}`);
1417
+ // Dispatch to matching subscribers
1418
+ for (const [pattern, callbacks] of this._eventSubscriptions.entries()) {
1419
+ if (this._matchesPattern(eventData.channel, pattern)) {
1420
+ callbacks.forEach((callback) => callback(eventData.data));
1421
+ }
1422
+ }
1423
+ // Also emit for backward compatibility
1424
+ this.emit('event', eventData);
1425
+ }
1426
+ });
1427
+ this._brokerEventHandlerSetup = true;
1428
+ }
1429
+ /**
1430
+ * Clean up protocol-specific event subscription
1431
+ *
1432
+ * @private
1433
+ * @param pattern Event pattern to clean up
1434
+ */
1435
+ _cleanupProtocolEventSubscription(pattern) {
1436
+ if (this.protocol === 'broker') {
1437
+ // Send unsubscribe message to broker
1438
+ const broker = this._brokerConnections[0];
1439
+ if (broker?.ws && broker.ws.readyState === WebSocket.OPEN) {
1440
+ const unsubscribeMessage = {
1441
+ jsonrpc: '2.0',
1442
+ method: KadiMessages.EVENT_UNSUBSCRIBE,
1443
+ params: {
1444
+ channels: [pattern],
1445
+ networkId: this.networks[0] || 'global'
1446
+ }
1447
+ };
1448
+ broker.ws.send(JSON.stringify(unsubscribeMessage));
1449
+ this.logger.debug('_cleanupProtocolEventSubscription', `Unsubscribed from: ${pattern}`);
1450
+ }
1451
+ }
1452
+ }
1453
+ /**
1454
+ * Check if event name matches pattern
1455
+ *
1456
+ * @private
1457
+ * @param eventName Event name to check
1458
+ * @param pattern Pattern to match against (supports wildcards for broker protocol)
1459
+ * @returns True if matches
1460
+ */
1461
+ _matchesPattern(eventName, pattern) {
1462
+ // Exact match for all protocols
1463
+ if (eventName === pattern)
1464
+ return true;
1465
+ // Wildcard matching only for broker protocol
1466
+ if (this.protocol === 'broker' && pattern.includes('*')) {
1467
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
1468
+ return regex.test(eventName);
1469
+ }
1470
+ return false;
1471
+ }
1472
+ /**
1473
+ * Dispatch event to subscribers - This is the final delivery step
1474
+ *
1475
+ * After all the routing through transports and pattern matching,
1476
+ * this function actually calls the user's callback functions with the event data.
1477
+ *
1478
+ * Example: If user did `client.subscribeToEvent('user.login', myFunction)`
1479
+ * then when 'user.login' event arrives, this calls `myFunction(eventData)`
1480
+ *
1481
+ * @private
1482
+ * @param pattern Pattern that matched (e.g., 'user.login' or 'payment.*')
1483
+ * @param data Event data to pass to the callbacks
1484
+ */
1485
+ _dispatchEvent(pattern, data) {
1486
+ // Get all callback functions registered for this pattern
1487
+ // Example: If pattern is 'user.login', this gets [callback1, callback2] array
1488
+ const callbacks = this._eventSubscriptions.get(pattern);
1489
+ if (callbacks) {
1490
+ // Call each registered callback with the event data
1491
+ // If multiple functions subscribed to same pattern, all get called
1492
+ callbacks.forEach((callback) => {
1493
+ try {
1494
+ // This is THE moment the user's function gets called!
1495
+ // Example: callback(data) might be userFunction({userId: 123, timestamp: ...})
1496
+ callback(data);
1497
+ }
1498
+ catch (error) {
1499
+ // If user's callback throws error, log it but continue with other callbacks
1500
+ // This prevents one bad callback from breaking all event delivery
1501
+ this.logger.error('_dispatchEvent', `Error in event callback for ${pattern}:`, error);
1502
+ }
1503
+ });
1504
+ }
1505
+ }
1506
+ // Getters for compatibility
1507
+ get isConnected() {
1508
+ return this._isConnected;
1509
+ }
1510
+ get agentId() {
1511
+ return this._agentId;
1512
+ }
1513
+ get broker() {
1514
+ return this._brokerConnections[0];
1515
+ }
1516
+ }
1517
+ export default KadiClient;
1518
+ //# sourceMappingURL=KadiClient.js.map