@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
package/src/loadAbility.js
DELETED
|
@@ -1,1086 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import { WebSocket } from 'ws';
|
|
5
|
-
import { generateKeyPairSync, randomUUID } from 'crypto';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import EventEmitter from 'events';
|
|
8
|
-
|
|
9
|
-
import { getProjectJSON, getAbilityJSON } from './utils/agentUtils.js';
|
|
10
|
-
import { rootDir } from './utils/pathUtils.js';
|
|
11
|
-
import { StdioFrameReader, Ipc } from './transport/IpcMessageBuilder.js';
|
|
12
|
-
import {
|
|
13
|
-
Broker,
|
|
14
|
-
IdFactory,
|
|
15
|
-
toBase64Der
|
|
16
|
-
} from './transport/BrokerMessageBuilder.js';
|
|
17
|
-
|
|
18
|
-
import { createComponentLogger, formatObject } from './utils/logger.js';
|
|
19
|
-
import { FRAME_HEADERS, FRAME_VALUES } from './transport/IpcMessageBuilder.js';
|
|
20
|
-
|
|
21
|
-
const logger = createComponentLogger('loadAbility');
|
|
22
|
-
|
|
23
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
-
|
|
25
|
-
// ===== ability loader cache (avoid re-spawning on repeated loads) =====
|
|
26
|
-
// key: `${name}@${version}:protocol` -> proxy/module
|
|
27
|
-
const __abilityCache = new Map();
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Load an ability by name and return a uniform interface (module or proxy).
|
|
31
|
-
*
|
|
32
|
-
* Supports three protocols via an optional manifest at:
|
|
33
|
-
* abilities/<name>/<version>/ability.json
|
|
34
|
-
*
|
|
35
|
-
* - protocol: "native" -> in-process import
|
|
36
|
-
* - protocol: "stdio" -> spawn local process, JSON-RPC over stdio
|
|
37
|
-
* - protocol: "broker" -> request/response via the KADI Broker
|
|
38
|
-
*/
|
|
39
|
-
export async function loadAbility(abilityName, protocol) {
|
|
40
|
-
logger.lifecycle('start', `Loading ability: ${abilityName}`);
|
|
41
|
-
logger.info('loadAbility', `Protocol: ${protocol || 'auto'}`);
|
|
42
|
-
|
|
43
|
-
// ------------------------------
|
|
44
|
-
// 1) Resolve version to load (project or nested ability context)
|
|
45
|
-
// ------------------------------
|
|
46
|
-
let agentJSON = await getProjectJSON();
|
|
47
|
-
let abilityVersion = agentJSON?.abilities?.[abilityName] || null;
|
|
48
|
-
|
|
49
|
-
if (!abilityVersion) {
|
|
50
|
-
// Fallback when loadAbility() is called from inside an ability directory
|
|
51
|
-
logger.warn(
|
|
52
|
-
'config',
|
|
53
|
-
`Ability ${abilityName} not found in Project configuration`
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
// Identify parent ability name/version from current file path
|
|
57
|
-
const filePath = __filename;
|
|
58
|
-
const dirPath = path.dirname(filePath);
|
|
59
|
-
const pathParts = dirPath.split(path.sep);
|
|
60
|
-
const abilitiesIndex = pathParts.indexOf('abilities');
|
|
61
|
-
|
|
62
|
-
if (abilitiesIndex !== -1 && pathParts.length > abilitiesIndex + 2) {
|
|
63
|
-
const parentAbility = pathParts[abilitiesIndex + 1];
|
|
64
|
-
const parentVersion = pathParts[abilitiesIndex + 2];
|
|
65
|
-
|
|
66
|
-
logger.info(
|
|
67
|
-
'context',
|
|
68
|
-
`Loading from nested ability context: ${parentAbility}:${parentVersion}`
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
agentJSON = await getAbilityJSON(parentAbility, parentVersion);
|
|
72
|
-
abilityVersion = agentJSON?.abilities?.[abilityName] || null;
|
|
73
|
-
|
|
74
|
-
if (!abilityVersion) {
|
|
75
|
-
logger.error(
|
|
76
|
-
'config',
|
|
77
|
-
`Ability ${abilityName} not found in ${parentAbility}:${parentVersion} configuration`
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Ability ${abilityName} not found in ${parentAbility}:${parentVersion} configuration`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
logError('Ability not found in Project agent.json');
|
|
86
|
-
throw new Error('Ability not found in Project agent.json');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const abilityDir = path.join(
|
|
91
|
-
rootDir,
|
|
92
|
-
'abilities',
|
|
93
|
-
abilityName,
|
|
94
|
-
abilityVersion
|
|
95
|
-
);
|
|
96
|
-
logger.success(
|
|
97
|
-
'version',
|
|
98
|
-
`Resolved ${abilityName} version: ${abilityVersion}`
|
|
99
|
-
);
|
|
100
|
-
logger.trace('paths', `Ability directory: ${abilityDir}`);
|
|
101
|
-
|
|
102
|
-
// Check if ability directory exists
|
|
103
|
-
if (!fs.existsSync(abilityDir)) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Ability '${abilityName}' version '${abilityVersion}' is not installed.\n` +
|
|
106
|
-
`Expected location: ${abilityDir}\n` +
|
|
107
|
-
`Please install the ability using: kadi install`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ------------------------------
|
|
112
|
-
// 2) Read ability manifest (optional). If missing -> default to node-module
|
|
113
|
-
// ------------------------------
|
|
114
|
-
const manifestPath = path.join(abilityDir, 'agent.json');
|
|
115
|
-
|
|
116
|
-
logger.trace('manifest', `Looking for agent.json at: ${manifestPath}`);
|
|
117
|
-
|
|
118
|
-
const manifest = fs.existsSync(manifestPath)
|
|
119
|
-
? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
120
|
-
: null;
|
|
121
|
-
|
|
122
|
-
logger.info('manifest', `Agent.json found: ${manifest ? 'yes' : 'no'}`);
|
|
123
|
-
|
|
124
|
-
if (!protocol) {
|
|
125
|
-
const firstKey = Object.keys(manifest?.interfaces)[0];
|
|
126
|
-
protocol = firstKey;
|
|
127
|
-
logger.info('protocol', `Auto-selected protocol: ${protocol}`);
|
|
128
|
-
} else {
|
|
129
|
-
protocol = protocol.toLowerCase();
|
|
130
|
-
logger.info('protocol', `Using specified protocol: ${protocol}`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check cache with protocol-specific key
|
|
134
|
-
const cacheKey = `${abilityName}@${abilityVersion}:${protocol}`;
|
|
135
|
-
if (__abilityCache.has(cacheKey)) {
|
|
136
|
-
logger.info('cache', `Using cached ability: ${cacheKey}`);
|
|
137
|
-
return __abilityCache.get(cacheKey);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const startCmd = manifest?.scripts?.start || null;
|
|
141
|
-
if (!startCmd) {
|
|
142
|
-
logger.error('config', `Missing start command for ${abilityName}`);
|
|
143
|
-
throw new Error(
|
|
144
|
-
`Ability ${abilityName} missing start command (scripts.start)`
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
logger.trace('command', `Start command: ${startCmd}`);
|
|
149
|
-
|
|
150
|
-
// ------------------------------
|
|
151
|
-
// 3) Dispatch by protocol
|
|
152
|
-
// ------------------------------
|
|
153
|
-
let loaded;
|
|
154
|
-
|
|
155
|
-
if (protocol === 'native') {
|
|
156
|
-
logger.info('native', `Loading ${abilityName} as native JavaScript module`);
|
|
157
|
-
|
|
158
|
-
const entryFromManifest = manifest?.interfaces?.native?.entry;
|
|
159
|
-
let entryRelPath = entryFromManifest;
|
|
160
|
-
|
|
161
|
-
if (!entryRelPath) {
|
|
162
|
-
const moduleJSON = getAbilityJSON(abilityName, abilityVersion);
|
|
163
|
-
entryRelPath = moduleJSON.entry;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (!entryRelPath) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`Ability '${abilityName}' is missing entry point configuration.\n` +
|
|
169
|
-
`Please specify 'entry' in the ability's agent.json or 'interface.entry' in the manifest.`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const modulePath = path.join(abilityDir, entryRelPath);
|
|
174
|
-
|
|
175
|
-
if (!fs.existsSync(modulePath)) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
`Ability '${abilityName}' entry file not found: ${modulePath}\n` +
|
|
178
|
-
`Expected entry point: ${entryRelPath}`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
console.error(`Loading module from: ${modulePath}`);
|
|
183
|
-
// const mod = await import(modulePath + `?v=${Date.now()}`);
|
|
184
|
-
const mod = await import(modulePath);
|
|
185
|
-
const rawModule = mod.default || mod;
|
|
186
|
-
|
|
187
|
-
// Detect "ability export" shape
|
|
188
|
-
const isAbilityExport =
|
|
189
|
-
rawModule &&
|
|
190
|
-
typeof rawModule.on === 'function' && // EventEmitter
|
|
191
|
-
typeof rawModule.publishEvent === 'function' &&
|
|
192
|
-
rawModule.methodHandlers instanceof Map;
|
|
193
|
-
|
|
194
|
-
const eventEmitter = new EventEmitter();
|
|
195
|
-
|
|
196
|
-
let functions = {};
|
|
197
|
-
let call; // (method: string, params: any) => Promise<any>
|
|
198
|
-
|
|
199
|
-
if (isAbilityExport) {
|
|
200
|
-
// 1) expose registered methods
|
|
201
|
-
for (const name of rawModule.methodHandlers.keys()) {
|
|
202
|
-
// value doesn't matter for buildAbilityProxy if it only
|
|
203
|
-
// needs the keys,
|
|
204
|
-
// but giving a callable makes it future-proof.
|
|
205
|
-
functions[name] = async (params) => {
|
|
206
|
-
const handler = rawModule.methodHandlers.get(name);
|
|
207
|
-
return await handler(params);
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 2) route calls through the handlers map
|
|
212
|
-
call = async (method, params) => {
|
|
213
|
-
const handler = rawModule.methodHandlers.get(method);
|
|
214
|
-
if (typeof handler !== 'function') {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`Method '${method}' is not registered on KadiAbility`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
return await handler(params);
|
|
220
|
-
};
|
|
221
|
-
} else {
|
|
222
|
-
// existing behavior for plain function modules
|
|
223
|
-
for (const [key, value] of Object.entries(rawModule)) {
|
|
224
|
-
if (typeof value === 'function' && !key.startsWith('_')) {
|
|
225
|
-
functions[key] = value;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
call = async (method, params) => {
|
|
229
|
-
const func = rawModule[method];
|
|
230
|
-
if (typeof func !== 'function') {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Method '${method}' is not a function or doesn't exist`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
return await func(params);
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Create uniform proxy
|
|
240
|
-
loaded = buildAbilityProxy({
|
|
241
|
-
call,
|
|
242
|
-
functions,
|
|
243
|
-
eventEmitter
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Wire native events (works for the KadiAbility instance)
|
|
247
|
-
setupNativeEventListener(rawModule, eventEmitter);
|
|
248
|
-
} else if (protocol === 'stdio') {
|
|
249
|
-
logger.info(
|
|
250
|
-
'stdio',
|
|
251
|
-
`Loading ${abilityName} with stdio JSON-RPC transport`
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
const timeoutMs = manifest?.interface?.timeoutMs ?? 15000;
|
|
255
|
-
const env = { ...process.env, KADI_PROTOCOL: protocol };
|
|
256
|
-
const logFile = path.join(abilityDir, `${abilityName}.log`);
|
|
257
|
-
|
|
258
|
-
// Use the enhanced spawnJSONRPCProcess that supports events
|
|
259
|
-
const rpc = spawnJSONRPCProcess({
|
|
260
|
-
command: startCmd,
|
|
261
|
-
cwd: abilityDir,
|
|
262
|
-
env,
|
|
263
|
-
timeoutMs,
|
|
264
|
-
logFile
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Create IPC helper bound to this rpc instance
|
|
268
|
-
const IPC = Ipc.with(rpc);
|
|
269
|
-
|
|
270
|
-
// Handshake: init
|
|
271
|
-
const initRest = await IPC.init({ api: '1.0' });
|
|
272
|
-
|
|
273
|
-
// Get static contract from the agent.json (if any)
|
|
274
|
-
let staticFunctions = null;
|
|
275
|
-
if (Array.isArray(manifest?.exports)) {
|
|
276
|
-
const tools = manifest.exports;
|
|
277
|
-
staticFunctions = Object.fromEntries(tools.map((t) => [t.name]));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Always call discovery when enabled to get dynamic methods
|
|
281
|
-
let discoveredFunctions = null;
|
|
282
|
-
if (manifest?.interface?.discover !== false) {
|
|
283
|
-
try {
|
|
284
|
-
const disc = await IPC.discover();
|
|
285
|
-
discoveredFunctions = disc?.functions || null;
|
|
286
|
-
} catch (error) {
|
|
287
|
-
console.warn(`Discovery failed for ${abilityName}: ${error.message}`);
|
|
288
|
-
discoveredFunctions = null;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Merge static and discovered functions
|
|
293
|
-
let functions = { ...staticFunctions };
|
|
294
|
-
if (discoveredFunctions) {
|
|
295
|
-
functions = { ...functions, ...discoveredFunctions };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Create EventEmitter for events
|
|
299
|
-
const eventEmitter = new EventEmitter();
|
|
300
|
-
|
|
301
|
-
// Set up event listener for __kadi_event notifications
|
|
302
|
-
setupStdioEventListener(rpc, eventEmitter);
|
|
303
|
-
|
|
304
|
-
// Create proxy with event support
|
|
305
|
-
loaded = buildAbilityProxy({
|
|
306
|
-
call: (method, params) => IPC.call(method, params),
|
|
307
|
-
functions,
|
|
308
|
-
eventEmitter
|
|
309
|
-
});
|
|
310
|
-
} else if (protocol === 'broker') {
|
|
311
|
-
logger.info('broker', `Loading ${abilityName} with broker transport`);
|
|
312
|
-
|
|
313
|
-
const timeoutMs = manifest?.interface?.timeoutMs ?? 10000;
|
|
314
|
-
const scope = randomUUID();
|
|
315
|
-
logger.trace('broker', `Generated agent scope: ${scope}`);
|
|
316
|
-
|
|
317
|
-
const brokerUrl =
|
|
318
|
-
manifest?.brokers?.local ||
|
|
319
|
-
manifest?.brokers?.remote ||
|
|
320
|
-
(await getProjectJSON())?.brokers?.local;
|
|
321
|
-
|
|
322
|
-
logger.info('broker', `Broker URL: ${brokerUrl}`);
|
|
323
|
-
|
|
324
|
-
const serviceName =
|
|
325
|
-
manifest?.interface?.serviceName ||
|
|
326
|
-
`ability.${abilityName}.${abilityVersion.replace(/\./g, '_')}`;
|
|
327
|
-
|
|
328
|
-
if (startCmd) {
|
|
329
|
-
// Spawn the ability process
|
|
330
|
-
const env = {
|
|
331
|
-
...process.env,
|
|
332
|
-
KADI_PROTOCOL: protocol,
|
|
333
|
-
KADI_BROKER_URL: brokerUrl,
|
|
334
|
-
KADI_SERVICE_NAME: serviceName,
|
|
335
|
-
KADI_AGENT_SCOPE: scope
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
logger.info('spawn', `Spawning ${abilityName} ability process`);
|
|
339
|
-
|
|
340
|
-
const child = spawn(startCmd, {
|
|
341
|
-
cwd: abilityDir,
|
|
342
|
-
env,
|
|
343
|
-
shell: true,
|
|
344
|
-
stdio: 'inherit',
|
|
345
|
-
detached: false
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
child.on('error', (error) => {
|
|
349
|
-
logger.error(
|
|
350
|
-
'spawn',
|
|
351
|
-
`Failed to spawn ${abilityName}: ${error.message}`
|
|
352
|
-
);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
child.on('exit', (code, signal) => {
|
|
356
|
-
if (code !== 0) {
|
|
357
|
-
logger.warn(
|
|
358
|
-
'spawn',
|
|
359
|
-
`Ability process exited with code ${code}, signal ${signal}`
|
|
360
|
-
);
|
|
361
|
-
} else {
|
|
362
|
-
logger.success('spawn', 'Ability process exited cleanly');
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Give the ability time to connect to the broker
|
|
367
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
logger.info('broker', 'Creating broker RPC client');
|
|
371
|
-
|
|
372
|
-
let rpc;
|
|
373
|
-
try {
|
|
374
|
-
rpc = createBrokerRPCClient({
|
|
375
|
-
brokerUrl,
|
|
376
|
-
serviceName,
|
|
377
|
-
timeoutMs,
|
|
378
|
-
scope
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
await rpc.testConnection();
|
|
382
|
-
logger.success('broker', 'Agent connected to broker successfully');
|
|
383
|
-
} catch (error) {
|
|
384
|
-
logger.error(
|
|
385
|
-
'broker',
|
|
386
|
-
`Agent failed to connect to broker: ${error.message}`
|
|
387
|
-
);
|
|
388
|
-
throw new Error(
|
|
389
|
-
`Failed to connect to KADI Broker at ${brokerUrl}\n` +
|
|
390
|
-
`Original error: ${error.message}\n` +
|
|
391
|
-
`\n` +
|
|
392
|
-
`Common solutions:\n` +
|
|
393
|
-
`• Make sure the KADI Broker is running\n` +
|
|
394
|
-
`• Check if the broker URL is correct in agent.json\n` +
|
|
395
|
-
`• Verify network connectivity to the broker\n` +
|
|
396
|
-
`• Ensure the broker is accepting connections`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Get static contract from the agent.json (if any)
|
|
401
|
-
let staticFunctions = null;
|
|
402
|
-
if (Array.isArray(manifest?.exports)) {
|
|
403
|
-
const tools = manifest.exports;
|
|
404
|
-
staticFunctions = Object.fromEntries(tools.map((t) => [t.name]));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Get the full method list from the broker
|
|
408
|
-
let brokerFunctions = null;
|
|
409
|
-
if (manifest?.interface?.discover !== false) {
|
|
410
|
-
try {
|
|
411
|
-
logger.trace(
|
|
412
|
-
'discovery',
|
|
413
|
-
`Querying broker for methods with scopes: ${JSON.stringify([scope])}`
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
const brokerMethods = await rpc.getAvailableMethods([scope]);
|
|
417
|
-
|
|
418
|
-
logger.success(
|
|
419
|
-
'discovery',
|
|
420
|
-
`Broker returned ${brokerMethods.tools?.length || 0} methods`
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
if (logger.enabled) {
|
|
424
|
-
brokerMethods.tools?.forEach((tool) => {
|
|
425
|
-
logger.trace('discovery', ` - ${tool.name}`);
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
brokerFunctions = Object.fromEntries(
|
|
430
|
-
brokerMethods.tools.map((t) => [t.name])
|
|
431
|
-
);
|
|
432
|
-
} catch (error) {
|
|
433
|
-
logger.warn(
|
|
434
|
-
'discovery',
|
|
435
|
-
`Broker method discovery failed: ${error.message}`
|
|
436
|
-
);
|
|
437
|
-
brokerFunctions = null;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Merge static and broker functions
|
|
442
|
-
let functions = { ...staticFunctions };
|
|
443
|
-
if (brokerFunctions) {
|
|
444
|
-
functions = { ...functions, ...brokerFunctions };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Create EventEmitter for events
|
|
448
|
-
const eventEmitter = new EventEmitter();
|
|
449
|
-
|
|
450
|
-
// Set up event listener for kadi.event messages
|
|
451
|
-
setupBrokerEventListener(rpc, eventEmitter);
|
|
452
|
-
|
|
453
|
-
// Create proxy with event support
|
|
454
|
-
loaded = buildAbilityProxy({
|
|
455
|
-
call: (method, params) => rpc.call(method, params),
|
|
456
|
-
functions,
|
|
457
|
-
eventEmitter
|
|
458
|
-
});
|
|
459
|
-
} else {
|
|
460
|
-
throw new Error(`Unsupported ability interface protocol: ${protocol}`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
__abilityCache.set(cacheKey, loaded);
|
|
464
|
-
|
|
465
|
-
logger.success(
|
|
466
|
-
'complete',
|
|
467
|
-
`Successfully loaded ${abilityName} with ${Object.keys(loaded.__list?.() || {}).length} methods`
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
return loaded;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/* =========================================================================
|
|
474
|
-
* Helpers: small, dependency-free shims to keep the loader readable.
|
|
475
|
-
* ======================================================================= */
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Spawn a child process and speak newline-delimited JSON-RPC 2.0 over stdio.
|
|
479
|
-
* Enhanced to support event notifications via __kadi_event messages.
|
|
480
|
-
*
|
|
481
|
-
* Minimal reference implementation; production code should add heartbeats,
|
|
482
|
-
* backpressure control, and graceful shutdown handling.
|
|
483
|
-
* - write JSON-RPC requests to the child's stdin,
|
|
484
|
-
* - read/parse JSON-RPC responses from the child's stdout,
|
|
485
|
-
* - handle __kadi_event notifications for event publishing
|
|
486
|
-
* - enforce timeouts
|
|
487
|
-
* - logFile if present will see its content append with information echoed to stderr
|
|
488
|
-
*
|
|
489
|
-
* Reference: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
|
|
490
|
-
*/
|
|
491
|
-
function spawnJSONRPCProcess({ command, cwd, env, timeoutMs, logFile }) {
|
|
492
|
-
const ability = spawn(command, {
|
|
493
|
-
cwd,
|
|
494
|
-
env,
|
|
495
|
-
shell: true,
|
|
496
|
-
stdio: ['pipe', 'pipe', 'pipe'] // IMPORTANT: we need pipes, not 'inherit'
|
|
497
|
-
// 'pipe' for stdin, stdout, stderr
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
// ────────────────────────────────────
|
|
501
|
-
// Optional file logger for stderr
|
|
502
|
-
// ────────────────────────────────────
|
|
503
|
-
let logStream = null;
|
|
504
|
-
if (logFile) {
|
|
505
|
-
try {
|
|
506
|
-
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
507
|
-
logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
508
|
-
} catch (e) {
|
|
509
|
-
console.error('Unable to open log file', logFile, e);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const pending = new Map(); // id -> { resolve, reject, timer }
|
|
514
|
-
let nextId = 1;
|
|
515
|
-
|
|
516
|
-
// Storage for event handlers
|
|
517
|
-
const eventHandlers = [];
|
|
518
|
-
|
|
519
|
-
// Create frame reader for parsing LSP-style messages from stdout
|
|
520
|
-
const frameReader = new StdioFrameReader(ability.stdout, {
|
|
521
|
-
maxBufferSize: 8 * 1024 * 1024 // 8MB buffer limit
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
// Set up frame processing with rate limiting to prevent event loop blocking
|
|
525
|
-
let processedFrames = 0;
|
|
526
|
-
const maxFramesPerChunk = 10; // Prevent infinite loops under extreme load
|
|
527
|
-
let pendingProcessing = false;
|
|
528
|
-
|
|
529
|
-
frameReader.onMessage((result) => {
|
|
530
|
-
// Rate limiting: if we've processed too many frames in this tick, defer to next tick
|
|
531
|
-
if (processedFrames >= maxFramesPerChunk && !pendingProcessing) {
|
|
532
|
-
pendingProcessing = true;
|
|
533
|
-
setImmediate(() => {
|
|
534
|
-
processedFrames = 0;
|
|
535
|
-
pendingProcessing = false;
|
|
536
|
-
// Process this result in the next tick
|
|
537
|
-
handleResult(result);
|
|
538
|
-
});
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
handleResult(result);
|
|
543
|
-
processedFrames++;
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
function handleResult(result) {
|
|
547
|
-
if (!result.success) {
|
|
548
|
-
// Frame parsing failed - log detailed error and fail all pending calls
|
|
549
|
-
logger.error(`[rpc] Frame corruption detected: ${result.error}`);
|
|
550
|
-
logger.error(`[rpc] Corruption type: ${result.corruption_type}`);
|
|
551
|
-
logger.error(`[rpc] Details: ${result.message}`);
|
|
552
|
-
|
|
553
|
-
// Fail all pending calls since we can't trust the stream anymore
|
|
554
|
-
for (const { reject, timer } of pending.values()) {
|
|
555
|
-
clearTimeout(timer);
|
|
556
|
-
reject(
|
|
557
|
-
new Error(`Frame corruption: ${result.error} - ${result.message}`)
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
pending.clear();
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const msg = result.data;
|
|
565
|
-
|
|
566
|
-
// Handle event notifications (__kadi_event messages)
|
|
567
|
-
if (!msg.id && msg.method === '__kadi_event' && msg.params) {
|
|
568
|
-
// This is an event notification from the ability
|
|
569
|
-
const { eventName, data, timestamp } = msg.params;
|
|
570
|
-
|
|
571
|
-
// Log the event for debugging
|
|
572
|
-
if (logger && logger.enabled) {
|
|
573
|
-
logger.trace('event', `Received event notification: ${eventName}`);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Call all registered event handlers
|
|
577
|
-
for (const handler of eventHandlers) {
|
|
578
|
-
try {
|
|
579
|
-
handler({
|
|
580
|
-
method: '__kadi_event',
|
|
581
|
-
params: { eventName, data, timestamp }
|
|
582
|
-
});
|
|
583
|
-
} catch (err) {
|
|
584
|
-
console.error(`[rpc] Error in event handler: ${err.message}`);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return; // Don't process further - events don't have responses
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Handle regular JSON-RPC responses
|
|
592
|
-
if (msg.id && pending.has(msg.id)) {
|
|
593
|
-
const { resolve, reject, timer } = pending.get(msg.id);
|
|
594
|
-
clearTimeout(timer);
|
|
595
|
-
pending.delete(msg.id);
|
|
596
|
-
if (msg.error) reject(new Error(msg.error.message || 'RPC error'));
|
|
597
|
-
else resolve(msg.result);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
ability.stderr.on('data', (chunk) => {
|
|
602
|
-
// Optional: forward ability stderr to your logger
|
|
603
|
-
console.error(`[${cwd}]`, chunk.toString().trim());
|
|
604
|
-
if (logStream) logStream.write(chunk);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
ability.on('close', (code) => {
|
|
608
|
-
// Fail all pending calls on exit
|
|
609
|
-
for (const { reject, timer } of pending.values()) {
|
|
610
|
-
clearTimeout(timer);
|
|
611
|
-
reject(new Error(`Ability process exited with code ${code}`));
|
|
612
|
-
}
|
|
613
|
-
pending.clear();
|
|
614
|
-
if (logStream) logStream.end();
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
function call(method, params) {
|
|
618
|
-
const id = nextId++;
|
|
619
|
-
const body = JSON.stringify({
|
|
620
|
-
jsonrpc: '2.0',
|
|
621
|
-
id,
|
|
622
|
-
method,
|
|
623
|
-
params: params ?? {}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// Compose LSP-style frame. We *could* omit Content-Type because UTF-8 JSON
|
|
627
|
-
// is the default, but including it keeps packet dumps self-describing.
|
|
628
|
-
const header =
|
|
629
|
-
`${FRAME_HEADERS.CONTENT_LENGTH}: ${Buffer.byteLength(body, 'utf8')}\r\n` +
|
|
630
|
-
`${FRAME_HEADERS.CONTENT_TYPE}: ${FRAME_VALUES.CONTENT_TYPE_VALUE}\r\n` +
|
|
631
|
-
`\r\n`;
|
|
632
|
-
|
|
633
|
-
return new Promise((resolve, reject) => {
|
|
634
|
-
const timer = setTimeout(() => {
|
|
635
|
-
pending.delete(id);
|
|
636
|
-
reject(new Error(`RPC timeout after ${timeoutMs}ms for ${method}`));
|
|
637
|
-
}, timeoutMs);
|
|
638
|
-
|
|
639
|
-
pending.set(id, { resolve, reject, timer });
|
|
640
|
-
|
|
641
|
-
try {
|
|
642
|
-
ability.stdin.write(header + body, 'utf8');
|
|
643
|
-
} catch (e) {
|
|
644
|
-
clearTimeout(timer);
|
|
645
|
-
pending.delete(id);
|
|
646
|
-
reject(e);
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Register a handler for event notifications
|
|
653
|
-
* @param {Function} handler - Function to call when __kadi_event messages arrive
|
|
654
|
-
*/
|
|
655
|
-
function onEvent(handler) {
|
|
656
|
-
if (typeof handler === 'function') {
|
|
657
|
-
eventHandlers.push(handler);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* Remove an event handler
|
|
663
|
-
* @param {Function} handler - Handler to remove
|
|
664
|
-
*/
|
|
665
|
-
function offEvent(handler) {
|
|
666
|
-
const index = eventHandlers.indexOf(handler);
|
|
667
|
-
if (index > -1) {
|
|
668
|
-
eventHandlers.splice(index, 1);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Return the RPC interface with event support
|
|
673
|
-
return {
|
|
674
|
-
call,
|
|
675
|
-
onEvent, // Method to register event handlers
|
|
676
|
-
offEvent, // Method to unregister event handlers
|
|
677
|
-
// Expose the process for advanced use cases
|
|
678
|
-
_process: ability
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Create a thin client that speaks request/response over the KADI Broker.
|
|
684
|
-
* Enhanced to support event notifications via kadi.event messages.
|
|
685
|
-
*/
|
|
686
|
-
function createBrokerRPCClient({ brokerUrl, serviceName, timeoutMs, scope }) {
|
|
687
|
-
const idFactory = new IdFactory();
|
|
688
|
-
|
|
689
|
-
// Storage for event handlers
|
|
690
|
-
const eventHandlers = [];
|
|
691
|
-
|
|
692
|
-
function rpc(ws, messageBuilder) {
|
|
693
|
-
return new Promise((resolve) => {
|
|
694
|
-
const message = messageBuilder.build();
|
|
695
|
-
const onMessage = (raw) => {
|
|
696
|
-
try {
|
|
697
|
-
const msg = JSON.parse(raw.toString());
|
|
698
|
-
if (msg.id === message.id) {
|
|
699
|
-
ws.off('message', onMessage);
|
|
700
|
-
resolve(msg);
|
|
701
|
-
}
|
|
702
|
-
} catch {}
|
|
703
|
-
};
|
|
704
|
-
ws.on('message', onMessage);
|
|
705
|
-
ws.send(JSON.stringify(message));
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
async function connect() {
|
|
710
|
-
return new Promise((resolve, reject) => {
|
|
711
|
-
const ws = new WebSocket(brokerUrl);
|
|
712
|
-
|
|
713
|
-
// Handle connection errors
|
|
714
|
-
ws.once('error', (error) => {
|
|
715
|
-
reject(new Error(`WebSocket connection failed: ${error.message}`));
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
ws.once('open', async () => {
|
|
719
|
-
try {
|
|
720
|
-
// Remove error listener since connection succeeded
|
|
721
|
-
ws.removeAllListeners('error');
|
|
722
|
-
|
|
723
|
-
// Add error handler for runtime errors
|
|
724
|
-
ws.on('error', (error) => {
|
|
725
|
-
console.error(`Broker WebSocket error: ${error.message}`);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
ws.on('message', (raw) => {
|
|
729
|
-
try {
|
|
730
|
-
const msg = JSON.parse(raw.toString());
|
|
731
|
-
|
|
732
|
-
// Check if this is an kadi.event notification
|
|
733
|
-
if (msg.method === 'kadi.event' && msg.params) {
|
|
734
|
-
const { eventName, eventData, timestamp, from } = msg.params;
|
|
735
|
-
|
|
736
|
-
// Log the event for debugging
|
|
737
|
-
if (logger && logger.enabled) {
|
|
738
|
-
logger.trace(
|
|
739
|
-
'event',
|
|
740
|
-
`Received broker event: ${eventName} from ${from}`
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Call all registered event handlers
|
|
745
|
-
for (const handler of eventHandlers) {
|
|
746
|
-
try {
|
|
747
|
-
handler({
|
|
748
|
-
method: 'kadi.event',
|
|
749
|
-
params: { eventName, eventData, timestamp, from }
|
|
750
|
-
});
|
|
751
|
-
} catch (err) {
|
|
752
|
-
console.error(
|
|
753
|
-
`[broker-rpc] Error in event handler: ${err.message}`
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
} catch (err) {
|
|
759
|
-
// Ignore parsing errors for non-JSON messages
|
|
760
|
-
}
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
// hello/auth (ephemeral ed25519) using BrokerMessageBuilder
|
|
764
|
-
const helloMsg = Broker.hello({ role: 'agent', version: '0.1' }).id(
|
|
765
|
-
idFactory.next()
|
|
766
|
-
);
|
|
767
|
-
const hello = await rpc(ws, helloMsg);
|
|
768
|
-
const nonce = hello?.result?.nonce;
|
|
769
|
-
|
|
770
|
-
if (!nonce) {
|
|
771
|
-
throw new Error('No nonce received from broker hello');
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
775
|
-
const publicKeyBase64Der = toBase64Der(publicKey);
|
|
776
|
-
|
|
777
|
-
const authMsg = Broker.authenticate({
|
|
778
|
-
publicKeyBase64Der,
|
|
779
|
-
privateKey,
|
|
780
|
-
nonce,
|
|
781
|
-
wantNewId: true
|
|
782
|
-
}).id(idFactory.next());
|
|
783
|
-
|
|
784
|
-
const auth = await rpc(ws, authMsg);
|
|
785
|
-
if (auth.error) {
|
|
786
|
-
throw new Error('Broker auth failed: ' + auth.error.message);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Register capabilities (no tools since we are a client)
|
|
790
|
-
const registerMsg = Broker.registerCapabilities({
|
|
791
|
-
displayName: `client:${serviceName}`,
|
|
792
|
-
scopes: ['global', scope],
|
|
793
|
-
tools: []
|
|
794
|
-
}).id(idFactory.next());
|
|
795
|
-
|
|
796
|
-
await rpc(ws, registerMsg);
|
|
797
|
-
|
|
798
|
-
// Heartbeat using ping notification
|
|
799
|
-
setInterval(() => {
|
|
800
|
-
if (ws.readyState === ws.OPEN) {
|
|
801
|
-
ws.send(Broker.ping().toString());
|
|
802
|
-
}
|
|
803
|
-
}, 25_000);
|
|
804
|
-
|
|
805
|
-
resolve(ws);
|
|
806
|
-
} catch (error) {
|
|
807
|
-
reject(error);
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const ready = connect();
|
|
814
|
-
|
|
815
|
-
return {
|
|
816
|
-
async testConnection() {
|
|
817
|
-
// This will throw if connection fails during the handshake
|
|
818
|
-
const ws = await ready;
|
|
819
|
-
if (!ws || ws.readyState !== ws.OPEN) {
|
|
820
|
-
throw new Error('WebSocket connection failed');
|
|
821
|
-
}
|
|
822
|
-
},
|
|
823
|
-
|
|
824
|
-
async call(toolName, args) {
|
|
825
|
-
const ws = await ready;
|
|
826
|
-
|
|
827
|
-
// Use BrokerMessageBuilder for callAbility
|
|
828
|
-
const callMsg = Broker.callAbility({
|
|
829
|
-
toolName,
|
|
830
|
-
args
|
|
831
|
-
}).id(idFactory.next());
|
|
832
|
-
|
|
833
|
-
const ack = await rpc(ws, callMsg);
|
|
834
|
-
if (ack.error) {
|
|
835
|
-
throw new Error(ack.error.message || 'agent.callAbility failed');
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
const expectedRequestId = ack?.result?.requestId;
|
|
839
|
-
return await new Promise((resolve, reject) => {
|
|
840
|
-
const timer = setTimeout(() => {
|
|
841
|
-
ws.off('message', onMessage);
|
|
842
|
-
reject(new Error('broker call timeout'));
|
|
843
|
-
}, timeoutMs || 10000);
|
|
844
|
-
|
|
845
|
-
const onMessage = (raw) => {
|
|
846
|
-
try {
|
|
847
|
-
const msg = JSON.parse(raw.toString());
|
|
848
|
-
if (msg.method === 'ability.result') {
|
|
849
|
-
const { requestId, result, error } = msg.params || {};
|
|
850
|
-
if (!expectedRequestId || requestId === expectedRequestId) {
|
|
851
|
-
clearTimeout(timer);
|
|
852
|
-
ws.off('message', onMessage);
|
|
853
|
-
if (error) reject(new Error(error.message || 'ability error'));
|
|
854
|
-
else resolve(result);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
} catch {}
|
|
858
|
-
};
|
|
859
|
-
ws.on('message', onMessage);
|
|
860
|
-
});
|
|
861
|
-
},
|
|
862
|
-
|
|
863
|
-
async getAvailableMethods(scopes = ['global']) {
|
|
864
|
-
const ws = await ready;
|
|
865
|
-
const msg = Broker.listTools({ scopes }).id(idFactory.next());
|
|
866
|
-
const response = await rpc(ws, msg);
|
|
867
|
-
if (response.error) {
|
|
868
|
-
throw new Error(response.error.message || 'listTools failed');
|
|
869
|
-
}
|
|
870
|
-
return response.result;
|
|
871
|
-
},
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* Register a handler for event notifications
|
|
875
|
-
* @param {Function} handler - Function to call when kadi.event messages
|
|
876
|
-
* arrive
|
|
877
|
-
*/
|
|
878
|
-
onEvent(handler) {
|
|
879
|
-
if (typeof handler === 'function') {
|
|
880
|
-
eventHandlers.push(handler);
|
|
881
|
-
}
|
|
882
|
-
},
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* Remove an event handler
|
|
886
|
-
* @param {Function} handler - Handler to remove
|
|
887
|
-
*/
|
|
888
|
-
offEvent(handler) {
|
|
889
|
-
const index = eventHandlers.indexOf(handler);
|
|
890
|
-
if (index > -1) {
|
|
891
|
-
eventHandlers.splice(index, 1);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Set up event listener for stdio transport
|
|
899
|
-
* Listens for __kadi_event notifications and emits them on the EventEmitter
|
|
900
|
-
*
|
|
901
|
-
* @param {Object} rpc - The RPC object returned from spawnJSONRPCProcess
|
|
902
|
-
* @param {EventEmitter} eventEmitter - The EventEmitter to emit events on
|
|
903
|
-
*/
|
|
904
|
-
function setupStdioEventListener(rpc, eventEmitter) {
|
|
905
|
-
if (!rpc.onEvent) {
|
|
906
|
-
logger.warn('event', 'RPC object does not support event notifications');
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Register handler for __kadi_event messages
|
|
911
|
-
rpc.onEvent((message) => {
|
|
912
|
-
if (message.method === '__kadi_event' && message.params) {
|
|
913
|
-
const { eventName, data, timestamp } = message.params;
|
|
914
|
-
|
|
915
|
-
logger.trace('event', `Received stdio event: ${eventName}`);
|
|
916
|
-
logger.trace('event-data', { eventName, data, timestamp });
|
|
917
|
-
|
|
918
|
-
// Emit the event on the EventEmitter
|
|
919
|
-
// This allows agents to do: ability.events.on('eventName', handler)
|
|
920
|
-
eventEmitter.emit(eventName, data);
|
|
921
|
-
|
|
922
|
-
// Also emit a generic 'event' event for catch-all handlers
|
|
923
|
-
eventEmitter.emit('event', { eventName, data, timestamp });
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
logger.trace('event', 'Stdio event listener configured');
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Set up event listener for broker transport
|
|
932
|
-
* Listens for kadi.event messages and emits them on the EventEmitter
|
|
933
|
-
*
|
|
934
|
-
* @param {Object} rpc - The RPC client returned from createBrokerRPCClient
|
|
935
|
-
* @param {EventEmitter} eventEmitter - The EventEmitter to emit events on
|
|
936
|
-
*/
|
|
937
|
-
function setupBrokerEventListener(rpc, eventEmitter) {
|
|
938
|
-
if (!rpc.onEvent) {
|
|
939
|
-
logger.warn('event', 'RPC client does not support event notifications');
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Register handler for kadi.event messages
|
|
944
|
-
rpc.onEvent((message) => {
|
|
945
|
-
if (message.method === 'kadi.event' && message.params) {
|
|
946
|
-
const { eventName, eventData, timestamp, from } = message.params;
|
|
947
|
-
|
|
948
|
-
logger.trace('event', `Received broker event: ${eventName} from ${from}`);
|
|
949
|
-
logger.trace('event-data', { eventName, eventData, timestamp, from });
|
|
950
|
-
|
|
951
|
-
// Emit the event on the EventEmitter
|
|
952
|
-
// Use eventData (broker's field name) as the data payload
|
|
953
|
-
eventEmitter.emit(eventName, eventData);
|
|
954
|
-
|
|
955
|
-
// Also emit a generic 'event' event for catch-all handlers
|
|
956
|
-
eventEmitter.emit('event', {
|
|
957
|
-
eventName,
|
|
958
|
-
data: eventData,
|
|
959
|
-
timestamp,
|
|
960
|
-
source: from
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
logger.trace('event', 'Broker event listener configured');
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* Set up event listener for native transport
|
|
970
|
-
* For native abilities that are KadiAbility instances
|
|
971
|
-
*
|
|
972
|
-
* @param {any} rawModule - The imported module (might be a KadiAbility instance)
|
|
973
|
-
* @param {EventEmitter} eventEmitter - The EventEmitter to emit events on
|
|
974
|
-
*/
|
|
975
|
-
function setupNativeEventListener(rawModule, eventEmitter) {
|
|
976
|
-
// Check if the module is an EventEmitter (like KadiAbility)
|
|
977
|
-
if (rawModule && typeof rawModule.on === 'function') {
|
|
978
|
-
logger.trace('event', 'Native module appears to be an EventEmitter');
|
|
979
|
-
|
|
980
|
-
// Remove any existing ability:event listeners first
|
|
981
|
-
rawModule.removeAllListeners('ability:event');
|
|
982
|
-
|
|
983
|
-
// Listen for ability:event which KadiAbility emits for native protocol
|
|
984
|
-
rawModule.on('ability:event', ({ eventName, data }) => {
|
|
985
|
-
logger.trace('event', `Received native event: ${eventName}`);
|
|
986
|
-
logger.trace('event-data', { eventName, data });
|
|
987
|
-
|
|
988
|
-
// Emit the event on the proxy's EventEmitter
|
|
989
|
-
eventEmitter.emit(eventName, data);
|
|
990
|
-
|
|
991
|
-
// Also emit a generic 'event' event
|
|
992
|
-
eventEmitter.emit('event', { eventName, data, timestamp: Date.now() });
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
logger.trace('event', 'Native event listener configured');
|
|
996
|
-
} else {
|
|
997
|
-
logger.trace(
|
|
998
|
-
'event',
|
|
999
|
-
'Native module is not an EventEmitter, skipping event setup'
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
/**
|
|
1005
|
-
* Enhanced buildAbilityProxy with event support
|
|
1006
|
-
* Replace the existing buildAbilityProxy function with this version
|
|
1007
|
-
*/
|
|
1008
|
-
function buildAbilityProxy({ call, functions, eventEmitter }) {
|
|
1009
|
-
// Create EventEmitter if not provided
|
|
1010
|
-
const events = eventEmitter || new EventEmitter();
|
|
1011
|
-
|
|
1012
|
-
// Set max listeners to avoid warnings when many handlers are attached
|
|
1013
|
-
events.setMaxListeners(100);
|
|
1014
|
-
|
|
1015
|
-
const base = {
|
|
1016
|
-
call: async (method, params) => {
|
|
1017
|
-
try {
|
|
1018
|
-
return await call(method, params);
|
|
1019
|
-
} catch (error) {
|
|
1020
|
-
// Enhance error messages for better user experience
|
|
1021
|
-
if (functions && !functions[method]) {
|
|
1022
|
-
const availableMethods = Object.keys(functions);
|
|
1023
|
-
throw new Error(
|
|
1024
|
-
`Method '${method}' is not exposed by this ability.\n` +
|
|
1025
|
-
`Available methods: ${availableMethods.length > 0 ? availableMethods.join(', ') : 'none'}\n` +
|
|
1026
|
-
`Use ability.__list() to see all available methods.`
|
|
1027
|
-
);
|
|
1028
|
-
}
|
|
1029
|
-
throw error;
|
|
1030
|
-
}
|
|
1031
|
-
},
|
|
1032
|
-
__list: () => (functions ? Object.keys(functions) : []),
|
|
1033
|
-
events // Expose the EventEmitter for event subscriptions
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
if (!functions) {
|
|
1037
|
-
// No discovery -> return generic proxy that routes any property call to RPC .call(name)
|
|
1038
|
-
return new Proxy(base, {
|
|
1039
|
-
get(target, prop) {
|
|
1040
|
-
if (prop in target) return target[prop];
|
|
1041
|
-
if (prop === 'then') return undefined; // Avoid being treated as a Promise/thenable
|
|
1042
|
-
if (typeof prop !== 'string') return undefined;
|
|
1043
|
-
return async (params) => {
|
|
1044
|
-
try {
|
|
1045
|
-
return await call(prop, params);
|
|
1046
|
-
} catch (error) {
|
|
1047
|
-
// For abilities without discovery, provide a generic helpful message
|
|
1048
|
-
throw new Error(
|
|
1049
|
-
`Failed to call method '${prop}' on ability.\n` +
|
|
1050
|
-
`Original error: ${error.message}\n` +
|
|
1051
|
-
`Tip: Use ability.__list() to see available methods or check if the method name is correct.`
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
|
-
}
|
|
1056
|
-
});
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// Discovery available → define concrete methods plus dynamic fallback
|
|
1060
|
-
for (const fnName of Object.keys(functions)) {
|
|
1061
|
-
base[fnName] = async (params) => {
|
|
1062
|
-
try {
|
|
1063
|
-
return await call(fnName, params);
|
|
1064
|
-
} catch (error) {
|
|
1065
|
-
throw new Error(`Error calling method '${fnName}': ${error.message}`);
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
return new Proxy(base, {
|
|
1071
|
-
get(target, prop) {
|
|
1072
|
-
if (prop in target) return target[prop];
|
|
1073
|
-
if (prop === 'then') return undefined; // Avoid thenable detection
|
|
1074
|
-
if (typeof prop !== 'string') return undefined;
|
|
1075
|
-
// Unknown method name → provide helpful error
|
|
1076
|
-
return async (params) => {
|
|
1077
|
-
const availableMethods = Object.keys(functions);
|
|
1078
|
-
throw new Error(
|
|
1079
|
-
`Method '${prop}' is not exposed by this ability.\n` +
|
|
1080
|
-
`Available methods: ${availableMethods.length > 0 ? availableMethods.join(', ') : 'none'}\n` +
|
|
1081
|
-
`Use ability.__list() to see all available methods.`
|
|
1082
|
-
);
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
}
|