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.
- package/README.md +85 -236
- package/dist/index.cjs +75 -12
- package/package.json +18 -18
- package/packages/api-types/package.json +1 -1
- package/packages/benchmark/package.json +4 -4
- package/packages/bridge/package.json +8 -8
- package/packages/cli-tester/package.json +1 -1
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/dist/server.d.ts +5 -0
- package/packages/daemon/dist/server.d.ts.map +1 -1
- package/packages/daemon/dist/server.js +31 -0
- package/packages/daemon/dist/server.js.map +1 -1
- package/packages/daemon/package.json +12 -12
- package/packages/daemon/src/server.ts +37 -0
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/dist/cloud.d.ts +7 -114
- package/packages/mcp/dist/cloud.d.ts.map +1 -1
- package/packages/mcp/dist/cloud.js +21 -431
- package/packages/mcp/dist/cloud.js.map +1 -1
- package/packages/mcp/dist/errors.d.ts +4 -22
- package/packages/mcp/dist/errors.d.ts.map +1 -1
- package/packages/mcp/dist/errors.js +4 -43
- package/packages/mcp/dist/errors.js.map +1 -1
- package/packages/mcp/dist/hybrid-client.d.ts.map +1 -1
- package/packages/mcp/dist/hybrid-client.js +7 -1
- package/packages/mcp/dist/hybrid-client.js.map +1 -1
- package/packages/mcp/package.json +4 -3
- package/packages/mcp/src/cloud.ts +29 -511
- package/packages/mcp/src/errors.ts +12 -49
- package/packages/mcp/src/hybrid-client.ts +8 -1
- package/packages/mcp/tests/discover.test.ts +72 -11
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/dist/types.d.ts +17 -1
- package/packages/protocol/dist/types.d.ts.map +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/types.ts +23 -0
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/dist/browser-client.d.ts +212 -0
- package/packages/sdk/dist/browser-client.d.ts.map +1 -0
- package/packages/sdk/dist/browser-client.js +750 -0
- package/packages/sdk/dist/browser-client.js.map +1 -0
- package/packages/sdk/dist/browser-framing.d.ts +46 -0
- package/packages/sdk/dist/browser-framing.d.ts.map +1 -0
- package/packages/sdk/dist/browser-framing.js +122 -0
- package/packages/sdk/dist/browser-framing.js.map +1 -0
- package/packages/sdk/dist/client.d.ts +129 -2
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +312 -2
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/discovery.d.ts +10 -0
- package/packages/sdk/dist/discovery.d.ts.map +1 -0
- package/packages/sdk/dist/discovery.js +22 -0
- package/packages/sdk/dist/discovery.js.map +1 -0
- package/packages/sdk/dist/errors.d.ts +9 -0
- package/packages/sdk/dist/errors.d.ts.map +1 -0
- package/packages/sdk/dist/errors.js +9 -0
- package/packages/sdk/dist/errors.js.map +1 -0
- package/packages/sdk/dist/index.d.ts +18 -2
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +27 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/transports/index.d.ts +92 -0
- package/packages/sdk/dist/transports/index.d.ts.map +1 -0
- package/packages/sdk/dist/transports/index.js +129 -0
- package/packages/sdk/dist/transports/index.js.map +1 -0
- package/packages/sdk/dist/transports/socket-transport.d.ts +30 -0
- package/packages/sdk/dist/transports/socket-transport.d.ts.map +1 -0
- package/packages/sdk/dist/transports/socket-transport.js +94 -0
- package/packages/sdk/dist/transports/socket-transport.js.map +1 -0
- package/packages/sdk/dist/transports/types.d.ts +69 -0
- package/packages/sdk/dist/transports/types.d.ts.map +1 -0
- package/packages/sdk/dist/transports/types.js +10 -0
- package/packages/sdk/dist/transports/types.js.map +1 -0
- package/packages/sdk/dist/transports/websocket-transport.d.ts +55 -0
- package/packages/sdk/dist/transports/websocket-transport.d.ts.map +1 -0
- package/packages/sdk/dist/transports/websocket-transport.js +180 -0
- package/packages/sdk/dist/transports/websocket-transport.js.map +1 -0
- package/packages/sdk/package.json +28 -4
- package/packages/sdk/src/browser-client.ts +985 -0
- package/packages/sdk/src/browser-framing.test.ts +115 -0
- package/packages/sdk/src/browser-framing.ts +150 -0
- package/packages/sdk/src/client.test.ts +425 -0
- package/packages/sdk/src/client.ts +397 -3
- package/packages/sdk/src/discovery.ts +38 -0
- package/packages/sdk/src/errors.ts +17 -0
- package/packages/sdk/src/index.ts +82 -1
- package/packages/sdk/src/transports/index.ts +197 -0
- package/packages/sdk/src/transports/socket-transport.ts +115 -0
- package/packages/sdk/src/transports/types.ts +77 -0
- package/packages/sdk/src/transports/websocket-transport.ts +245 -0
- package/packages/sdk/tsconfig.json +1 -1
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/storage/src/jsonl-adapter.test.ts +8 -3
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/dist/cjs/discovery.js +328 -0
- package/packages/utils/dist/cjs/errors.js +81 -0
- package/packages/utils/dist/discovery.d.ts +123 -0
- package/packages/utils/dist/discovery.d.ts.map +1 -0
- package/packages/utils/dist/discovery.js +439 -0
- package/packages/utils/dist/discovery.js.map +1 -0
- package/packages/utils/dist/errors.d.ts +29 -0
- package/packages/utils/dist/errors.d.ts.map +1 -0
- package/packages/utils/dist/errors.js +50 -0
- package/packages/utils/dist/errors.js.map +1 -0
- package/packages/utils/package.json +15 -2
- package/packages/utils/src/consolidation.test.ts +125 -0
- package/packages/utils/src/discovery.test.ts +196 -0
- package/packages/utils/src/discovery.ts +524 -0
- package/packages/utils/src/errors.test.ts +83 -0
- package/packages/utils/src/errors.ts +56 -0
- 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
|
-
|
|
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<
|
|
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
|
-
|
|
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';
|