@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/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
- // Auto-select first broker as default if not specified (matches Python behavior)
245
- const brokers = config.brokers ?? {};
246
- const firstBrokerName = Object.keys(brokers)[0];
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 url = this.config.brokers[brokerName];
489
- if (!url) {
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
- // Start heartbeat
534
- state.heartbeatTimer = setInterval(() => {
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
- jsonrpc: '2.0',
565
- id: this.nextRequestId++,
566
- method: 'kadi.session.authenticate',
567
- params: {
568
- publicKey: this._publicKeyBase64,
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: ToolDefinition[], networks: string[]): JsonRpcRequest {
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, this.config.networks)
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 ?? this.config.networks[0] ?? 'global';
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
- const response: JsonRpcResponse = {
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 response: JsonRpcResponse = {
1478
- jsonrpc: '2.0',
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
- const response: JsonRpcResponse = {
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
- // Restart heartbeat
1705
- state.heartbeatTimer = setInterval(() => {
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 notification = {
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): ToolDefinition[] {
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) => t.definition);
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 immediately returns { status: 'pending', requestId }
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
- jsonrpc: '2.0',
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 response: JsonRpcResponse = {
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 response: JsonRpcResponse = {
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: ToolDefinition[] } {
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';
@@ -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
+ }