@kadi.build/core 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * - Explicit over implicit (no proxy magic)
15
15
  * - Readable, traceable code flow
16
16
  */
17
- import type { ClientConfig, BrokerState, ToolDefinition, ZodToolDefinition, ToolHandler, LoadedAbility, RegisterToolOptions, LoadNativeOptions, LoadStdioOptions, LoadBrokerOptions, InvokeRemoteOptions, RequestContext, BrokerEventHandler, PublishOptions, SubscribeOptions, EmitOptions } from './types.js';
17
+ import type { ClientConfig, BrokerState, ToolDefinition, ZodToolDefinition, ToolHandler, LoadedAbility, ToolExecutionBridge, RegisterToolOptions, LoadNativeOptions, LoadStdioOptions, LoadBrokerOptions, InvokeRemoteOptions, BrokerEventHandler, PublishOptions, SubscribeOptions, EmitOptions } from './types.js';
18
18
  /**
19
19
  * The main client for building KADI agents.
20
20
  *
@@ -112,33 +112,8 @@ export declare class KadiClient {
112
112
  private registerWithBroker;
113
113
  /**
114
114
  * Send a JSON-RPC request and wait for the response.
115
- *
116
- * Design: Returns the result directly (not the full JSON-RPC response).
117
- *
118
- * Why? The JSON-RPC envelope (jsonrpc, id) is transport metadata.
119
- * Once we've matched the response to the pending request, that metadata
120
- * has served its purpose. Callers care about the result, not the envelope.
121
- *
122
- * Errors are handled via Promise rejection in handleBrokerResponse,
123
- * so by the time this resolves, we know it's a successful response.
124
- *
125
- * IMPORTANT: The type parameter T is NOT validated at runtime.
126
- * It provides TypeScript type hints but the broker could return any shape.
127
- * Callers MUST validate critical fields before using them.
128
- *
129
- * @template T - Expected type of the result (defaults to unknown for safety)
130
- * @param state - Broker connection state
131
- * @param request - JSON-RPC request to send
132
- * @returns Promise resolving to the typed result
133
- *
134
- * @example
135
- * ```typescript
136
- * // Caller specifies expected result type
137
- * const result = await this.sendRequest<{ nonce: string }>(state, helloMessage);
138
- * // IMPORTANT: Validate before using - the type is not runtime-enforced
139
- * if (!result.nonce) throw new Error('Missing nonce');
140
- * console.log(result.nonce);
141
- * ```
115
+ * Returns the result directly (not the JSON-RPC envelope).
116
+ * Note: Type parameter T is not validated at runtime.
142
117
  */
143
118
  private sendRequest;
144
119
  /**
@@ -152,30 +127,19 @@ export declare class KadiClient {
152
127
  * 1. Client sends kadi.ability.request → gets { status: 'pending', requestId }
153
128
  * 2. Provider executes tool and sends result back to broker
154
129
  * 3. Broker sends this notification with the actual result
155
- *
156
- * The notification contains:
157
- * - requestId: matches what we got in step 1
158
- * - result: the tool's return value (if successful)
159
- * - error: error message (if failed)
160
130
  */
161
131
  private handleAbilityResponse;
132
+ /**
133
+ * Resolve or reject a pending invocation based on response content.
134
+ */
135
+ private resolveAbilityResponse;
136
+ /**
137
+ * Get a connected broker's state, throwing if not available.
138
+ */
139
+ private getConnectedBrokerState;
162
140
  /**
163
141
  * Handle kadi.event.delivery notification from broker.
164
- *
165
- * When an event is published to a channel that matches one of our subscribed
166
- * patterns, the broker sends us this notification with the event data.
167
- *
168
- * Flow:
169
- * 1. Some agent calls publish('user.login', data)
170
- * 2. Broker routes to all subscribers matching 'user.*' or 'user.#' etc.
171
- * 3. We receive this notification and dispatch to local handlers
172
- *
173
- * The notification params contain:
174
- * - channel: The exact channel name (e.g., 'user.login')
175
- * - data: The event payload
176
- * - networkId: Which network the event was published to
177
- * - source: Session ID of the publisher
178
- * - timestamp: When the event was published
142
+ * Dispatches to local handlers matching the event channel.
179
143
  */
