@kadi.build/core 0.7.1 → 0.8.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 +15 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +95 -127
- 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/broker.d.ts.map +1 -1
- package/dist/transports/broker.js +3 -1
- package/dist/transports/broker.js.map +1 -1
- 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 +57 -16
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +118 -132
- package/src/index.ts +7 -1
- package/src/protocol.ts +161 -0
- package/src/transports/broker.ts +3 -1
- package/src/transports/stdio.ts +2 -6
- package/src/types.ts +60 -18
package/src/client.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
BrokerState,
|
|
32
32
|
RegisteredTool,
|
|
33
33
|
ToolDefinition,
|
|
34
|
+
BrokerToolDefinition,
|
|
34
35
|
ZodToolDefinition,
|
|
35
36
|
ToolHandler,
|
|
36
37
|
LoadedAbility,
|
|
@@ -52,6 +53,7 @@ import type {
|
|
|
52
53
|
EmitOptions,
|
|
53
54
|
} from './types.js';
|
|
54
55
|
import { KadiError } from './errors.js';
|
|
56
|
+
import * as protocol from './protocol.js';
|
|
55
57
|
import { zodToJsonSchema, isZodSchema } from './zod.js';
|
|
56
58
|
import { resolveAbilityEntry, resolveAbilityScript } from './lockfile.js';
|
|
57
59
|
import { loadNativeTransport } from './transports/native.js';
|
|
@@ -156,8 +158,8 @@ function isMcpCallToolResult(result: unknown): boolean {
|
|
|
156
158
|
* const client = new KadiClient({
|
|
157
159
|
* name: 'my-agent',
|
|
158
160
|
* brokers: {
|
|
159
|
-
* production: 'ws://broker-prod:8080',
|
|
160
|
-
* internal: 'ws://broker-internal:8080',
|
|
161
|
+
* production: { url: 'ws://broker-prod:8080/kadi' },
|
|
162
|
+
* internal: { url: 'ws://broker-internal:8080/kadi', networks: ['private'] },
|
|
161
163
|
* },
|
|
162
164
|
* defaultBroker: 'production',
|
|
163
165
|
* });
|
|
@@ -241,17 +243,24 @@ export class KadiClient {
|
|
|
241
243
|
this._agentId = crypto.createHash('sha256').update(this._publicKeyBase64).digest('hex').substring(0, 16);
|
|
242
244
|
|
|
243
245
|
// Resolve configuration with defaults
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
246
|
+
const rawBrokers = config.brokers ?? {};
|
|
247
|
+
const normalizedBrokers: Record<string, { url: string; networks: string[] }> = {};
|
|
248
|
+
|
|
249
|
+
for (const [name, entry] of Object.entries(rawBrokers)) {
|
|
250
|
+
normalizedBrokers[name] = {
|
|
251
|
+
url: entry.url,
|
|
252
|
+
networks: entry.networks ?? ['global'],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const firstBrokerName = Object.keys(normalizedBrokers)[0];
|
|
247
257
|
|
|
248
258
|
this.config = {
|
|
249
259
|
name: config.name,
|
|
250
260
|
version: config.version ?? '1.0.0',
|
|
251
261
|
description: config.description ?? '',
|
|
252
|
-
brokers,
|
|
262
|
+
brokers: normalizedBrokers,
|
|
253
263
|
defaultBroker: config.defaultBroker ?? firstBrokerName,
|
|
254
|
-
networks: config.networks ?? ['global'],
|
|
255
264
|
heartbeatInterval: config.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
|
|
256
265
|
requestTimeout: config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
|
|
257
266
|
autoReconnect: config.autoReconnect ?? DEFAULT_AUTO_RECONNECT,
|
|
@@ -445,8 +454,7 @@ export class KadiClient {
|
|
|
445
454
|
const failures: string[] = [];
|
|
446
455
|
for (const [i, result] of results.entries()) {
|
|
447
456
|
if (result.status === 'rejected') {
|
|
448
|
-
const failedBroker = brokerNames[i]
|
|
449
|
-
if (failedBroker === undefined) continue;
|
|
457
|
+
const failedBroker = brokerNames[i]!;
|
|
450
458
|
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
451
459
|
console.error(`[KADI] Failed to connect to broker "${failedBroker}": ${reason}`);
|
|
452
460
|
failures.push(failedBroker);
|
|
@@ -485,8 +493,8 @@ export class KadiClient {
|
|
|
485
493
|
* across all broker connections, ensuring consistent identity.
|
|
486
494
|
*/
|
|
487
495
|
private async connectToBroker(brokerName: string): Promise<void> {
|
|
488
|
-
const
|
|
489
|
-
if (!
|
|
496
|
+
const brokerConfig = this.config.brokers[brokerName];
|
|
497
|
+
if (!brokerConfig) {
|
|
490
498
|
throw new KadiError(`Broker "${brokerName}" not found in configuration`, 'UNKNOWN_BROKER', {
|
|
491
499
|
broker: brokerName,
|
|
492
500
|
available: Object.keys(this.config.brokers),
|
|
@@ -494,6 +502,8 @@ export class KadiClient {
|
|
|
494
502
|
});
|
|
495
503
|
}
|
|
496
504
|
|
|
505
|
+
const url = brokerConfig.url;
|
|
506
|
+
|
|
497
507
|
// Check if already connected
|
|
498
508
|
if (this.brokers.has(brokerName)) {
|
|
499
509
|
const existing = this.brokers.get(brokerName)!;
|
|
@@ -506,6 +516,7 @@ export class KadiClient {
|
|
|
506
516
|
const state: BrokerState = {
|
|
507
517
|
name: brokerName,
|
|
508
518
|
url,
|
|
519
|
+
networks: brokerConfig.networks,
|
|
509
520
|
ws: null,
|
|
510
521
|
heartbeatTimer: null,
|
|
511
522
|
pendingRequests: new Map(),
|
|
@@ -530,12 +541,8 @@ export class KadiClient {
|
|
|
530
541
|
// Register with broker (transitions to "ready" state)
|
|
531
542
|
await this.registerWithBroker(state);
|
|
532
543
|
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
this.sendHeartbeat(state);
|
|
536
|
-
}, this.config.heartbeatInterval);
|
|
537
|
-
|
|
538
|
-
state.status = 'connected';
|
|
544
|
+
// Finalize connection (heartbeat + status)
|
|
545
|
+
this.finalizeConnection(state);
|
|
539
546
|
}
|
|
540
547
|
|
|
541
548
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -543,12 +550,7 @@ export class KadiClient {
|
|
|
543
550
|
// ─────────────────────────────────────────────────────────────
|
|
544
551
|
|
|
545
552
|
private buildHelloMessage(): JsonRpcRequest {
|
|
546
|
-
return
|
|
547
|
-
jsonrpc: '2.0',
|
|
548
|
-
id: this.nextRequestId++,
|
|
549
|
-
method: 'kadi.session.hello',
|
|
550
|
-
params: { role: 'agent' },
|
|
551
|
-
};
|
|
553
|
+
return protocol.hello(this.nextRequestId++);
|
|
552
554
|
}
|
|
553
555
|
|
|
554
556
|
private buildAuthMessage(nonce: string): JsonRpcRequest {
|
|
@@ -560,38 +562,20 @@ export class KadiClient {
|
|
|
560
562
|
});
|
|
561
563
|
const signature = crypto.sign(null, Buffer.from(nonce, 'utf8'), privateKey);
|
|
562
564
|
|
|
563
|
-
return
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
signature: signature.toString('base64'),
|
|
570
|
-
nonce,
|
|
571
|
-
},
|
|
572
|
-
};
|
|
565
|
+
return protocol.authenticate(
|
|
566
|
+
this.nextRequestId++,
|
|
567
|
+
this._publicKeyBase64,
|
|
568
|
+
signature.toString('base64'),
|
|
569
|
+
nonce,
|
|
570
|
+
);
|
|
573
571
|
}
|
|
574
572
|
|
|
575
|
-
private buildRegisterMessage(tools:
|
|
576
|
-
return
|
|
577
|
-
jsonrpc: '2.0',
|
|
578
|
-
id: this.nextRequestId++,
|
|
579
|
-
method: 'kadi.agent.register',
|
|
580
|
-
params: {
|
|
581
|
-
tools,
|
|
582
|
-
networks,
|
|
583
|
-
displayName: this.config.name,
|
|
584
|
-
},
|
|
585
|
-
};
|
|
573
|
+
private buildRegisterMessage(tools: BrokerToolDefinition[], networks: string[]): JsonRpcRequest {
|
|
574
|
+
return protocol.register(this.nextRequestId++, tools, networks, this.config.name);
|
|
586
575
|
}
|
|
587
576
|
|
|
588
577
|
private buildHeartbeatMessage(): JsonRpcRequest {
|
|
589
|
-
return
|
|
590
|
-
jsonrpc: '2.0',
|
|
591
|
-
id: this.nextRequestId++,
|
|
592
|
-
method: 'kadi.session.heartbeat',
|
|
593
|
-
params: { timestamp: Date.now() },
|
|
594
|
-
};
|
|
578
|
+
return protocol.heartbeat(this.nextRequestId++);
|
|
595
579
|
}
|
|
596
580
|
|
|
597
581
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -718,10 +702,17 @@ export class KadiClient {
|
|
|
718
702
|
// Note: If registration fails, sendRequest will reject with the error.
|
|
719
703
|
// We don't need to check response.error here - errors are already
|
|
720
704
|
// handled via Promise rejection in handleBrokerResponse.
|
|
721
|
-
await this.sendRequest<{ status: string }>(
|
|
705
|
+
const { droppedNetworks } = await this.sendRequest<{ status: string; droppedNetworks?: string[] }>(
|
|
722
706
|
state,
|
|
723
|
-
this.buildRegisterMessage(tools,
|
|
707
|
+
this.buildRegisterMessage(tools, state.networks)
|
|
724
708
|
);
|
|
709
|
+
|
|
710
|
+
if (droppedNetworks && droppedNetworks.length > 0) {
|
|
711
|
+
console.warn(
|
|
712
|
+
`[KADI] Broker "${state.name}" dropped networks ${JSON.stringify(droppedNetworks)} ` +
|
|
713
|
+
`during registration. Tools scoped to those networks will not be discoverable.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
725
716
|
}
|
|
726
717
|
|
|
727
718
|
/**
|
|
@@ -1037,12 +1028,7 @@ export class KadiClient {
|
|
|
1037
1028
|
|
|
1038
1029
|
// If this is the first handler for this pattern, subscribe on broker
|
|
1039
1030
|
if (!state.subscribedPatterns.has(pattern)) {
|
|
1040
|
-
await this.sendRequest(state,
|
|
1041
|
-
jsonrpc: '2.0',
|
|
1042
|
-
id: this.nextRequestId++,
|
|
1043
|
-
method: 'kadi.event.subscribe',
|
|
1044
|
-
params: { pattern },
|
|
1045
|
-
});
|
|
1031
|
+
await this.sendRequest(state, protocol.eventSubscribe(this.nextRequestId++, pattern));
|
|
1046
1032
|
state.subscribedPatterns.add(pattern);
|
|
1047
1033
|
}
|
|
1048
1034
|
}
|
|
@@ -1097,12 +1083,7 @@ export class KadiClient {
|
|
|
1097
1083
|
// Unsubscribe from broker if we were subscribed
|
|
1098
1084
|
if (state.subscribedPatterns.has(pattern) && state.status === 'connected') {
|
|
1099
1085
|
try {
|
|
1100
|
-
await this.sendRequest(state,
|
|
1101
|
-
jsonrpc: '2.0',
|
|
1102
|
-
id: this.nextRequestId++,
|
|
1103
|
-
method: 'kadi.event.unsubscribe',
|
|
1104
|
-
params: { pattern },
|
|
1105
|
-
});
|
|
1086
|
+
await this.sendRequest(state, protocol.eventUnsubscribe(this.nextRequestId++, pattern));
|
|
1106
1087
|
} catch {
|
|
1107
1088
|
// Ignore errors during unsubscribe (broker might be disconnecting)
|
|
1108
1089
|
}
|
|
@@ -1134,20 +1115,11 @@ export class KadiClient {
|
|
|
1134
1115
|
async publish(channel: string, data: unknown, options: PublishOptions = {}): Promise<void> {
|
|
1135
1116
|
const { state } = this.getConnectedBrokerState(options.broker);
|
|
1136
1117
|
|
|
1137
|
-
// Resolve which network to publish to
|
|
1138
|
-
const networkId = options.network ??
|
|
1118
|
+
// Resolve which network to publish to (prefer broker-specific, fallback to global)
|
|
1119
|
+
const networkId = options.network ?? state.networks[0] ?? '';
|
|
1139
1120
|
|
|
1140
1121
|
// Send publish request to broker
|
|
1141
|
-
await this.sendRequest(state,
|
|
1142
|
-
jsonrpc: '2.0',
|
|
1143
|
-
id: this.nextRequestId++,
|
|
1144
|
-
method: 'kadi.event.publish',
|
|
1145
|
-
params: {
|
|
1146
|
-
channel,
|
|
1147
|
-
data,
|
|
1148
|
-
networkId,
|
|
1149
|
-
},
|
|
1150
|
-
});
|
|
1122
|
+
await this.sendRequest(state, protocol.eventPublish(this.nextRequestId++, channel, data, networkId));
|
|
1151
1123
|
}
|
|
1152
1124
|
|
|
1153
1125
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -1463,34 +1435,17 @@ export class KadiClient {
|
|
|
1463
1435
|
responseResult = result;
|
|
1464
1436
|
}
|
|
1465
1437
|
|
|
1466
|
-
|
|
1467
|
-
jsonrpc: '2.0',
|
|
1468
|
-
id: requestId,
|
|
1469
|
-
result: responseResult,
|
|
1470
|
-
};
|
|
1471
|
-
state.ws?.send(JSON.stringify(response));
|
|
1438
|
+
state.ws?.send(JSON.stringify(protocol.resultResponse(requestId, responseResult)));
|
|
1472
1439
|
} catch (error) {
|
|
1473
1440
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1474
1441
|
|
|
1475
1442
|
// For MCP clients, wrap errors in CallToolResult format too
|
|
1476
1443
|
if (context.callerProtocol === 'mcp') {
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
id: requestId,
|
|
1480
|
-
result: { content: [{ type: 'text', text: errorMessage }], isError: true },
|
|
1481
|
-
};
|
|
1482
|
-
state.ws?.send(JSON.stringify(response));
|
|
1444
|
+
const mcpResult = { content: [{ type: 'text', text: errorMessage }], isError: true };
|
|
1445
|
+
state.ws?.send(JSON.stringify(protocol.resultResponse(requestId, mcpResult)));
|
|
1483
1446
|
} else {
|
|
1484
1447
|
// KADI clients receive JSON-RPC error
|
|
1485
|
-
|
|
1486
|
-
jsonrpc: '2.0',
|
|
1487
|
-
id: requestId,
|
|
1488
|
-
error: {
|
|
1489
|
-
code: -32000,
|
|
1490
|
-
message: errorMessage,
|
|
1491
|
-
},
|
|
1492
|
-
};
|
|
1493
|
-
state.ws?.send(JSON.stringify(response));
|
|
1448
|
+
state.ws?.send(JSON.stringify(protocol.errorResponse(requestId, -32000, errorMessage)));
|
|
1494
1449
|
}
|
|
1495
1450
|
}
|
|
1496
1451
|
}
|
|
@@ -1520,6 +1475,17 @@ export class KadiClient {
|
|
|
1520
1475
|
}
|
|
1521
1476
|
}
|
|
1522
1477
|
|
|
1478
|
+
/**
|
|
1479
|
+
* Finalize a broker connection: start heartbeat and mark as connected.
|
|
1480
|
+
* Shared by initial connect and reconnect paths.
|
|
1481
|
+
*/
|
|
1482
|
+
private finalizeConnection(state: BrokerState): void {
|
|
1483
|
+
state.heartbeatTimer = setInterval(() => {
|
|
1484
|
+
this.sendHeartbeat(state);
|
|
1485
|
+
}, this.config.heartbeatInterval);
|
|
1486
|
+
state.status = 'connected';
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1523
1489
|
/**
|
|
1524
1490
|
* Send heartbeat to keep connection alive.
|
|
1525
1491
|
*/
|
|
@@ -1565,6 +1531,10 @@ export class KadiClient {
|
|
|
1565
1531
|
);
|
|
1566
1532
|
}
|
|
1567
1533
|
state.pendingInvocations.clear();
|
|
1534
|
+
|
|
1535
|
+
// Clear broker-side subscription tracking — subscriptions are lost when
|
|
1536
|
+
// the connection drops and must be re-established after reconnection.
|
|
1537
|
+
state.subscribedPatterns.clear();
|
|
1568
1538
|
}
|
|
1569
1539
|
|
|
1570
1540
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -1701,15 +1671,12 @@ export class KadiClient {
|
|
|
1701
1671
|
// Re-register tools
|
|
1702
1672
|
await this.registerWithBroker(state);
|
|
1703
1673
|
|
|
1704
|
-
//
|
|
1705
|
-
|
|
1706
|
-
this.sendHeartbeat(state);
|
|
1707
|
-
}, this.config.heartbeatInterval);
|
|
1674
|
+
// Finalize connection (heartbeat + status)
|
|
1675
|
+
this.finalizeConnection(state);
|
|
1708
1676
|
|
|
1709
1677
|
// Success!
|
|
1710
1678
|
console.error(`[KADI] Reconnected to broker "${state.name}" after ${state.reconnectAttempts} attempts`);
|
|
1711
1679
|
state.reconnectAttempts = 0;
|
|
1712
|
-
state.status = 'connected';
|
|
1713
1680
|
} catch (error) {
|
|
1714
1681
|
// Log the error and try again
|
|
1715
1682
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1819,12 +1786,7 @@ export class KadiClient {
|
|
|
1819
1786
|
this.eventHandler(event, data);
|
|
1820
1787
|
} else if (this.isServingStdio) {
|
|
1821
1788
|
// Stdio transport: write notification to stdout
|
|
1822
|
-
const
|
|
1823
|
-
jsonrpc: '2.0',
|
|
1824
|
-
method: 'event',
|
|
1825
|
-
params: { name: event, data },
|
|
1826
|
-
};
|
|
1827
|
-
const json = JSON.stringify(notification);
|
|
1789
|
+
const json = JSON.stringify(protocol.eventNotification(event, data));
|
|
1828
1790
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
1829
1791
|
}
|
|
1830
1792
|
|
|
@@ -1888,23 +1850,53 @@ export class KadiClient {
|
|
|
1888
1850
|
: undefined,
|
|
1889
1851
|
};
|
|
1890
1852
|
|
|
1853
|
+
// Validate per-tool networks against broker networks (skip when no brokers)
|
|
1854
|
+
const targetNetworks = options.networks ?? [];
|
|
1855
|
+
if (targetNetworks.length > 0 && Object.keys(this.config.brokers).length > 0) {
|
|
1856
|
+
const allNetworks = this.getAllConfiguredNetworks();
|
|
1857
|
+
const invalid = targetNetworks.filter((n) => !allNetworks.includes(n));
|
|
1858
|
+
if (invalid.length > 0) {
|
|
1859
|
+
throw new KadiError(
|
|
1860
|
+
`Tool "${definition.name}" has networks ${JSON.stringify(invalid)} not present in client networks ${JSON.stringify(allNetworks)}. Per-tool networks must be a subset of the client's networks — a tool can only be visible on networks the agent has joined.`,
|
|
1861
|
+
'INVALID_CONFIG',
|
|
1862
|
+
{
|
|
1863
|
+
toolName: definition.name,
|
|
1864
|
+
invalidNetworks: invalid,
|
|
1865
|
+
clientNetworks: allNetworks,
|
|
1866
|
+
}
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1891
1871
|
// Store registration
|
|
1892
1872
|
const registered: RegisteredTool = {
|
|
1893
1873
|
definition: jsonDefinition,
|
|
1894
1874
|
handler: handler as ToolHandler,
|
|
1895
|
-
registeredAt: new Date(),
|
|
1896
1875
|
targetBrokers: options.brokers ?? [],
|
|
1876
|
+
targetNetworks,
|
|
1897
1877
|
};
|
|
1898
1878
|
this.tools.set(definition.name, registered);
|
|
1899
1879
|
}
|
|
1900
1880
|
|
|
1881
|
+
/**
|
|
1882
|
+
* Get the union of all networks configured across all brokers.
|
|
1883
|
+
* Used by registerTool() to validate per-tool network targeting.
|
|
1884
|
+
*/
|
|
1885
|
+
private getAllConfiguredNetworks(): string[] {
|
|
1886
|
+
const all = new Set<string>();
|
|
1887
|
+
for (const broker of Object.values(this.config.brokers)) {
|
|
1888
|
+
for (const n of broker.networks) all.add(n);
|
|
1889
|
+
}
|
|
1890
|
+
return [...all];
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1901
1893
|
/**
|
|
1902
1894
|
* Get tool definitions, optionally filtered for a specific broker.
|
|
1903
1895
|
*
|
|
1904
1896
|
* @param forBroker - If provided, only return tools targeted for this broker.
|
|
1905
1897
|
* Tools with empty targetBrokers are included for all brokers.
|
|
1906
1898
|
*/
|
|
1907
|
-
private getToolDefinitions(forBroker?: string):
|
|
1899
|
+
private getToolDefinitions(forBroker?: string): BrokerToolDefinition[] {
|
|
1908
1900
|
return Array.from(this.tools.values())
|
|
1909
1901
|
.filter((t) => {
|
|
1910
1902
|
// If no broker specified, return all tools (e.g., for readAgentJson)
|
|
@@ -1916,7 +1908,10 @@ export class KadiClient {
|
|
|
1916
1908
|
// Otherwise, only include if this broker is in the target list
|
|
1917
1909
|
return t.targetBrokers.includes(forBroker);
|
|
1918
1910
|
})
|
|
1919
|
-
.map((t) =>
|
|
1911
|
+
.map((t) => ({
|
|
1912
|
+
...t.definition,
|
|
1913
|
+
...(t.targetNetworks.length > 0 && { networks: t.targetNetworks }),
|
|
1914
|
+
}));
|
|
1920
1915
|
}
|
|
1921
1916
|
|
|
1922
1917
|
/**
|
|
@@ -2090,7 +2085,7 @@ export class KadiClient {
|
|
|
2090
2085
|
const ability = await loadBrokerTransport(name, {
|
|
2091
2086
|
broker: state,
|
|
2092
2087
|
requestTimeout: options.timeout ?? this.config.requestTimeout,
|
|
2093
|
-
networks: options.networks,
|
|
2088
|
+
networks: options.networks ?? state.networks,
|
|
2094
2089
|
// Provide subscribe/unsubscribe for ability.on()/off() support
|
|
2095
2090
|
subscribe: (pattern, handler) => this.subscribe(pattern, handler, { broker: brokerName }),
|
|
2096
2091
|
unsubscribe: (pattern, handler) => this.unsubscribe(pattern, handler, { broker: brokerName }),
|
|
@@ -2107,7 +2102,7 @@ export class KadiClient {
|
|
|
2107
2102
|
* Invoke a tool on a remote agent directly (without loading the ability).
|
|
2108
2103
|
*
|
|
2109
2104
|
* This uses the KADI async invocation pattern:
|
|
2110
|
-
* 1. Send kadi.ability.request → broker
|
|
2105
|
+
* 1. Send kadi.ability.request (with timeout) → broker returns { status: 'pending', requestId }
|
|
2111
2106
|
* 2. Broker forwards request to provider agent
|
|
2112
2107
|
* 3. Provider executes tool and sends result back to broker
|
|
2113
2108
|
* 4. Broker sends kadi.ability.response notification with the actual result
|
|
@@ -2132,6 +2127,11 @@ export class KadiClient {
|
|
|
2132
2127
|
params: unknown,
|
|
2133
2128
|
options: InvokeRemoteOptions = {}
|
|
2134
2129
|
): Promise<T> {
|
|
2130
|
+
// TODO: Refactor to delegate to invokeViaBroker() from transports/broker.ts
|
|
2131
|
+
// (with no targetAgentId). This method duplicates the same requestId generation,
|
|
2132
|
+
// pendingInvocations setup, and kadi.ability.request send logic.
|
|
2133
|
+
// kadi-core-py already does this — see client.py invoke_remote() → invoke_via_broker().
|
|
2134
|
+
// Trigger: next time kadi.ability.request params change.
|
|
2135
2135
|
const { state, brokerName } = this.getConnectedBrokerState(options.broker);
|
|
2136
2136
|
const timeout = options.timeout ?? this.config.requestTimeout;
|
|
2137
2137
|
|
|
@@ -2179,12 +2179,8 @@ export class KadiClient {
|
|
|
2179
2179
|
try {
|
|
2180
2180
|
const pendingResult = await this.sendRequest<{ status: string; requestId: string }>(
|
|
2181
2181
|
state,
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
id: ++this.nextRequestId,
|
|
2185
|
-
method: 'kadi.ability.request',
|
|
2186
|
-
params: { toolName, toolInput: params, requestId },
|
|
2187
|
-
}
|
|
2182
|
+
// timeout sent so the broker can enforce a matching server-side deadline
|
|
2183
|
+
protocol.abilityRequest(++this.nextRequestId, toolName, params, requestId, timeout)
|
|
2188
2184
|
);
|
|
2189
2185
|
|
|
2190
2186
|
// Validate the broker accepted the request
|
|
@@ -2359,12 +2355,7 @@ export class KadiClient {
|
|
|
2359
2355
|
* Send a response via stdio.
|
|
2360
2356
|
*/
|
|
2361
2357
|
private sendStdioResponse(id: string | number, result: unknown): void {
|
|
2362
|
-
const
|
|
2363
|
-
jsonrpc: '2.0',
|
|
2364
|
-
id,
|
|
2365
|
-
result,
|
|
2366
|
-
};
|
|
2367
|
-
const json = JSON.stringify(response);
|
|
2358
|
+
const json = JSON.stringify(protocol.resultResponse(id, result));
|
|
2368
2359
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
2369
2360
|
}
|
|
2370
2361
|
|
|
@@ -2372,12 +2363,7 @@ export class KadiClient {
|
|
|
2372
2363
|
* Send an error response via stdio.
|
|
2373
2364
|
*/
|
|
2374
2365
|
private sendStdioError(id: string | number, error: { code: number; message: string }): void {
|
|
2375
|
-
const
|
|
2376
|
-
jsonrpc: '2.0',
|
|
2377
|
-
id,
|
|
2378
|
-
error,
|
|
2379
|
-
};
|
|
2380
|
-
const json = JSON.stringify(response);
|
|
2366
|
+
const json = JSON.stringify(protocol.errorResponse(id, error.code, error.message));
|
|
2381
2367
|
process.stdout.write(`Content-Length: ${Buffer.byteLength(json)}\r\n\r\n${json}`);
|
|
2382
2368
|
}
|
|
2383
2369
|
|
|
@@ -2412,7 +2398,7 @@ export class KadiClient {
|
|
|
2412
2398
|
/**
|
|
2413
2399
|
* Get agent information (for readAgentJson protocol).
|
|
2414
2400
|
*/
|
|
2415
|
-
readAgentJson(): { name: string; version: string; tools:
|
|
2401
|
+
readAgentJson(): { name: string; version: string; tools: BrokerToolDefinition[] } {
|
|
2416
2402
|
return {
|
|
2417
2403
|
name: this.config.name,
|
|
2418
2404
|
version: this.config.version,
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* const client = new KadiClient({
|
|
11
11
|
* name: 'my-agent',
|
|
12
12
|
* brokers: {
|
|
13
|
-
* production: 'ws://broker:8080',
|
|
13
|
+
* production: { url: 'ws://broker:8080/kadi' },
|
|
14
14
|
* },
|
|
15
15
|
* defaultBroker: 'production',
|
|
16
16
|
* });
|
|
@@ -39,10 +39,12 @@ export type { ErrorCode, ErrorContext } from './errors.js';
|
|
|
39
39
|
export type {
|
|
40
40
|
// Configuration
|
|
41
41
|
ClientConfig,
|
|
42
|
+
BrokerEntry,
|
|
42
43
|
ResolvedConfig,
|
|
43
44
|
|
|
44
45
|
// Tools
|
|
45
46
|
ToolDefinition,
|
|
47
|
+
BrokerToolDefinition,
|
|
46
48
|
ZodToolDefinition,
|
|
47
49
|
ToolHandler,
|
|
48
50
|
RegisteredTool,
|
|
@@ -67,6 +69,7 @@ export type {
|
|
|
67
69
|
BrokerState,
|
|
68
70
|
BrokerStatus,
|
|
69
71
|
PendingRequest,
|
|
72
|
+
PendingInvocation,
|
|
70
73
|
|
|
71
74
|
// Broker Events (Pub/Sub)
|
|
72
75
|
BrokerEvent,
|
|
@@ -109,6 +112,9 @@ export { loadStdioTransport } from './transports/stdio.js';
|
|
|
109
112
|
export { loadBrokerTransport } from './transports/broker.js';
|
|
110
113
|
export type { BrokerTransportOptions } from './transports/broker.js';
|
|
111
114
|
|
|
115
|
+
// Protocol message builders
|
|
116
|
+
export * as protocol from './protocol.js';
|
|
117
|
+
|
|
112
118
|
// Crypto utilities (Ed25519 to X25519 conversion)
|
|
113
119
|
export { convertToEncryptionKey, convertToEncryptionKeyPair } from './crypto.js';
|
|
114
120
|
export type { EncryptionKeyPair } from './crypto.js';
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KADI Protocol Message Builders
|
|
3
|
+
* ==============================
|
|
4
|
+
*
|
|
5
|
+
* Pure functions that construct JSON-RPC 2.0 message objects for the KADI protocol.
|
|
6
|
+
*
|
|
7
|
+
* All functions return typed objects ready for `JSON.stringify()`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import * as protocol from './protocol.js';
|
|
12
|
+
*
|
|
13
|
+
* const msg = protocol.heartbeat(1);
|
|
14
|
+
* ws.send(JSON.stringify(msg));
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Python Equivalent:
|
|
18
|
+
* kadi-core-py/src/kadi/protocol.py
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { JsonRpcNotification, JsonRpcRequest, JsonRpcResponse } from './types.js';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Generic request builder
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/** Build a standard JSON-RPC 2.0 request. */
|
|
28
|
+
export function request(id: string | number, method: string, params: unknown): JsonRpcRequest {
|
|
29
|
+
return { jsonrpc: '2.0', id, method, params };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ===========================================================================
|
|
33
|
+
// Session
|
|
34
|
+
// ===========================================================================
|
|
35
|
+
|
|
36
|
+
/** Build a `kadi.session.hello` request. */
|
|
37
|
+
export function hello(id: number): JsonRpcRequest {
|
|
38
|
+
return request(id, 'kadi.session.hello', { role: 'agent' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build a `kadi.session.authenticate` request. */
|
|
42
|
+
export function authenticate(
|
|
43
|
+
id: number,
|
|
44
|
+
publicKey: string,
|
|
45
|
+
signature: string,
|
|
46
|
+
nonce: string,
|
|
47
|
+
): JsonRpcRequest {
|
|
48
|
+
return request(id, 'kadi.session.authenticate', { publicKey, signature, nonce });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Build a `kadi.session.heartbeat` request. */
|
|
52
|
+
export function heartbeat(id: number): JsonRpcRequest {
|
|
53
|
+
return request(id, 'kadi.session.heartbeat', { timestamp: Date.now() });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ===========================================================================
|
|
57
|
+
// Agent
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
|
|
60
|
+
/** Build a `kadi.agent.register` request. */
|
|
61
|
+
export function register(
|
|
62
|
+
id: number,
|
|
63
|
+
tools: unknown[],
|
|
64
|
+
networks: string[],
|
|
65
|
+
displayName?: string,
|
|
66
|
+
): JsonRpcRequest {
|
|
67
|
+
const params: Record<string, unknown> = { tools, networks };
|
|
68
|
+
if (displayName !== undefined) {
|
|
69
|
+
params.displayName = displayName;
|
|
70
|
+
}
|
|
71
|
+
return request(id, 'kadi.agent.register', params);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
// Ability
|
|
76
|
+
// ===========================================================================
|
|
77
|
+
|
|
78
|
+
/** Build a `kadi.ability.request` request. */
|
|
79
|
+
export function abilityRequest(
|
|
80
|
+
id: number,
|
|
81
|
+
toolName: string,
|
|
82
|
+
toolInput: unknown,
|
|
83
|
+
requestId: string,
|
|
84
|
+
timeout?: number,
|
|
85
|
+
): JsonRpcRequest {
|
|
86
|
+
const params: Record<string, unknown> = { toolName, toolInput, requestId };
|
|
87
|
+
if (timeout !== undefined) {
|
|
88
|
+
params.timeout = timeout;
|
|
89
|
+
}
|
|
90
|
+
return request(id, 'kadi.ability.request', params);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Build a `kadi.ability.list` request. */
|
|
94
|
+
export function abilityList(
|
|
95
|
+
id: number,
|
|
96
|
+
networks: string[],
|
|
97
|
+
includeProviders = true,
|
|
98
|
+
): JsonRpcRequest {
|
|
99
|
+
return request(id, 'kadi.ability.list', { networks, includeProviders });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ===========================================================================
|
|
103
|
+
// Event
|
|
104
|
+
// ===========================================================================
|
|
105
|
+
|
|
106
|
+
/** Build a `kadi.event.subscribe` request. */
|
|
107
|
+
export function eventSubscribe(id: number, pattern: string): JsonRpcRequest {
|
|
108
|
+
return request(id, 'kadi.event.subscribe', { pattern });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Build a `kadi.event.unsubscribe` request. */
|
|
112
|
+
export function eventUnsubscribe(id: number, pattern: string): JsonRpcRequest {
|
|
113
|
+
return request(id, 'kadi.event.unsubscribe', { pattern });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Build a `kadi.event.publish` request. */
|
|
117
|
+
export function eventPublish(
|
|
118
|
+
id: number,
|
|
119
|
+
channel: string,
|
|
120
|
+
data: unknown,
|
|
121
|
+
networkId: string,
|
|
122
|
+
): JsonRpcRequest {
|
|
123
|
+
return request(id, 'kadi.event.publish', { channel, data, networkId });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ===========================================================================
|
|
127
|
+
// Response (JSON-RPC result/error envelopes)
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
|
|
130
|
+
/** Build a JSON-RPC 2.0 success response. */
|
|
131
|
+
export function resultResponse(id: string | number, result: unknown): JsonRpcResponse {
|
|
132
|
+
return { jsonrpc: '2.0', id, result };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Build a JSON-RPC 2.0 error response. */
|
|
136
|
+
export function errorResponse(
|
|
137
|
+
id: string | number,
|
|
138
|
+
code: number,
|
|
139
|
+
message: string,
|
|
140
|
+
data?: unknown,
|
|
141
|
+
): JsonRpcResponse {
|
|
142
|
+
const error: { code: number; message: string; data?: unknown } = { code, message };
|
|
143
|
+
if (data !== undefined) {
|
|
144
|
+
error.data = data;
|
|
145
|
+
}
|
|
146
|
+
return { jsonrpc: '2.0', id, error };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ===========================================================================
|
|
150
|
+
// Stdio
|
|
151
|
+
// ===========================================================================
|
|
152
|
+
|
|
153
|
+
/** Build a `readAgentJson` request for stdio transport. */
|
|
154
|
+
export function readAgentJson(id: number): JsonRpcRequest {
|
|
155
|
+
return request(id, 'readAgentJson', {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Build an `event` notification (no `id` field). */
|
|
159
|
+
export function eventNotification(name: string, data: unknown): JsonRpcNotification {
|
|
160
|
+
return { jsonrpc: '2.0', method: 'event', params: { name, data } };
|
|
161
|
+
}
|