@kadi.build/core 0.0.1-alpha.1 → 0.0.1-alpha.11
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 +361 -230
- package/dist/abilities/AbilityCache.d.ts +242 -0
- package/dist/abilities/AbilityCache.d.ts.map +1 -0
- package/dist/abilities/AbilityCache.js +285 -0
- package/dist/abilities/AbilityCache.js.map +1 -0
- package/dist/abilities/AbilityContext.d.ts +215 -0
- package/dist/abilities/AbilityContext.d.ts.map +1 -0
- package/dist/abilities/AbilityContext.js +36 -0
- package/dist/abilities/AbilityContext.js.map +1 -0
- package/dist/abilities/AbilityLoader.d.ts +203 -0
- package/dist/abilities/AbilityLoader.d.ts.map +1 -0
- package/dist/abilities/AbilityLoader.js +343 -0
- package/dist/abilities/AbilityLoader.js.map +1 -0
- package/dist/abilities/AbilityProxy.d.ts +496 -0
- package/dist/abilities/AbilityProxy.d.ts.map +1 -0
- package/dist/abilities/AbilityProxy.js +551 -0
- package/dist/abilities/AbilityProxy.js.map +1 -0
- package/dist/abilities/AbilityValidator.d.ts +172 -0
- package/dist/abilities/AbilityValidator.d.ts.map +1 -0
- package/dist/abilities/AbilityValidator.js +253 -0
- package/dist/abilities/AbilityValidator.js.map +1 -0
- package/dist/abilities/index.d.ts +26 -0
- package/dist/abilities/index.d.ts.map +1 -0
- package/dist/abilities/index.js +23 -0
- package/dist/abilities/index.js.map +1 -0
- package/dist/abilities/types.d.ts +223 -0
- package/dist/abilities/types.d.ts.map +1 -0
- package/dist/abilities/types.js +10 -0
- package/dist/abilities/types.js.map +1 -0
- package/dist/api/index.d.ts +92 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +124 -0
- package/dist/api/index.js.map +1 -0
- package/dist/broker/BrokerConnection.d.ts +253 -0
- package/dist/broker/BrokerConnection.d.ts.map +1 -0
- package/dist/broker/BrokerConnection.js +434 -0
- package/dist/broker/BrokerConnection.js.map +1 -0
- package/dist/broker/BrokerConnectionManager.d.ts +216 -0
- package/dist/broker/BrokerConnectionManager.d.ts.map +1 -0
- package/dist/broker/BrokerConnectionManager.js +305 -0
- package/dist/broker/BrokerConnectionManager.js.map +1 -0
- package/dist/broker/BrokerProtocol.d.ts +280 -0
- package/dist/broker/BrokerProtocol.d.ts.map +1 -0
- package/dist/broker/BrokerProtocol.js +466 -0
- package/dist/broker/BrokerProtocol.js.map +1 -0
- package/dist/broker/index.d.ts +9 -0
- package/dist/broker/index.d.ts.map +1 -0
- package/dist/broker/index.js +9 -0
- package/dist/broker/index.js.map +1 -0
- package/dist/client/KadiClient.d.ts +459 -0
- package/dist/client/KadiClient.d.ts.map +1 -0
- package/dist/client/KadiClient.js +902 -0
- package/dist/client/KadiClient.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +7 -0
- package/dist/client/index.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +138 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +226 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/ConfigResolver.d.ts +135 -0
- package/dist/config/ConfigResolver.d.ts.map +1 -0
- package/dist/config/ConfigResolver.js +282 -0
- package/dist/config/ConfigResolver.js.map +1 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/errors/index.d.ts +9 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +8 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/events/EventHub.d.ts +172 -0
- package/dist/events/EventHub.d.ts.map +1 -0
- package/dist/events/EventHub.js +333 -0
- package/dist/events/EventHub.js.map +1 -0
- package/dist/events/index.d.ts +7 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +7 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/index.d.ts +33 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +33 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/schemas/index.d.ts +22 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +27 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/kadi-extensions.d.ts +231 -0
- package/dist/schemas/kadi-extensions.d.ts.map +1 -0
- package/dist/schemas/kadi-extensions.js +14 -0
- package/dist/schemas/kadi-extensions.js.map +1 -0
- package/dist/schemas/mcp/schema.d.ts +1399 -0
- package/dist/schemas/mcp/schema.d.ts.map +1 -0
- package/dist/schemas/mcp/schema.js +53 -0
- package/dist/schemas/mcp/schema.js.map +1 -0
- package/dist/schemas/mcp/version.d.ts +37 -0
- package/dist/schemas/mcp/version.d.ts.map +1 -0
- package/dist/schemas/mcp/version.js +39 -0
- package/dist/schemas/mcp/version.js.map +1 -0
- package/dist/schemas/schema-builders.d.ts +178 -0
- package/dist/schemas/schema-builders.d.ts.map +1 -0
- package/dist/schemas/schema-builders.js +258 -0
- package/dist/schemas/schema-builders.js.map +1 -0
- package/dist/schemas/zod-helpers.d.ts +129 -0
- package/dist/schemas/zod-helpers.d.ts.map +1 -0
- package/dist/schemas/zod-helpers.js +225 -0
- package/dist/schemas/zod-helpers.js.map +1 -0
- package/dist/schemas/zod-to-json-schema.d.ts +159 -0
- package/dist/schemas/zod-to-json-schema.d.ts.map +1 -0
- package/dist/schemas/zod-to-json-schema.js +154 -0
- package/dist/schemas/zod-to-json-schema.js.map +1 -0
- package/dist/tools/ToolRegistry.d.ts +256 -0
- package/dist/tools/ToolRegistry.d.ts.map +1 -0
- package/dist/tools/ToolRegistry.js +340 -0
- package/dist/tools/ToolRegistry.js.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/transports/BrokerTransport.d.ts +151 -0
- package/dist/transports/BrokerTransport.d.ts.map +1 -0
- package/dist/transports/BrokerTransport.js +261 -0
- package/dist/transports/BrokerTransport.js.map +1 -0
- package/dist/transports/NativeTransport.d.ts +178 -0
- package/dist/transports/NativeTransport.d.ts.map +1 -0
- package/dist/transports/NativeTransport.js +397 -0
- package/dist/transports/NativeTransport.js.map +1 -0
- package/dist/transports/StdioTransport.d.ts +250 -0
- package/dist/transports/StdioTransport.d.ts.map +1 -0
- package/dist/transports/StdioTransport.js +487 -0
- package/dist/transports/StdioTransport.js.map +1 -0
- package/dist/transports/index.d.ts +10 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +9 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/types/broker.d.ts +279 -0
- package/dist/types/broker.d.ts.map +1 -0
- package/dist/types/broker.js +19 -0
- package/dist/types/broker.js.map +1 -0
- package/dist/types/config.d.ts +325 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +17 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/errors.d.ts +178 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +165 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/events.d.ts +210 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +8 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +21 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/protocol.d.ts +48 -0
- package/dist/types/protocol.d.ts.map +1 -0
- package/dist/types/protocol.js +11 -0
- package/dist/types/protocol.js.map +1 -0
- package/dist/types/tools.d.ts +67 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +16 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/types/transport.d.ts +250 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/types/transport.js +18 -0
- package/dist/types/transport.js.map +1 -0
- package/dist/types/zod-tools.d.ts +198 -0
- package/dist/types/zod-tools.d.ts.map +1 -0
- package/dist/types/zod-tools.js +14 -0
- package/dist/types/zod-tools.js.map +1 -0
- package/dist/utils/StdioMessageReader.d.ts +122 -0
- package/dist/utils/StdioMessageReader.d.ts.map +1 -0
- package/dist/utils/StdioMessageReader.js +209 -0
- package/dist/utils/StdioMessageReader.js.map +1 -0
- package/dist/utils/StdioMessageWriter.d.ts +104 -0
- package/dist/utils/StdioMessageWriter.d.ts.map +1 -0
- package/dist/utils/StdioMessageWriter.js +162 -0
- package/dist/utils/StdioMessageWriter.js.map +1 -0
- package/dist/validation/SchemaValidator.d.ts +208 -0
- package/dist/validation/SchemaValidator.d.ts.map +1 -0
- package/dist/validation/SchemaValidator.js +411 -0
- package/dist/validation/SchemaValidator.js.map +1 -0
- package/dist/validation/index.d.ts +11 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +10 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +70 -5
- package/agent.json +0 -18
- package/broker.js +0 -214
- package/index.js +0 -370
- package/ipc.js +0 -220
- package/ipcInterfaces/pythonAbilityIPC.py +0 -177
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KADI Client
|
|
3
|
+
*
|
|
4
|
+
* Thin orchestrator that composes specialized modules to provide the
|
|
5
|
+
* complete KADI client functionality.
|
|
6
|
+
*
|
|
7
|
+
* This is dramatically simpler than the old implementation because each
|
|
8
|
+
* concern has been properly separated.
|
|
9
|
+
*
|
|
10
|
+
* @module client/KadiClient
|
|
11
|
+
*/
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
import { KadiError, ErrorCode } from '../types/index.js';
|
|
14
|
+
import { KadiMessages } from '../messages/index.js';
|
|
15
|
+
import { ConfigResolver } from '../config/ConfigResolver.js';
|
|
16
|
+
import { ToolRegistry } from '../tools/ToolRegistry.js';
|
|
17
|
+
import { EventHub } from '../events/EventHub.js';
|
|
18
|
+
import { BrokerConnectionManager } from '../broker/BrokerConnectionManager.js';
|
|
19
|
+
import { BrokerProtocol, PROTOCOL_VERSION } from '../broker/BrokerProtocol.js';
|
|
20
|
+
import { AbilityLoader } from '../abilities/index.js';
|
|
21
|
+
// Import Zod utilities for automatic schema conversion
|
|
22
|
+
import { zodToJsonSchema, isZodSchema } from '../schemas/zod-to-json-schema.js';
|
|
23
|
+
/**
|
|
24
|
+
* KADI Client
|
|
25
|
+
*
|
|
26
|
+
* Main client class for KADI framework.
|
|
27
|
+
* Orchestrates tool registry, event hub, broker connections, and ability loading.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // Create client
|
|
32
|
+
* const client = new KadiClient({
|
|
33
|
+
* name: 'my-service',
|
|
34
|
+
* brokers: { local: 'ws://localhost:8080' },
|
|
35
|
+
* networks: ['global']
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Register a tool
|
|
39
|
+
* client.registerTool('greet', async ({ name }) => {
|
|
40
|
+
* return { message: `Hello, ${name}!` };
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Connect to brokers
|
|
44
|
+
* await client.connect();
|
|
45
|
+
*
|
|
46
|
+
* // Load an ability (explicit transport)
|
|
47
|
+
* const calc = await client.load('calculator', 'broker');
|
|
48
|
+
* const result = await calc.add({ a: 5, b: 3 });
|
|
49
|
+
*
|
|
50
|
+
* // Subscribe to events
|
|
51
|
+
* client.subscribeToEvent('user.*', (data) => {
|
|
52
|
+
* console.log('User event:', data);
|
|
53
|
+
* });
|
|
54
|
+
*
|
|
55
|
+
* // Publish an event
|
|
56
|
+
* client.publishEvent('user.login', { userId: '123' });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export class KadiClient extends EventEmitter {
|
|
60
|
+
/**
|
|
61
|
+
* Resolved configuration
|
|
62
|
+
*/
|
|
63
|
+
config;
|
|
64
|
+
/**
|
|
65
|
+
* Tool registry
|
|
66
|
+
*/
|
|
67
|
+
tools;
|
|
68
|
+
/**
|
|
69
|
+
* Event hub
|
|
70
|
+
*/
|
|
71
|
+
events;
|
|
72
|
+
/**
|
|
73
|
+
* Broker connection manager
|
|
74
|
+
*/
|
|
75
|
+
brokers;
|
|
76
|
+
/**
|
|
77
|
+
* Broker protocol instances (per broker)
|
|
78
|
+
*/
|
|
79
|
+
protocols = new Map();
|
|
80
|
+
/**
|
|
81
|
+
* Ability loader
|
|
82
|
+
*/
|
|
83
|
+
abilityLoader;
|
|
84
|
+
/**
|
|
85
|
+
* Whether client is initialized
|
|
86
|
+
*/
|
|
87
|
+
initialized = false;
|
|
88
|
+
/**
|
|
89
|
+
* Create a new KADI Client
|
|
90
|
+
*
|
|
91
|
+
* @param config - Client configuration
|
|
92
|
+
*/
|
|
93
|
+
constructor(config) {
|
|
94
|
+
super();
|
|
95
|
+
// Initialize specialized modules
|
|
96
|
+
this.tools = new ToolRegistry();
|
|
97
|
+
this.events = new EventHub('unknown'); // Will be updated after config resolution
|
|
98
|
+
this.brokers = new BrokerConnectionManager();
|
|
99
|
+
this.abilityLoader = new AbilityLoader();
|
|
100
|
+
// Resolve configuration (synchronous placeholder - will be async in connect())
|
|
101
|
+
// For now, create a minimal config
|
|
102
|
+
this.config = this.createMinimalConfig(config);
|
|
103
|
+
// Update event hub source to match client name
|
|
104
|
+
this.events.setSource(this.config.name);
|
|
105
|
+
// Forward broker events
|
|
106
|
+
this.setupBrokerEventForwarding();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Connect to configured brokers
|
|
110
|
+
*
|
|
111
|
+
* @throws {KadiError} If connection fails
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* await client.connect();
|
|
116
|
+
* console.log('Connected to brokers');
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
async connect() {
|
|
120
|
+
if (this.initialized) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Resolve full configuration
|
|
124
|
+
const resolver = new ConfigResolver();
|
|
125
|
+
const resolved = await resolver.resolve(this.config);
|
|
126
|
+
Object.assign(this.config, resolved);
|
|
127
|
+
// Connect to brokers if configured
|
|
128
|
+
if (this.config.brokers && Object.keys(this.config.brokers).length > 0) {
|
|
129
|
+
await this.connectToBrokers();
|
|
130
|
+
}
|
|
131
|
+
this.initialized = true;
|
|
132
|
+
this.emit('ready');
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Connect to all configured brokers
|
|
136
|
+
*/
|
|
137
|
+
async connectToBrokers() {
|
|
138
|
+
if (!this.config.brokers) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const brokerConfigs = Object.entries(this.config.brokers).map(([name, url]) => ({
|
|
142
|
+
name,
|
|
143
|
+
url,
|
|
144
|
+
heartbeatInterval: this.config.advanced.heartbeatInterval,
|
|
145
|
+
requestTimeout: this.config.advanced.requestTimeout,
|
|
146
|
+
connectionTimeout: this.config.advanced.connectionTimeout
|
|
147
|
+
}));
|
|
148
|
+
// Connect to all brokers
|
|
149
|
+
await this.brokers.connectMultiple(brokerConfigs, this.config.defaultBroker);
|
|
150
|
+
// Perform handshake and register capabilities for each broker
|
|
151
|
+
for (const brokerName of this.brokers.getBrokerNames()) {
|
|
152
|
+
const connection = this.brokers.getConnection(brokerName);
|
|
153
|
+
if (!connection) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Create protocol instance
|
|
157
|
+
const protocol = new BrokerProtocol(connection);
|
|
158
|
+
this.protocols.set(brokerName, protocol);
|
|
159
|
+
// Perform handshake
|
|
160
|
+
await protocol.handshake({
|
|
161
|
+
role: this.config.role || 'agent',
|
|
162
|
+
name: this.config.name,
|
|
163
|
+
version: this.config.version,
|
|
164
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
165
|
+
networks: this.config.networks
|
|
166
|
+
});
|
|
167
|
+
// Register capabilities
|
|
168
|
+
await this.registerCapabilitiesWithBroker(protocol);
|
|
169
|
+
// Start heartbeat
|
|
170
|
+
protocol.startHeartbeat(this.config.advanced.heartbeatInterval ?? 30000);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Register capabilities with a broker
|
|
175
|
+
*
|
|
176
|
+
* @param protocol - Broker protocol instance
|
|
177
|
+
*/
|
|
178
|
+
async registerCapabilitiesWithBroker(protocol) {
|
|
179
|
+
const toolDefinitions = this.tools.extractDefinitions();
|
|
180
|
+
if (toolDefinitions.length > 0) {
|
|
181
|
+
await protocol.registerCapabilities({
|
|
182
|
+
tools: toolDefinitions,
|
|
183
|
+
networks: this.config.networks,
|
|
184
|
+
displayName: this.config.name
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Register a tool (MCP-based)
|
|
190
|
+
*
|
|
191
|
+
* @template TInput - Tool input type
|
|
192
|
+
* @template TOutput - Tool output type
|
|
193
|
+
* @param definition - MCP-compliant tool definition
|
|
194
|
+
* @param handler - Tool handler function
|
|
195
|
+
* @returns this for chaining
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* client.registerTool({
|
|
200
|
+
* name: 'add',
|
|
201
|
+
* description: 'Add two numbers',
|
|
202
|
+
* version: '1.0.0',
|
|
203
|
+
* tags: ['math'],
|
|
204
|
+
* inputSchema: {
|
|
205
|
+
* type: 'object',
|
|
206
|
+
* properties: {
|
|
207
|
+
* a: { type: 'number' },
|
|
208
|
+
* b: { type: 'number' }
|
|
209
|
+
* },
|
|
210
|
+
* required: ['a', 'b']
|
|
211
|
+
* },
|
|
212
|
+
* outputSchema: {
|
|
213
|
+
* type: 'object',
|
|
214
|
+
* properties: {
|
|
215
|
+
* result: { type: 'number' }
|
|
216
|
+
* }
|
|
217
|
+
* }
|
|
218
|
+
* }, async ({ a, b }) => {
|
|
219
|
+
* return { result: a + b };
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
registerTool(definition, handler) {
|
|
224
|
+
// Support both JSON Schema and Zod schemas
|
|
225
|
+
// Zod approach: { input: z.object(...), output: z.object(...) }
|
|
226
|
+
// JSON Schema approach: { inputSchema: {...}, outputSchema: {...} }
|
|
227
|
+
let finalDefinition;
|
|
228
|
+
// Check if using Zod schemas (has 'input' field instead of 'inputSchema')
|
|
229
|
+
if ('input' in definition && definition.input !== undefined) {
|
|
230
|
+
const zodDef = definition;
|
|
231
|
+
// Validate that input is actually a Zod schema
|
|
232
|
+
if (!isZodSchema(zodDef.input)) {
|
|
233
|
+
throw new KadiError(`Tool '${definition.name}': 'input' must be a Zod schema. Use 'inputSchema' for JSON Schema.`, ErrorCode.INVALID_INPUT, 400, { toolName: definition.name });
|
|
234
|
+
}
|
|
235
|
+
// Convert Zod schemas to JSON Schema
|
|
236
|
+
finalDefinition = {
|
|
237
|
+
name: zodDef.name,
|
|
238
|
+
description: zodDef.description,
|
|
239
|
+
title: zodDef.title,
|
|
240
|
+
version: zodDef.version,
|
|
241
|
+
tags: zodDef.tags,
|
|
242
|
+
networks: zodDef.networks,
|
|
243
|
+
// Convert input Zod schema → JSON Schema
|
|
244
|
+
inputSchema: zodToJsonSchema(zodDef.input),
|
|
245
|
+
// Convert output Zod schema → JSON Schema (if provided)
|
|
246
|
+
outputSchema: zodDef.output && isZodSchema(zodDef.output)
|
|
247
|
+
? zodToJsonSchema(zodDef.output)
|
|
248
|
+
: undefined
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Traditional JSON Schema approach - use as-is
|
|
253
|
+
finalDefinition = definition;
|
|
254
|
+
}
|
|
255
|
+
// Register the tool with the converted schemas
|
|
256
|
+
this.tools.register(finalDefinition, handler);
|
|
257
|
+
// If already connected to brokers, register the new tool
|
|
258
|
+
if (this.initialized && this.protocols.size > 0) {
|
|
259
|
+
for (const protocol of this.protocols.values()) {
|
|
260
|
+
protocol.registerCapabilities({
|
|
261
|
+
tools: [finalDefinition], // Use converted definition
|
|
262
|
+
networks: this.config.networks,
|
|
263
|
+
displayName: this.config.name
|
|
264
|
+
}).catch(error => {
|
|
265
|
+
this.emit('error', new KadiError(`Failed to register tool with broker: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.BROKER_RESPONSE_ERROR, 500, { toolName: definition.name }));
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get a registered tool by name
|
|
273
|
+
*
|
|
274
|
+
* Retrieves a tool that has been registered with this client.
|
|
275
|
+
* Useful for inspecting registered tools or accessing their metadata.
|
|
276
|
+
*
|
|
277
|
+
* @param name - Tool name to retrieve
|
|
278
|
+
* @returns The registered tool with its definition and handler, or undefined if not found
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* ```typescript
|
|
282
|
+
* const tool = client.getRegisteredTool('add');
|
|
283
|
+
* if (tool) {
|
|
284
|
+
* console.log('Tool:', tool.definition.name);
|
|
285
|
+
* console.log('Description:', tool.definition.description);
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
getRegisteredTool(name) {
|
|
290
|
+
return this.tools.get(name);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get all registered tools
|
|
294
|
+
*
|
|
295
|
+
* Returns an array of all tools that have been registered with this client.
|
|
296
|
+
* Useful for listing available tools or debugging.
|
|
297
|
+
*
|
|
298
|
+
* @returns Array of all registered tools
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* const allTools = client.getAllRegisteredTools();
|
|
303
|
+
* console.log('Available tools:', allTools.map(t => t.definition.name));
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
getAllRegisteredTools() {
|
|
307
|
+
return this.tools.getAll();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check if a tool is registered
|
|
311
|
+
*
|
|
312
|
+
* @param name - Tool name to check
|
|
313
|
+
* @returns true if the tool is registered, false otherwise
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```typescript
|
|
317
|
+
* if (client.hasRegisteredTool('add')) {
|
|
318
|
+
* // Tool is available
|
|
319
|
+
* }
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
hasRegisteredTool(name) {
|
|
323
|
+
return this.tools.has(name);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Load an ability
|
|
327
|
+
*
|
|
328
|
+
* **Explicit Transport, Excellent Developer Experience:**
|
|
329
|
+
* - Explicit transport selection (no magic)
|
|
330
|
+
* - Transport-specific options (only relevant fields shown)
|
|
331
|
+
* - Smart caching (avoids duplicate loads)
|
|
332
|
+
* - Runtime validation (optional)
|
|
333
|
+
* - Helpful errors with suggestions
|
|
334
|
+
*
|
|
335
|
+
* @param name - Ability name
|
|
336
|
+
* @param transport - Transport type ('native', 'stdio', or 'broker')
|
|
337
|
+
* @param options - Load options (transport-specific, optional)
|
|
338
|
+
* @returns Proxied ability with dynamic method access
|
|
339
|
+
*
|
|
340
|
+
* @throws {KadiError} If loading fails
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```typescript
|
|
344
|
+
* // Simple broker load
|
|
345
|
+
* const calc = await client.load('calculator', 'broker');
|
|
346
|
+
* await calc.add({ a: 5, b: 3 });
|
|
347
|
+
*
|
|
348
|
+
* // With options
|
|
349
|
+
* const remote = await client.load('gpu-service', 'broker', {
|
|
350
|
+
* networks: ['global', 'gpu-cluster']
|
|
351
|
+
* });
|
|
352
|
+
*
|
|
353
|
+
* // With runtime validation
|
|
354
|
+
* const validated = await client.load('calculator', 'broker', {
|
|
355
|
+
* validate: true // Runtime check for expected methods
|
|
356
|
+
* });
|
|
357
|
+
*
|
|
358
|
+
* // Native transport
|
|
359
|
+
* const local = await client.load('math-lib', 'native', {
|
|
360
|
+
* path: './abilities/math'
|
|
361
|
+
* });
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
async load(name, transport, options = {}) {
|
|
365
|
+
return await this.abilityLoader.load(name, transport, options, this);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Subscribe to events
|
|
369
|
+
*
|
|
370
|
+
* @template T - Event data type
|
|
371
|
+
* @param pattern - Event pattern (supports wildcards)
|
|
372
|
+
* @param callback - Event callback
|
|
373
|
+
* @returns Unsubscribe function
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```typescript
|
|
377
|
+
* const unsubscribe = client.subscribeToEvent('user.*', (data) => {
|
|
378
|
+
* console.log('User event:', data);
|
|
379
|
+
* });
|
|
380
|
+
*
|
|
381
|
+
* // Later...
|
|
382
|
+
* unsubscribe();
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
subscribeToEvent(pattern, callback) {
|
|
386
|
+
// Subscribe locally
|
|
387
|
+
const localUnsub = this.events.subscribe(pattern, callback);
|
|
388
|
+
// Subscribe on broker if connected
|
|
389
|
+
if (this.protocols.size > 0) {
|
|
390
|
+
const protocol = this.getBrokerProtocol();
|
|
391
|
+
protocol.subscribeToEvents({
|
|
392
|
+
channels: [pattern],
|
|
393
|
+
networkId: this.config.networks[0]
|
|
394
|
+
}).catch(error => {
|
|
395
|
+
this.emit('error', new KadiError(`Failed to subscribe to broker events: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.EVENT_SUBSCRIPTION_FAILED, 500, { pattern }));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return localUnsub;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Publish an event
|
|
402
|
+
*
|
|
403
|
+
* @template T - Event data type
|
|
404
|
+
* @param eventName - Event name
|
|
405
|
+
* @param data - Event data
|
|
406
|
+
* @param options - Publishing options
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```typescript
|
|
410
|
+
* client.publishEvent('user.login', {
|
|
411
|
+
* userId: '123',
|
|
412
|
+
* timestamp: Date.now()
|
|
413
|
+
* });
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
publishEvent(eventName, data, options) {
|
|
417
|
+
// Publish locally
|
|
418
|
+
this.events.publish(eventName, data, options);
|
|
419
|
+
// Publish to broker if connected
|
|
420
|
+
if (this.protocols.size > 0) {
|
|
421
|
+
const protocol = this.getBrokerProtocol();
|
|
422
|
+
protocol.publishEvent({
|
|
423
|
+
channel: eventName,
|
|
424
|
+
data,
|
|
425
|
+
networkId: this.config.networks[0],
|
|
426
|
+
hints: options
|
|
427
|
+
}).catch(error => {
|
|
428
|
+
this.emit('error', new KadiError(`Failed to publish event to broker: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.EVENT_PUBLISH_FAILED, 500, { eventName }));
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Disconnect from all brokers and cleanup
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```typescript
|
|
437
|
+
* await client.disconnect();
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
async disconnect() {
|
|
441
|
+
// Stop all broker heartbeats
|
|
442
|
+
for (const protocol of this.protocols.values()) {
|
|
443
|
+
protocol.cleanup();
|
|
444
|
+
}
|
|
445
|
+
// Disconnect from all brokers
|
|
446
|
+
await this.brokers.disconnectAll();
|
|
447
|
+
// Unload all abilities
|
|
448
|
+
await this.abilityLoader.unloadAll();
|
|
449
|
+
// Clear event subscriptions
|
|
450
|
+
this.events.clear();
|
|
451
|
+
// Clear protocols
|
|
452
|
+
this.protocols.clear();
|
|
453
|
+
this.initialized = false;
|
|
454
|
+
this.emit('disconnected');
|
|
455
|
+
// If running in stdio mode, exit process
|
|
456
|
+
if (this.stdioServer) {
|
|
457
|
+
this.stdioServer.shutdown();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Stdio server instance (when running in stdio mode)
|
|
462
|
+
*/
|
|
463
|
+
stdioServer;
|
|
464
|
+
/**
|
|
465
|
+
* Start serving this ability in the specified mode
|
|
466
|
+
*
|
|
467
|
+
* **Universal Entry Point**:
|
|
468
|
+
* Starts the appropriate server based on the mode parameter.
|
|
469
|
+
* This enables the same ability code to work across all transports
|
|
470
|
+
*
|
|
471
|
+
* **Modes**:
|
|
472
|
+
* - **native**: No-op (ability already loaded in-process as ES module)
|
|
473
|
+
* - **stdio**: Starts JSON-RPC server over stdin/stdout (for child processes)
|
|
474
|
+
* - **broker**: Connects to WebSocket broker and serves remotely
|
|
475
|
+
*
|
|
476
|
+
* @param mode - Transport mode to serve in ('native' | 'stdio' | 'broker')
|
|
477
|
+
* @returns Promise that resolves when server is ready (never resolves for stdio/broker - they keep running)
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```typescript
|
|
481
|
+
* // ability.ts
|
|
482
|
+
* const client = new KadiClient({
|
|
483
|
+
* name: 'calculator',
|
|
484
|
+
* version: '1.0.0',
|
|
485
|
+
* brokers: { default: 'ws://localhost:8080' } // For broker mode
|
|
486
|
+
* });
|
|
487
|
+
*
|
|
488
|
+
* client.registerTool({ name: 'add', ... }, async ({ a, b }) => {
|
|
489
|
+
* return { result: a + b };
|
|
490
|
+
* });
|
|
491
|
+
*
|
|
492
|
+
* export default client;
|
|
493
|
+
*
|
|
494
|
+
* // If running standalone, start server
|
|
495
|
+
* if (import.meta.url === `file://${process.argv[1]}`) {
|
|
496
|
+
* await client.serve('stdio'); // or 'broker' for distributed mode
|
|
497
|
+
* }
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
async serve(mode) {
|
|
501
|
+
if (mode === 'stdio') {
|
|
502
|
+
return this.serveStdio();
|
|
503
|
+
}
|
|
504
|
+
if (mode === 'broker') {
|
|
505
|
+
return this.serveBroker();
|
|
506
|
+
}
|
|
507
|
+
// Native mode - ability already loaded in-process, nothing to serve
|
|
508
|
+
return Promise.resolve();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Serve via stdio (JSON-RPC over stdin/stdout)
|
|
512
|
+
*
|
|
513
|
+
* **Steps**:
|
|
514
|
+
* 1. Redirect console.log to stderr (keeps stdout clean for JSON-RPC)
|
|
515
|
+
* 2. Create Content-Length message reader/writer for stdin/stdout
|
|
516
|
+
* 3. Listen for incoming JSON-RPC requests (invoke, readAgentJson, shutdown)
|
|
517
|
+
* 4. Execute tool handlers for 'invoke' requests
|
|
518
|
+
* 5. Send JSON-RPC responses back via stdout
|
|
519
|
+
* 6. Handle graceful shutdown on SIGTERM/SIGINT
|
|
520
|
+
* 7. Keep process alive until shutdown
|
|
521
|
+
*
|
|
522
|
+
* @returns Promise that never resolves (server runs until killed)
|
|
523
|
+
*/
|
|
524
|
+
async serveStdio() {
|
|
525
|
+
const { StdioMessageReader } = await import('../utils/StdioMessageReader.js');
|
|
526
|
+
const { StdioMessageWriter } = await import('../utils/StdioMessageWriter.js');
|
|
527
|
+
// Step 1: Redirect console.log to stderr (keeps stdout clean for JSON-RPC)
|
|
528
|
+
const originalLog = console.log;
|
|
529
|
+
console.log = console.error;
|
|
530
|
+
// Step 2: Create Content-Length message reader/writer for stdin/stdout
|
|
531
|
+
const reader = new StdioMessageReader();
|
|
532
|
+
const writer = new StdioMessageWriter(process.stdout);
|
|
533
|
+
// Step 3: Listen for incoming JSON-RPC requests
|
|
534
|
+
reader.on('message', async (message) => {
|
|
535
|
+
try {
|
|
536
|
+
const { id, method, params } = message;
|
|
537
|
+
if (method === 'invoke') {
|
|
538
|
+
// Step 4: Execute tool handler for 'invoke' request
|
|
539
|
+
const { toolName, toolInput } = params;
|
|
540
|
+
const result = await this.invoke(toolName, toolInput);
|
|
541
|
+
// Step 5: Send JSON-RPC success response
|
|
542
|
+
await writer.write({
|
|
543
|
+
jsonrpc: '2.0',
|
|
544
|
+
id,
|
|
545
|
+
result
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
else if (method === 'readAgentJson') {
|
|
549
|
+
// Return agent.json representation (used during discovery)
|
|
550
|
+
const agentJson = this.readAgentJson();
|
|
551
|
+
await writer.write({
|
|
552
|
+
jsonrpc: '2.0',
|
|
553
|
+
id,
|
|
554
|
+
result: agentJson
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
else if (method === 'shutdown') {
|
|
558
|
+
// Graceful shutdown request
|
|
559
|
+
await writer.write({
|
|
560
|
+
jsonrpc: '2.0',
|
|
561
|
+
id,
|
|
562
|
+
result: { success: true }
|
|
563
|
+
});
|
|
564
|
+
// Restore console.log
|
|
565
|
+
console.log = originalLog;
|
|
566
|
+
// Exit after short delay to allow response to flush
|
|
567
|
+
setTimeout(() => {
|
|
568
|
+
process.exit(0);
|
|
569
|
+
}, 100);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// Unknown method - send error
|
|
573
|
+
await writer.write({
|
|
574
|
+
jsonrpc: '2.0',
|
|
575
|
+
id,
|
|
576
|
+
error: {
|
|
577
|
+
code: -32601,
|
|
578
|
+
message: `Method not found: ${method}`
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
// Step 5: Send JSON-RPC error response on failure
|
|
585
|
+
const kadiError = error instanceof KadiError ? error : KadiError.from(error);
|
|
586
|
+
await writer.write({
|
|
587
|
+
jsonrpc: '2.0',
|
|
588
|
+
id: message.id,
|
|
589
|
+
error: {
|
|
590
|
+
code: kadiError.statusCode || -32603,
|
|
591
|
+
message: kadiError.message,
|
|
592
|
+
data: kadiError.context
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
reader.on('error', (error) => {
|
|
598
|
+
// Log parse errors to stderr
|
|
599
|
+
console.error('[KADI Stdio Server] Parse error:', error.message);
|
|
600
|
+
});
|
|
601
|
+
// Feed stdin data to reader for Content-Length framing
|
|
602
|
+
process.stdin.on('data', (chunk) => {
|
|
603
|
+
reader.onData(chunk);
|
|
604
|
+
});
|
|
605
|
+
// Step 6: Handle graceful shutdown on SIGTERM/SIGINT
|
|
606
|
+
const cleanup = () => {
|
|
607
|
+
console.log = originalLog;
|
|
608
|
+
reader.destroy();
|
|
609
|
+
writer.destroy();
|
|
610
|
+
process.exit(0);
|
|
611
|
+
};
|
|
612
|
+
process.on('SIGTERM', cleanup);
|
|
613
|
+
process.on('SIGINT', cleanup);
|
|
614
|
+
// Store server instance for programmatic shutdown
|
|
615
|
+
this.stdioServer = {
|
|
616
|
+
shutdown: () => {
|
|
617
|
+
console.log = originalLog;
|
|
618
|
+
reader.destroy();
|
|
619
|
+
writer.destroy();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
// Step 7: Keep process alive until shutdown
|
|
623
|
+
return new Promise(() => {
|
|
624
|
+
// Never resolves - server runs until killed
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Serve via broker (WebSocket-based distributed mode)
|
|
629
|
+
*
|
|
630
|
+
* Connects to the broker, registers this ability's tools, and handles
|
|
631
|
+
* incoming tool invocation requests.
|
|
632
|
+
*
|
|
633
|
+
* **Steps**:
|
|
634
|
+
* 1. Get broker URL from constructor config
|
|
635
|
+
* 2. Create and connect to broker via BrokerConnection
|
|
636
|
+
* 3. Perform handshake and authentication
|
|
637
|
+
* 4. Register all tools from the registry
|
|
638
|
+
* 5. Listen for incoming 'kadi.ability.request' messages
|
|
639
|
+
* 6. Execute tool handlers and send responses
|
|
640
|
+
* 7. Handle graceful shutdown on SIGTERM/SIGINT
|
|
641
|
+
* 8. Keep process alive until shutdown
|
|
642
|
+
*
|
|
643
|
+
* @returns Promise that never resolves (server runs until killed)
|
|
644
|
+
* @throws {KadiError} If broker config missing or connection fails
|
|
645
|
+
* @private
|
|
646
|
+
*/
|
|
647
|
+
async serveBroker() {
|
|
648
|
+
// Step 1: Validate broker configuration exists
|
|
649
|
+
if (!this.brokers) {
|
|
650
|
+
throw new KadiError('Cannot serve in broker mode - no broker configuration provided', ErrorCode.INVALID_CONFIG, 400, {
|
|
651
|
+
suggestion: 'Add brokers configuration to KadiClient constructor',
|
|
652
|
+
example: 'new KadiClient({ brokers: { default: "ws://localhost:8080" } })'
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
// Step 2: Connect to broker (this performs handshake and registers capabilities)
|
|
656
|
+
await this.connect();
|
|
657
|
+
// Step 3: Get connection for error/disconnect handling
|
|
658
|
+
const connection = this.brokers.getDefaultConnection();
|
|
659
|
+
// Note: Tool invocation requests are handled by setupBrokerEventForwarding()
|
|
660
|
+
// which was called during initialization. No need to duplicate the listener here.
|
|
661
|
+
// Handle connection errors
|
|
662
|
+
connection.on('error', (error) => {
|
|
663
|
+
console.error('[KADI Broker Server] Connection error:', error);
|
|
664
|
+
});
|
|
665
|
+
// Handle disconnection
|
|
666
|
+
connection.on('disconnected', (code, reason) => {
|
|
667
|
+
console.log(`[KADI Broker Server] Disconnected from broker (code: ${code}, reason: ${reason})`);
|
|
668
|
+
// Broker connection should auto-reconnect if configured
|
|
669
|
+
});
|
|
670
|
+
// Step 7: Handle graceful shutdown on SIGTERM/SIGINT
|
|
671
|
+
const shutdown = async () => {
|
|
672
|
+
console.log('\n[KADI Broker Server] Shutting down...');
|
|
673
|
+
await connection.disconnect();
|
|
674
|
+
console.log('[KADI Broker Server] Disconnected from broker');
|
|
675
|
+
process.exit(0);
|
|
676
|
+
};
|
|
677
|
+
process.on('SIGTERM', shutdown);
|
|
678
|
+
process.on('SIGINT', shutdown);
|
|
679
|
+
// Step 8: Keep process alive until shutdown
|
|
680
|
+
return new Promise(() => {
|
|
681
|
+
// Never resolves - server runs until killed
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Read agent.json representation
|
|
686
|
+
*
|
|
687
|
+
* Returns the runtime representation of this client's agent.json configuration.
|
|
688
|
+
* This enables KadiClient to function as an ability module that can be loaded
|
|
689
|
+
* natively by other agents.
|
|
690
|
+
*
|
|
691
|
+
* **What this returns:**
|
|
692
|
+
* - Represents what would be in the ability's agent.json file
|
|
693
|
+
* - Runtime `tools` field is mapped from agent.json's `exports` field
|
|
694
|
+
* - Used for discovery, type generation, and validation
|
|
695
|
+
*
|
|
696
|
+
* **Registration Pattern (Option A):**
|
|
697
|
+
* Ability developers write their code once using KadiClient.registerTool(),
|
|
698
|
+
* and it works across all transports (native, stdio, broker).
|
|
699
|
+
*
|
|
700
|
+
* @returns agent.json representation with name, version, and tools
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```typescript
|
|
704
|
+
* // ability.ts (alongside agent.json file)
|
|
705
|
+
* const client = new KadiClient({ name: 'calculator', version: '1.0.0' });
|
|
706
|
+
* client.registerTool({ name: 'add', ... }, handler);
|
|
707
|
+
* export default client;
|
|
708
|
+
*
|
|
709
|
+
* // When loaded natively:
|
|
710
|
+
* const agentJson = client.readAgentJson();
|
|
711
|
+
* // { name: 'calculator', version: '1.0.0', tools: [...] }
|
|
712
|
+
* // ↑ Represents agent.json's runtime structure
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
readAgentJson() {
|
|
716
|
+
return {
|
|
717
|
+
name: this.config.name,
|
|
718
|
+
version: this.config.version,
|
|
719
|
+
description: this.config.description,
|
|
720
|
+
tools: this.tools.extractDefinitions()
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Invoke a tool by name
|
|
725
|
+
*
|
|
726
|
+
* Enables KadiClient to be used as an ability module. When loaded natively,
|
|
727
|
+
* this method is called by NativeTransport to execute registered tools.
|
|
728
|
+
*
|
|
729
|
+
* **Write Once, Run Anywhere:**
|
|
730
|
+
* The same tool handler works whether the ability is loaded natively,
|
|
731
|
+
* via stdio, or through a broker.
|
|
732
|
+
*
|
|
733
|
+
* @template TInput - Tool input type
|
|
734
|
+
* @template TOutput - Tool output type
|
|
735
|
+
* @param toolName - Name of the tool to invoke
|
|
736
|
+
* @param params - Tool input parameters
|
|
737
|
+
* @returns Promise resolving to tool result
|
|
738
|
+
*
|
|
739
|
+
* @throws {KadiError} If tool not found or invocation fails
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* // Registered tool:
|
|
744
|
+
* client.registerTool({ name: 'add', ... }, ({ a, b }) => ({ result: a + b }));
|
|
745
|
+
*
|
|
746
|
+
* // Invoke via agent.json protocol (native loading):
|
|
747
|
+
* const result = await client.invoke('add', { a: 5, b: 3 });
|
|
748
|
+
* // { result: 8 }
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
async invoke(toolName, params) {
|
|
752
|
+
// Look up the tool handler
|
|
753
|
+
const handler = this.tools.getHandler(toolName);
|
|
754
|
+
if (!handler) {
|
|
755
|
+
throw new KadiError(`Tool '${toolName}' not found`, ErrorCode.TOOL_NOT_FOUND, 404, {
|
|
756
|
+
toolName,
|
|
757
|
+
availableTools: this.tools.extractDefinitions().map(t => t.name),
|
|
758
|
+
suggestion: 'Check tool name or register tool first'
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
// Execute the handler
|
|
763
|
+
const result = await handler(params);
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
// Enhance error with context
|
|
768
|
+
if (error instanceof KadiError) {
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
throw new KadiError(`Tool invocation failed: ${error instanceof Error ? error.message : String(error)}`, ErrorCode.TOOL_INVOCATION_FAILED, 500, {
|
|
772
|
+
toolName,
|
|
773
|
+
input: params,
|
|
774
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Get broker manager (implements IBrokerClient)
|
|
780
|
+
*/
|
|
781
|
+
getBrokerManager() {
|
|
782
|
+
return this.brokers;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Get broker protocol instance (implements IBrokerClient)
|
|
786
|
+
*
|
|
787
|
+
* @param brokerName - Optional broker name (uses default if not specified)
|
|
788
|
+
*/
|
|
789
|
+
getBrokerProtocol(brokerName) {
|
|
790
|
+
const name = brokerName ?? this.brokers.getDefaultBrokerName();
|
|
791
|
+
if (!name) {
|
|
792
|
+
throw new KadiError('No broker available', ErrorCode.BROKER_NOT_CONNECTED, 503);
|
|
793
|
+
}
|
|
794
|
+
const protocol = this.protocols.get(name);
|
|
795
|
+
if (!protocol) {
|
|
796
|
+
throw new KadiError(`Broker protocol not initialized for '${name}'`, ErrorCode.BROKER_NOT_CONNECTED, 503, { brokerName: name });
|
|
797
|
+
}
|
|
798
|
+
return protocol;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Networks (implements IBrokerClient)
|
|
802
|
+
*/
|
|
803
|
+
get networks() {
|
|
804
|
+
return this.config.networks;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Create minimal configuration for constructor
|
|
808
|
+
* Full resolution happens in connect()
|
|
809
|
+
*/
|
|
810
|
+
createMinimalConfig(config) {
|
|
811
|
+
const normalized = typeof config === 'string'
|
|
812
|
+
? { brokers: { default: config } }
|
|
813
|
+
: config;
|
|
814
|
+
return {
|
|
815
|
+
name: normalized.name ?? 'unnamed-client',
|
|
816
|
+
version: normalized.version ?? '1.0.0',
|
|
817
|
+
description: normalized.description ?? '',
|
|
818
|
+
role: normalized.role ?? 'agent',
|
|
819
|
+
transport: normalized.transport ?? 'native',
|
|
820
|
+
brokers: normalized.brokers ?? { default: '' },
|
|
821
|
+
defaultBroker: normalized.defaultBroker ?? 'default',
|
|
822
|
+
networks: normalized.networks ?? ['global'],
|
|
823
|
+
abilityAgentJSON: normalized.abilityAgentJSON ?? '',
|
|
824
|
+
autoConnect: normalized.autoConnect ?? false,
|
|
825
|
+
advanced: {
|
|
826
|
+
heartbeatInterval: 30000,
|
|
827
|
+
requestTimeout: 30000,
|
|
828
|
+
connectionTimeout: 10000,
|
|
829
|
+
autoReconnect: true,
|
|
830
|
+
maxReconnectAttempts: 5,
|
|
831
|
+
verbose: false
|
|
832
|
+
},
|
|
833
|
+
sources: {}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Setup broker event forwarding
|
|
838
|
+
*/
|
|
839
|
+
setupBrokerEventForwarding() {
|
|
840
|
+
this.brokers.on('brokerMessage', async (brokerName, message) => {
|
|
841
|
+
if (!message || typeof message !== 'object' || !('method' in message)) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const msg = message;
|
|
845
|
+
// Handle incoming tool invocation requests
|
|
846
|
+
if (msg.method === KadiMessages.ABILITY_REQUEST && msg.params) {
|
|
847
|
+
await this.handleIncomingToolRequest(brokerName, msg);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
// Handle event deliveries
|
|
851
|
+
if (msg.method === KadiMessages.EVENT_DELIVERY && msg.params) {
|
|
852
|
+
const { channel, data } = msg.params;
|
|
853
|
+
if (channel && data !== undefined) {
|
|
854
|
+
this.events.publish(channel, data);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Handle incoming tool invocation requests from broker
|
|
861
|
+
*
|
|
862
|
+
* Flow:
|
|
863
|
+
* 1. Extract id from top-level message (JSON-RPC request id)
|
|
864
|
+
* 2. Look up tool handler by name
|
|
865
|
+
* 3. Execute handler with toolInput
|
|
866
|
+
* 4. Send JSON-RPC response back to broker with matching id
|
|
867
|
+
*
|
|
868
|
+
* @param brokerName - Name of the broker connection to use for response
|
|
869
|
+
* @param message - Full JSON-RPC request message with id, method, and params
|
|
870
|
+
* @private
|
|
871
|
+
*/
|
|
872
|
+
async handleIncomingToolRequest(brokerName, message) {
|
|
873
|
+
const { toolName, toolInput } = message.params || {};
|
|
874
|
+
const requestId = message.id;
|
|
875
|
+
try {
|
|
876
|
+
// Step 1: Look up the tool handler
|
|
877
|
+
const handler = this.tools.getHandler(toolName);
|
|
878
|
+
if (!handler) {
|
|
879
|
+
throw new KadiError(`Tool '${toolName}' not found`, ErrorCode.TOOL_NOT_FOUND, 404, { toolName });
|
|
880
|
+
}
|
|
881
|
+
// Step 2: Execute the handler
|
|
882
|
+
const result = await handler(toolInput);
|
|
883
|
+
// Step 3: Send JSON-RPC response back to broker
|
|
884
|
+
// CRITICAL: id must match the incoming request id for proper routing
|
|
885
|
+
const connection = this.brokers.getConnection(brokerName);
|
|
886
|
+
if (!connection) {
|
|
887
|
+
console.error(`[DEBUG] No connection found for broker: ${brokerName}`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
connection.sendResponse(requestId, result);
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
// Send JSON-RPC error response back to broker with same id for proper routing
|
|
894
|
+
const connection = this.brokers.getConnection(brokerName);
|
|
895
|
+
if (connection) {
|
|
896
|
+
const kadiError = error instanceof KadiError ? error : KadiError.from(error);
|
|
897
|
+
connection.sendError(requestId, kadiError.code, kadiError.message);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
//# sourceMappingURL=KadiClient.js.map
|