180
144
  private handleEventDelivery;
181
145
  /**
@@ -405,8 +369,6 @@ export declare class KadiClient {
405
369
  * ```
406
370
  */
407
371
  registerTool<TInput, TOutput>(definition: ZodToolDefinition<TInput, TOutput>, handler: ToolHandler<TInput, TOutput>, options?: RegisterToolOptions): void;
408
- /**
409
- */
410
372
  /**
411
373
  * Get tool definitions, optionally filtered for a specific broker.
412
374
  *
@@ -415,13 +377,33 @@ export declare class KadiClient {
415
377
  */
416
378
  private getToolDefinitions;
417
379
  /**
418
- * Invoke a local tool by name.
380
+ * Execute a tool registered on this client.
381
+ *
382
+ * This is a PRIVATE method - not for external use.
383
+ * It handles INCOMING tool calls from:
384
+ * - Broker (when another agent calls your tools)
385
+ * - Stdio (when running as an ability)
386
+ * - Native transport (when loaded in-process)
419
387
  *
420
- * @param toolName - Name of the tool to invoke
388
+ * To call tools on OTHER agents, use:
389
+ * - `loadBroker(name).invoke(tool, params)` - for repeated calls to the same agent
390
+ * - `invokeRemote(tool, params, { timeout })` - for one-off calls with custom timeout
391
+ *
392
+ * @param toolName - Name of the registered tool to execute
421
393
  * @param params - Input parameters for the tool
422
- * @param context - Optional request context (provided when invoked via broker)
394
+ * @param context - Request context with caller info (provided by broker/stdio)
395
+ * @internal
396
+ */
397
+ private executeToolHandler;
398
+ /**
399
+ * Create a bridge for internal transport use.
400
+ *
401
+ * This allows native transport to call tools on this client without
402
+ * exposing the full client interface.
403
+ *
404
+ * @internal - Not for external use
423
405
  */
424
- invoke(toolName: string, params: unknown, context?: RequestContext): Promise<unknown>;
406
+ createToolBridge(): ToolExecutionBridge;
425
407
  /**
426
408
  * Load an in-process ability via dynamic import.
427
409
  *
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -26,7 +26,7 @@ import { loadBrokerTransport } from './transports/broker.js';
26
26
  // CONSTANTS
27
27
  // ═══════════════════════════════════════════════════════════════
28
28
  const DEFAULT_HEARTBEAT_INTERVAL = 25000; // 25 seconds
29
- const DEFAULT_REQUEST_TIMEOUT = 30000; // 30 seconds
29
+ const DEFAULT_REQUEST_TIMEOUT = 600000; // 10 minutes
30
30
  const DEFAULT_AUTO_RECONNECT = true;
31
31
  const DEFAULT_MAX_RECONNECT_DELAY = 30000; // 30 seconds cap
32
32
  const RECONNECT_BASE_DELAY = 1000; // 1 second initial delay
@@ -424,33 +424,8 @@ export class KadiClient {
424
424
  }
425
425
  /**
426
426
  * Send a JSON-RPC request and wait for the response.
427
- *
428
- * Design: Returns the result directly (not the full JSON-RPC response).
429
- *
430
- * Why? The JSON-RPC envelope (jsonrpc, id) is transport metadata.
431
- * Once we've matched the response to the pending request, that metadata
432
- * has served its purpose. Callers care about the result, not the envelope.
433
- *
434
- * Errors are handled via Promise rejection in handleBrokerResponse,
435
- * so by the time this resolves, we know it's a successful response.
436
- *
437
- * IMPORTANT: The type parameter T is NOT validated at runtime.
438
- * It provides TypeScript type hints but the broker could return any shape.
439
- * Callers MUST validate critical fields before using them.
440
- *
441
- * @template T - Expected type of the result (defaults to unknown for safety)
442
- * @param state - Broker connection state
443
- * @param request - JSON-RPC request to send
444
- * @returns Promise resolving to the typed result
445
- *
446
- * @example
447
- * ```typescript
448
- * // Caller specifies expected result type
449
- * const result = await this.sendRequest<{ nonce: string }>(state, helloMessage);
450
- * // IMPORTANT: Validate before using - the type is not runtime-enforced
451
- * if (!result.nonce) throw new Error('Missing nonce');
452
- * console.log(result.nonce);
453
- * ```
427
+ * Returns the result directly (not the JSON-RPC envelope).
428
+ * Note: Type parameter T is not validated at runtime.
454
429
  */
