agent-relay 2.1.5 → 2.1.6

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.
Files changed (117) hide show
  1. package/README.md +85 -236
  2. package/dist/index.cjs +75 -12
  3. package/package.json +18 -18
  4. package/packages/api-types/package.json +1 -1
  5. package/packages/benchmark/package.json +4 -4
  6. package/packages/bridge/package.json +8 -8
  7. package/packages/cli-tester/package.json +1 -1
  8. package/packages/config/package.json +2 -2
  9. package/packages/continuity/package.json +2 -2
  10. package/packages/daemon/dist/server.d.ts +5 -0
  11. package/packages/daemon/dist/server.d.ts.map +1 -1
  12. package/packages/daemon/dist/server.js +31 -0
  13. package/packages/daemon/dist/server.js.map +1 -1
  14. package/packages/daemon/package.json +12 -12
  15. package/packages/daemon/src/server.ts +37 -0
  16. package/packages/hooks/package.json +4 -4
  17. package/packages/mcp/dist/cloud.d.ts +7 -114
  18. package/packages/mcp/dist/cloud.d.ts.map +1 -1
  19. package/packages/mcp/dist/cloud.js +21 -431
  20. package/packages/mcp/dist/cloud.js.map +1 -1
  21. package/packages/mcp/dist/errors.d.ts +4 -22
  22. package/packages/mcp/dist/errors.d.ts.map +1 -1
  23. package/packages/mcp/dist/errors.js +4 -43
  24. package/packages/mcp/dist/errors.js.map +1 -1
  25. package/packages/mcp/dist/hybrid-client.d.ts.map +1 -1
  26. package/packages/mcp/dist/hybrid-client.js +7 -1
  27. package/packages/mcp/dist/hybrid-client.js.map +1 -1
  28. package/packages/mcp/package.json +4 -3
  29. package/packages/mcp/src/cloud.ts +29 -511
  30. package/packages/mcp/src/errors.ts +12 -49
  31. package/packages/mcp/src/hybrid-client.ts +8 -1
  32. package/packages/mcp/tests/discover.test.ts +72 -11
  33. package/packages/memory/package.json +2 -2
  34. package/packages/policy/package.json +2 -2
  35. package/packages/protocol/dist/types.d.ts +17 -1
  36. package/packages/protocol/dist/types.d.ts.map +1 -1
  37. package/packages/protocol/package.json +1 -1
  38. package/packages/protocol/src/types.ts +23 -0
  39. package/packages/resiliency/package.json +1 -1
  40. package/packages/sdk/dist/browser-client.d.ts +212 -0
  41. package/packages/sdk/dist/browser-client.d.ts.map +1 -0
  42. package/packages/sdk/dist/browser-client.js +750 -0
  43. package/packages/sdk/dist/browser-client.js.map +1 -0
  44. package/packages/sdk/dist/browser-framing.d.ts +46 -0
  45. package/packages/sdk/dist/browser-framing.d.ts.map +1 -0
  46. package/packages/sdk/dist/browser-framing.js +122 -0
  47. package/packages/sdk/dist/browser-framing.js.map +1 -0
  48. package/packages/sdk/dist/client.d.ts +129 -2
  49. package/packages/sdk/dist/client.d.ts.map +1 -1
  50. package/packages/sdk/dist/client.js +312 -2
  51. package/packages/sdk/dist/client.js.map +1 -1
  52. package/packages/sdk/dist/discovery.d.ts +10 -0
  53. package/packages/sdk/dist/discovery.d.ts.map +1 -0
  54. package/packages/sdk/dist/discovery.js +22 -0
  55. package/packages/sdk/dist/discovery.js.map +1 -0
  56. package/packages/sdk/dist/errors.d.ts +9 -0
  57. package/packages/sdk/dist/errors.d.ts.map +1 -0
  58. package/packages/sdk/dist/errors.js +9 -0
  59. package/packages/sdk/dist/errors.js.map +1 -0
  60. package/packages/sdk/dist/index.d.ts +18 -2
  61. package/packages/sdk/dist/index.d.ts.map +1 -1
  62. package/packages/sdk/dist/index.js +27 -1
  63. package/packages/sdk/dist/index.js.map +1 -1
  64. package/packages/sdk/dist/transports/index.d.ts +92 -0
  65. package/packages/sdk/dist/transports/index.d.ts.map +1 -0
  66. package/packages/sdk/dist/transports/index.js +129 -0
  67. package/packages/sdk/dist/transports/index.js.map +1 -0
  68. package/packages/sdk/dist/transports/socket-transport.d.ts +30 -0
  69. package/packages/sdk/dist/transports/socket-transport.d.ts.map +1 -0
  70. package/packages/sdk/dist/transports/socket-transport.js +94 -0
  71. package/packages/sdk/dist/transports/socket-transport.js.map +1 -0
  72. package/packages/sdk/dist/transports/types.d.ts +69 -0
  73. package/packages/sdk/dist/transports/types.d.ts.map +1 -0
  74. package/packages/sdk/dist/transports/types.js +10 -0
  75. package/packages/sdk/dist/transports/types.js.map +1 -0
  76. package/packages/sdk/dist/transports/websocket-transport.d.ts +55 -0
  77. package/packages/sdk/dist/transports/websocket-transport.d.ts.map +1 -0
  78. package/packages/sdk/dist/transports/websocket-transport.js +180 -0
  79. package/packages/sdk/dist/transports/websocket-transport.js.map +1 -0
  80. package/packages/sdk/package.json +28 -4
  81. package/packages/sdk/src/browser-client.ts +985 -0
  82. package/packages/sdk/src/browser-framing.test.ts +115 -0
  83. package/packages/sdk/src/browser-framing.ts +150 -0
  84. package/packages/sdk/src/client.test.ts +425 -0
  85. package/packages/sdk/src/client.ts +397 -3
  86. package/packages/sdk/src/discovery.ts +38 -0
  87. package/packages/sdk/src/errors.ts +17 -0
  88. package/packages/sdk/src/index.ts +82 -1
  89. package/packages/sdk/src/transports/index.ts +197 -0
  90. package/packages/sdk/src/transports/socket-transport.ts +115 -0
  91. package/packages/sdk/src/transports/types.ts +77 -0
  92. package/packages/sdk/src/transports/websocket-transport.ts +245 -0
  93. package/packages/sdk/tsconfig.json +1 -1
  94. package/packages/spawner/package.json +1 -1
  95. package/packages/state/package.json +1 -1
  96. package/packages/storage/package.json +2 -2
  97. package/packages/storage/src/jsonl-adapter.test.ts +8 -3
  98. package/packages/telemetry/package.json +1 -1
  99. package/packages/trajectory/package.json +2 -2
  100. package/packages/user-directory/package.json +2 -2
  101. package/packages/utils/dist/cjs/discovery.js +328 -0
  102. package/packages/utils/dist/cjs/errors.js +81 -0
  103. package/packages/utils/dist/discovery.d.ts +123 -0
  104. package/packages/utils/dist/discovery.d.ts.map +1 -0
  105. package/packages/utils/dist/discovery.js +439 -0
  106. package/packages/utils/dist/discovery.js.map +1 -0
  107. package/packages/utils/dist/errors.d.ts +29 -0
  108. package/packages/utils/dist/errors.d.ts.map +1 -0
  109. package/packages/utils/dist/errors.js +50 -0
  110. package/packages/utils/dist/errors.js.map +1 -0
  111. package/packages/utils/package.json +15 -2
  112. package/packages/utils/src/consolidation.test.ts +125 -0
  113. package/packages/utils/src/discovery.test.ts +196 -0
  114. package/packages/utils/src/discovery.ts +524 -0
  115. package/packages/utils/src/errors.test.ts +83 -0
  116. package/packages/utils/src/errors.ts +56 -0
  117. package/packages/wrapper/package.json +6 -6
