@kadi.build/core 0.0.1-alpha.3 → 0.0.1-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +754 -606
- package/dist/KadiClient.d.ts +440 -0
- package/dist/KadiClient.d.ts.map +1 -0
- package/dist/KadiClient.js +1518 -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 +106 -0
- package/dist/loadAbility.d.ts.map +1 -0
- package/dist/loadAbility.js +376 -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 +125 -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 +106 -0
- package/dist/transports/BrokerTransport.d.ts.map +1 -0
- package/dist/transports/BrokerTransport.js +177 -0
- package/dist/transports/BrokerTransport.js.map +1 -0
- package/dist/transports/NativeTransport.d.ts +82 -0
- package/dist/transports/NativeTransport.d.ts.map +1 -0
- package/dist/transports/NativeTransport.js +263 -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 +445 -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 +139 -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 +187 -0
- package/dist/utils/agentUtils.d.ts.map +1 -0
- package/dist/utils/agentUtils.js +185 -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 +56 -5
- package/agent.json +0 -18
- package/examples/example-abilities/echo-js/README.md +0 -131
- package/examples/example-abilities/echo-js/agent.json +0 -63
- package/examples/example-abilities/echo-js/package.json +0 -24
- package/examples/example-abilities/echo-js/service.js +0 -43
- package/examples/example-abilities/hash-go/agent.json +0 -53
- package/examples/example-abilities/hash-go/cmd/hash_ability/main.go +0 -340
- package/examples/example-abilities/hash-go/go.mod +0 -3
- package/examples/example-agent/abilities/echo-js/0.0.1/README.md +0 -131
- package/examples/example-agent/abilities/echo-js/0.0.1/agent.json +0 -63
- package/examples/example-agent/abilities/echo-js/0.0.1/package-lock.json +0 -93
- package/examples/example-agent/abilities/echo-js/0.0.1/package.json +0 -24
- package/examples/example-agent/abilities/echo-js/0.0.1/service.js +0 -41
- package/examples/example-agent/abilities/hash-go/0.0.1/agent.json +0 -53
- package/examples/example-agent/abilities/hash-go/0.0.1/bin/hash_ability +0 -0
- package/examples/example-agent/abilities/hash-go/0.0.1/cmd/hash_ability/main.go +0 -340
- package/examples/example-agent/abilities/hash-go/0.0.1/go.mod +0 -3
- package/examples/example-agent/agent.json +0 -39
- package/examples/example-agent/index.js +0 -102
- package/examples/example-agent/package-lock.json +0 -93
- package/examples/example-agent/package.json +0 -17
- package/src/KadiAbility.js +0 -478
- package/src/index.js +0 -65
- package/src/loadAbility.js +0 -1086
- package/src/servers/BaseRpcServer.js +0 -404
- package/src/servers/BrokerRpcServer.js +0 -776
- package/src/servers/StdioRpcServer.js +0 -360
- package/src/transport/BrokerMessageBuilder.js +0 -377
- package/src/transport/IpcMessageBuilder.js +0 -1229
- package/src/utils/agentUtils.js +0 -137
- package/src/utils/commandUtils.js +0 -64
- package/src/utils/configUtils.js +0 -72
- package/src/utils/logger.js +0 -161
- package/src/utils/pathUtils.js +0 -86
|
@@ -1,776 +0,0 @@
|
|
|
1
|
-
import { BaseRpcServer } from './BaseRpcServer.js';
|
|
2
|
-
import { Broker, IdFactory } from '../transport/BrokerMessageBuilder.js';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
|
-
import { createComponentLogger } from '../utils/logger.js';
|
|
6
|
-
/**
|
|
7
|
-
* RPC Server for broker transport using WebSocket
|
|
8
|
-
*
|
|
9
|
-
* This server connects abilities to the Kadi broker via WebSocket and speaks
|
|
10
|
-
* the broker's native protocol directly (agent.message/ability.result),
|
|
11
|
-
* providing distributed ability execution across the network.
|
|
12
|
-
*/
|
|
13
|
-
export class BrokerRpcServer extends BaseRpcServer {
|
|
14
|
-
/**
|
|
15
|
-
* Create a new BrokerRpcServer instance
|
|
16
|
-
*
|
|
17
|
-
* @param {Object} options - Configuration options
|
|
18
|
-
* @param {string} options.brokerUrl - Broker WebSocket URL
|
|
19
|
-
* @param {string} options.serviceName - Service name for broker registration
|
|
20
|
-
* @param {number} options.connectTimeoutMs - Connection timeout
|
|
21
|
-
* @param {number} options.heartbeatIntervalMs - Heartbeat interval
|
|
22
|
-
*/
|
|
23
|
-
constructor(options = {}) {
|
|
24
|
-
super({ ...options, protocol: 'broker' });
|
|
25
|
-
|
|
26
|
-
this.brokerUrl =
|
|
27
|
-
options.brokerUrl || process.env.KADI_BROKER_URL || 'ws://localhost:8080';
|
|
28
|
-
this.serviceName =
|
|
29
|
-
options.serviceName || process.env.KADI_SERVICE_NAME || 'unnamed-ability';
|
|
30
|
-
this.connectTimeoutMs = options.connectTimeoutMs || 10000;
|
|
31
|
-
this.heartbeatIntervalMs = options.heartbeatIntervalMs || 25000;
|
|
32
|
-
|
|
33
|
-
this.logger = createComponentLogger('BrokerRpcServer');
|
|
34
|
-
this.logger.lifecycle('constructor', 'BrokerRpcServer initialized');
|
|
35
|
-
this.logger.trace('constructor', `Broker URL: ${this.brokerUrl}`);
|
|
36
|
-
this.logger.trace('constructor', `Service name: ${this.serviceName}`);
|
|
37
|
-
this.logger.trace(
|
|
38
|
-
'constructor',
|
|
39
|
-
`Connect timeout: ${this.connectTimeoutMs}ms`
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
this._ws = null;
|
|
43
|
-
this._isConnected = false;
|
|
44
|
-
this._idFactory = new IdFactory();
|
|
45
|
-
this._requestMetadata = new Map(); // Store broker metadata by request ID
|
|
46
|
-
this._heartbeatTimer = null;
|
|
47
|
-
this._reconnectAttempts = 0;
|
|
48
|
-
this._maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
|
49
|
-
this._reconnectDelayMs = options.reconnectDelayMs || 1000;
|
|
50
|
-
|
|
51
|
-
this.ability = null;
|
|
52
|
-
this.resolve = null;
|
|
53
|
-
this.reject = null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Start serving the ability via broker
|
|
58
|
-
*
|
|
59
|
-
* @param {KadiAbility} ability - The ability instance to serve
|
|
60
|
-
* @returns {Promise<void>} - Promise that resolves when server stops
|
|
61
|
-
*/
|
|
62
|
-
async serve(ability) {
|
|
63
|
-
if (this.isServing) {
|
|
64
|
-
this.logger.error('serve', 'BrokerRpcServer is already serving');
|
|
65
|
-
throw new Error('BrokerRpcServer is already serving');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
this.ability = ability;
|
|
69
|
-
this.isServing = true;
|
|
70
|
-
|
|
71
|
-
this.logger.lifecycle(
|
|
72
|
-
'serve',
|
|
73
|
-
`Starting ${ability.name || 'unnamed ability'} on broker`
|
|
74
|
-
);
|
|
75
|
-
this.logger.info('serve', `Broker URL: ${this.brokerUrl}`);
|
|
76
|
-
this.logger.info('serve', `Service name: ${this.serviceName}`);
|
|
77
|
-
this.logger.info(
|
|
78
|
-
'serve',
|
|
79
|
-
`Available methods: ${ability.getMethodNames().join(', ')}`
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
// Connect to broker and complete handshake
|
|
84
|
-
await this._connect();
|
|
85
|
-
await this._handshake();
|
|
86
|
-
|
|
87
|
-
// Emit start event
|
|
88
|
-
this.emit('start', {
|
|
89
|
-
name: ability.name,
|
|
90
|
-
version: ability.version,
|
|
91
|
-
methods: ability.getMethodNames()
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
this.logger.success(
|
|
95
|
-
'serve',
|
|
96
|
-
'Broker service started, ready to receive tool calls'
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
// Keep the process alive and handle messages
|
|
100
|
-
return new Promise((resolve, reject) => {
|
|
101
|
-
this.resolve = resolve;
|
|
102
|
-
this.reject = reject;
|
|
103
|
-
});
|
|
104
|
-
} catch (error) {
|
|
105
|
-
this.isServing = false;
|
|
106
|
-
this.logger.error(
|
|
107
|
-
'serve',
|
|
108
|
-
`Failed to start broker service: ${error.message}`
|
|
109
|
-
);
|
|
110
|
-
this.emit('error', error);
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Publish an event to connected agents via the broker
|
|
117
|
-
*
|
|
118
|
-
* Events are sent as 'kadi.event' messages through the WebSocket connection.
|
|
119
|
-
* The broker will route these to subscribed agents.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} eventName - Name of the event to publish
|
|
122
|
-
* @param {any} data - Event data payload (must be JSON-serializable)
|
|
123
|
-
*/
|
|
124
|
-
async publishEvent(eventName, data = {}) {
|
|
125
|
-
this.logger.trace(
|
|
126
|
-
'publishEvent',
|
|
127
|
-
`Publishing event via broker: ${eventName}`
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
if (!this._ws || !this._isConnected) {
|
|
131
|
-
this.logger.warn(
|
|
132
|
-
'publishEvent',
|
|
133
|
-
`Cannot publish event ${eventName}: not connected to broker`
|
|
134
|
-
);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
// Send event as a broker notification (no id field)
|
|
140
|
-
const eventMessage = {
|
|
141
|
-
jsonrpc: '2.0',
|
|
142
|
-
method: 'kadi.event',
|
|
143
|
-
params: {
|
|
144
|
-
eventName,
|
|
145
|
-
eventData: data,
|
|
146
|
-
timestamp: Date.now(),
|
|
147
|
-
from: this.serviceName // Include source for routing
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
this._ws.send(JSON.stringify(eventMessage));
|
|
152
|
-
|
|
153
|
-
this.logger.trace('publishEvent', `Event ${eventName} sent to broker`);
|
|
154
|
-
} catch (error) {
|
|
155
|
-
// Events are best-effort - log but don't throw
|
|
156
|
-
this.logger.warn(
|
|
157
|
-
'publishEvent',
|
|
158
|
-
`Failed to publish event ${eventName}: ${error.message}`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Handle requests in broker-native format
|
|
165
|
-
*
|
|
166
|
-
* This implementation handles incoming tool calls from the broker and
|
|
167
|
-
* converts them to internal method calls. Broker requests come as
|
|
168
|
-
* agent.message notifications with tool execution parameters.
|
|
169
|
-
*
|
|
170
|
-
* @param {Object} message - The broker message object
|
|
171
|
-
*/
|
|
172
|
-
async _handleBrokerMessage(message) {
|
|
173
|
-
try {
|
|
174
|
-
this.logger.trace(
|
|
175
|
-
'message',
|
|
176
|
-
`Received broker message: ${message.method}`
|
|
177
|
-
);
|
|
178
|
-
this.logger.trace('message-detail', JSON.stringify(message, null, 2));
|
|
179
|
-
|
|
180
|
-
// Handle incoming tool calls
|
|
181
|
-
if (message.method === 'agent.message' && message.params) {
|
|
182
|
-
await this._handleToolCall(message);
|
|
183
|
-
}
|
|
184
|
-
// Handle ability results (responses to our tool calls)
|
|
185
|
-
else if (message.method === 'ability.result') {
|
|
186
|
-
this.logger.trace('message', 'Received ability.result (not handling)');
|
|
187
|
-
}
|
|
188
|
-
// Handle other broker messages
|
|
189
|
-
else {
|
|
190
|
-
this.logger.warn(
|
|
191
|
-
'message',
|
|
192
|
-
`Unhandled broker message: ${message.method}`
|
|
193
|
-
);
|
|
194
|
-
this.logger.trace('message-detail', JSON.stringify(message, null, 2));
|
|
195
|
-
}
|
|
196
|
-
} catch (error) {
|
|
197
|
-
this.logger.error(
|
|
198
|
-
'message',
|
|
199
|
-
`Failed to handle broker message: ${error.message}`
|
|
200
|
-
);
|
|
201
|
-
this.logError('Failed to handle broker message:', error);
|
|
202
|
-
this.emit('error', error);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Handle incoming tool calls from the broker
|
|
208
|
-
*
|
|
209
|
-
* @param {Object} message - Broker message containing tool call
|
|
210
|
-
*/
|
|
211
|
-
async _handleToolCall(message) {
|
|
212
|
-
const { toolName, args, requestId, from } = message.params;
|
|
213
|
-
|
|
214
|
-
this.logger.request(requestId, toolName, 'Handling tool call');
|
|
215
|
-
this.logger.trace(
|
|
216
|
-
'broker-tool-call',
|
|
217
|
-
`From: ${from}, Args: ${JSON.stringify(args)}`
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
// Convert broker message to internal request format
|
|
222
|
-
const request = {
|
|
223
|
-
id: requestId,
|
|
224
|
-
method: toolName,
|
|
225
|
-
params: args || {}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// Store broker metadata for response routing
|
|
229
|
-
this._requestMetadata.set(requestId, { requestId, from });
|
|
230
|
-
|
|
231
|
-
// Process the request using base class logic
|
|
232
|
-
const response = await this.handleRequest(request);
|
|
233
|
-
|
|
234
|
-
// Send response back to broker
|
|
235
|
-
await this._sendBrokerResponse(requestId, from, response);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
this.logger.error(
|
|
238
|
-
'tool-call',
|
|
239
|
-
`Error handling tool call ${toolName}: ${error.message}`
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
this.logError(`Error handling tool call ${toolName}:`, error);
|
|
243
|
-
|
|
244
|
-
// Send error response to broker
|
|
245
|
-
await this._sendBrokerError(requestId, from, error.message);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Send a successful result back to the broker
|
|
251
|
-
*
|
|
252
|
-
* @param {string} requestId - Original request ID
|
|
253
|
-
* @param {string} toSessionId - Target session ID
|
|
254
|
-
* @param {Object} response - Response object
|
|
255
|
-
*/
|
|
256
|
-
async _sendBrokerResponse(requestId, toSessionId, response) {
|
|
257
|
-
if (!response) {
|
|
258
|
-
// Notification - no response needed
|
|
259
|
-
this.logger.trace(
|
|
260
|
-
'response',
|
|
261
|
-
`No response needed for notification ${requestId}`
|
|
262
|
-
);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const brokerResponse = {
|
|
267
|
-
jsonrpc: '2.0',
|
|
268
|
-
method: 'ability.result',
|
|
269
|
-
params: {
|
|
270
|
-
requestId,
|
|
271
|
-
toSessionId,
|
|
272
|
-
result: response.result
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
this.logger.response(requestId, 'success', 'Sending broker response');
|
|
277
|
-
this.logger.trace(
|
|
278
|
-
'response-detail',
|
|
279
|
-
JSON.stringify(brokerResponse, null, 2)
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
this._ws.send(JSON.stringify(brokerResponse));
|
|
283
|
-
|
|
284
|
-
// Clean up metadata
|
|
285
|
-
this._requestMetadata.delete(requestId);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Send an error result back to the broker
|
|
290
|
-
*
|
|
291
|
-
* @param {string} requestId - Original request ID
|
|
292
|
-
* @param {string} toSessionId - Target session ID
|
|
293
|
-
* @param {string} errorMessage - Error message
|
|
294
|
-
*/
|
|
295
|
-
async _sendBrokerError(requestId, toSessionId, errorMessage) {
|
|
296
|
-
const brokerResponse = {
|
|
297
|
-
jsonrpc: '2.0',
|
|
298
|
-
method: 'ability.result',
|
|
299
|
-
params: {
|
|
300
|
-
requestId,
|
|
301
|
-
toSessionId,
|
|
302
|
-
error: errorMessage
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
this.logger.response(
|
|
307
|
-
requestId,
|
|
308
|
-
'error',
|
|
309
|
-
`Sending broker error: ${errorMessage}`
|
|
310
|
-
);
|
|
311
|
-
this.logger.trace(
|
|
312
|
-
'broker-response-detail',
|
|
313
|
-
JSON.stringify(brokerResponse, null, 2)
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
this._ws.send(JSON.stringify(brokerResponse));
|
|
317
|
-
|
|
318
|
-
// Clean up metadata
|
|
319
|
-
this._requestMetadata.delete(requestId);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Connect to the broker WebSocket
|
|
324
|
-
*/
|
|
325
|
-
async _connect() {
|
|
326
|
-
this.logger.info('connect', `Connecting to broker at ${this.brokerUrl}`);
|
|
327
|
-
|
|
328
|
-
return new Promise((resolve, reject) => {
|
|
329
|
-
const timeout = setTimeout(() => {
|
|
330
|
-
this.logger.error(
|
|
331
|
-
'connect',
|
|
332
|
-
`Connection timeout after ${this.connectTimeoutMs}ms`
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
reject(
|
|
336
|
-
new Error(`Connection timeout after ${this.connectTimeoutMs}ms`)
|
|
337
|
-
);
|
|
338
|
-
}, this.connectTimeoutMs);
|
|
339
|
-
|
|
340
|
-
this._ws = new WebSocket(this.brokerUrl);
|
|
341
|
-
|
|
342
|
-
this._ws.on('open', () => {
|
|
343
|
-
clearTimeout(timeout);
|
|
344
|
-
this._isConnected = true;
|
|
345
|
-
this._reconnectAttempts = 0;
|
|
346
|
-
this.logger.success(
|
|
347
|
-
'connect',
|
|
348
|
-
`Connected to broker at ${this.brokerUrl}`
|
|
349
|
-
);
|
|
350
|
-
resolve();
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
this._ws.on('error', (error) => {
|
|
354
|
-
clearTimeout(timeout);
|
|
355
|
-
this.logger.error(
|
|
356
|
-
'connect',
|
|
357
|
-
`WebSocket connection error: ${error.message || error}`
|
|
358
|
-
);
|
|
359
|
-
this.logError('WebSocket connection error:', error.message);
|
|
360
|
-
|
|
361
|
-
reject(new Error(`Failed to connect to broker: ${error.message}`));
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
this._ws.on('close', (code, reason) => {
|
|
365
|
-
this._isConnected = false;
|
|
366
|
-
this.logger.warn(
|
|
367
|
-
'connect',
|
|
368
|
-
`Disconnected from broker (${code}): ${reason}`
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
this.isServing &&
|
|
373
|
-
this._reconnectAttempts < this._maxReconnectAttempts
|
|
374
|
-
) {
|
|
375
|
-
this._attemptReconnect();
|
|
376
|
-
} else {
|
|
377
|
-
this.shutdown('broker_disconnected');
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
this._ws.on('message', (data) => {
|
|
382
|
-
try {
|
|
383
|
-
const message = JSON.parse(data.toString());
|
|
384
|
-
this._handleBrokerMessage(message);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
this.logError('Failed to parse broker message:', error);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Attempt to reconnect to the broker
|
|
394
|
-
*/
|
|
395
|
-
async _attemptReconnect() {
|
|
396
|
-
this._reconnectAttempts++;
|
|
397
|
-
const delay =
|
|
398
|
-
this._reconnectDelayMs * Math.pow(2, this._reconnectAttempts - 1);
|
|
399
|
-
|
|
400
|
-
this.logger.info(
|
|
401
|
-
'reconnect',
|
|
402
|
-
`Attempting to reconnect (${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms`
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
setTimeout(async () => {
|
|
406
|
-
try {
|
|
407
|
-
await this._connect();
|
|
408
|
-
await this._handshake();
|
|
409
|
-
this.logger.success('reconnect', 'Successfully reconnected to broker');
|
|
410
|
-
} catch (error) {
|
|
411
|
-
this.logger.error('reconnect', `Reconnection failed: ${error.message}`);
|
|
412
|
-
if (this._reconnectAttempts >= this._maxReconnectAttempts) {
|
|
413
|
-
this.shutdown('max_reconnect_attempts_reached');
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}, delay);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Complete the broker handshake
|
|
421
|
-
*/
|
|
422
|
-
async _handshake() {
|
|
423
|
-
this.logger.info('handshake', 'Starting broker handshake');
|
|
424
|
-
|
|
425
|
-
// Step 1: Send hello as agent
|
|
426
|
-
const hello = Broker.hello({ role: 'agent' })
|
|
427
|
-
.id(this._idFactory.next())
|
|
428
|
-
.build();
|
|
429
|
-
this._ws.send(JSON.stringify(hello));
|
|
430
|
-
|
|
431
|
-
// Wait for hello response with nonce
|
|
432
|
-
const helloResponse = await this._waitForResponse(hello.id);
|
|
433
|
-
if (!helloResponse.result || !helloResponse.result.nonce) {
|
|
434
|
-
this.logger.trace('handshake', 'Received nonce from broker');
|
|
435
|
-
|
|
436
|
-
throw new Error('Invalid hello response from broker');
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const nonce = helloResponse.result.nonce;
|
|
440
|
-
this.log(`Received nonce from broker`);
|
|
441
|
-
|
|
442
|
-
// Step 2: Generate ephemeral keys and authenticate
|
|
443
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
444
|
-
const publicKeyBase64 = publicKey
|
|
445
|
-
.export({ format: 'der', type: 'spki' })
|
|
446
|
-
.toString('base64');
|
|
447
|
-
|
|
448
|
-
const authenticate = Broker.authenticate({
|
|
449
|
-
publicKeyBase64Der: publicKeyBase64,
|
|
450
|
-
privateKey,
|
|
451
|
-
nonce,
|
|
452
|
-
wantNewId: true
|
|
453
|
-
})
|
|
454
|
-
.id(this._idFactory.next())
|
|
455
|
-
.build();
|
|
456
|
-
|
|
457
|
-
this.logger.trace('handshake', 'Sending authentication to broker');
|
|
458
|
-
this._ws.send(JSON.stringify(authenticate));
|
|
459
|
-
|
|
460
|
-
// Wait for authentication response
|
|
461
|
-
const authResponse = await this._waitForResponse(authenticate.id);
|
|
462
|
-
if (!authResponse.result || !authResponse.result.agentId) {
|
|
463
|
-
this.logger.error(
|
|
464
|
-
'handshake',
|
|
465
|
-
`Authentication failed: ${JSON.stringify(authResponse.error)}`
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
throw new Error(
|
|
469
|
-
`Authentication failed: ${JSON.stringify(authResponse.error)}`
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
this.logger.success(
|
|
474
|
-
'handshake',
|
|
475
|
-
`Authenticated as agent: ${authResponse.result.agentId}`
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
// Step 3: Register capabilities
|
|
479
|
-
await this._registerCapabilities();
|
|
480
|
-
|
|
481
|
-
// Step 4: Start heartbeat
|
|
482
|
-
this._startHeartbeat();
|
|
483
|
-
|
|
484
|
-
this.logger.success('handshake', 'Handshake completed successfully');
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Register capabilities with the broker
|
|
489
|
-
*/
|
|
490
|
-
async _registerCapabilities() {
|
|
491
|
-
if (
|
|
492
|
-
!this.ability ||
|
|
493
|
-
typeof this.ability.extractToolsForBroker !== 'function'
|
|
494
|
-
) {
|
|
495
|
-
this.logger.warn(
|
|
496
|
-
'capabilities',
|
|
497
|
-
'No ability reference or extractToolsForBroker method not found'
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
try {
|
|
504
|
-
this.logger.info('capabilities', 'Extracting tools from ability');
|
|
505
|
-
const tools = await this.ability.extractToolsForBroker();
|
|
506
|
-
// this.log(`Extracted tools:`, JSON.stringify(tools, null, 2));
|
|
507
|
-
|
|
508
|
-
if (tools.length > 0) {
|
|
509
|
-
const registerCapabilities = Broker.registerCapabilities({
|
|
510
|
-
displayName: this.serviceName,
|
|
511
|
-
tools,
|
|
512
|
-
mailboxMode: 'persistent',
|
|
513
|
-
scopes: [this.ability.scope] // We want to register the tools in the same scope as the agent
|
|
514
|
-
})
|
|
515
|
-
.id(this._idFactory.next())
|
|
516
|
-
.build();
|
|
517
|
-
|
|
518
|
-
this._ws.send(JSON.stringify(registerCapabilities));
|
|
519
|
-
|
|
520
|
-
// Wait for registration response
|
|
521
|
-
const registrationResponse = await this._waitForResponse(
|
|
522
|
-
registerCapabilities.id
|
|
523
|
-
);
|
|
524
|
-
this.logger.trace(
|
|
525
|
-
'capabilities',
|
|
526
|
-
`Registration response: ${JSON.stringify(registrationResponse, null, 2)}`
|
|
527
|
-
);
|
|
528
|
-
this.logger.success(
|
|
529
|
-
'capabilities',
|
|
530
|
-
`Registered ${tools.length} capabilities with broker`
|
|
531
|
-
);
|
|
532
|
-
} else {
|
|
533
|
-
this.logger.warn(
|
|
534
|
-
'capabilities',
|
|
535
|
-
'No tools found, skipping capability registration'
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
} catch (error) {
|
|
539
|
-
this.logger.error(
|
|
540
|
-
'capabilities',
|
|
541
|
-
`Failed to register capabilities: ${error.message}`
|
|
542
|
-
);
|
|
543
|
-
throw error;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
/**
|
|
548
|
-
* Start heartbeat to keep connection alive
|
|
549
|
-
*/
|
|
550
|
-
_startHeartbeat() {
|
|
551
|
-
this.logger.trace(
|
|
552
|
-
'heartbeat',
|
|
553
|
-
`Starting heartbeat every ${this.heartbeatIntervalMs}ms`
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
this._heartbeatTimer = setInterval(() => {
|
|
557
|
-
if (this._isConnected && this._ws.readyState === WebSocket.OPEN) {
|
|
558
|
-
if (this.logger.enabled) {
|
|
559
|
-
this.logger.trace('heartbeat', 'Sending ping to broker');
|
|
560
|
-
}
|
|
561
|
-
this._ws.send(
|
|
562
|
-
JSON.stringify({
|
|
563
|
-
jsonrpc: '2.0',
|
|
564
|
-
method: 'kadi.ping'
|
|
565
|
-
})
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
}, this.heartbeatIntervalMs);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Wait for a response to a specific message
|
|
573
|
-
*
|
|
574
|
-
* @param {string|number} messageId - Message ID to wait for
|
|
575
|
-
* @param {number} timeoutMs - Timeout in milliseconds
|
|
576
|
-
* @returns {Promise<Object>} - Response message
|
|
577
|
-
*/
|
|
578
|
-
async _waitForResponse(messageId, timeoutMs = 5000) {
|
|
579
|
-
this.logger.trace(
|
|
580
|
-
'wait-response',
|
|
581
|
-
`Waiting for response to message ${messageId} (timeout: ${timeoutMs}ms)`
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
return new Promise((resolve, reject) => {
|
|
585
|
-
const timeout = setTimeout(() => {
|
|
586
|
-
this._ws.off('message', handler);
|
|
587
|
-
this.logger.error(
|
|
588
|
-
'wait-response',
|
|
589
|
-
`Timeout waiting for response to message ${messageId}`
|
|
590
|
-
);
|
|
591
|
-
|
|
592
|
-
reject(
|
|
593
|
-
new Error(`Timeout waiting for response to message ${messageId}`)
|
|
594
|
-
);
|
|
595
|
-
}, timeoutMs);
|
|
596
|
-
|
|
597
|
-
const handler = (data) => {
|
|
598
|
-
try {
|
|
599
|
-
const message = JSON.parse(data.toString());
|
|
600
|
-
if (message.id === messageId) {
|
|
601
|
-
clearTimeout(timeout);
|
|
602
|
-
this._ws.off('message', handler);
|
|
603
|
-
this.logger.trace(
|
|
604
|
-
'wait-response',
|
|
605
|
-
`Received response for message ${messageId}`
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
resolve(message);
|
|
609
|
-
}
|
|
610
|
-
} catch (error) {
|
|
611
|
-
// Ignore parsing errors for non-matching messages
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
this._ws.on('message', handler);
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Gracefully shutdown the broker server
|
|
621
|
-
*
|
|
622
|
-
* @param {string} reason - Reason for shutdown
|
|
623
|
-
*/
|
|
624
|
-
async shutdown(reason = 'unknown') {
|
|
625
|
-
this.logger.lifecycle(
|
|
626
|
-
'shutdown',
|
|
627
|
-
`Shutting down broker server, reason: ${reason}`
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
await super.shutdown(reason);
|
|
631
|
-
|
|
632
|
-
// Stop heartbeat
|
|
633
|
-
if (this._heartbeatTimer) {
|
|
634
|
-
this.logger.trace('shutdown', 'Stopping heartbeat timer');
|
|
635
|
-
|
|
636
|
-
clearInterval(this._heartbeatTimer);
|
|
637
|
-
this._heartbeatTimer = null;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Close WebSocket connection
|
|
641
|
-
if (this._ws) {
|
|
642
|
-
this._isConnected = false;
|
|
643
|
-
this.logger.trace('shutdown', 'Closing WebSocket connection');
|
|
644
|
-
|
|
645
|
-
this._ws.close();
|
|
646
|
-
this._ws = null;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Clear request metadata
|
|
650
|
-
this._requestMetadata.clear();
|
|
651
|
-
|
|
652
|
-
// Resolve the serve promise
|
|
653
|
-
if (this.resolve) {
|
|
654
|
-
this.logger.trace('shutdown', 'Resolving serve promise');
|
|
655
|
-
this.resolve();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Send a message to the broker
|
|
661
|
-
*
|
|
662
|
-
* @param {Object} message - Message to send
|
|
663
|
-
*/
|
|
664
|
-
async sendMessage(message) {
|
|
665
|
-
if (!this._ws || !this._isConnected) {
|
|
666
|
-
throw new Error('Not connected to broker');
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
this._ws.send(JSON.stringify(message));
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* Call another ability through the broker
|
|
674
|
-
*
|
|
675
|
-
* @param {string} toolName - Name of the tool/ability to call
|
|
676
|
-
* @param {Object} args - Arguments to pass
|
|
677
|
-
* @returns {Promise<any>} - Result from the ability
|
|
678
|
-
*/
|
|
679
|
-
async callAbility(toolName, args = {}) {
|
|
680
|
-
this.logger.info('call-ability', `Calling ability: ${toolName}`);
|
|
681
|
-
this.logger.trace('call-ability', `Args: ${JSON.stringify(args)}`);
|
|
682
|
-
|
|
683
|
-
const callMessage = Broker.callAbility({ toolName, args })
|
|
684
|
-
.id(this._idFactory.next())
|
|
685
|
-
.build();
|
|
686
|
-
|
|
687
|
-
const ack = await this._sendAndWaitForResponse(callMessage);
|
|
688
|
-
if (ack.error) {
|
|
689
|
-
this.logger.error(
|
|
690
|
-
'call-ability',
|
|
691
|
-
`Call failed: ${ack.error.message || 'agent.callAbility failed'}`
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
throw new Error(ack.error.message || 'agent.callAbility failed');
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const expectedRequestId = ack.result?.requestId;
|
|
698
|
-
this.logger.trace(
|
|
699
|
-
'call-ability',
|
|
700
|
-
`Waiting for ability result, requestId: ${expectedRequestId}`
|
|
701
|
-
);
|
|
702
|
-
|
|
703
|
-
return new Promise((resolve, reject) => {
|
|
704
|
-
const timeout = setTimeout(() => {
|
|
705
|
-
this._ws.off('message', handler);
|
|
706
|
-
this.logger.error(
|
|
707
|
-
'call-ability',
|
|
708
|
-
`Ability call timeout for ${toolName}`
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
reject(new Error('Ability call timeout'));
|
|
712
|
-
}, this.timeoutMs);
|
|
713
|
-
|
|
714
|
-
const handler = (data) => {
|
|
715
|
-
try {
|
|
716
|
-
const message = JSON.parse(data.toString());
|
|
717
|
-
if (message.method === 'ability.result') {
|
|
718
|
-
const { requestId, result, error } = message.params || {};
|
|
719
|
-
if (requestId === expectedRequestId) {
|
|
720
|
-
clearTimeout(timeout);
|
|
721
|
-
this._ws.off('message', handler);
|
|
722
|
-
if (error) {
|
|
723
|
-
this.logger.error(
|
|
724
|
-
'call-ability',
|
|
725
|
-
`Ability ${toolName} returned error: ${error.message || error}`
|
|
726
|
-
);
|
|
727
|
-
|
|
728
|
-
reject(new Error(error.message || 'Ability error'));
|
|
729
|
-
} else {
|
|
730
|
-
this.logger.success(
|
|
731
|
-
'call-ability',
|
|
732
|
-
`Ability ${toolName} completed successfully`
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
resolve(result);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
} catch (parseError) {
|
|
740
|
-
// Ignore parsing errors
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
this._ws.on('message', handler);
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* Send a message and wait for its response
|
|
750
|
-
*
|
|
751
|
-
* @param {Object} message - Message to send
|
|
752
|
-
* @returns {Promise<Object>} - Response message
|
|
753
|
-
*/
|
|
754
|
-
async _sendAndWaitForResponse(message) {
|
|
755
|
-
this.logger.trace(
|
|
756
|
-
'send-wait',
|
|
757
|
-
`Sending message and waiting for response: ${message.method || 'unknown'}`
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
this._ws.send(JSON.stringify(message));
|
|
761
|
-
const response = await this._waitForResponse(message.id);
|
|
762
|
-
|
|
763
|
-
if (response.error) {
|
|
764
|
-
this.logger.warn(
|
|
765
|
-
'send-wait',
|
|
766
|
-
`Response contained error: ${JSON.stringify(response.error)}`
|
|
767
|
-
);
|
|
768
|
-
} else {
|
|
769
|
-
this.logger.trace('send-wait', 'Response received successfully');
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return response;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
export default BrokerRpcServer;
|