455
430
  sendRequest(state, request) {
456
431
  // Fail fast if WebSocket is not connected
@@ -528,66 +503,64 @@ export class KadiClient {
528
503
  * 1. Client sends kadi.ability.request → gets { status: 'pending', requestId }
529
504
  * 2. Provider executes tool and sends result back to broker
530
505
  * 3. Broker sends this notification with the actual result
531
- *
532
- * The notification contains:
533
- * - requestId: matches what we got in step 1
534
- * - result: the tool's return value (if successful)
535
- * - error: error message (if failed)
536
506
  */
537
507
  handleAbilityResponse(state, notification) {
538
- // The error field can be either a string or an object with code/message.
539
- // We accept both forms and normalize for consistent error handling.
540
508
  const params = notification.params;
541
- // Validate notification has requestId
542
509
  if (!params?.requestId) {
543
- // Malformed notification - ignore
544
510
  return;
545
511
  }
546
- // Find pending invocation
547
512
  const pending = state.pendingInvocations.get(params.requestId);
548
513
  if (!pending) {
549
- // No one waiting for this - might be stale or already timed out
514
+ // No listener - response is orphaned (timed out or unknown requestId)
550
515
  return;
551
516
  }
552
- // Clean up: remove from pending and clear timeout
553
517
  clearTimeout(pending.timeout);
554
518
  state.pendingInvocations.delete(params.requestId);
555
- // Resolve or reject based on response content
556
- if (params.error) {
557
- // Normalize error - could be string or { code, message } object
558
- const errorMessage = typeof params.error === 'string' ? params.error : params.error.message;
559
- const errorCode = typeof params.error === 'object' ? params.error.code : undefined;
519
+ this.resolveAbilityResponse(pending, params, state.name);
520
+ }
521
+ /**
522
+ * Resolve or reject a pending invocation based on response content.
523
+ */
524
+ resolveAbilityResponse(pending, response, brokerName) {
525
+ if (response.error) {
526
+ const errorMessage = typeof response.error === 'string' ? response.error : response.error.message;
527
+ const errorCode = typeof response.error === 'object' ? response.error.code : undefined;
560
528
  pending.reject(new KadiError(`Tool "${pending.toolName}" failed: ${errorMessage}`, 'TOOL_INVOCATION_FAILED', {
561
529
  toolName: pending.toolName,
562
- requestId: params.requestId,
563
- broker: state.name,
564
- errorCode, // Include error code if available
530
+ broker: brokerName,
531
+ errorCode,
565
532
  }));
566
533
  }
567
534
  else {
568
- pending.resolve(params.result);
535
+ pending.resolve(response.result);
569
536
  }
570
537
  }
538
+ /**
539
+ * Get a connected broker's state, throwing if not available.
540
+ */
541
+ getConnectedBrokerState(optionsBroker) {
542
+ const brokerName = optionsBroker ?? this.config.defaultBroker;
543
+ if (!brokerName) {
544
+ throw new KadiError('No broker specified and no defaultBroker configured', 'INVALID_CONFIG', {
545
+ hint: 'Either specify a broker in options or set defaultBroker in config',
546
+ });
547
+ }
548
+ const state = this.brokers.get(brokerName);
549
+ if (!state || state.status !== 'connected') {
550
+ throw new KadiError(`Broker "${brokerName}" is not connected`, 'BROKER_NOT_CONNECTED', {
551
+ broker: brokerName,
552
+ status: state?.status ?? 'not found',
553
+ hint: 'Call client.connect() first',
554
+ });
555
+ }
556
+ return { state, brokerName };
557
+ }
571
558
  // ═══════════════════════════════════════════════════════════════
572
559
  // BROKER EVENTS (Pub/Sub)
573
560
  // ═══════════════════════════════════════════════════════════════
574
561
  /**
575
562
  * Handle kadi.event.delivery notification from broker.
576
- *
577
- * When an event is published to a channel that matches one of our subscribed
578
- * patterns, the broker sends us this notification with the event data.
579
- *
580
- * Flow:
581
- * 1. Some agent calls publish('user.login', data)
582
- * 2. Broker routes to all subscribers matching 'user.*' or 'user.#' etc.
583
- * 3. We receive this notification and dispatch to local handlers
584
- *
585
- * The notification params contain:
586
- * - channel: The exact channel name (e.g., 'user.login')
587
- * - data: The event payload
588
- * - networkId: Which network the event was published to
589
- * - source: Session ID of the publisher
590
- * - timestamp: When the event was published
563
+ * Dispatches to local handlers matching the event channel.
591
564
  */
592
565
  handleEventDelivery(state, notification) {
593
566
  const params = notification.params;
@@ -706,22 +679,7 @@ export class KadiClient {
706
679
  * ```
707
680
  */
708
681
  async subscribe(pattern, handler, options = {}) {
709
- // Resolve which broker to use
710
- const brokerName = options.broker ?? this.config.defaultBroker;
711
- if (!brokerName) {
712
- throw new KadiError('No broker specified and no defaultBroker configured', 'INVALID_CONFIG', {
713
- hint: 'Either specify a broker in options or set defaultBroker in config',
714
- });
715
- }
716
- // Get broker state
717
- const state = this.brokers.get(brokerName);
718
- if (!state || state.status !== 'connected') {
719
- throw new KadiError(`Broker "${brokerName}" is not connected`, 'BROKER_NOT_CONNECTED', {
720
- broker: brokerName,
721
- status: state?.status ?? 'not found',
722
- hint: 'Call client.connect() first',
723
- });
724
- }
682
+ const { state } = this.getConnectedBrokerState(options.broker);
725
683
  // Add handler to local tracking
726
684
  let handlers = state.eventHandlers.get(pattern);
727
685
  if (!handlers) {
@@ -817,22 +775,7 @@ export class KadiClient {
817
775
  * ```
818
776
  */
819
777
  async publish(channel, data, options = {}) {
820
- // Resolve which broker to use
821
- const brokerName = options.broker ?? this.config.defaultBroker;
822
- if (!brokerName) {
823
- throw new KadiError('No broker specified and no defaultBroker configured', 'INVALID_CONFIG', {
824
- hint: 'Either specify a broker in options or set defaultBroker in config',
825
- });
826
- }
827
- // Get broker state
828
- const state = this.brokers.get(brokerName);
829
- if (!state || state.status !== 'connected') {
830
- throw new KadiError(`Broker "${brokerName}" is not connected`, 'BROKER_NOT_CONNECTED', {
831
- broker: brokerName,
832
- status: state?.status ?? 'not found',
833
- hint: 'Call client.connect() first',
834
- });
835
- }
778
+ const { state } = this.getConnectedBrokerState(options.broker);
836
779
  // Resolve which network to publish to
837
780
  const networkId = options.network ?? this.config.networks[0] ?? 'global';
838
781
  // Send publish request to broker
@@ -885,7 +828,7 @@ export class KadiClient {
885
828
  */
886
829
  async handleInvokeRequest(state, requestId, toolName, toolInput, context) {
887
830
  try {
888
- const result = await this.invoke(toolName, toolInput, context);
831
+ const result = await this.executeToolHandler(toolName, toolInput, context);
889
832
  // Format response based on caller protocol
890
833
  let responseResult;
891
834
  if (context.callerProtocol === 'mcp') {
@@ -1269,8 +1212,6 @@ export class KadiClient {
1269
1212
  };
1270
1213
  this.tools.set(definition.name, registered);
1271
1214
  }
1272
- /**
1273
- */
1274
1215
  /**
1275
1216
  * Get tool definitions, optionally filtered for a specific broker.
1276
1217
  *
@@ -1292,13 +1233,24 @@ export class KadiClient {
1292
1233
  .map((t) => t.definition);
1293
1234
  }
1294
1235
  /**
1295
- * Invoke a local tool by name.
1236
+ * Execute a tool registered on this client.
1237
+ *
1238
+ * This is a PRIVATE method - not for external use.
1239
+ * It handles INCOMING tool calls from:
1240
+ * - Broker (when another agent calls your tools)
1241
+ * - Stdio (when running as an ability)
1242
+ * - Native transport (when loaded in-process)
1296
1243
  *
1297
- * @param toolName - Name of the tool to invoke
1244
+ * To call tools on OTHER agents, use:
1245
+ * - `loadBroker(name).invoke(tool, params)` - for repeated calls to the same agent
1246
+ * - `invokeRemote(tool, params, { timeout })` - for one-off calls with custom timeout
1247
+ *
1248
+ * @param toolName - Name of the registered tool to execute
1298
1249
  * @param params - Input parameters for the tool
1299
- * @param context - Optional request context (provided when invoked via broker)
1250
+ * @param context - Request context with caller info (provided by broker/stdio)
1251
+ * @internal
1300
1252
  */
1301
- async invoke(toolName, params, context) {
1253
+ async executeToolHandler(toolName, params, context) {
1302
1254
  const tool = this.tools.get(toolName);
1303
1255
  if (!tool) {
1304
1256
  throw new KadiError(`Tool "${toolName}" not found`, 'TOOL_NOT_FOUND', {
@@ -1307,8 +1259,26 @@ export class KadiClient {
1307
1259
  hint: 'Register the tool first with registerTool()',
1308
1260
  });
1309
1261
  }
1262
+ // TODO: Validate params against tool.definition.input schema before invoking.
1263
+ // Currently the schema is only used for discovery (kadi.ability.list) and TypeScript types.
1264
+ // Tool handlers must do their own validation, which defeats the purpose of requiring a schema.
1265
+ // Should call tool.definition.input.safeParse(params) and throw on validation errors.
1310
1266
  return tool.handler(params, context);
1311
1267
  }
1268
+ /**
1269
+ * Create a bridge for internal transport use.
1270
+ *
1271
+ * This allows native transport to call tools on this client without
1272
+ * exposing the full client interface.
1273
+ *
1274
+ * @internal - Not for external use
1275
+ */
1276
+ createToolBridge() {
1277
+ return {
1278
+ executeToolHandler: (toolName, params, context) => this.executeToolHandler(toolName, params, context),
1279
+ getRegisteredTools: () => Array.from(this.tools.values()).map((t) => t.definition),
1280
+ };
1281
+ }
1312
1282
  // ─────────────────────────────────────────────────────────────
1313
1283
  // ABILITY LOADING
1314
1284
  // ─────────────────────────────────────────────────────────────
@@ -1339,7 +1309,9 @@ export class KadiClient {
1339
1309
  path = entry.absolutePath;
1340
1310
  entrypoint = entry.entrypoint;
1341
1311
  }
1342
- return loadNativeTransport(path, entrypoint);
1312
+ return loadNativeTransport(path, entrypoint, {
1313
+ timeout: options.timeout ?? this.config.requestTimeout,
1314
+ });
1343
1315
  }
1344
1316
  /**
1345
1317
  * Load a child process ability via stdio.
@@ -1376,7 +1348,7 @@ export class KadiClient {
1376
1348
  // ─────────────────────────────────────────────────────────────────────────
1377
1349
  if (options.command) {
1378
1350
  return loadStdioTransport(options.command, options.args ?? [], {
1379
- timeoutMs: this.config.requestTimeout,
1351
+ timeoutMs: options.timeout ?? this.config.requestTimeout,
1380
1352
  });
1381
1353
  }
1382
1354
  // ─────────────────────────────────────────────────────────────────────────
@@ -1384,7 +1356,7 @@ export class KadiClient {
1384
1356
  // ─────────────────────────────────────────────────────────────────────────
1385
1357
  const { command, args, cwd } = await resolveAbilityScript(name, options.script ?? 'start', options.projectRoot);
1386
1358
  return loadStdioTransport(command, args, {
1387
- timeoutMs: this.config.requestTimeout,
1359
+ timeoutMs: options.timeout ?? this.config.requestTimeout,
1388
1360
  cwd, // Run in ability's directory so relative paths work
1389
1361
  });
1390
1362
  }
@@ -1404,22 +1376,10 @@ export class KadiClient {
1404
1376
  * ```
1405
1377
  */
1406
1378
  async loadBroker(name, options = {}) {
1407
- const brokerName = options.broker ?? this.config.defaultBroker;
1408
- if (!brokerName) {
1409
- throw new KadiError('No broker specified and no defaultBroker configured', 'INVALID_CONFIG', {
1410
- hint: 'Either specify a broker in options or set defaultBroker in config',
1411
- });
1412
- }
1413
- const state = this.brokers.get(brokerName);
1414
- if (!state || state.status !== 'connected') {
1415
- throw new KadiError(`Broker "${brokerName}" is not connected`, 'BROKER_NOT_CONNECTED', {
1416
- broker: brokerName,
1417
- hint: 'Call client.connect() first',
1418
- });
1419
- }
1379
+ const { state, brokerName } = this.getConnectedBrokerState(options.broker);
1420
1380
  return loadBrokerTransport(name, {
1421
1381
  broker: state,
1422
- requestTimeout: this.config.requestTimeout,
1382
+ requestTimeout: options.timeout ?? this.config.requestTimeout,
1423
1383
  networks: options.networks,
1424
1384
  // Provide subscribe/unsubscribe for ability.on()/off() support
1425
1385
  subscribe: (pattern, handler) => this.subscribe(pattern, handler, { broker: brokerName }),
@@ -1454,49 +1414,17 @@ export class KadiClient {
1454
1414
  * ```
1455
1415
  */
1456
1416
  async invokeRemote(toolName, params, options = {}) {
1457
- // ─────────────────────────────────────────────────────────────
1458
- // VALIDATION
1459
- // ─────────────────────────────────────────────────────────────
1460
- const brokerName = options.broker ?? this.config.defaultBroker;
1461
- if (!brokerName) {
1462
- throw new KadiError('No broker specified and no defaultBroker configured', 'INVALID_CONFIG', {
1463
- hint: 'Either specify a broker in options or set defaultBroker in config',
1464
- });
1465
- }
1466
- const state = this.brokers.get(brokerName);
1467
- if (!state || state.status !== 'connected') {
1468
- throw new KadiError(`Broker "${brokerName}" is not connected`, 'BROKER_NOT_CONNECTED', {
1469
- broker: brokerName,
1470
- hint: 'Call client.connect() first',
1471
- });
1472
- }
1417
+ const { state, brokerName } = this.getConnectedBrokerState(options.broker);
1473
1418
  const timeout = options.timeout ?? this.config.requestTimeout;
1474
- // ─────────────────────────────────────────────────────────────
1475
- // PHASE 1: Send request, get "pending" acknowledgment
1476
- // ─────────────────────────────────────────────────────────────
1477
- // Use existing sendRequest() for the request/response part.
1478
- // This handles the JSON-RPC id matching for us.
1479
- const pendingResult = await this.sendRequest(state, {
1480
- jsonrpc: '2.0',
1481
- id: ++this.nextRequestId,
1482
- method: 'kadi.ability.request',
1483
- params: { toolName, toolInput: params },
1484
- });
1485
- // Validate the pending response
1486
- if (pendingResult.status !== 'pending' || !pendingResult.requestId) {
1487
- throw new KadiError('Unexpected response from broker: expected pending acknowledgment', 'BROKER_ERROR', {
1488
- broker: brokerName,
1489
- toolName,
1490
- response: pendingResult,
1491
- });
1492
- }
1493
- const requestId = pendingResult.requestId;
1494
- // ─────────────────────────────────────────────────────────────
1495
- // PHASE 2: Wait for kadi.ability.response notification
1496
- // ─────────────────────────────────────────────────────────────
1497
- // The actual result comes as a notification (no id) with our requestId.
1498
- // We store in pendingInvocations and handleAbilityResponse() will resolve.
1499
- return new Promise((resolve, reject) => {
1419
+ // Generate requestId FIRST, before any async operations.
1420
+ // This allows us to set up the response listener before sending,
1421
+ // eliminating the race condition where fast responses arrive
1422
+ // before the listener exists.
1423
+ const requestId = crypto.randomUUID();
1424
+ // Set up result listener BEFORE sending the request.
1425
+ // When the broker sends kadi.ability.response, handleAbilityResponse()
1426
+ // will find this listener and resolve/reject the promise.
1427
+ const resultPromise = new Promise((resolve, reject) => {
1500
1428
  const timeoutHandle = setTimeout(() => {
1501
1429
  state.pendingInvocations.delete(requestId);
1502
1430
  reject(new KadiError(`Tool invocation "${toolName}" timed out waiting for result`, 'BROKER_TIMEOUT', {
@@ -1514,6 +1442,33 @@ export class KadiClient {
1514
1442
  sentAt: new Date(),
1515
1443
  });
1516
1444
  });
1445
+ // Helper to clean up the pending invocation on failure
1446
+ const cleanupPendingInvocation = () => {
1447
+ const pending = state.pendingInvocations.get(requestId);
1448
+ if (pending) {
1449
+ clearTimeout(pending.timeout);
1450
+ state.pendingInvocations.delete(requestId);
1451
+ }
1452
+ };
1453
+ // NOW send the request (listener already exists, no race possible)
1454
+ try {
1455
+ const pendingResult = await this.sendRequest(state, {
1456
+ jsonrpc: '2.0',
1457
+ id: ++this.nextRequestId,
1458
+ method: 'kadi.ability.request',
1459
+ params: { toolName, toolInput: params, requestId },
1460
+ });
1461
+ // Validate the broker accepted the request
1462
+ if (pendingResult.status !== 'pending') {
1463
+ cleanupPendingInvocation();
1464
+ throw new KadiError('Unexpected response from broker: expected pending acknowledgment', 'BROKER_ERROR', { broker: brokerName, toolName, response: pendingResult });
1465
+ }
1466
+ }
1467
+ catch (error) {
1468
+ cleanupPendingInvocation();
1469
+ throw error;
1470
+ }
1471
+ return resultPromise;
1517
1472
  }
1518
1473
  // ─────────────────────────────────────────────────────────────
1519
1474
  // SERVE MODE
@@ -1629,7 +1584,7 @@ export class KadiClient {
1629
1584
  }
1630
1585
  else if (message.method === 'invoke') {
1631
1586
  const params = message.params;
1632
- result = await this.invoke(params.toolName, params.toolInput);
1587
+ result = await this.executeToolHandler(params.toolName, params.toolInput);
1633
1588
  }
1634
1589
  else if (message.method === 'shutdown') {
1635
1590
  // Graceful shutdown - send response first, then cleanup
@@ -1735,13 +1690,9 @@ export class KadiClient {
1735
1690
  * Get list of connected broker names.
1736
1691
  */
1737
1692
  getConnectedBrokers() {
1738
- const connected = [];
1739
- for (const [name, state] of this.brokers) {
1740
- if (state.status === 'connected') {
1741
- connected.push(name);
1742
- }
1743
- }
1744
- return connected;
1693
+ return Array.from(this.brokers.entries())
1694
+ .filter(([, state]) => state.status === 'connected')
1695
+ .map(([name]) => name);
1745
1696
  }
1746
1697
  }
1747
1698
  //# sourceMappingURL=client.js.map