@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.
- package/README.md +362 -1305
- package/dist/client.d.ts +573 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1673 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +107 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +147 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +37 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -23
- package/dist/index.js.map +1 -1
- package/dist/lockfile.d.ts +190 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/lockfile.js +373 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/transports/broker.d.ts +75 -0
- package/dist/transports/broker.d.ts.map +1 -0
- package/dist/transports/broker.js +383 -0
- package/dist/transports/broker.js.map +1 -0
- package/dist/transports/native.d.ts +39 -0
- package/dist/transports/native.d.ts.map +1 -0
- package/dist/transports/native.js +189 -0
- package/dist/transports/native.js.map +1 -0
- package/dist/transports/stdio.d.ts +46 -0
- package/dist/transports/stdio.d.ts.map +1 -0
- package/dist/transports/stdio.js +460 -0
- package/dist/transports/stdio.js.map +1 -0
- package/dist/types.d.ts +664 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/zod.d.ts +34 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +60 -0
- package/dist/zod.js.map +1 -0
- package/package.json +13 -28
- package/dist/KadiClient.d.ts +0 -470
- package/dist/KadiClient.d.ts.map +0 -1
- package/dist/KadiClient.js +0 -1572
- package/dist/KadiClient.js.map +0 -1
- package/dist/errors/error-codes.d.ts +0 -985
- package/dist/errors/error-codes.d.ts.map +0 -1
- package/dist/errors/error-codes.js +0 -638
- package/dist/errors/error-codes.js.map +0 -1
- package/dist/loadAbility.d.ts +0 -105
- package/dist/loadAbility.d.ts.map +0 -1
- package/dist/loadAbility.js +0 -370
- package/dist/loadAbility.js.map +0 -1
- package/dist/messages/BrokerMessages.d.ts +0 -84
- package/dist/messages/BrokerMessages.d.ts.map +0 -1
- package/dist/messages/BrokerMessages.js +0 -125
- package/dist/messages/BrokerMessages.js.map +0 -1
- package/dist/messages/MessageBuilder.d.ts +0 -83
- package/dist/messages/MessageBuilder.d.ts.map +0 -1
- package/dist/messages/MessageBuilder.js +0 -144
- package/dist/messages/MessageBuilder.js.map +0 -1
- package/dist/schemas/events.schemas.d.ts +0 -177
- package/dist/schemas/events.schemas.d.ts.map +0 -1
- package/dist/schemas/events.schemas.js +0 -265
- package/dist/schemas/events.schemas.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -3
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -4
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/kadi.schemas.d.ts +0 -70
- package/dist/schemas/kadi.schemas.d.ts.map +0 -1
- package/dist/schemas/kadi.schemas.js +0 -120
- package/dist/schemas/kadi.schemas.js.map +0 -1
- package/dist/transports/BrokerTransport.d.ts +0 -102
- package/dist/transports/BrokerTransport.d.ts.map +0 -1
- package/dist/transports/BrokerTransport.js +0 -177
- package/dist/transports/BrokerTransport.js.map +0 -1
- package/dist/transports/NativeTransport.d.ts +0 -82
- package/dist/transports/NativeTransport.d.ts.map +0 -1
- package/dist/transports/NativeTransport.js +0 -263
- package/dist/transports/NativeTransport.js.map +0 -1
- package/dist/transports/StdioTransport.d.ts +0 -112
- package/dist/transports/StdioTransport.d.ts.map +0 -1
- package/dist/transports/StdioTransport.js +0 -450
- package/dist/transports/StdioTransport.js.map +0 -1
- package/dist/transports/Transport.d.ts +0 -93
- package/dist/transports/Transport.d.ts.map +0 -1
- package/dist/transports/Transport.js +0 -13
- package/dist/transports/Transport.js.map +0 -1
- package/dist/types/broker.d.ts +0 -31
- package/dist/types/broker.d.ts.map +0 -1
- package/dist/types/broker.js +0 -6
- package/dist/types/broker.js.map +0 -1
- package/dist/types/core.d.ts +0 -139
- package/dist/types/core.d.ts.map +0 -1
- package/dist/types/core.js +0 -26
- package/dist/types/core.js.map +0 -1
- package/dist/types/events.d.ts +0 -186
- package/dist/types/events.d.ts.map +0 -1
- package/dist/types/events.js +0 -16
- package/dist/types/events.js.map +0 -1
- package/dist/types/index.d.ts +0 -9
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -13
- package/dist/types/index.js.map +0 -1
- package/dist/types/protocol.d.ts +0 -160
- package/dist/types/protocol.d.ts.map +0 -1
- package/dist/types/protocol.js +0 -5
- package/dist/types/protocol.js.map +0 -1
- package/dist/utils/agentUtils.d.ts +0 -187
- package/dist/utils/agentUtils.d.ts.map +0 -1
- package/dist/utils/agentUtils.js +0 -185
- package/dist/utils/agentUtils.js.map +0 -1
- package/dist/utils/commandUtils.d.ts +0 -45
- package/dist/utils/commandUtils.d.ts.map +0 -1
- package/dist/utils/commandUtils.js +0 -145
- package/dist/utils/commandUtils.js.map +0 -1
- package/dist/utils/configUtils.d.ts +0 -55
- package/dist/utils/configUtils.d.ts.map +0 -1
- package/dist/utils/configUtils.js +0 -100
- package/dist/utils/configUtils.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -59
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -122
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/pathUtils.d.ts +0 -48
- package/dist/utils/pathUtils.d.ts.map +0 -1
- package/dist/utils/pathUtils.js +0 -128
- package/dist/utils/pathUtils.js.map +0 -1
package/dist/KadiClient.js
DELETED
|
@@ -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
|