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