@kadi.build/core 0.0.1-alpha.4 → 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.
- package/README.md +359 -188
- package/dist/KadiClient.d.ts +153 -16
- package/dist/KadiClient.d.ts.map +1 -1
- package/dist/KadiClient.js +395 -39
- package/dist/KadiClient.js.map +1 -1
- package/dist/loadAbility.d.ts +64 -23
- package/dist/loadAbility.d.ts.map +1 -1
- package/dist/loadAbility.js +81 -40
- package/dist/loadAbility.js.map +1 -1
- package/dist/messages/BrokerMessages.d.ts.map +1 -1
- package/dist/messages/BrokerMessages.js +0 -2
- package/dist/messages/BrokerMessages.js.map +1 -1
- package/dist/transports/BrokerTransport.d.ts +14 -4
- package/dist/transports/BrokerTransport.d.ts.map +1 -1
- package/dist/transports/BrokerTransport.js +61 -29
- package/dist/transports/BrokerTransport.js.map +1 -1
- package/dist/transports/NativeTransport.d.ts +2 -12
- package/dist/transports/NativeTransport.d.ts.map +1 -1
- package/dist/transports/NativeTransport.js +66 -24
- package/dist/transports/NativeTransport.js.map +1 -1
- package/dist/transports/StdioTransport.d.ts.map +1 -1
- package/dist/transports/StdioTransport.js +8 -3
- package/dist/transports/StdioTransport.js.map +1 -1
- package/dist/types/core.d.ts +1 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js.map +1 -1
- package/dist/utils/agentUtils.d.ts +86 -1
- package/dist/utils/agentUtils.d.ts.map +1 -1
- package/dist/utils/agentUtils.js +20 -1
- package/dist/utils/agentUtils.js.map +1 -1
- package/package.json +1 -1
package/dist/KadiClient.js
CHANGED
|
@@ -7,6 +7,7 @@ import { StdioTransport } from './transports/StdioTransport.js';
|
|
|
7
7
|
import { createComponentLogger } from './utils/logger.js';
|
|
8
8
|
import { IdFactory, KadiMessages } from './messages/BrokerMessages.js';
|
|
9
9
|
import { loadAbility } from './loadAbility.js';
|
|
10
|
+
import { getAgentJSON } from './utils/agentUtils.js';
|
|
10
11
|
// Internal event transport constants
|
|
11
12
|
// These are NOT user events - they're internal mechanisms for transporting events between protocols
|
|
12
13
|
/**
|
|
@@ -83,9 +84,10 @@ export class KadiClient extends EventEmitter {
|
|
|
83
84
|
description;
|
|
84
85
|
role;
|
|
85
86
|
protocol;
|
|
86
|
-
|
|
87
|
+
network;
|
|
87
88
|
networks;
|
|
88
|
-
|
|
89
|
+
brokers;
|
|
90
|
+
defaultBroker;
|
|
89
91
|
abilityAgentJSON;
|
|
90
92
|
logger;
|
|
91
93
|
protocolHandler;
|
|
@@ -97,6 +99,24 @@ export class KadiClient extends EventEmitter {
|
|
|
97
99
|
_idFactory;
|
|
98
100
|
_pendingResponses = new Map();
|
|
99
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
|
+
*/
|
|
100
120
|
_eventSubscriptions = new Map();
|
|
101
121
|
constructor(options = {}) {
|
|
102
122
|
super();
|
|
@@ -108,14 +128,17 @@ export class KadiClient extends EventEmitter {
|
|
|
108
128
|
this.role = options.role || 'ability';
|
|
109
129
|
this.protocol =
|
|
110
130
|
options.protocol || process.env.KADI_PROTOCOL || 'native';
|
|
111
|
-
this.
|
|
131
|
+
this.network = options.network || options.scope || 'global'; // Support legacy 'scope' parameter
|
|
112
132
|
this.networks = options.networks || ['global'];
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
115
137
|
this.abilityAgentJSON = options.abilityAgentJSON;
|
|
116
138
|
// Initialize ID factory
|
|
117
139
|
this._idFactory = new IdFactory();
|
|
118
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}`);
|
|
119
142
|
// Create protocol handler for stdio (broker is handled internally)
|
|
120
143
|
if (this.protocol !== 'broker') {
|
|
121
144
|
this.protocolHandler = ProtocolHandlerFactory.create(this.protocol, options);
|
|
@@ -129,6 +152,119 @@ export class KadiClient extends EventEmitter {
|
|
|
129
152
|
}
|
|
130
153
|
}
|
|
131
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
|
+
}
|
|
132
268
|
/**
|
|
133
269
|
* Register a tool for this service
|
|
134
270
|
*
|
|
@@ -213,15 +349,22 @@ export class KadiClient extends EventEmitter {
|
|
|
213
349
|
return;
|
|
214
350
|
}
|
|
215
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
|
|
216
357
|
if (this.protocol === 'broker' && this._brokerConnections.length > 0) {
|
|
217
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;"
|
|
218
361
|
const broker = this._brokerConnections[0];
|
|
219
362
|
if (broker.ws && broker.ws.readyState === WebSocket.OPEN) {
|
|
220
363
|
const eventMessage = {
|
|
221
364
|
jsonrpc: '2.0',
|
|
222
365
|
method: KadiMessages.EVENT_PUBLISH,
|
|
223
366
|
params: {
|
|
224
|
-
channel: eventName, // RabbitMQ calls this "routing key"
|
|
367
|
+
channel: eventName, // RabbitMQ calls this a "routing key"
|
|
225
368
|
data
|
|
226
369
|
}
|
|
227
370
|
};
|
|
@@ -237,29 +380,77 @@ export class KadiClient extends EventEmitter {
|
|
|
237
380
|
}
|
|
238
381
|
}
|
|
239
382
|
/**
|
|
240
|
-
* Connect to brokers (for event subscription and/or broker protocol)
|
|
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
|
|
241
385
|
*/
|
|
242
|
-
async connectToBrokers(
|
|
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
|
+
}
|
|
243
393
|
// Allow any protocol to connect to brokers for event subscription
|
|
244
394
|
// The protocol only determines how this client itself serves, not what it can connect to
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await this.connectToBroker(
|
|
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(', ')}`);
|
|
251
434
|
}
|
|
252
435
|
this._isConnected = true;
|
|
253
436
|
this.emit('connected');
|
|
254
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
|
+
}
|
|
255
444
|
/**
|
|
256
445
|
* Connect to a single broker
|
|
257
446
|
*/
|
|
258
|
-
async connectToBroker(url) {
|
|
259
|
-
|
|
447
|
+
async connectToBroker(url, brokerName) {
|
|
448
|
+
const displayName = brokerName ? `${brokerName} (${url})` : url;
|
|
449
|
+
this.logger.info('connectToBroker', `Connecting to broker: ${displayName}`);
|
|
260
450
|
const ws = new WebSocket(url);
|
|
261
451
|
const connection = {
|
|
262
452
|
url,
|
|
453
|
+
brokerName,
|
|
263
454
|
ws,
|
|
264
455
|
isAuthenticated: false,
|
|
265
456
|
agentId: '' // Use agentId instead of sessionId
|
|
@@ -290,9 +481,10 @@ export class KadiClient extends EventEmitter {
|
|
|
290
481
|
this.handleBrokerMessage(connection, data.toString());
|
|
291
482
|
});
|
|
292
483
|
ws.on('close', () => {
|
|
293
|
-
this.logger.info('connectToBroker', `
|
|
484
|
+
this.logger.info('connectToBroker', `WebSocket close event for ${brokerName} (${url})`);
|
|
294
485
|
// Clean up heartbeat timer
|
|
295
486
|
if (connection.heartbeatTimer) {
|
|
487
|
+
this.logger.debug('connectToBroker', `Cleaning up heartbeat timer in close event for ${brokerName}`);
|
|
296
488
|
clearInterval(connection.heartbeatTimer);
|
|
297
489
|
connection.heartbeatTimer = undefined;
|
|
298
490
|
this.logger.debug('connectToBroker', 'Heartbeat timer cleaned up');
|
|
@@ -414,11 +606,9 @@ export class KadiClient extends EventEmitter {
|
|
|
414
606
|
sendRequest(connection, message) {
|
|
415
607
|
return new Promise((resolve, reject) => {
|
|
416
608
|
const id = message.id;
|
|
417
|
-
|
|
418
|
-
console.log(`[KadiClient] 📤 SENDING REQUEST:`, JSON.stringify(message, null, 2));
|
|
609
|
+
this.logger.trace('sendBrokerRequest', `Sending ${message.method} request (ID: ${id})`);
|
|
419
610
|
const timeout = setTimeout(() => {
|
|
420
|
-
|
|
421
|
-
console.log(`[KadiClient] ⏰ TIMEOUT for request ${id} (${message.method})`);
|
|
611
|
+
this.logger.warn('sendBrokerRequest', `Request timeout for ${message.method} (ID: ${id})`);
|
|
422
612
|
this._pendingResponses.delete(id);
|
|
423
613
|
reject(new Error(`Request timeout: ${message.method}`));
|
|
424
614
|
}, 30000);
|
|
@@ -427,8 +617,7 @@ export class KadiClient extends EventEmitter {
|
|
|
427
617
|
reject,
|
|
428
618
|
timer: timeout
|
|
429
619
|
});
|
|
430
|
-
|
|
431
|
-
console.log(`[KadiClient] 📝 STORED pending response for ID: ${id}`);
|
|
620
|
+
this.logger.trace('sendBrokerRequest', `Stored pending response for ${message.method} (ID: ${id})`);
|
|
432
621
|
connection.ws.send(JSON.stringify(message));
|
|
433
622
|
});
|
|
434
623
|
}
|
|
@@ -533,9 +722,9 @@ export class KadiClient extends EventEmitter {
|
|
|
533
722
|
return;
|
|
534
723
|
}
|
|
535
724
|
try {
|
|
536
|
-
|
|
725
|
+
this.logger.debug('handleToolInvocation', `Executing tool: ${toolName}`);
|
|
537
726
|
const result = await method.handler(toolInput);
|
|
538
|
-
|
|
727
|
+
this.logger.debug('handleToolInvocation', `Tool ${toolName} completed successfully`);
|
|
539
728
|
// Send kadi.ability.result with success result
|
|
540
729
|
const resultMessage = {
|
|
541
730
|
jsonrpc: '2.0',
|
|
@@ -546,11 +735,11 @@ export class KadiClient extends EventEmitter {
|
|
|
546
735
|
result
|
|
547
736
|
}
|
|
548
737
|
};
|
|
549
|
-
|
|
738
|
+
this.logger.trace('handleToolInvocation', `Sending result for tool: ${toolName}`);
|
|
550
739
|
connection.ws.send(JSON.stringify(resultMessage));
|
|
551
740
|
}
|
|
552
741
|
catch (error) {
|
|
553
|
-
|
|
742
|
+
this.logger.error('handleToolInvocation', `Tool ${toolName} failed: ${error.message}`);
|
|
554
743
|
// Send kadi.ability.result with error
|
|
555
744
|
const errorMessage = {
|
|
556
745
|
jsonrpc: '2.0',
|
|
@@ -619,7 +808,7 @@ export class KadiClient extends EventEmitter {
|
|
|
619
808
|
name: this.name,
|
|
620
809
|
version: this.version,
|
|
621
810
|
description: this.description,
|
|
622
|
-
|
|
811
|
+
network: this.network
|
|
623
812
|
});
|
|
624
813
|
}
|
|
625
814
|
}
|
|
@@ -632,8 +821,15 @@ export class KadiClient extends EventEmitter {
|
|
|
632
821
|
protocol: this.protocol,
|
|
633
822
|
tools: this.getToolNames()
|
|
634
823
|
});
|
|
635
|
-
// Keep process alive
|
|
636
|
-
|
|
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
|
+
});
|
|
637
833
|
case 'stdio':
|
|
638
834
|
// For stdio serving, delegate to StdioTransport's serve method
|
|
639
835
|
// This handles JSON-RPC over stdin/stdout when running as a child process
|
|
@@ -729,7 +925,7 @@ export class KadiClient extends EventEmitter {
|
|
|
729
925
|
* @returns The result from the remote tool invocation
|
|
730
926
|
*/
|
|
731
927
|
async callTool(targetAgent, toolName, params) {
|
|
732
|
-
if (this.
|
|
928
|
+
if (this._brokerConnections.length === 0) {
|
|
733
929
|
throw new Error('Must be connected to broker to call remote tools');
|
|
734
930
|
}
|
|
735
931
|
const broker = this._brokerConnections[0];
|
|
@@ -768,10 +964,130 @@ export class KadiClient extends EventEmitter {
|
|
|
768
964
|
}
|
|
769
965
|
}
|
|
770
966
|
/**
|
|
771
|
-
*
|
|
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"});
|
|
772
1061
|
*/
|
|
773
1062
|
async loadAbility(nameOrPath, protocol = 'native', options = {}) {
|
|
774
|
-
|
|
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);
|
|
775
1091
|
this._abilities.set(ability.name, ability);
|
|
776
1092
|
// For native/stdio protocols, connect ability events to this client's event system
|
|
777
1093
|
if (protocol === 'native' || protocol === 'stdio') {
|
|
@@ -804,21 +1120,40 @@ export class KadiClient extends EventEmitter {
|
|
|
804
1120
|
return ability;
|
|
805
1121
|
}
|
|
806
1122
|
/**
|
|
807
|
-
* Disconnect from brokers
|
|
1123
|
+
* Disconnect from brokers and cleanup resources
|
|
808
1124
|
*/
|
|
809
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`);
|
|
810
1139
|
for (const connection of this._brokerConnections) {
|
|
811
1140
|
// Clean up heartbeat timer before closing
|
|
812
1141
|
if (connection.heartbeatTimer) {
|
|
1142
|
+
this.logger.debug('disconnect', `Clearing heartbeat timer for ${connection.brokerName}`);
|
|
813
1143
|
clearInterval(connection.heartbeatTimer);
|
|
814
1144
|
connection.heartbeatTimer = undefined;
|
|
815
1145
|
}
|
|
816
1146
|
if (connection.ws) {
|
|
1147
|
+
this.logger.debug('disconnect', `Closing WebSocket for ${connection.brokerName}`);
|
|
817
1148
|
connection.ws.close();
|
|
818
1149
|
}
|
|
819
1150
|
}
|
|
820
1151
|
this._brokerConnections.length = 0;
|
|
821
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}`);
|
|
822
1157
|
this.emit('disconnected');
|
|
823
1158
|
}
|
|
824
1159
|
/**
|
|
@@ -836,6 +1171,7 @@ export class KadiClient extends EventEmitter {
|
|
|
836
1171
|
}
|
|
837
1172
|
/**
|
|
838
1173
|
* Find agent.json file
|
|
1174
|
+
* TODO: Not sure, but maybe this function can be moved to pathUtils.ts?
|
|
839
1175
|
*/
|
|
840
1176
|
async _findAgentJson() {
|
|
841
1177
|
const possiblePaths = [
|
|
@@ -971,9 +1307,13 @@ export class KadiClient extends EventEmitter {
|
|
|
971
1307
|
this.on(ABILITY_EVENT_TRANSPORT, (envelope) => {
|
|
972
1308
|
this.logger.trace('subscribeToEvent', `Received ${ABILITY_EVENT_TRANSPORT}: ${envelope.eventName}`);
|
|
973
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
|
|
974
1312
|
for (const [registeredPattern] of this._eventSubscriptions) {
|
|
1313
|
+
// Example: If event is 'user.login' and pattern is 'user.*', this matches!
|
|
975
1314
|
if (this._matchesPattern(envelope.eventName, registeredPattern)) {
|
|
976
1315
|
this.logger.debug('subscribeToEvent', `Pattern matched (${envelope.eventName} vs ${registeredPattern}), dispatching event`);
|
|
1316
|
+
// Found a match! Call all callbacks registered for this pattern
|
|
977
1317
|
this._dispatchEvent(registeredPattern, envelope.data);
|
|
978
1318
|
}
|
|
979
1319
|
}
|
|
@@ -1002,8 +1342,11 @@ export class KadiClient extends EventEmitter {
|
|
|
1002
1342
|
/**
|
|
1003
1343
|
* Subscribe to broker events for this specific pattern.
|
|
1004
1344
|
* Broker subscriptions are per-pattern, unlike the transport listeners above.
|
|
1345
|
+
* Only connect to broker if we're actually using the broker protocol.
|
|
1005
1346
|
*/
|
|
1006
|
-
if (this.
|
|
1347
|
+
if (this.protocol === 'broker' &&
|
|
1348
|
+
this.brokers &&
|
|
1349
|
+
Object.keys(this.brokers).length > 0) {
|
|
1007
1350
|
if (this._brokerConnections.length === 0) {
|
|
1008
1351
|
this.logger.debug('subscribeToEvent', 'Connecting to broker for event subscription');
|
|
1009
1352
|
this.connectToBrokers()
|
|
@@ -1127,21 +1470,34 @@ export class KadiClient extends EventEmitter {
|
|
|
1127
1470
|
return false;
|
|
1128
1471
|
}
|
|
1129
1472
|
/**
|
|
1130
|
-
* Dispatch event to subscribers
|
|
1131
|
-
*
|
|
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)`
|
|
1132
1480
|
*
|
|
1133
1481
|
* @private
|
|
1134
|
-
* @param pattern Pattern that matched
|
|
1135
|
-
* @param data Event data
|
|
1482
|
+
* @param pattern Pattern that matched (e.g., 'user.login' or 'payment.*')
|
|
1483
|
+
* @param data Event data to pass to the callbacks
|
|
1136
1484
|
*/
|
|
1137
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
|
|
1138
1488
|
const callbacks = this._eventSubscriptions.get(pattern);
|
|
1139
1489
|
if (callbacks) {
|
|
1490
|
+
// Call each registered callback with the event data
|
|
1491
|
+
// If multiple functions subscribed to same pattern, all get called
|
|
1140
1492
|
callbacks.forEach((callback) => {
|
|
1141
1493
|
try {
|
|
1494
|
+
// This is THE moment the user's function gets called!
|
|
1495
|
+
// Example: callback(data) might be userFunction({userId: 123, timestamp: ...})
|
|
1142
1496
|
callback(data);
|
|
1143
1497
|
}
|
|
1144
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
|
|
1145
1501
|
this.logger.error('_dispatchEvent', `Error in event callback for ${pattern}:`, error);
|
|
1146
1502
|
}
|
|
1147
1503
|
});
|