@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 +36 -54
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +121 -170
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/transports/broker.d.ts +1 -1
- package/dist/transports/broker.d.ts.map +1 -1
- package/dist/transports/broker.js +64 -36
- package/dist/transports/broker.js.map +1 -1
- package/dist/transports/native.d.ts +6 -1
- package/dist/transports/native.d.ts.map +1 -1
- package/dist/transports/native.js +37 -7
- package/dist/transports/native.js.map +1 -1
- package/dist/transports/stdio.d.ts.map +1 -1
- package/dist/transports/stdio.js +14 -9
- package/dist/transports/stdio.js.map +1 -1
- package/dist/types.d.ts +106 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 -
|
|
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
|
-
|
|
406
|
+
createToolBridge(): ToolExecutionBridge;
|
|
425
407
|
/**
|
|
426
408
|
* Load an in-process ability via dynamic import.
|
|
427
409
|
*
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EACV,YAAY,EAEZ,WAAW,EAEX,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
errorCode, // Include error code if available
|
|
530
|
+
broker: brokerName,
|
|
531
|
+
errorCode,
|
|
565
532
|
}));
|
|
566
533
|
}
|
|
567
534
|
else {
|
|
568
|
-
pending.resolve(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 -
|
|
1250
|
+
* @param context - Request context with caller info (provided by broker/stdio)
|
|
1251
|
+
* @internal
|
|
1300
1252
|
*/
|
|
1301
|
-
async
|
|
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
|
|
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
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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.
|
|
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
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|