@kadi.build/core 0.0.1-alpha.9 → 0.1.0

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