@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.
Files changed (199) hide show
  1. package/README.md +361 -230
  2. package/dist/abilities/AbilityCache.d.ts +242 -0
  3. package/dist/abilities/AbilityCache.d.ts.map +1 -0
  4. package/dist/abilities/AbilityCache.js +285 -0
  5. package/dist/abilities/AbilityCache.js.map +1 -0
  6. package/dist/abilities/AbilityContext.d.ts +215 -0
  7. package/dist/abilities/AbilityContext.d.ts.map +1 -0
  8. package/dist/abilities/AbilityContext.js +36 -0
  9. package/dist/abilities/AbilityContext.js.map +1 -0
  10. package/dist/abilities/AbilityLoader.d.ts +203 -0
  11. package/dist/abilities/AbilityLoader.d.ts.map +1 -0
  12. package/dist/abilities/AbilityLoader.js +343 -0
  13. package/dist/abilities/AbilityLoader.js.map +1 -0
  14. package/dist/abilities/AbilityProxy.d.ts +496 -0
  15. package/dist/abilities/AbilityProxy.d.ts.map +1 -0
  16. package/dist/abilities/AbilityProxy.js +551 -0
  17. package/dist/abilities/AbilityProxy.js.map +1 -0
  18. package/dist/abilities/AbilityValidator.d.ts +172 -0
  19. package/dist/abilities/AbilityValidator.d.ts.map +1 -0
  20. package/dist/abilities/AbilityValidator.js +253 -0
  21. package/dist/abilities/AbilityValidator.js.map +1 -0
  22. package/dist/abilities/index.d.ts +26 -0
  23. package/dist/abilities/index.d.ts.map +1 -0
  24. package/dist/abilities/index.js +23 -0
  25. package/dist/abilities/index.js.map +1 -0
  26. package/dist/abilities/types.d.ts +223 -0
  27. package/dist/abilities/types.d.ts.map +1 -0
  28. package/dist/abilities/types.js +10 -0
  29. package/dist/abilities/types.js.map +1 -0
  30. package/dist/api/index.d.ts +92 -0
  31. package/dist/api/index.d.ts.map +1 -0
  32. package/dist/api/index.js +124 -0
  33. package/dist/api/index.js.map +1 -0
  34. package/dist/broker/BrokerConnection.d.ts +253 -0
  35. package/dist/broker/BrokerConnection.d.ts.map +1 -0
  36. package/dist/broker/BrokerConnection.js +434 -0
  37. package/dist/broker/BrokerConnection.js.map +1 -0
  38. package/dist/broker/BrokerConnectionManager.d.ts +216 -0
  39. package/dist/broker/BrokerConnectionManager.d.ts.map +1 -0
  40. package/dist/broker/BrokerConnectionManager.js +305 -0
  41. package/dist/broker/BrokerConnectionManager.js.map +1 -0
  42. package/dist/broker/BrokerProtocol.d.ts +280 -0
  43. package/dist/broker/BrokerProtocol.d.ts.map +1 -0
  44. package/dist/broker/BrokerProtocol.js +466 -0
  45. package/dist/broker/BrokerProtocol.js.map +1 -0
  46. package/dist/broker/index.d.ts +9 -0
  47. package/dist/broker/index.d.ts.map +1 -0
  48. package/dist/broker/index.js +9 -0
  49. package/dist/broker/index.js.map +1 -0
  50. package/dist/client/KadiClient.d.ts +459 -0
  51. package/dist/client/KadiClient.d.ts.map +1 -0
  52. package/dist/client/KadiClient.js +902 -0
  53. package/dist/client/KadiClient.js.map +1 -0
  54. package/dist/client/index.d.ts +7 -0
  55. package/dist/client/index.d.ts.map +1 -0
  56. package/dist/client/index.js +7 -0
  57. package/dist/client/index.js.map +1 -0
  58. package/dist/config/ConfigLoader.d.ts +138 -0
  59. package/dist/config/ConfigLoader.d.ts.map +1 -0
  60. package/dist/config/ConfigLoader.js +226 -0
  61. package/dist/config/ConfigLoader.js.map +1 -0
  62. package/dist/config/ConfigResolver.d.ts +135 -0
  63. package/dist/config/ConfigResolver.d.ts.map +1 -0
  64. package/dist/config/ConfigResolver.js +282 -0
  65. package/dist/config/ConfigResolver.js.map +1 -0
  66. package/dist/config/index.d.ts +8 -0
  67. package/dist/config/index.d.ts.map +1 -0
  68. package/dist/config/index.js +8 -0
  69. package/dist/config/index.js.map +1 -0
  70. package/dist/errors/index.d.ts +9 -0
  71. package/dist/errors/index.d.ts.map +1 -0
  72. package/dist/errors/index.js +8 -0
  73. package/dist/errors/index.js.map +1 -0
  74. package/dist/events/EventHub.d.ts +172 -0
  75. package/dist/events/EventHub.d.ts.map +1 -0
  76. package/dist/events/EventHub.js +333 -0
  77. package/dist/events/EventHub.js.map +1 -0
  78. package/dist/events/index.d.ts +7 -0
  79. package/dist/events/index.d.ts.map +1 -0
  80. package/dist/events/index.js +7 -0
  81. package/dist/events/index.js.map +1 -0
  82. package/dist/index.d.ts +50 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +67 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/messages/index.d.ts +33 -0
  87. package/dist/messages/index.d.ts.map +1 -0
  88. package/dist/messages/index.js +33 -0
  89. package/dist/messages/index.js.map +1 -0
  90. package/dist/schemas/index.d.ts +22 -0
  91. package/dist/schemas/index.d.ts.map +1 -0
  92. package/dist/schemas/index.js +27 -0
  93. package/dist/schemas/index.js.map +1 -0
  94. package/dist/schemas/kadi-extensions.d.ts +231 -0
  95. package/dist/schemas/kadi-extensions.d.ts.map +1 -0
  96. package/dist/schemas/kadi-extensions.js +14 -0
  97. package/dist/schemas/kadi-extensions.js.map +1 -0
  98. package/dist/schemas/mcp/schema.d.ts +1399 -0
  99. package/dist/schemas/mcp/schema.d.ts.map +1 -0
  100. package/dist/schemas/mcp/schema.js +53 -0
  101. package/dist/schemas/mcp/schema.js.map +1 -0
  102. package/dist/schemas/mcp/version.d.ts +37 -0
  103. package/dist/schemas/mcp/version.d.ts.map +1 -0
  104. package/dist/schemas/mcp/version.js +39 -0
  105. package/dist/schemas/mcp/version.js.map +1 -0
  106. package/dist/schemas/schema-builders.d.ts +178 -0
  107. package/dist/schemas/schema-builders.d.ts.map +1 -0
  108. package/dist/schemas/schema-builders.js +258 -0
  109. package/dist/schemas/schema-builders.js.map +1 -0
  110. package/dist/schemas/zod-helpers.d.ts +129 -0
  111. package/dist/schemas/zod-helpers.d.ts.map +1 -0
  112. package/dist/schemas/zod-helpers.js +225 -0
  113. package/dist/schemas/zod-helpers.js.map +1 -0
  114. package/dist/schemas/zod-to-json-schema.d.ts +159 -0
  115. package/dist/schemas/zod-to-json-schema.d.ts.map +1 -0
  116. package/dist/schemas/zod-to-json-schema.js +154 -0
  117. package/dist/schemas/zod-to-json-schema.js.map +1 -0
  118. package/dist/tools/ToolRegistry.d.ts +256 -0
  119. package/dist/tools/ToolRegistry.d.ts.map +1 -0
  120. package/dist/tools/ToolRegistry.js +340 -0
  121. package/dist/tools/ToolRegistry.js.map +1 -0
  122. package/dist/tools/index.d.ts +7 -0
  123. package/dist/tools/index.d.ts.map +1 -0
  124. package/dist/tools/index.js +7 -0
  125. package/dist/tools/index.js.map +1 -0
  126. package/dist/transports/BrokerTransport.d.ts +151 -0
  127. package/dist/transports/BrokerTransport.d.ts.map +1 -0
  128. package/dist/transports/BrokerTransport.js +261 -0
  129. package/dist/transports/BrokerTransport.js.map +1 -0
  130. package/dist/transports/NativeTransport.d.ts +178 -0
  131. package/dist/transports/NativeTransport.d.ts.map +1 -0
  132. package/dist/transports/NativeTransport.js +397 -0
  133. package/dist/transports/NativeTransport.js.map +1 -0
  134. package/dist/transports/StdioTransport.d.ts +250 -0
  135. package/dist/transports/StdioTransport.d.ts.map +1 -0
  136. package/dist/transports/StdioTransport.js +487 -0
  137. package/dist/transports/StdioTransport.js.map +1 -0
  138. package/dist/transports/index.d.ts +10 -0
  139. package/dist/transports/index.d.ts.map +1 -0
  140. package/dist/transports/index.js +9 -0
  141. package/dist/transports/index.js.map +1 -0
  142. package/dist/types/broker.d.ts +279 -0
  143. package/dist/types/broker.d.ts.map +1 -0
  144. package/dist/types/broker.js +19 -0
  145. package/dist/types/broker.js.map +1 -0
  146. package/dist/types/config.d.ts +325 -0
  147. package/dist/types/config.d.ts.map +1 -0
  148. package/dist/types/config.js +17 -0
  149. package/dist/types/config.js.map +1 -0
  150. package/dist/types/errors.d.ts +178 -0
  151. package/dist/types/errors.d.ts.map +1 -0
  152. package/dist/types/errors.js +165 -0
  153. package/dist/types/errors.js.map +1 -0
  154. package/dist/types/events.d.ts +210 -0
  155. package/dist/types/events.d.ts.map +1 -0
  156. package/dist/types/events.js +8 -0
  157. package/dist/types/events.js.map +1 -0
  158. package/dist/types/index.d.ts +34 -0
  159. package/dist/types/index.d.ts.map +1 -0
  160. package/dist/types/index.js +21 -0
  161. package/dist/types/index.js.map +1 -0
  162. package/dist/types/protocol.d.ts +48 -0
  163. package/dist/types/protocol.d.ts.map +1 -0
  164. package/dist/types/protocol.js +11 -0
  165. package/dist/types/protocol.js.map +1 -0
  166. package/dist/types/tools.d.ts +67 -0
  167. package/dist/types/tools.d.ts.map +1 -0
  168. package/dist/types/tools.js +16 -0
  169. package/dist/types/tools.js.map +1 -0
  170. package/dist/types/transport.d.ts +250 -0
  171. package/dist/types/transport.d.ts.map +1 -0
  172. package/dist/types/transport.js +18 -0
  173. package/dist/types/transport.js.map +1 -0
  174. package/dist/types/zod-tools.d.ts +198 -0
  175. package/dist/types/zod-tools.d.ts.map +1 -0
  176. package/dist/types/zod-tools.js +14 -0
  177. package/dist/types/zod-tools.js.map +1 -0
  178. package/dist/utils/StdioMessageReader.d.ts +122 -0
  179. package/dist/utils/StdioMessageReader.d.ts.map +1 -0
  180. package/dist/utils/StdioMessageReader.js +209 -0
  181. package/dist/utils/StdioMessageReader.js.map +1 -0
  182. package/dist/utils/StdioMessageWriter.d.ts +104 -0
  183. package/dist/utils/StdioMessageWriter.d.ts.map +1 -0
  184. package/dist/utils/StdioMessageWriter.js +162 -0
  185. package/dist/utils/StdioMessageWriter.js.map +1 -0
  186. package/dist/validation/SchemaValidator.d.ts +208 -0
  187. package/dist/validation/SchemaValidator.d.ts.map +1 -0
  188. package/dist/validation/SchemaValidator.js +411 -0
  189. package/dist/validation/SchemaValidator.js.map +1 -0
  190. package/dist/validation/index.d.ts +11 -0
  191. package/dist/validation/index.d.ts.map +1 -0
  192. package/dist/validation/index.js +10 -0
  193. package/dist/validation/index.js.map +1 -0
  194. package/package.json +70 -5
  195. package/agent.json +0 -18
  196. package/broker.js +0 -214
  197. package/index.js +0 -370
  198. package/ipc.js +0 -220
  199. 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