@@ -7,6 +7,8 @@
7
7
 
8
8
  import net from 'node:net';
9
9
  import { randomUUID } from 'node:crypto';
10
+ import { discoverSocket } from '@agent-relay/utils/discovery';
11
+ import { DaemonNotRunningError, ConnectionError } from '@agent-relay/utils/errors';
10
12
  // Import shared protocol types and framing utilities from @agent-relay/protocol
11
13
  import {
12
14
  type Envelope,
@@ -52,6 +54,7 @@ import {
52
54
  type MetricsResponsePayload,
53
55
  type CreateProposalOptions,
54
56
  type VoteOptions,
57
+ type AgentReadyPayload,
55
58
  PROTOCOL_VERSION,
56
59
  encodeFrameLegacy,
57
60
  FrameParser,
@@ -66,6 +69,50 @@ export interface SyncOptions {
66
69
  thread?: string;
67
70
  }
68
71
 
72
+ /**
73
+ * Options for the request() method.
74
+ */
75
+ export interface RequestOptions {
76
+ /** Timeout in milliseconds (default: 30000) */
77
+ timeout?: number;
78
+ /** Optional structured data to include with the request */
79
+ data?: Record<string, unknown>;
80
+ /** Optional thread identifier for grouping related messages */
81
+ thread?: string;
82
+ /** Message kind (default: 'message') */
83
+ kind?: PayloadKind;
84
+ }
85
+
86
+ /**
87
+ * Response from the request() method.
88
+ */
89
+ export interface RequestResponse {
90
+ /** Sender of the response */
91
+ from: string;
92
+ /** Response body text */
93
+ body: string;
94
+ /** Optional structured data from the response */
95
+ data?: Record<string, unknown>;
96
+ /** The correlation ID used for this request/response */
97
+ correlationId: string;
98
+ /** Thread identifier if set */
99
+ thread?: string;
100
+ /** The full payload for advanced use cases */
101
+ payload: SendPayload;
102
+ }
103
+
104
+ /**
105
+ * Extended spawn result with optional readiness information.
106
+ * When `waitForReady` is true, the spawn will wait for the agent to complete
107
+ * its HELLO/WELCOME handshake before resolving.
108
+ */
109
+ export interface SpawnResult extends SpawnResultPayload {
110
+ /** Whether the agent is ready to receive messages (only set when waitForReady is true) */
111
+ ready?: boolean;
112
+ /** Agent ready details (only set when waitForReady is true and spawn succeeded) */
113
+ readyInfo?: AgentReadyPayload;
114
+ }
115
+
69
116
  export interface ClientConfig {
70
117
  /** Daemon socket path (default: /tmp/agent-relay.sock) */
71
118
  socketPath: string;
@@ -107,6 +154,16 @@ export interface ClientConfig {
107
154
 
108
155
  const DEFAULT_SOCKET_PATH = '/tmp/agent-relay.sock';
109
156
 
157
+ /**
158
+ * Resolve the socket path using discovery if not explicitly provided.
159
+ * Falls back to /tmp/agent-relay.sock if discovery fails.
160
+ */
161
+ function resolveSocketPath(configPath?: string): string {
162
+ if (configPath) return configPath;
163
+ const discovery = discoverSocket();
164
+ return discovery?.socketPath || DEFAULT_SOCKET_PATH;
165
+ }
166
+
110
167
  const DEFAULT_CLIENT_CONFIG: ClientConfig = {
111
168
  socketPath: DEFAULT_SOCKET_PATH,
112
169
  agentName: 'agent',
@@ -204,6 +261,19 @@ export class RelayClient {
204
261
  timeoutHandle: NodeJS.Timeout;
205
262
  }> = new Map();
206
263
 
264
+ private pendingRequests: Map<string, {
265
+ resolve: (response: RequestResponse) => void;
266
+ reject: (err: Error) => void;
267
+ timeoutHandle: NodeJS.Timeout;
268
+ targetAgent: string;
269
+ }> = new Map();
270
+
271
+ private pendingAgentReady: Map<string, {
272
+ resolve: (info: AgentReadyPayload) => void;
273
+ reject: (err: Error) => void;
274
+ timeoutHandle: NodeJS.Timeout;
275
+ }> = new Map();
276
+
207
277
  // Event handlers
208
278
  onMessage?: (from: string, payload: SendPayload, messageId: string, meta?: SendMeta, originalTo?: string) => void;
209
279
  /**
@@ -212,9 +282,19 @@ export class RelayClient {
212
282
  onChannelMessage?: (from: string, channel: string, body: string, envelope: Envelope<ChannelMessagePayload>) => void;
213
283
  onStateChange?: (state: ClientState) => void;
214
284
  onError?: (error: Error) => void;
285
+ /**
286
+ * Callback when an agent becomes ready (completes HELLO/WELCOME handshake).
287
+ * This is broadcast by the daemon when any agent connects.
288
+ * Useful for knowing when a spawned agent is ready to receive messages.
289
+ */
290
+ onAgentReady?: (info: AgentReadyPayload) => void;
215
291
 
216
292
  constructor(config: Partial<ClientConfig> = {}) {
217
293
  this.config = { ...DEFAULT_CLIENT_CONFIG, ...config };
294
+ // Use socket discovery if no explicit socketPath was provided
295
+ if (!config.socketPath) {
296
+ this.config.socketPath = resolveSocketPath();
297
+ }
218
298
  this.parser = new FrameParser();
219
299
  this.parser.setLegacyMode(true);
220
300
  this.reconnectDelay = this.config.reconnectDelayMs;
@@ -268,7 +348,12 @@ export class RelayClient {
268
348
 
269
349
  this.socket.on('error', (err) => {
270
350
  if (this._state === 'CONNECTING') {
271
- settleReject(err);
351
+ const errno = (err as NodeJS.ErrnoException).code;
352
+ if (errno === 'ECONNREFUSED' || errno === 'ENOENT') {
353
+ settleReject(new DaemonNotRunningError(`Cannot connect to daemon at ${this.config.socketPath}`));
354
+ } else {
355
+ settleReject(new ConnectionError(err.message));
356
+ }
272
357
  }
273
358
  this.handleError(err);
274
359
  });
@@ -425,6 +510,124 @@ export class RelayClient {
425
510
  });
426
511
  }
427
512
 
513
+ /**
514
+ * Send a request to another agent and wait for their response.
515
+ *
516
+ * This implements a request/response pattern where the message is sent with
517
+ * a correlation ID, and the method waits for the target agent to respond
518
+ * with a message containing that correlation ID.
519
+ *
520
+ * @example
521
+ * ```typescript
522
+ * // Simple request
523
+ * const response = await client.request('Worker', 'Process this task');
524
+ * console.log(response.body); // Worker's response
525
+ *
526
+ * // With options
527
+ * const response = await client.request('Worker', 'Process task', {
528
+ * timeout: 60000,
529
+ * data: { taskId: '123', priority: 'high' },
530
+ * thread: 'task-thread-1',
531
+ * });
532
+ * ```
533
+ *
534
+ * @param to - Target agent name
535
+ * @param body - Request message body
536
+ * @param options - Request options (timeout, data, thread, kind)
537
+ * @returns Promise that resolves with the response from the target agent
538
+ * @throws Error if client is not ready, send fails, timeout occurs, or agent disconnects
539
+ */
540
+ async request(to: string, body: string, options: RequestOptions = {}): Promise<RequestResponse> {
541
+ if (this._state !== 'READY') {
542
+ throw new Error('Client not ready');
543
+ }
544
+
545
+ const correlationId = randomUUID();
546
+ const timeoutMs = options.timeout ?? 30000;
547
+ const kind = options.kind ?? 'message';
548
+
549
+ return new Promise<RequestResponse>((resolve, reject) => {
550
+ const timeoutHandle = setTimeout(() => {
551
+ this.pendingRequests.delete(correlationId);
552
+ reject(new Error(`Request timeout after ${timeoutMs}ms waiting for response from ${to}`));
553
+ }, timeoutMs);
554
+
555
+ this.pendingRequests.set(correlationId, {
556
+ resolve,
557
+ reject,
558
+ timeoutHandle,
559
+ targetAgent: to,
560
+ });
561
+
562
+ const envelope: SendEnvelope = {
563
+ v: PROTOCOL_VERSION,
564
+ type: 'SEND',
565
+ id: generateId(),
566
+ ts: Date.now(),
567
+ to,
568
+ payload: {
569
+ kind,
570
+ body,
571
+ data: {
572
+ ...options.data,
573
+ _correlationId: correlationId,
574
+ },
575
+ thread: options.thread,
576
+ },
577
+ payload_meta: {
578
+ replyTo: correlationId,
579
+ },
580
+ };
581
+
582
+ const sent = this.send(envelope);
583
+ if (!sent) {
584
+ clearTimeout(timeoutHandle);
585
+ this.pendingRequests.delete(correlationId);
586
+ reject(new Error('Failed to send request'));
587
+ }
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Respond to a request from another agent.
593
+ *
594
+ * This is a convenience method for responding to messages that have a
595
+ * correlation ID. The response will be routed back to the requesting agent.
596
+ *
597
+ * @param correlationId - The correlation ID from the original request (from data._correlationId or meta.replyTo)
598
+ * @param to - Target agent (the one who sent the original request)
599
+ * @param body - Response body
600
+ * @param data - Optional structured data to include in the response
601
+ * @returns true if the message was sent
602
+ */
603
+ respond(correlationId: string, to: string, body: string, data?: Record<string, unknown>): boolean {
604
+ if (this._state !== 'READY') {
605
+ return false;
606
+ }
607
+
608
+ const envelope: SendEnvelope = {
609
+ v: PROTOCOL_VERSION,
610
+ type: 'SEND',
611
+ id: generateId(),
612
+ ts: Date.now(),
613
+ to,
614
+ payload: {
615
+ kind: 'message',
616
+ body,
617
+ data: {
618
+ ...data,
619
+ _correlationId: correlationId,
620
+ _isResponse: true,
621
+ },
622
+ },
623
+ payload_meta: {
624
+ replyTo: correlationId,
625
+ },
626
+ };
627
+
628
+ return this.send(envelope);
629
+ }
630
+
428
631
  /**
429
632
  * Broadcast a message to all agents.
430
633
  */
@@ -545,7 +748,10 @@ export class RelayClient {
545
748
  * @param options.interactive - Interactive mode
546
749
  * @param options.shadowOf - Spawn as shadow of this agent
547
750
  * @param options.shadowSpeakOn - Shadow speak-on triggers
751
+ * @param options.waitForReady - Wait for the agent to be ready before resolving (default: false)
752
+ * @param options.readyTimeoutMs - Timeout for agent to become ready (default: 60000ms)
548
753
  * @param timeoutMs - Timeout for spawn operation (default: 30000ms)
754
+ * @returns Spawn result. When waitForReady is true, includes `ready` and `readyInfo` fields.
549
755
  */
550
756
  async spawn(
551
757
  options: {
@@ -557,18 +763,52 @@ export class RelayClient {
557
763
  interactive?: boolean;
558
764
  shadowOf?: string;
559
765
  shadowSpeakOn?: SpeakOnTrigger[];
766
+ /** Wait for the agent to complete connection before resolving */
767
+ waitForReady?: boolean;
768
+ /** Timeout for agent to become ready (default: 60000ms). Only used when waitForReady is true. */
769
+ readyTimeoutMs?: number;
560
770
  },
561
771
  timeoutMs = 30000
562
- ): Promise<SpawnResultPayload> {
772
+ ): Promise<SpawnResult> {
563
773
  if (this._state !== 'READY') {
564
774
  throw new Error('Client not ready');
565
775
  }
566
776
 
567
777
  const envelopeId = generateId();
778
+ const waitForReady = options.waitForReady ?? false;
779
+ const readyTimeoutMs = options.readyTimeoutMs ?? 60000;
780
+
781
+ // If waitForReady, set up the agent ready listener BEFORE spawning
782
+ // This ensures we don't miss the AGENT_READY event if it arrives quickly
783
+ let readyPromise: Promise<AgentReadyPayload> | undefined;
784
+ if (waitForReady) {
785
+ // Check if we're already waiting for this agent (prevents overwriting existing waiter)
786
+ if (this.pendingAgentReady.has(options.name)) {
787
+ throw new Error(`Already waiting for agent ${options.name} to be ready`);
788
+ }
568
789
 
569
- return new Promise<SpawnResultPayload>((resolve, reject) => {
790
+ readyPromise = new Promise<AgentReadyPayload>((resolve, reject) => {
791
+ const timeoutHandle = setTimeout(() => {
792
+ this.pendingAgentReady.delete(options.name);
793
+ reject(new Error(`Agent ${options.name} did not become ready within ${readyTimeoutMs}ms`));
794
+ }, readyTimeoutMs);
795
+
796
+ this.pendingAgentReady.set(options.name, { resolve, reject, timeoutHandle });
797
+ });
798
+ }
799
+
800
+ // Send the spawn request
801
+ const spawnResult = await new Promise<SpawnResultPayload>((resolve, reject) => {
570
802
  const timeoutHandle = setTimeout(() => {
571
803
  this.pendingSpawns.delete(envelopeId);
804
+ // Also clean up pending agent ready if spawn times out
805
+ if (waitForReady) {
806
+ const pending = this.pendingAgentReady.get(options.name);
807
+ if (pending) {
808
+ clearTimeout(pending.timeoutHandle);
809
+ this.pendingAgentReady.delete(options.name);
810
+ }
811
+ }
572
812
  reject(new Error(`Spawn timeout after ${timeoutMs}ms`));
573
813
  }, timeoutMs);
574
814
 
@@ -596,9 +836,88 @@ export class RelayClient {
596
836
  if (!sent) {
597
837
  clearTimeout(timeoutHandle);
598
838
  this.pendingSpawns.delete(envelopeId);
839
+ // Also clean up pending agent ready if send fails
840
+ if (waitForReady) {
841
+ const pending = this.pendingAgentReady.get(options.name);
842
+ if (pending) {
843
+ clearTimeout(pending.timeoutHandle);
844
+ this.pendingAgentReady.delete(options.name);
845
+ }
846
+ }
599
847
  reject(new Error('Failed to send spawn message'));
600
848
  }
601
849
  });
850
+
851
+ // If spawn failed or we don't need to wait for ready, return immediately
852
+ if (!spawnResult.success || !waitForReady || !readyPromise) {
853
+ // Clean up pending agent ready if spawn failed
854
+ if (!spawnResult.success && waitForReady) {
855
+ const pending = this.pendingAgentReady.get(options.name);
856
+ if (pending) {
857
+ clearTimeout(pending.timeoutHandle);
858
+ this.pendingAgentReady.delete(options.name);
859
+ }
860
+ }
861
+ return spawnResult;
862
+ }
863
+
864
+ // Wait for the agent to become ready
865
+ try {
866
+ const readyInfo = await readyPromise;
867
+ return {
868
+ ...spawnResult,
869
+ ready: true,
870
+ readyInfo,
871
+ };
872
+ } catch (err) {
873
+ // Agent spawned but didn't become ready in time
874
+ // Return the spawn result with ready: false
875
+ return {
876
+ ...spawnResult,
877
+ ready: false,
878
+ };
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Wait for an agent to become ready (complete HELLO/WELCOME handshake).
884
+ * This is useful when you want to wait for an agent that was spawned through
885
+ * another mechanism, or to verify an agent is connected before sending messages.
886
+ *
887
+ * @example
888
+ * ```typescript
889
+ * // Wait for an agent that might be spawning
890
+ * try {
891
+ * const readyInfo = await client.waitForAgentReady('Worker', 30000);
892
+ * console.log(`Worker is ready: ${readyInfo.cli}`);
893
+ * } catch (err) {
894
+ * console.error('Worker did not become ready in time');
895
+ * }
896
+ * ```
897
+ *
898
+ * @param name - Agent name to wait for
899
+ * @param timeoutMs - Timeout in milliseconds (default: 60000ms)
900
+ * @returns Promise that resolves with AgentReadyPayload when the agent connects
901
+ * @throws Error if the agent doesn't become ready within the timeout
902
+ */
903
+ async waitForAgentReady(name: string, timeoutMs = 60000): Promise<AgentReadyPayload> {
904
+ if (this._state !== 'READY') {
905
+ throw new Error('Client not ready');
906
+ }
907
+
908
+ // Check if we're already waiting for this agent
909
+ if (this.pendingAgentReady.has(name)) {
910
+ throw new Error(`Already waiting for agent ${name} to be ready`);
911
+ }
912
+
913
+ return new Promise<AgentReadyPayload>((resolve, reject) => {
914
+ const timeoutHandle = setTimeout(() => {
915
+ this.pendingAgentReady.delete(name);
916
+ reject(new Error(`Agent ${name} did not become ready within ${timeoutMs}ms`));
917
+ }, timeoutMs);
918
+
919
+ this.pendingAgentReady.set(name, { resolve, reject, timeoutHandle });
920
+ });
602
921
  }
603
922
 
604
923
  /**
@@ -1169,6 +1488,10 @@ export class RelayClient {
1169
1488
  this.handleReleaseResult(envelope as Envelope<ReleaseResultPayload>);
1170
1489
  break;
1171
1490
 
1491
+ case 'AGENT_READY':
1492
+ this.handleAgentReady(envelope as Envelope<AgentReadyPayload>);
1493
+ break;
1494
+
1172
1495
  case 'ERROR':
1173
1496
  this.handleErrorFrame(envelope as Envelope<ErrorPayload>);
1174
1497
  break;
@@ -1221,6 +1544,26 @@ export class RelayClient {
1221
1544
  return;
1222
1545
  }
1223
1546
 
1547
+ // Check if this is a response to a pending request
1548
+ const correlationId = this.extractCorrelationId(envelope);
1549
+ if (correlationId && envelope.from) {
1550
+ const pending = this.pendingRequests.get(correlationId);
1551
+ if (pending) {
1552
+ // This is a response to our request
1553
+ clearTimeout(pending.timeoutHandle);
1554
+ this.pendingRequests.delete(correlationId);
1555
+ pending.resolve({
1556
+ from: envelope.from,
1557
+ body: envelope.payload.body,
1558
+ data: envelope.payload.data,
1559
+ correlationId,
1560
+ thread: envelope.payload.thread,
1561
+ payload: envelope.payload,
1562
+ });
1563
+ // Still call onMessage so the app is aware of the response if needed
1564
+ }
1565
+ }
1566
+
1224
1567
  if (this.onMessage && envelope.from) {
1225
1568
  this.onMessage(
1226
1569
  envelope.from,
@@ -1232,6 +1575,22 @@ export class RelayClient {
1232
1575
  }
1233
1576
  }
1234
1577
 
1578
+ /**
1579
+ * Extract correlation ID from a delivered message.
1580
+ * Checks both payload_meta.replyTo and payload.data._correlationId
1581
+ */
1582
+ private extractCorrelationId(envelope: DeliverEnvelope): string | undefined {
1583
+ // Check payload_meta.replyTo first (the preferred location)
1584
+ if (envelope.payload_meta?.replyTo) {
1585
+ return envelope.payload_meta.replyTo;
1586
+ }
1587
+ // Fall back to checking data._correlationId
1588
+ if (envelope.payload.data && typeof envelope.payload.data._correlationId === 'string') {
1589
+ return envelope.payload.data._correlationId;
1590
+ }
1591
+ return undefined;
1592
+ }
1593
+
1235
1594
  private handleChannelMessage(envelope: Envelope<ChannelMessagePayload> & { from?: string }): void {
1236
1595
  const duplicate = this.dedupeCache.check(envelope.id);
1237
1596
  if (duplicate) {
@@ -1300,6 +1659,23 @@ export class RelayClient {
1300
1659
  pending.resolve(envelope.payload);
1301
1660
  }
1302
1661
 
1662
+ private handleAgentReady(envelope: Envelope<AgentReadyPayload>): void {
1663
+ const agentName = envelope.payload.name;
1664
+
1665
+ // Resolve any pending waitForReady promises for this agent
1666
+ const pending = this.pendingAgentReady.get(agentName);
1667
+ if (pending) {
1668
+ clearTimeout(pending.timeoutHandle);
1669
+ this.pendingAgentReady.delete(agentName);
1670
+ pending.resolve(envelope.payload);
1671
+ }
1672
+
1673
+ // Call the onAgentReady callback if registered
1674
+ if (this.onAgentReady) {
1675
+ this.onAgentReady(envelope.payload);
1676
+ }
1677
+ }
1678
+
1303
1679
  private handleQueryResponse(envelope: Envelope): void {
1304
1680
  // Query responses use the envelope id to match requests
1305
1681
  const pending = this.pendingQueries.get(envelope.id);
@@ -1346,6 +1722,8 @@ export class RelayClient {
1346
1722
  this.rejectPendingSpawns(new Error('Disconnected while awaiting spawn result'));
1347
1723
  this.rejectPendingReleases(new Error('Disconnected while awaiting release result'));
1348
1724
  this.rejectPendingQueries(new Error('Disconnected while awaiting query response'));
1725
+ this.rejectPendingRequests(new Error('Disconnected while awaiting request response'));
1726
+ this.rejectPendingAgentReady(new Error('Disconnected while awaiting agent ready'));
1349
1727
 
1350
1728
  if (this._destroyed) {
1351
1729
  this.setState('DISCONNECTED');
@@ -1405,6 +1783,22 @@ export class RelayClient {
1405
1783
  }
1406
1784
  }
1407
1785
 
1786
+ private rejectPendingRequests(error: Error): void {
1787
+ for (const [correlationId, pending] of this.pendingRequests.entries()) {
1788
+ clearTimeout(pending.timeoutHandle);
1789
+ pending.reject(error);
1790
+ this.pendingRequests.delete(correlationId);
1791
+ }
1792
+ }
1793
+
1794
+ private rejectPendingAgentReady(error: Error): void {
1795
+ for (const [agentName, pending] of this.pendingAgentReady.entries()) {
1796
+ clearTimeout(pending.timeoutHandle);
1797
+ pending.reject(error);
1798
+ this.pendingAgentReady.delete(agentName);
1799
+ }
1800
+ }
1801
+
1408
1802
  private scheduleReconnect(): void {
1409
1803
  this.setState('BACKOFF');
1410
1804
  this.reconnectAttempts++;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Socket Discovery & Cloud Workspace Detection
3
+ *
4
+ * Re-exports all discovery functionality from @agent-relay/utils,
5
+ * which is the single source of truth. This module exists so SDK
6
+ * consumers can import discovery from either '@agent-relay/sdk'
7
+ * or '@agent-relay/sdk/discovery'.
8
+ */
9
+
10
+ export {
11
+ // Types
12
+ type CloudWorkspace,
13
+ type DiscoveryResult,
14
+ type CloudConnectionOptions,
15
+ type CloudConnectionInfo,
16
+
17
+ // Cloud workspace detection
18
+ detectCloudWorkspace,
19
+ isCloudWorkspace,
20
+
21
+ // Socket discovery
22
+ getCloudSocketPath,
23
+ getCloudOutboxPath,
24
+ discoverSocket,
25
+
26
+ // Cloud API helpers
27
+ cloudApiRequest,
28
+ getWorkspaceStatus,
29
+
30
+ // Connection factory
31
+ getConnectionInfo,
32
+
33
+ // Debug helpers
34
+ getCloudEnvironmentSummary,
35
+
36
+ // Agent identity
37
+ discoverAgentName,
38
+ } from '@agent-relay/utils/discovery';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Error Types for Agent Relay
3
+ *
4
+ * Re-exports error classes from @agent-relay/utils, which is the single
5
+ * source of truth. This module exists so SDK consumers can import errors
6
+ * from either '@agent-relay/sdk' or '@agent-relay/sdk/errors'.
7
+ */
8
+
9
+ export {
10
+ RelayError,
11
+ DaemonNotRunningError,
12
+ AgentNotFoundError,
13
+ TimeoutError,
14
+ ConnectionError,
15
+ ChannelNotFoundError,
16
+ SpawnError,
17
+ } from '@agent-relay/utils/errors';
@@ -24,16 +24,66 @@
24
24
  * const client = new RelayClient({ agentName: 'MyAgent' });
25
25
  * await client.connect();
26
26
  * ```
27
+ *
28
+ * ## Browser Usage (WebSocket)
29
+ *
30
+ * ```typescript
31
+ * import { BrowserRelayClient } from '@agent-relay/sdk';
32
+ *
33
+ * const client = new BrowserRelayClient({
34
+ * agentName: 'BrowserAgent',
35
+ * transport: { wsUrl: 'wss://relay.example.com/ws' },
36
+ * });
37
+ * await client.connect();
38
+ * ```
27
39
  */
28
40
 
29
- // Main client
41
+ // Main client (Node.js, Unix sockets)
30
42
  export {
31
43
  RelayClient,
32
44
  type ClientState,
33
45
  type ClientConfig,
34
46
  type SyncOptions,
47
+ type RequestOptions,
48
+ type RequestResponse,
49
+ type SpawnResult,
35
50
  } from './client.js';
36
51
 
52
+ // Browser-compatible client (WebSocket)
53
+ export {
54
+ BrowserRelayClient,
55
+ type BrowserClientState,
56
+ type BrowserClientConfig,
57
+ type BrowserRequestOptions,
58
+ type BrowserRequestResponse,
59
+ } from './browser-client.js';
60
+
61
+ // Transport abstractions
62
+ export {
63
+ // Types
64
+ type Transport,
65
+ type TransportConfig,
66
+ type TransportEvents,
67
+ type TransportState,
68
+ type TransportFactory,
69
+ // Socket transport (Node.js)
70
+ SocketTransport,
71
+ createSocketTransport,
72
+ type SocketTransportConfig,
73
+ // WebSocket transport (Browser + Node.js)
74
+ WebSocketTransport,
75
+ createWebSocketTransport,
76
+ socketPathToWsUrl,
77
+ type WebSocketTransportConfig,
78
+ // Auto-detection
79
+ createAutoTransport,
80
+ type AutoTransportOptions,
81
+ detectEnvironment,
82
+ type EnvironmentInfo,
83
+ isBrowser,
84
+ isNode,
85
+ } from './transports/index.js';
86
+
37
87
  // Standalone relay (in-process daemon for simple use cases)
38
88
  export {
39
89
  createRelay,
@@ -63,6 +113,8 @@ export {
63
113
  type SpawnResultPayload,
64
114
  type ReleasePayload,
65
115
  type ReleaseResultPayload,
116
+ // Agent lifecycle types
117
+ type AgentReadyPayload,
66
118
  // Channel types
67
119
  type ChannelMessagePayload,
68
120
  type ChannelJoinPayload,
@@ -101,3 +153,32 @@ export {
101
153
  type GetLogsOptions,
102
154
  type LogsResult,
103
155
  } from './logs.js';
156
+
157
+ // Discovery (socket discovery, cloud workspace detection, agent identity)
158
+ export {
159
+ discoverSocket,
160
+ discoverAgentName,
161
+ detectCloudWorkspace,
162
+ isCloudWorkspace,
163
+ getCloudSocketPath,
164
+ getCloudOutboxPath,
165
+ getConnectionInfo,
166
+ getCloudEnvironmentSummary,
167
+ cloudApiRequest,
168
+ getWorkspaceStatus,
169
+ type DiscoveryResult,
170
+ type CloudWorkspace,
171
+ type CloudConnectionOptions,
172
+ type CloudConnectionInfo,
173
+ } from './discovery.js';
174
+
175
+ // Error types
176
+ export {
177
+ RelayError,
178
+ DaemonNotRunningError,
179
+ AgentNotFoundError,
180
+ TimeoutError,
181
+ ConnectionError,
182
+ ChannelNotFoundError,
183
+ SpawnError,
184
+ } from './errors.js';