@kadi.build/core 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/client.d.ts +16 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +113 -142
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +109 -0
- package/dist/protocol.js.map +1 -0
- package/dist/transports/stdio.d.ts.map +1 -1
- package/dist/transports/stdio.js +2 -6
- package/dist/transports/stdio.js.map +1 -1
- package/dist/types.d.ts +61 -23
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +143 -145
- package/src/index.ts +7 -1
- package/src/protocol.ts +161 -0
- package/src/transports/stdio.ts +2 -6
- package/src/types.ts +58 -25
package/README.md
CHANGED
|
@@ -107,6 +107,59 @@ client.registerTool({
|
|
|
107
107
|
client.registerTool(definition, handler, { brokers: ['internal'] });
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
### Targeting Specific Networks
|
|
111
|
+
|
|
112
|
+
By default, a tool is visible on **all networks** the agent has joined. You can restrict a tool to specific networks using the `networks` option — the tool will only be discoverable and invocable by agents on those networks.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const client = new KadiClient({
|
|
116
|
+
name: 'multi-network-agent',
|
|
117
|
+
brokers: { default: 'ws://localhost:8080/kadi' },
|
|
118
|
+
networks: ['global', 'internal', 'gpu-cluster'],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Visible on ALL agent networks (global, internal, gpu-cluster)
|
|
122
|
+
client.registerTool({
|
|
123
|
+
name: 'echo',
|
|
124
|
+
description: 'Echo back the input',
|
|
125
|
+
input: z.object({ message: z.string() }),
|
|
126
|
+
}, async ({ message }) => ({ echo: message }));
|
|
127
|
+
|
|
128
|
+
// Visible ONLY on gpu-cluster
|
|
129
|
+
client.registerTool({
|
|
130
|
+
name: 'gpu-inference',
|
|
131
|
+
description: 'Run ML inference on GPU',
|
|
132
|
+
input: z.object({ model: z.string(), prompt: z.string() }),
|
|
133
|
+
}, async ({ model, prompt }) => {
|
|
134
|
+
return { result: await runInference(model, prompt) };
|
|
135
|
+
}, { networks: ['gpu-cluster'] });
|
|
136
|
+
|
|
137
|
+
// Visible on internal AND global only (not gpu-cluster)
|
|
138
|
+
client.registerTool({
|
|
139
|
+
name: 'audit-log',
|
|
140
|
+
description: 'Retrieve audit logs',
|
|
141
|
+
input: z.object({ limit: z.number() }),
|
|
142
|
+
}, async ({ limit }) => {
|
|
143
|
+
return { logs: await getAuditLogs(limit) };
|
|
144
|
+
}, { networks: ['internal', 'global'] });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Per-tool networks must be a **subset** of the client's `networks`. Attempting to scope a tool to a network the agent hasn't joined throws an `INVALID_CONFIG` error:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Throws KadiError — 'finance' is not in client networks
|
|
151
|
+
client.registerTool(definition, handler, { networks: ['finance'] });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
You can combine `brokers` and `networks` to control both which brokers a tool registers with and which networks it's visible on:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
client.registerTool(definition, handler, {
|
|
158
|
+
brokers: ['production'],
|
|
159
|
+
networks: ['internal'],
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
110
163
|
---
|
|
111
164
|
|
|
112
165
|
## Connecting to Brokers
|
package/dist/client.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Readable, traceable code flow
|
|
16
16
|
*/
|
|
17
17
|
import { type EncryptionKeyPair } from './crypto.js';
|
|
18
|
-
import type { ClientConfig, ClientIdentity, BrokerState,
|
|
18
|
+
import type { ClientConfig, ClientIdentity, BrokerState, BrokerToolDefinition, ZodToolDefinition, ToolHandler, LoadedAbility, ToolExecutionBridge, RegisterToolOptions, LoadNativeOptions, LoadStdioOptions, LoadBrokerOptions, InvokeRemoteOptions, BrokerEventHandler, PublishOptions, SubscribeOptions, EmitOptions } from './types.js';
|
|
19
19
|
/**
|
|
20
20
|
* The main client for building KADI agents.
|
|
21
21
|
*
|
|
@@ -25,8 +25,8 @@ import type { ClientConfig, ClientIdentity, BrokerState, ToolDefinition, ZodTool
|
|
|
25
25
|
* const client = new KadiClient({
|
|
26
26
|
* name: 'my-agent',
|
|
27
27
|
* brokers: {
|
|
28
|
-
* production: 'ws://broker-prod:8080',
|
|
29
|
-
* internal: 'ws://broker-internal:8080',
|
|
28
|
+
* production: { url: 'ws://broker-prod:8080/kadi' },
|
|
29
|
+
* internal: { url: 'ws://broker-internal:8080/kadi', networks: ['private'] },
|
|
30
30
|
* },
|
|
31
31
|
* defaultBroker: 'production',
|
|
32
32
|
* });
|
|
@@ -404,6 +404,11 @@ export declare class KadiClient {
|
|
|
404
404
|
* Handle response to a pending request.
|
|
405
405
|
*/
|
|
406
406
|
private handleBrokerResponse;
|
|
407
|
+
/**
|
|
408
|
+
* Finalize a broker connection: start heartbeat and mark as connected.
|
|
409
|
+
* Shared by initial connect and reconnect paths.
|
|
410
|
+
*/
|
|
411
|
+
private finalizeConnection;
|
|
407
412
|
/**
|
|
408
413
|
* Send heartbeat to keep connection alive.
|
|
409
414
|
*/
|
|
@@ -521,8 +526,12 @@ export declare class KadiClient {
|
|
|
521
526
|
* output: z.object({ result: z.number() }),
|
|
522
527
|
* }, async ({ a, b }) => ({ result: a + b }));
|
|
523
528
|
*
|
|
524
|
-
* // Register
|
|
525
|
-
* client.registerTool(def, handler, {
|
|
529
|
+
* // Register on specific brokers with per-broker networks
|
|
530
|
+
* client.registerTool(def, handler, {
|
|
531
|
+
* brokers: {
|
|
532
|
+
* internal: { networks: ['private'] },
|
|
533
|
+
* },
|
|
534
|
+
* });
|
|
526
535
|
* ```
|
|
527
536
|
*/
|
|
528
537
|
registerTool<TInput, TOutput>(definition: ZodToolDefinition<TInput, TOutput>, handler: ToolHandler<TInput, TOutput>, options?: RegisterToolOptions): void;
|
|
@@ -530,7 +539,7 @@ export declare class KadiClient {
|
|
|
530
539
|
* Get tool definitions, optionally filtered for a specific broker.
|
|
531
540
|
*
|
|
532
541
|
* @param forBroker - If provided, only return tools targeted for this broker.
|
|
533
|
-
* Tools with empty
|
|
542
|
+
* Tools with empty brokerNetworks are included for all brokers.
|
|
534
543
|
*/
|
|
535
544
|
private getToolDefinitions;
|
|
536
545
|
/**
|
|
@@ -697,7 +706,7 @@ export declare class KadiClient {
|
|
|
697
706
|
readAgentJson(): {
|
|
698
707
|
name: string;
|
|
699
708
|
version: string;
|
|
700
|
-
tools:
|
|
709
|
+
tools: BrokerToolDefinition[];
|
|
701
710
|
};
|
|
702
711
|
/**
|
|
703
712
|
* Get broker connection state (for broker transport).
|
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,EAGL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EACV,YAAY,EAEZ,cAAc,EACd,WAAW,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -20,6 +20,7 @@ import { convertToEncryptionKey, convertToEncryptionKeyPair, } from './crypto.js
|
|
|
20
20
|
// @ts-expect-error - tweetnacl-sealedbox-js doesn't have type definitions
|
|
21
21
|
import sealedbox from 'tweetnacl-sealedbox-js';
|
|
22
22
|
import { KadiError } from './errors.js';
|
|
23
|
+
import * as protocol from './protocol.js';
|
|
23
24
|
import { zodToJsonSchema, isZodSchema } from './zod.js';
|
|
24
25
|
import { resolveAbilityEntry, resolveAbilityScript } from './lockfile.js';
|
|
25
26
|
import { loadNativeTransport } from './transports/native.js';
|
|
@@ -112,8 +113,8 @@ function isMcpCallToolResult(result) {
|
|
|
112
113
|
* const client = new KadiClient({
|
|
113
114
|
* name: 'my-agent',
|
|
114
115
|
* brokers: {
|
|
115
|
-
* production: 'ws://broker-prod:8080',
|
|
116
|
-
* internal: 'ws://broker-internal:8080',
|
|
116
|
+
* production: { url: 'ws://broker-prod:8080/kadi' },
|
|
117
|
+
* internal: { url: 'ws://broker-internal:8080/kadi', networks: ['private'] },
|
|
117
118
|
* },
|
|
118
119
|
* defaultBroker: 'production',
|
|
119
120
|
* });
|
|
@@ -182,16 +183,21 @@ export class KadiClient {
|
|
|
182
183
|
// Derive agentId using same algorithm as broker: SHA256(publicKey).hex().substring(0, 16)
|
|
183
184
|
this._agentId = crypto.createHash('sha256').update(this._publicKeyBase64).digest('hex').substring(0, 16);
|
|
184
185
|
// Resolve configuration with defaults
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
186
|
+
const rawBrokers = config.brokers ?? {};
|
|
187
|
+
const normalizedBrokers = {};
|
|
188
|
+
for (const [name, entry] of Object.entries(rawBrokers)) {
|
|
189
|
+
normalizedBrokers[name] = {
|
|
190
|
+
url: entry.url,
|
|
191
|
+
networks: entry.networks ?? ['global'],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const firstBrokerName = Object.keys(normalizedBrokers)[0];
|
|
188
195
|
this.config = {
|
|
189
196
|
name: config.name,
|
|
190
197
|
version: config.version ?? '1.0.0',
|
|
191
198
|
description: config.description ?? '',
|
|
192
|
-
brokers,
|
|
199
|
+
brokers: normalizedBrokers,
|
|
193
200
|
defaultBroker: config.defaultBroker ?? firstBrokerName,
|
|
194
|
-
networks: config.networks ?? ['global'],
|
|
195
201
|
heartbeatInterval: config.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
|
|
196
202
|
requestTimeout: config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
|
|
197
203
|
autoReconnect: config.autoReconnect ?? DEFAULT_AUTO_RECONNECT,
|
|
@@ -364,8 +370,6 @@ export class KadiClient {
|
|
|
364
370
|
for (const [i, result] of results.entries()) {
|
|
365
371
|
if (result.status === 'rejected') {
|
|
366
372
|
const failedBroker = brokerNames[i];
|
|
367
|
-
if (failedBroker === undefined)
|
|
368
|
-
continue;
|
|
369
373
|
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
370
374
|
console.error(`[KADI] Failed to connect to broker "${failedBroker}": ${reason}`);
|
|
371
375
|
failures.push(failedBroker);
|
|
@@ -397,14 +401,15 @@ export class KadiClient {
|
|
|
397
401
|
* across all broker connections, ensuring consistent identity.
|
|
398
402
|
*/
|
|
399
403
|
async connectToBroker(brokerName) {
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
404
|
+
const brokerConfig = this.config.brokers[brokerName];
|
|
405
|
+
if (!brokerConfig) {
|
|
402
406
|
throw new KadiError(`Broker "${brokerName}" not found in configuration`, 'UNKNOWN_BROKER', {
|
|
403
407
|
broker: brokerName,
|
|
404
408
|
available: Object.keys(this.config.brokers),
|
|
405
409
|
hint: 'Check your brokers configuration',
|
|
406
410
|
});
|
|
407
411
|
}
|
|
412
|
+
const url = brokerConfig.url;
|
|
408
413
|
// Check if already connected
|
|
409
414
|
if (this.brokers.has(brokerName)) {
|
|
410
415
|
const existing = this.brokers.get(brokerName);
|
|
@@ -416,6 +421,7 @@ export class KadiClient {
|
|
|
416
421
|
const state = {
|
|
417
422
|
name: brokerName,
|
|
418
423
|
url,
|
|
424
|
+
networks: brokerConfig.networks,
|
|
419
425
|
ws: null,
|
|
420
426
|
heartbeatTimer: null,
|
|
421
427
|
pendingRequests: new Map(),
|
|
@@ -436,22 +442,14 @@ export class KadiClient {
|
|
|
436
442
|
await this.performHandshake(state);
|
|
437
443
|
// Register with broker (transitions to "ready" state)
|
|
438
444
|
await this.registerWithBroker(state);
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
this.sendHeartbeat(state);
|
|
442
|
-
}, this.config.heartbeatInterval);
|
|
443
|
-
state.status = 'connected';
|
|
445
|
+
// Finalize connection (heartbeat + status)
|
|
446
|
+
this.finalizeConnection(state);
|
|
444
447
|
}
|
|
445
448
|
// ─────────────────────────────────────────────────────────────
|
|
446
449
|
// MESSAGE BUILDERS
|
|
447
450
|
// ─────────────────────────────────────────────────────────────
|
|
448
451
|
buildHelloMessage() {
|
|
449
|
-
return
|
|
450
|
-
jsonrpc: '2.0',
|
|
451
|
-
id: this.nextRequestId++,
|
|
452
|
-
method: 'kadi.session.hello',
|
|
453
|
-
params: { role: 'agent' },
|
|
454
|
-
};
|
|
452
|
+
return protocol.hello(this.nextRequestId++);
|
|
455
453
|
}
|
|
456
454
|
buildAuthMessage(nonce) {
|
|
457
455
|
// Sign the nonce using the client's private key
|
|
@@ -461,36 +459,13 @@ export class KadiClient {
|
|
|
461
459
|
type: 'pkcs8',
|
|
462
460
|
});
|
|
463
461
|
const signature = crypto.sign(null, Buffer.from(nonce, 'utf8'), privateKey);
|
|
464
|
-
return
|
|
465
|
-
jsonrpc: '2.0',
|
|
466
|
-
id: this.nextRequestId++,
|
|
467
|
-
method: 'kadi.session.authenticate',
|
|
468
|
-
params: {
|
|
469
|
-
publicKey: this._publicKeyBase64,
|
|
470
|
-
signature: signature.toString('base64'),
|
|
471
|
-
nonce,
|
|
472
|
-
},
|
|
473
|
-
};
|
|
462
|
+
return protocol.authenticate(this.nextRequestId++, this._publicKeyBase64, signature.toString('base64'), nonce);
|
|
474
463
|
}
|
|
475
464
|
buildRegisterMessage(tools, networks) {
|
|
476
|
-
return
|
|
477
|
-
jsonrpc: '2.0',
|
|
478
|
-
id: this.nextRequestId++,
|
|
479
|
-
method: 'kadi.agent.register',
|
|
480
|
-
params: {
|
|
481
|
-
tools,
|
|
482
|
-
networks,
|
|
483
|
-
displayName: this.config.name,
|
|
484
|
-
},
|
|
485
|
-
};
|
|
465
|
+
return protocol.register(this.nextRequestId++, tools, networks, this.config.name);
|
|
486
466
|
}
|
|
487
467
|
buildHeartbeatMessage() {
|
|
488
|
-
return
|
|
489
|
-
jsonrpc: '2.0',
|
|
490
|
-
id: this.nextRequestId++,
|
|
491
|
-
method: 'kadi.session.heartbeat',
|
|
492
|
-
params: { timestamp: Date.now() },
|
|
493
|
-
};
|
|
468
|
+
return protocol.heartbeat(this.nextRequestId++);
|
|
494
469
|
}
|
|
495
470
|
// ─────────────────────────────────────────────────────────────
|
|
496
471
|
// CONNECTION HELPERS
|
|
@@ -593,7 +568,11 @@ export class KadiClient {
|
|
|
593
568
|
// Note: If registration fails, sendRequest will reject with the error.
|
|
594
569
|
// We don't need to check response.error here - errors are already
|
|
595
570
|
// handled via Promise rejection in handleBrokerResponse.
|
|
596
|
-
await this.sendRequest(state, this.buildRegisterMessage(tools,
|
|
571
|
+
const { droppedNetworks } = await this.sendRequest(state, this.buildRegisterMessage(tools, state.networks));
|
|
572
|
+
if (droppedNetworks && droppedNetworks.length > 0) {
|
|
573
|
+
console.warn(`[KADI] Broker "${state.name}" dropped networks ${JSON.stringify(droppedNetworks)} ` +
|
|
574
|
+
`during registration. Tools scoped to those networks will not be discoverable.`);
|
|
575
|
+
}
|
|
597
576
|
}
|
|
598
577
|
/**
|
|
599
578
|
* Send a JSON-RPC request and wait for the response.
|
|
@@ -862,12 +841,7 @@ export class KadiClient {
|
|
|
862
841
|
handlers.add(handler);
|
|
863
842
|
// If this is the first handler for this pattern, subscribe on broker
|
|
864
843
|
if (!state.subscribedPatterns.has(pattern)) {
|
|
865
|
-
await this.sendRequest(state,
|
|
866
|
-
jsonrpc: '2.0',
|
|
867
|
-
id: this.nextRequestId++,
|
|
868
|
-
method: 'kadi.event.subscribe',
|
|
869
|
-
params: { pattern },
|
|
870
|
-
});
|
|
844
|
+
await this.sendRequest(state, protocol.eventSubscribe(this.nextRequestId++, pattern));
|
|
871
845
|
state.subscribedPatterns.add(pattern);
|
|
872
846
|
}
|
|
873
847
|
}
|
|
@@ -913,12 +887,7 @@ export class KadiClient {
|
|
|
913
887
|
// Unsubscribe from broker if we were subscribed
|
|
914
888
|
if (state.subscribedPatterns.has(pattern) && state.status === 'connected') {
|
|
915
889
|
try {
|
|
916
|
-
await this.sendRequest(state,
|
|
917
|
-
jsonrpc: '2.0',
|
|
918
|
-
id: this.nextRequestId++,
|
|
919
|
-
method: 'kadi.event.unsubscribe',
|
|
920
|
-
params: { pattern },
|
|
921
|
-
});
|
|
890
|
+
await this.sendRequest(state, protocol.eventUnsubscribe(this.nextRequestId++, pattern));
|
|
922
891
|
}
|
|
923
892
|
catch {
|
|
924
893
|
// Ignore errors during unsubscribe (broker might be disconnecting)
|
|
@@ -949,19 +918,10 @@ export class KadiClient {
|
|
|
949
918
|
*/
|
|
950
919
|
async publish(channel, data, options = {}) {
|
|
951
920
|
const { state } = this.getConnectedBrokerState(options.broker);
|
|
952
|
-
// Resolve which network to publish to
|
|
953
|
-
const networkId = options.network ??
|
|
921
|
+
// Resolve which network to publish to (prefer broker-specific, fallback to global)
|
|
922
|
+
const networkId = options.network ?? state.networks[0] ?? '';
|
|
954
923
|
// Send publish request to broker
|
|
955
|
-
await this.sendRequest(state,
|
|
956
|
-
jsonrpc: '2.0',
|
|
957
|
-
id: this.nextRequestId++,
|
|
958
|
-
method: 'kadi.event.publish',
|
|
959
|
-
params: {
|
|
960
|
-
channel,
|
|
961
|
-
data,
|
|
962
|
-
networkId,
|
|
963
|
-
},
|
|
964
|
-
});
|
|
924
|
+
await this.sendRequest(state, protocol.eventPublish(this.nextRequestId++, channel, data, networkId));
|
|
965
925
|
}
|
|
966
926
|
// ─────────────────────────────────────────────────────────────
|
|
967
927
|
// DEPLOYMENT SECRETS (Trusted Introducer Pattern)
|
|
@@ -1191,35 +1151,18 @@ export class KadiClient {
|
|
|
1191
1151
|
// KADI clients receive raw structured data
|
|
1192
1152
|
responseResult = result;
|
|
1193
1153
|
}
|
|
1194
|
-
|
|
1195
|
-
jsonrpc: '2.0',
|
|
1196
|
-
id: requestId,
|
|
1197
|
-
result: responseResult,
|
|
1198
|
-
};
|
|
1199
|
-
state.ws?.send(JSON.stringify(response));
|
|
1154
|
+
state.ws?.send(JSON.stringify(protocol.resultResponse(requestId, responseResult)));
|
|
1200
1155
|
}
|
|
1201
1156
|
catch (error) {
|
|
1202
1157
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1203
1158
|
// For MCP clients, wrap errors in CallToolResult format too
|
|
1204
1159
|
if (context.callerProtocol === 'mcp') {
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
id: requestId,
|
|
1208
|
-
result: { content: [{ type: 'text', text: errorMessage }], isError: true },
|
|
1209
|
-
};
|
|
1210
|
-
state.ws?.send(JSON.stringify(response));
|
|
1160
|
+
const mcpResult = { content: [{ type: 'text', text: errorMessage }], isError: true };
|
|
1161
|
+
state.ws?.send(JSON.stringify(protocol.resultResponse(requestId, mcpResult)));
|
|
1211
1162
|
}
|
|
1212
1163
|
else {
|
|
1213
1164
|
// KADI clients receive JSON-RPC error
|
|
1214
|
-
|
|
1215
|
-
jsonrpc: '2.0',
|
|
1216
|
-
id: requestId,
|
|
1217
|
-
error: {
|
|
1218
|
-
code: -32000,
|
|
1219
|
-
message: errorMessage,
|
|
1220
|
-
},
|
|
1221
|
-
};
|
|
1222
|
-
state.ws?.send(JSON.stringify(response));
|
|
1165
|
+
state.ws?.send(JSON.stringify(protocol.errorResponse(requestId, -32000, errorMessage)));
|
|
1223
1166
|
}
|
|
1224
1167
|
}
|
|
1225
1168
|
}
|
|
@@ -1244,6 +1187,16 @@ export class KadiClient {
|
|
|
1244
1187
|
pending.resolve(response.result);
|
|
1245
1188
|
}
|
|
1246
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Finalize a broker connection: start heartbeat and mark as connected.
|
|
1192
|
+
* Shared by initial connect and reconnect paths.
|
|
1193
|
+
*/
|
|
1194
|
+
finalizeConnection(state) {
|
|
1195
|
+
state.heartbeatTimer = setInterval(() => {
|
|
1196
|
+
this.sendHeartbeat(state);
|
|
1197
|
+
}, this.config.heartbeatInterval);
|
|
1198
|
+
state.status = 'connected';
|
|
1199
|
+
}
|
|
1247
1200
|
/**
|
|
1248
1201
|
* Send heartbeat to keep connection alive.
|
|
1249
1202
|
*/
|
|
@@ -1283,6 +1236,9 @@ export class KadiClient {
|
|
|
1283
1236
|
}));
|
|
1284
1237
|
}
|
|
1285
1238
|
state.pendingInvocations.clear();
|
|
1239
|
+
// Clear broker-side subscription tracking — subscriptions are lost when
|
|
1240
|
+
// the connection drops and must be re-established after reconnection.
|
|
1241
|
+
state.subscribedPatterns.clear();
|
|
1286
1242
|
}
|
|
1287
1243
|
// ─────────────────────────────────────────────────────────────
|
|
1288
1244
|
// RECONNECTION LOGIC
|
|
@@ -1395,14 +1351,11 @@ export class KadiClient {
|
|
|
1395
1351
|
await this.performHandshake(state);
|
|
1396
1352
|
// Re-register tools
|
|
1397
1353
|
await this.registerWithBroker(state);
|
|
1398
|
-
//
|
|
1399
|
-
|
|
1400
|
-
this.sendHeartbeat(state);
|
|
1401
|
-
}, this.config.heartbeatInterval);
|
|
1354
|
+
// Finalize connection (heartbeat + status)
|
|
1355
|
+
this.finalizeConnection(state);
|
|
1402
1356
|
// Success!
|
|
1403
1357
|
console.error(`[KADI] Reconnected to broker "${state.name}" after ${state.reconnectAttempts} attempts`);
|
|
1404
1358
|
state.reconnectAttempts = 0;
|
|
1405
|
-
state.status = 'connected';
|
|
1406
1359
|
}
|
|
1407
1360
|
catch (error) {
|
|
1408
1361
|
// Log the error and try again
|
|
@@ -1509,12 +1462,7 @@ export class KadiClient {
|
|
|
1509
1462
|
}
|
|
1510
1463
|
else if (this.isServingStdio) {
|
|
1511
1464
|
// Stdio transport: write notification to stdout
|
|
1512
|
-
const
|
|
1513
|
-
jsonrpc: '2.0',
|
|
1514
|
-
method: 'event',
|
|
1515
|
-
params: { name: event, data },
|
|
1516
|
-
};
|
|
1517
|
-
const json = JSON.stringify(notification);
|
|
1465
|
+
const json = JSON.stringify(protocol.eventNotification(event, data));
|
|
1518
1466
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
1519
1467
|
}
|
|
1520
1468
|
// Broadcast: also publish to broker (if specified and connected)
|
|
@@ -1548,8 +1496,12 @@ export class KadiClient {
|
|
|
1548
1496
|
* output: z.object({ result: z.number() }),
|
|
1549
1497
|
* }, async ({ a, b }) => ({ result: a + b }));
|
|
1550
1498
|
*
|
|
1551
|
-
* // Register
|
|
1552
|
-
* client.registerTool(def, handler, {
|
|
1499
|
+
* // Register on specific brokers with per-broker networks
|
|
1500
|
+
* client.registerTool(def, handler, {
|
|
1501
|
+
* brokers: {
|
|
1502
|
+
* internal: { networks: ['private'] },
|
|
1503
|
+
* },
|
|
1504
|
+
* });
|
|
1553
1505
|
* ```
|
|
1554
1506
|
*/
|
|
1555
1507
|
registerTool(definition, handler, options = {}) {
|
|
@@ -1569,12 +1521,35 @@ export class KadiClient {
|
|
|
1569
1521
|
? zodToJsonSchema(definition.output)
|
|
1570
1522
|
: undefined,
|
|
1571
1523
|
};
|
|
1524
|
+
// Validate per-broker network scoping (skip when no brokers configured)
|
|
1525
|
+
const brokerScopes = options.brokers ?? {};
|
|
1526
|
+
if (Object.keys(brokerScopes).length > 0 && Object.keys(this.config.brokers).length > 0) {
|
|
1527
|
+
for (const [brokerName, scope] of Object.entries(brokerScopes)) {
|
|
1528
|
+
const brokerConfig = this.config.brokers[brokerName];
|
|
1529
|
+
if (!brokerConfig) {
|
|
1530
|
+
throw new KadiError(`Tool "${definition.name}" references broker "${brokerName}" which is not configured. Available brokers: ${JSON.stringify(Object.keys(this.config.brokers))}`, 'INVALID_CONFIG', {
|
|
1531
|
+
toolName: definition.name,
|
|
1532
|
+
broker: brokerName,
|
|
1533
|
+
availableBrokers: Object.keys(this.config.brokers),
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
const brokerNetworks = brokerConfig.networks;
|
|
1537
|
+
const invalid = scope.networks.filter((n) => !brokerNetworks.includes(n));
|
|
1538
|
+
if (invalid.length > 0) {
|
|
1539
|
+
throw new KadiError(`Tool "${definition.name}" has networks ${JSON.stringify(invalid)} not present on broker "${brokerName}" networks ${JSON.stringify(brokerNetworks)}. Per-tool networks must be a subset of the broker's networks — a tool can only be visible on networks the agent has joined.`, 'INVALID_CONFIG', {
|
|
1540
|
+
toolName: definition.name,
|
|
1541
|
+
broker: brokerName,
|
|
1542
|
+
invalidNetworks: invalid,
|
|
1543
|
+
brokerNetworks,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1572
1548
|
// Store registration
|
|
1573
1549
|
const registered = {
|
|
1574
1550
|
definition: jsonDefinition,
|
|
1575
1551
|
handler: handler,
|
|
1576
|
-
|
|
1577
|
-
targetBrokers: options.brokers ?? [],
|
|
1552
|
+
brokerNetworks: brokerScopes,
|
|
1578
1553
|
};
|
|
1579
1554
|
this.tools.set(definition.name, registered);
|
|
1580
1555
|
}
|
|
@@ -1582,21 +1557,31 @@ export class KadiClient {
|
|
|
1582
1557
|
* Get tool definitions, optionally filtered for a specific broker.
|
|
1583
1558
|
*
|
|
1584
1559
|
* @param forBroker - If provided, only return tools targeted for this broker.
|
|
1585
|
-
* Tools with empty
|
|
1560
|
+
* Tools with empty brokerNetworks are included for all brokers.
|
|
1586
1561
|
*/
|
|
1587
1562
|
getToolDefinitions(forBroker) {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
if (!forBroker)
|
|
1592
|
-
return
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1563
|
+
const results = [];
|
|
1564
|
+
for (const t of this.tools.values()) {
|
|
1565
|
+
const hasBrokerNetworks = Object.keys(t.brokerNetworks).length > 0;
|
|
1566
|
+
if (!forBroker) {
|
|
1567
|
+
// No broker filter — return all tools (e.g., for readAgentJson)
|
|
1568
|
+
results.push({ ...t.definition });
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
if (!hasBrokerNetworks) {
|
|
1572
|
+
// Empty brokerNetworks = register with all brokers, no network field
|
|
1573
|
+
results.push({ ...t.definition });
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
const scope = t.brokerNetworks[forBroker];
|
|
1577
|
+
if (!scope)
|
|
1578
|
+
continue; // Tool not registered on this broker
|
|
1579
|
+
results.push({
|
|
1580
|
+
...t.definition,
|
|
1581
|
+
...(scope.networks.length > 0 && { networks: scope.networks }),
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
return results;
|
|
1600
1585
|
}
|
|
1601
1586
|
/**
|
|
1602
1587
|
* Execute a tool registered on this client.
|
|
@@ -1752,7 +1737,7 @@ export class KadiClient {
|
|
|
1752
1737
|
const ability = await loadBrokerTransport(name, {
|
|
1753
1738
|
broker: state,
|
|
1754
1739
|
requestTimeout: options.timeout ?? this.config.requestTimeout,
|
|
1755
|
-
networks: options.networks,
|
|
1740
|
+
networks: options.networks ?? state.networks,
|
|
1756
1741
|
// Provide subscribe/unsubscribe for ability.on()/off() support
|
|
1757
1742
|
subscribe: (pattern, handler) => this.subscribe(pattern, handler, { broker: brokerName }),
|
|
1758
1743
|
unsubscribe: (pattern, handler) => this.unsubscribe(pattern, handler, { broker: brokerName }),
|
|
@@ -1831,13 +1816,9 @@ export class KadiClient {
|
|
|
1831
1816
|
};
|
|
1832
1817
|
// NOW send the request (listener already exists, no race possible)
|
|
1833
1818
|
try {
|
|
1834
|
-
const pendingResult = await this.sendRequest(state,
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
method: 'kadi.ability.request',
|
|
1838
|
-
// timeout sent so the broker can enforce a matching server-side deadline
|
|
1839
|
-
params: { toolName, toolInput: params, requestId, timeout },
|
|
1840
|
-
});
|
|
1819
|
+
const pendingResult = await this.sendRequest(state,
|
|
1820
|
+
// timeout sent so the broker can enforce a matching server-side deadline
|
|
1821
|
+
protocol.abilityRequest(++this.nextRequestId, toolName, params, requestId, timeout));
|
|
1841
1822
|
// Validate the broker accepted the request
|
|
1842
1823
|
if (pendingResult.status !== 'pending') {
|
|
1843
1824
|
cleanupPendingInvocation();
|
|
@@ -1990,24 +1971,14 @@ export class KadiClient {
|
|
|
1990
1971
|
* Send a response via stdio.
|
|
1991
1972
|
*/
|
|
1992
1973
|
sendStdioResponse(id, result) {
|
|
1993
|
-
const
|
|
1994
|
-
jsonrpc: '2.0',
|
|
1995
|
-
id,
|
|
1996
|
-
result,
|
|
1997
|
-
};
|
|
1998
|
-
const json = JSON.stringify(response);
|
|
1974
|
+
const json = JSON.stringify(protocol.resultResponse(id, result));
|
|
1999
1975
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
2000
1976
|
}
|
|
2001
1977
|
/**
|
|
2002
1978
|
* Send an error response via stdio.
|
|
2003
1979
|
*/
|
|
2004
1980
|
sendStdioError(id, error) {
|
|
2005
|
-
const
|
|
2006
|
-
jsonrpc: '2.0',
|
|
2007
|
-
id,
|
|
2008
|
-
error,
|
|
2009
|
-
};
|
|
2010
|
-
const json = JSON.stringify(response);
|
|
1981
|
+
const json = JSON.stringify(protocol.errorResponse(id, error.code, error.message));
|
|
2011
1982
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
2012
1983
|
}
|
|
2013
1984
|
/**
|