@agentuity/core 2.0.10 → 2.0.11
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/dist/services/coder/agents.d.ts +170 -0
- package/dist/services/coder/agents.d.ts.map +1 -0
- package/dist/services/coder/agents.js +77 -0
- package/dist/services/coder/agents.js.map +1 -0
- package/dist/services/coder/api-reference.d.ts.map +1 -1
- package/dist/services/coder/api-reference.js +391 -39
- package/dist/services/coder/api-reference.js.map +1 -1
- package/dist/services/coder/client.d.ts +43 -1
- package/dist/services/coder/client.d.ts.map +1 -1
- package/dist/services/coder/client.js +87 -1
- package/dist/services/coder/client.js.map +1 -1
- package/dist/services/coder/close-codes.d.ts +76 -0
- package/dist/services/coder/close-codes.d.ts.map +1 -0
- package/dist/services/coder/close-codes.js +77 -0
- package/dist/services/coder/close-codes.js.map +1 -0
- package/dist/services/coder/index.d.ts +9 -2
- package/dist/services/coder/index.d.ts.map +1 -1
- package/dist/services/coder/index.js +6 -1
- package/dist/services/coder/index.js.map +1 -1
- package/dist/services/coder/protocol.d.ts +1855 -0
- package/dist/services/coder/protocol.d.ts.map +1 -0
- package/dist/services/coder/protocol.js +976 -0
- package/dist/services/coder/protocol.js.map +1 -0
- package/dist/services/coder/sessions.d.ts +9 -0
- package/dist/services/coder/sessions.d.ts.map +1 -1
- package/dist/services/coder/sessions.js +30 -6
- package/dist/services/coder/sessions.js.map +1 -1
- package/dist/services/coder/sse.d.ts +255 -0
- package/dist/services/coder/sse.d.ts.map +1 -0
- package/dist/services/coder/sse.js +676 -0
- package/dist/services/coder/sse.js.map +1 -0
- package/dist/services/coder/types.d.ts +1013 -0
- package/dist/services/coder/types.d.ts.map +1 -1
- package/dist/services/coder/types.js +215 -1
- package/dist/services/coder/types.js.map +1 -1
- package/dist/services/coder/websocket.d.ts +346 -0
- package/dist/services/coder/websocket.d.ts.map +1 -0
- package/dist/services/coder/websocket.js +791 -0
- package/dist/services/coder/websocket.js.map +1 -0
- package/dist/services/oauth/types.d.ts +10 -0
- package/dist/services/oauth/types.d.ts.map +1 -1
- package/dist/services/oauth/types.js +3 -0
- package/dist/services/oauth/types.js.map +1 -1
- package/dist/services/project/deploy.d.ts +1 -1
- package/dist/services/sandbox/run.d.ts +2 -2
- package/dist/services/sandbox/types.d.ts +2 -2
- package/package.json +2 -2
- package/src/services/coder/agents.ts +148 -0
- package/src/services/coder/api-reference.ts +409 -43
- package/src/services/coder/client.ts +131 -0
- package/src/services/coder/close-codes.ts +83 -0
- package/src/services/coder/index.ts +29 -1
- package/src/services/coder/protocol.ts +1200 -0
- package/src/services/coder/sessions.ts +40 -10
- package/src/services/coder/sse.ts +796 -0
- package/src/services/coder/types.ts +249 -1
- package/src/services/coder/websocket.ts +943 -0
- package/src/services/oauth/types.ts +3 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for the Coder Hub real-time communication.
|
|
3
|
+
*
|
|
4
|
+
* Provides bidirectional communication between clients and the Coder Hub server,
|
|
5
|
+
* supporting multiple connection roles (lead, observer, controller) with
|
|
6
|
+
* automatic reconnection, heartbeat, and message queuing.
|
|
7
|
+
*
|
|
8
|
+
* @module coder/websocket
|
|
9
|
+
*
|
|
10
|
+
* @example Class-based API with callbacks
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { CoderHubWebSocketClient } from '@agentuity/core/coder';
|
|
13
|
+
*
|
|
14
|
+
* const client = new CoderHubWebSocketClient({
|
|
15
|
+
* apiKey: 'your-api-key',
|
|
16
|
+
* sessionId: 'session-123',
|
|
17
|
+
* role: 'observer',
|
|
18
|
+
* onInit: (init) => {
|
|
19
|
+
* console.log('Connected to session:', init.sessionId);
|
|
20
|
+
* console.log('Available agents:', init.agents);
|
|
21
|
+
* },
|
|
22
|
+
* onMessage: (msg) => {
|
|
23
|
+
* console.log('Received:', msg);
|
|
24
|
+
* },
|
|
25
|
+
* onStateChange: (state) => {
|
|
26
|
+
* console.log('Connection state:', state);
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* client.connect();
|
|
31
|
+
*
|
|
32
|
+
* // Send a message
|
|
33
|
+
* client.send({
|
|
34
|
+
* type: 'ping',
|
|
35
|
+
* timestamp: Date.now(),
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* // Close when done
|
|
39
|
+
* client.close();
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example Async iterator API
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { subscribeToCoderHub } from '@agentuity/core/coder';
|
|
45
|
+
*
|
|
46
|
+
* for await (const message of subscribeToCoderHub({
|
|
47
|
+
* sessionId: 'session-123',
|
|
48
|
+
* role: 'observer',
|
|
49
|
+
* })) {
|
|
50
|
+
* if (message.type === 'broadcast') {
|
|
51
|
+
* console.log('Event:', message.event, message.data);
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { z } from 'zod/v4';
|
|
58
|
+
import { StructuredError } from '../../error.ts';
|
|
59
|
+
import type { Logger } from '../../logger.ts';
|
|
60
|
+
import { APIClient } from '../api.ts';
|
|
61
|
+
import { getServiceUrls } from '../config.ts';
|
|
62
|
+
import { createMinimalLogger } from '../logger.ts';
|
|
63
|
+
import { getEnv } from '../env.ts';
|
|
64
|
+
import { isTerminalCloseCode } from './close-codes.ts';
|
|
65
|
+
import { discoverUrl } from './discover.ts';
|
|
66
|
+
import type {
|
|
67
|
+
ClientMessage,
|
|
68
|
+
CoderHubInitMessage,
|
|
69
|
+
CoderHubResponse,
|
|
70
|
+
ConnectionParams,
|
|
71
|
+
ServerMessage,
|
|
72
|
+
} from './protocol.ts';
|
|
73
|
+
import { CoderHubInitMessageSchema } from './protocol.ts';
|
|
74
|
+
import { normalizeCoderUrl } from './util.ts';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Connection state for the WebSocket client.
|
|
78
|
+
*
|
|
79
|
+
* - `'connecting'` - Initial WebSocket connection in progress
|
|
80
|
+
* - `'authenticating'` - WebSocket connected, sending auth message
|
|
81
|
+
* - `'connected'` - Authenticated and ready to send/receive messages
|
|
82
|
+
* - `'reconnecting'` - Reconnecting after disconnect
|
|
83
|
+
* - `'closed'` - Connection closed (manually or after max retries)
|
|
84
|
+
*/
|
|
85
|
+
export type CoderHubWebSocketState =
|
|
86
|
+
| 'connecting'
|
|
87
|
+
| 'authenticating'
|
|
88
|
+
| 'connected'
|
|
89
|
+
| 'reconnecting'
|
|
90
|
+
| 'closed';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Options for the WebSocket client.
|
|
94
|
+
*/
|
|
95
|
+
export const CoderHubWebSocketOptionsSchema = z.object({
|
|
96
|
+
/** API key for authentication. Falls back to AGENTUITY_SDK_KEY or AGENTUITY_CLI_KEY env vars. */
|
|
97
|
+
apiKey: z.string().optional().describe('API key for authentication'),
|
|
98
|
+
/** Organization ID for multi-tenant operations */
|
|
99
|
+
orgId: z.string().optional().describe('Organization ID for multi-tenant operations'),
|
|
100
|
+
/** WebSocket URL for the Coder Hub. Falls back to AGENTUITY_CODER_URL env var. */
|
|
101
|
+
url: z.string().optional().describe('WebSocket URL for the Coder Hub'),
|
|
102
|
+
/** Region used for Catalyst URL resolution when no explicit URL is provided */
|
|
103
|
+
region: z.string().optional().describe('Region used for Catalyst URL resolution'),
|
|
104
|
+
/** Session ID to connect to. For new sessions, leave empty and server will assign one. */
|
|
105
|
+
sessionId: z.string().optional().describe('Session ID to connect to'),
|
|
106
|
+
/**
|
|
107
|
+
* Connection role:
|
|
108
|
+
* - `'lead'` - Primary driver of the session (only one per session)
|
|
109
|
+
* - `'observer'` - Read-only observer (receive broadcasts)
|
|
110
|
+
* - `'controller'` - Bidirectional control (web UI)
|
|
111
|
+
*/
|
|
112
|
+
role: z.enum(['lead', 'observer', 'controller']).optional().describe('Connection role'),
|
|
113
|
+
/** Agent role for sub-agent connections (e.g., 'scout', 'builder') */
|
|
114
|
+
agent: z.string().optional().describe('Agent role for sub-agent connections'),
|
|
115
|
+
/** Parent session ID for sub-agent connections */
|
|
116
|
+
parentSessionId: z.string().optional().describe('Parent session ID for sub-agent connections'),
|
|
117
|
+
/** Initial task for driver mode sessions */
|
|
118
|
+
task: z.string().optional().describe('Initial task for driver mode'),
|
|
119
|
+
/** Human-readable session label */
|
|
120
|
+
label: z.string().optional().describe('Session label'),
|
|
121
|
+
/** Client origin (web, desktop, tui, sdk) */
|
|
122
|
+
origin: z.enum(['web', 'desktop', 'tui', 'sdk']).optional().describe('Client origin'),
|
|
123
|
+
/** Driver mode: 'rpc' for RPC bridge driver */
|
|
124
|
+
driverMode: z.enum(['rpc']).optional().describe('Driver mode'),
|
|
125
|
+
/** Driver instance ID for fencing stale reconnects */
|
|
126
|
+
driverInstanceId: z.string().optional().describe('Driver instance ID'),
|
|
127
|
+
/** Driver version for observability */
|
|
128
|
+
driverVersion: z.string().optional().describe('Driver version'),
|
|
129
|
+
/** Custom logger implementation */
|
|
130
|
+
logger: z.custom<Logger>().optional().describe('Custom logger implementation'),
|
|
131
|
+
/** Enable automatic reconnection on disconnect (default: true) */
|
|
132
|
+
autoReconnect: z.boolean().optional().describe('Enable automatic reconnection'),
|
|
133
|
+
/** Maximum reconnection attempts before giving up (default: 10) */
|
|
134
|
+
maxReconnectAttempts: z.number().optional().describe('Maximum reconnection attempts'),
|
|
135
|
+
/** Initial reconnection delay in milliseconds (default: 1000) */
|
|
136
|
+
reconnectDelayMs: z.number().optional().describe('Initial reconnection delay'),
|
|
137
|
+
/** Maximum reconnection delay in milliseconds (default: 30000) */
|
|
138
|
+
maxReconnectDelayMs: z.number().optional().describe('Maximum reconnection delay'),
|
|
139
|
+
/** Ping interval in milliseconds (default: 10000) */
|
|
140
|
+
heartbeatIntervalMs: z.number().optional().describe('Ping interval'),
|
|
141
|
+
/** Time without response before forcing reconnect in milliseconds (default: 30000) */
|
|
142
|
+
heartbeatTimeoutMs: z.number().optional().describe('Time without response before reconnect'),
|
|
143
|
+
/** Maximum queued messages while disconnected (default: 1000) */
|
|
144
|
+
maxMessageQueueSize: z
|
|
145
|
+
.number()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe('Maximum queued messages while disconnected'),
|
|
148
|
+
/** Callback when connection is authenticated and ready */
|
|
149
|
+
onOpen: z.custom<() => void>().optional().describe('Callback when connection opens'),
|
|
150
|
+
/** Callback when connection closes */
|
|
151
|
+
onClose: z
|
|
152
|
+
.custom<(code: number, reason: string) => void>()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe('Callback when connection closes'),
|
|
155
|
+
/** Callback on errors */
|
|
156
|
+
onError: z.custom<(error: Error) => void>().optional().describe('Callback on error'),
|
|
157
|
+
/** Callback for all incoming messages */
|
|
158
|
+
onMessage: z
|
|
159
|
+
.custom<(message: ServerMessage) => void>()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe('Callback for incoming messages'),
|
|
162
|
+
/** Callback when init message is received (after authentication) */
|
|
163
|
+
onInit: z
|
|
164
|
+
.custom<(message: CoderHubInitMessage) => void>()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('Callback when init message received'),
|
|
167
|
+
/** Callback when connection state changes */
|
|
168
|
+
onStateChange: z
|
|
169
|
+
.custom<(state: CoderHubWebSocketState) => void>()
|
|
170
|
+
.optional()
|
|
171
|
+
.describe('Callback on state change'),
|
|
172
|
+
});
|
|
173
|
+
export type CoderHubWebSocketOptions = z.infer<typeof CoderHubWebSocketOptionsSchema>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Error type for WebSocket operations.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* try {
|
|
181
|
+
* await client.sendAndWait({ type: 'tool', name: 'read', ... });
|
|
182
|
+
* } catch (err) {
|
|
183
|
+
* if (err instanceof CoderHubWebSocketError) {
|
|
184
|
+
* if (err.code === 'response_timeout') {
|
|
185
|
+
* console.log('Server did not respond in time');
|
|
186
|
+
* }
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export const CoderHubWebSocketError = StructuredError('CoderHubWebSocketError')<{
|
|
192
|
+
code:
|
|
193
|
+
| 'connection_failed'
|
|
194
|
+
| 'auth_failed'
|
|
195
|
+
| 'connection_error'
|
|
196
|
+
| 'max_reconnects_exceeded'
|
|
197
|
+
| 'send_while_disconnected'
|
|
198
|
+
| 'response_timeout'
|
|
199
|
+
| 'invalid_response';
|
|
200
|
+
sessionId?: string;
|
|
201
|
+
}>();
|
|
202
|
+
|
|
203
|
+
interface PendingRequest {
|
|
204
|
+
resolve: (response: CoderHubResponse) => void;
|
|
205
|
+
reject: (error: Error) => void;
|
|
206
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* WebSocket client for real-time Coder Hub communication.
|
|
211
|
+
*
|
|
212
|
+
* Supports multiple connection roles and provides automatic reconnection,
|
|
213
|
+
* heartbeat management, and message queuing for resilient connections.
|
|
214
|
+
*
|
|
215
|
+
* @example Observer connection
|
|
216
|
+
* ```typescript
|
|
217
|
+
* const client = new CoderHubWebSocketClient({
|
|
218
|
+
* sessionId: 'session-123',
|
|
219
|
+
* role: 'observer',
|
|
220
|
+
* onMessage: (msg) => {
|
|
221
|
+
* if (msg.type === 'broadcast') {
|
|
222
|
+
* console.log('Event:', msg.event);
|
|
223
|
+
* }
|
|
224
|
+
* },
|
|
225
|
+
* });
|
|
226
|
+
* client.connect();
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @example Controller connection with sendAndWait
|
|
230
|
+
* ```typescript
|
|
231
|
+
* const client = new CoderHubWebSocketClient({
|
|
232
|
+
* sessionId: 'session-123',
|
|
233
|
+
* role: 'controller',
|
|
234
|
+
* });
|
|
235
|
+
* client.connect();
|
|
236
|
+
*
|
|
237
|
+
* // Wait for connection
|
|
238
|
+
* await new Promise(resolve => {
|
|
239
|
+
* client.onInit = () => resolve(undefined);
|
|
240
|
+
* });
|
|
241
|
+
*
|
|
242
|
+
* // Send a request and wait for response
|
|
243
|
+
* const response = await client.sendAndWait({
|
|
244
|
+
* type: 'event',
|
|
245
|
+
* event: 'steer',
|
|
246
|
+
* data: { direction: 'continue' },
|
|
247
|
+
* });
|
|
248
|
+
* console.log('Response:', response);
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* @example Sub-agent connection
|
|
252
|
+
* ```typescript
|
|
253
|
+
* const client = new CoderHubWebSocketClient({
|
|
254
|
+
* role: 'observer', // Sub-agents connect as observers to parent
|
|
255
|
+
* agent: 'scout',
|
|
256
|
+
* parentSessionId: 'parent-session-456',
|
|
257
|
+
* });
|
|
258
|
+
* client.connect();
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
export class CoderHubWebSocketClient {
|
|
262
|
+
readonly #options: {
|
|
263
|
+
apiKey: string;
|
|
264
|
+
orgId: string;
|
|
265
|
+
url: string;
|
|
266
|
+
region: string;
|
|
267
|
+
sessionId: string;
|
|
268
|
+
role: 'lead' | 'observer' | 'controller';
|
|
269
|
+
agent: string;
|
|
270
|
+
parentSessionId: string;
|
|
271
|
+
task: string;
|
|
272
|
+
label: string;
|
|
273
|
+
origin: 'web' | 'desktop' | 'tui' | 'sdk';
|
|
274
|
+
driverMode: 'rpc' | undefined;
|
|
275
|
+
driverInstanceId: string;
|
|
276
|
+
driverVersion: string;
|
|
277
|
+
logger: Logger;
|
|
278
|
+
autoReconnect: boolean;
|
|
279
|
+
maxReconnectAttempts: number;
|
|
280
|
+
reconnectDelayMs: number;
|
|
281
|
+
maxReconnectDelayMs: number;
|
|
282
|
+
heartbeatIntervalMs: number;
|
|
283
|
+
heartbeatTimeoutMs: number;
|
|
284
|
+
maxMessageQueueSize: number;
|
|
285
|
+
onOpen: () => void;
|
|
286
|
+
onClose: (code: number, reason: string) => void;
|
|
287
|
+
onError: (error: Error) => void;
|
|
288
|
+
onMessage: (message: ServerMessage) => void;
|
|
289
|
+
onInit: (message: CoderHubInitMessage) => void;
|
|
290
|
+
onStateChange: (state: CoderHubWebSocketState) => void;
|
|
291
|
+
};
|
|
292
|
+
#state: CoderHubWebSocketState = 'closed';
|
|
293
|
+
#ws: WebSocket | null = null;
|
|
294
|
+
#reconnectAttempts = 0;
|
|
295
|
+
#reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
296
|
+
#intentionallyClosed = false;
|
|
297
|
+
#authenticated = false;
|
|
298
|
+
#initMessage: CoderHubInitMessage | null = null;
|
|
299
|
+
#heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
300
|
+
#lastInboundTimestamp = 0;
|
|
301
|
+
#messageQueue: ClientMessage[] = [];
|
|
302
|
+
#pendingRequests: Map<string, PendingRequest> = new Map();
|
|
303
|
+
#messageId = 0;
|
|
304
|
+
#sessionId: string | null = null;
|
|
305
|
+
|
|
306
|
+
constructor(options: CoderHubWebSocketOptions = {}) {
|
|
307
|
+
const apiKey =
|
|
308
|
+
options.apiKey ?? getEnv('AGENTUITY_SDK_KEY') ?? getEnv('AGENTUITY_CLI_KEY') ?? '';
|
|
309
|
+
this.#options = {
|
|
310
|
+
apiKey,
|
|
311
|
+
orgId: options.orgId ?? '',
|
|
312
|
+
url: options.url ?? '',
|
|
313
|
+
region: options.region ?? getEnv('AGENTUITY_REGION') ?? 'usc',
|
|
314
|
+
sessionId: options.sessionId ?? '',
|
|
315
|
+
role: options.role ?? 'observer',
|
|
316
|
+
agent: options.agent ?? '',
|
|
317
|
+
parentSessionId: options.parentSessionId ?? '',
|
|
318
|
+
task: options.task ?? '',
|
|
319
|
+
label: options.label ?? '',
|
|
320
|
+
origin: options.origin ?? 'sdk',
|
|
321
|
+
driverMode: options.driverMode,
|
|
322
|
+
driverInstanceId: options.driverInstanceId ?? '',
|
|
323
|
+
driverVersion: options.driverVersion ?? '',
|
|
324
|
+
logger: options.logger ?? createMinimalLogger(),
|
|
325
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
326
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
327
|
+
reconnectDelayMs: options.reconnectDelayMs ?? 1000,
|
|
328
|
+
maxReconnectDelayMs: options.maxReconnectDelayMs ?? 30000,
|
|
329
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs ?? 10000,
|
|
330
|
+
heartbeatTimeoutMs: options.heartbeatTimeoutMs ?? 30000,
|
|
331
|
+
maxMessageQueueSize: options.maxMessageQueueSize ?? 1000,
|
|
332
|
+
onOpen: options.onOpen ?? (() => {}),
|
|
333
|
+
onClose: options.onClose ?? (() => {}),
|
|
334
|
+
onError: options.onError ?? (() => {}),
|
|
335
|
+
onMessage: options.onMessage ?? (() => {}),
|
|
336
|
+
onInit: options.onInit ?? (() => {}),
|
|
337
|
+
onStateChange: options.onStateChange ?? (() => {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The current connection state.
|
|
343
|
+
*
|
|
344
|
+
* @see CoderHubWebSocketState for state descriptions
|
|
345
|
+
*/
|
|
346
|
+
get state(): CoderHubWebSocketState {
|
|
347
|
+
return this.#state;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The session ID for this connection.
|
|
352
|
+
*
|
|
353
|
+
* Returns the server-assigned session ID (from init message) if available,
|
|
354
|
+
* otherwise returns the session ID passed in options.
|
|
355
|
+
*/
|
|
356
|
+
get sessionId(): string | undefined {
|
|
357
|
+
return this.#sessionId ?? this.#options.sessionId ?? undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The init message received from the server after authentication.
|
|
362
|
+
*
|
|
363
|
+
* Contains session configuration, available agents, tools, and other metadata.
|
|
364
|
+
* Only available after successful authentication.
|
|
365
|
+
*/
|
|
366
|
+
get initMessage(): CoderHubInitMessage | null {
|
|
367
|
+
return this.#initMessage;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Whether the client is currently connected and authenticated.
|
|
372
|
+
*
|
|
373
|
+
* Returns `true` only when state is 'connected' AND WebSocket is open.
|
|
374
|
+
*/
|
|
375
|
+
get isConnected(): boolean {
|
|
376
|
+
return this.#state === 'connected' && this.#ws?.readyState === WebSocket.OPEN;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Establish the WebSocket connection and authenticate.
|
|
381
|
+
*
|
|
382
|
+
* If already connected or connecting, this is a no-op.
|
|
383
|
+
* Automatically reconnects on disconnection unless `close()` was called.
|
|
384
|
+
*
|
|
385
|
+
* The connection goes through these states:
|
|
386
|
+
* 1. `'connecting'` - WebSocket opening
|
|
387
|
+
* 2. `'authenticating'` - Sending auth message
|
|
388
|
+
* 3. `'connected'` - Received init message
|
|
389
|
+
*/
|
|
390
|
+
connect(): void {
|
|
391
|
+
if (this.#state !== 'closed') {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.#intentionallyClosed = false;
|
|
395
|
+
if (this.#reconnectTimer !== null) {
|
|
396
|
+
clearTimeout(this.#reconnectTimer);
|
|
397
|
+
this.#reconnectTimer = null;
|
|
398
|
+
}
|
|
399
|
+
this.#connectInternal();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Close the WebSocket connection.
|
|
404
|
+
*
|
|
405
|
+
* After calling `close()`, you can call `connect()` again to reconnect.
|
|
406
|
+
* Any pending requests will be rejected with an error.
|
|
407
|
+
*
|
|
408
|
+
* @param code - Optional close code (default: 1000 for normal close)
|
|
409
|
+
* @param reason - Optional close reason string
|
|
410
|
+
*/
|
|
411
|
+
close(code?: number, reason?: string): void {
|
|
412
|
+
this.#intentionallyClosed = true;
|
|
413
|
+
this.#clearTimers();
|
|
414
|
+
if (this.#ws) {
|
|
415
|
+
const ws = this.#ws;
|
|
416
|
+
ws.onopen = null;
|
|
417
|
+
ws.onmessage = null;
|
|
418
|
+
ws.onerror = null;
|
|
419
|
+
ws.onclose = null;
|
|
420
|
+
ws.close(code ?? 1000, reason ?? 'Client closed');
|
|
421
|
+
this.#ws = null;
|
|
422
|
+
}
|
|
423
|
+
this.#setState('closed');
|
|
424
|
+
this.#rejectAllPendingRequests('Connection closed');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Send a message to the server.
|
|
429
|
+
*
|
|
430
|
+
* If not connected, the message will be queued and sent when reconnected
|
|
431
|
+
* (up to `maxMessageQueueSize` messages). If the queue is full, an error
|
|
432
|
+
* is emitted via `onError`.
|
|
433
|
+
*
|
|
434
|
+
* @param message - The message to send
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```typescript
|
|
438
|
+
* client.send({
|
|
439
|
+
* type: 'ping',
|
|
440
|
+
* timestamp: Date.now(),
|
|
441
|
+
* });
|
|
442
|
+
*
|
|
443
|
+
* client.send({
|
|
444
|
+
* type: 'session_entry',
|
|
445
|
+
* path: 'entries.jsonl',
|
|
446
|
+
* line: JSON.stringify({ type: 'message', content: 'Hello' }),
|
|
447
|
+
* });
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
send(message: ClientMessage): void {
|
|
451
|
+
if (!this.isConnected) {
|
|
452
|
+
if (this.#messageQueue.length < this.#options.maxMessageQueueSize) {
|
|
453
|
+
this.#messageQueue.push(message);
|
|
454
|
+
} else {
|
|
455
|
+
this.#options.onError(
|
|
456
|
+
new CoderHubWebSocketError({
|
|
457
|
+
message: 'Message queue full, dropping message',
|
|
458
|
+
code: 'send_while_disconnected',
|
|
459
|
+
sessionId: this.sessionId,
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
this.#ws!.send(JSON.stringify(message));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Send a message and wait for a response.
|
|
470
|
+
*
|
|
471
|
+
* Automatically adds a unique `id` to the message and waits for a
|
|
472
|
+
* response with matching `id`. Useful for request/response patterns
|
|
473
|
+
* like tool calls or RPC commands.
|
|
474
|
+
*
|
|
475
|
+
* @param message - The message to send (without `id` field)
|
|
476
|
+
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
477
|
+
* @returns Promise that resolves with the response
|
|
478
|
+
* @throws {CoderHubWebSocketError} If timeout exceeded or connection closed
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* try {
|
|
483
|
+
* const response = await client.sendAndWait({
|
|
484
|
+
* type: 'tool',
|
|
485
|
+
* name: 'read_file',
|
|
486
|
+
* toolCallId: 'call-123',
|
|
487
|
+
* params: { path: '/src/index.ts' },
|
|
488
|
+
* });
|
|
489
|
+
* console.log('Tool result:', response.actions);
|
|
490
|
+
* } catch (err) {
|
|
491
|
+
* if (err instanceof CoderHubWebSocketError && err.code === 'response_timeout') {
|
|
492
|
+
* console.log('Tool call timed out');
|
|
493
|
+
* }
|
|
494
|
+
* }
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
async sendAndWait(
|
|
498
|
+
message: Omit<ClientMessage, 'id'>,
|
|
499
|
+
timeoutMs = 30000
|
|
500
|
+
): Promise<CoderHubResponse> {
|
|
501
|
+
const id = this.#nextId();
|
|
502
|
+
const fullMessage = { ...message, id } as ClientMessage;
|
|
503
|
+
|
|
504
|
+
return new Promise((resolve, reject) => {
|
|
505
|
+
const timeout = setTimeout(() => {
|
|
506
|
+
this.#pendingRequests.delete(id);
|
|
507
|
+
reject(
|
|
508
|
+
new CoderHubWebSocketError({
|
|
509
|
+
message: `Response timeout for request ${id}`,
|
|
510
|
+
code: 'response_timeout',
|
|
511
|
+
sessionId: this.sessionId,
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
}, timeoutMs);
|
|
515
|
+
|
|
516
|
+
this.#pendingRequests.set(id, { resolve, reject, timeout });
|
|
517
|
+
this.send(fullMessage);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#nextId(): string {
|
|
522
|
+
return `${Date.now()}-${++this.#messageId}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#setState(state: CoderHubWebSocketState): void {
|
|
526
|
+
if (this.#state !== state) {
|
|
527
|
+
this.#state = state;
|
|
528
|
+
this.#options.onStateChange(state);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#clearTimers(): void {
|
|
533
|
+
if (this.#reconnectTimer !== null) {
|
|
534
|
+
clearTimeout(this.#reconnectTimer);
|
|
535
|
+
this.#reconnectTimer = null;
|
|
536
|
+
}
|
|
537
|
+
if (this.#heartbeatTimer !== null) {
|
|
538
|
+
clearTimeout(this.#heartbeatTimer);
|
|
539
|
+
this.#heartbeatTimer = null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
#rejectAllPendingRequests(reason: string): void {
|
|
544
|
+
for (const [, pending] of this.#pendingRequests) {
|
|
545
|
+
clearTimeout(pending.timeout);
|
|
546
|
+
pending.reject(
|
|
547
|
+
new CoderHubWebSocketError({
|
|
548
|
+
message: reason,
|
|
549
|
+
code: 'connection_error',
|
|
550
|
+
sessionId: this.sessionId,
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
this.#pendingRequests.clear();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async #buildWsUrl(): Promise<string> {
|
|
558
|
+
let baseUrl = this.#options.url;
|
|
559
|
+
if (!baseUrl) {
|
|
560
|
+
const envUrl = getEnv('AGENTUITY_CODER_URL');
|
|
561
|
+
if (envUrl) {
|
|
562
|
+
baseUrl = normalizeCoderUrl(envUrl);
|
|
563
|
+
} else {
|
|
564
|
+
const catalystUrl = getServiceUrls(this.#options.region).catalyst;
|
|
565
|
+
const headers: Record<string, string> = {};
|
|
566
|
+
if (this.#options.orgId) {
|
|
567
|
+
headers['x-agentuity-orgid'] = this.#options.orgId;
|
|
568
|
+
}
|
|
569
|
+
const catalystClient = new APIClient(
|
|
570
|
+
catalystUrl,
|
|
571
|
+
this.#options.logger,
|
|
572
|
+
this.#options.apiKey,
|
|
573
|
+
{ headers }
|
|
574
|
+
);
|
|
575
|
+
baseUrl = await discoverUrl(catalystClient);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let wsUrl = baseUrl.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
|
|
580
|
+
wsUrl = wsUrl.replace(/\/$/, '');
|
|
581
|
+
const path = wsUrl.includes('/api/ws') ? '' : '/api/ws';
|
|
582
|
+
wsUrl = `${wsUrl}${path}`;
|
|
583
|
+
|
|
584
|
+
const params = new URLSearchParams();
|
|
585
|
+
const connectionParams: ConnectionParams = {
|
|
586
|
+
sessionId: this.#sessionId ?? (this.#options.sessionId || undefined),
|
|
587
|
+
role: this.#options.role || undefined,
|
|
588
|
+
agent: this.#options.agent || undefined,
|
|
589
|
+
parent: this.#options.parentSessionId || undefined,
|
|
590
|
+
task: this.#options.task || undefined,
|
|
591
|
+
label: this.#options.label || undefined,
|
|
592
|
+
orgId: this.#options.orgId || undefined,
|
|
593
|
+
origin: this.#options.origin || undefined,
|
|
594
|
+
driverMode: this.#options.driverMode || undefined,
|
|
595
|
+
driverInstanceId: this.#options.driverInstanceId || undefined,
|
|
596
|
+
driverVersion: this.#options.driverVersion || undefined,
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
for (const [key, value] of Object.entries(connectionParams)) {
|
|
600
|
+
if (value !== undefined && value !== '') {
|
|
601
|
+
params.set(key, String(value));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const queryString = params.toString();
|
|
606
|
+
return queryString ? `${wsUrl}?${queryString}` : wsUrl;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async #connectInternal(): Promise<void> {
|
|
610
|
+
if (this.#intentionallyClosed) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
this.#setState(this.#reconnectAttempts > 0 ? 'reconnecting' : 'connecting');
|
|
615
|
+
|
|
616
|
+
let wsUrl: string;
|
|
617
|
+
try {
|
|
618
|
+
wsUrl = await this.#buildWsUrl();
|
|
619
|
+
} catch (err) {
|
|
620
|
+
this.#setState('closed');
|
|
621
|
+
this.#options.onError(err as Error);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
this.#ws = new WebSocket(wsUrl);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
this.#setState('closed');
|
|
629
|
+
this.#options.onError(
|
|
630
|
+
new CoderHubWebSocketError({
|
|
631
|
+
message: `Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`,
|
|
632
|
+
code: 'connection_failed',
|
|
633
|
+
sessionId: this.sessionId,
|
|
634
|
+
})
|
|
635
|
+
);
|
|
636
|
+
this.#scheduleReconnect();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const ws = this.#ws;
|
|
641
|
+
|
|
642
|
+
ws.onopen = () => {
|
|
643
|
+
if (ws !== this.#ws) return;
|
|
644
|
+
this.#setState('authenticating');
|
|
645
|
+
ws.send(
|
|
646
|
+
JSON.stringify({
|
|
647
|
+
authorization: this.#options.apiKey,
|
|
648
|
+
org_id: this.#options.orgId,
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
ws.onmessage = (event: MessageEvent) => {
|
|
654
|
+
if (ws !== this.#ws) return;
|
|
655
|
+
this.#lastInboundTimestamp = Date.now();
|
|
656
|
+
const raw = typeof event.data === 'string' ? event.data : String(event.data);
|
|
657
|
+
|
|
658
|
+
let parsed: unknown;
|
|
659
|
+
try {
|
|
660
|
+
parsed = JSON.parse(raw);
|
|
661
|
+
} catch {
|
|
662
|
+
this.#options.logger.debug('Failed to parse WebSocket message: %s', raw);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!this.#authenticated) {
|
|
667
|
+
if (parsed && typeof parsed === 'object') {
|
|
668
|
+
const data = parsed as Record<string, unknown>;
|
|
669
|
+
if (data.error) {
|
|
670
|
+
this.#setState('closed');
|
|
671
|
+
this.#options.onError(
|
|
672
|
+
new CoderHubWebSocketError({
|
|
673
|
+
message: `Authentication failed: ${String(data.error)}`,
|
|
674
|
+
code: 'auth_failed',
|
|
675
|
+
sessionId: this.sessionId,
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
this.#intentionallyClosed = true;
|
|
679
|
+
ws.close(4401, 'Auth failed');
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Handle protocol failure messages
|
|
684
|
+
const msg = data as { type?: unknown; code?: string; message?: string };
|
|
685
|
+
if (msg.type === 'connection_rejected' || msg.type === 'protocol_error') {
|
|
686
|
+
this.#setState('closed');
|
|
687
|
+
this.#options.onError(
|
|
688
|
+
new CoderHubWebSocketError({
|
|
689
|
+
message: `Connection rejected: ${msg.message ?? msg.code ?? 'Unknown error'}`,
|
|
690
|
+
code: 'auth_failed',
|
|
691
|
+
sessionId: this.sessionId,
|
|
692
|
+
})
|
|
693
|
+
);
|
|
694
|
+
this.#intentionallyClosed = true;
|
|
695
|
+
ws.close(4401, 'Auth failed');
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const initResult = CoderHubInitMessageSchema.safeParse(parsed);
|
|
701
|
+
if (initResult.success) {
|
|
702
|
+
const initMsg = initResult.data;
|
|
703
|
+
this.#authenticated = true;
|
|
704
|
+
this.#initMessage = initMsg;
|
|
705
|
+
this.#sessionId = initMsg.sessionId ?? this.#options.sessionId ?? null;
|
|
706
|
+
this.#reconnectAttempts = 0;
|
|
707
|
+
this.#setState('connected');
|
|
708
|
+
this.#startHeartbeat();
|
|
709
|
+
this.#flushMessageQueue();
|
|
710
|
+
this.#options.onInit(initMsg);
|
|
711
|
+
this.#options.onOpen();
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const message = parsed as ServerMessage;
|
|
717
|
+
this.#options.onMessage(message);
|
|
718
|
+
|
|
719
|
+
if ('type' in message) {
|
|
720
|
+
if (message.type === 'broadcast' || message.type === 'presence') {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if ('id' in message && typeof message.id === 'string') {
|
|
726
|
+
const pending = this.#pendingRequests.get(message.id);
|
|
727
|
+
if (pending) {
|
|
728
|
+
clearTimeout(pending.timeout);
|
|
729
|
+
this.#pendingRequests.delete(message.id);
|
|
730
|
+
if ('actions' in message) {
|
|
731
|
+
pending.resolve(message as CoderHubResponse);
|
|
732
|
+
} else {
|
|
733
|
+
pending.reject(
|
|
734
|
+
new CoderHubWebSocketError({
|
|
735
|
+
message: `Malformed response for request ${message.id}: missing actions`,
|
|
736
|
+
code: 'invalid_response',
|
|
737
|
+
sessionId: this.sessionId,
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
ws.onerror = () => {
|
|
746
|
+
if (ws !== this.#ws) return;
|
|
747
|
+
this.#options.onError(
|
|
748
|
+
new CoderHubWebSocketError({
|
|
749
|
+
message: 'WebSocket connection error',
|
|
750
|
+
code: 'connection_error',
|
|
751
|
+
sessionId: this.sessionId,
|
|
752
|
+
})
|
|
753
|
+
);
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
ws.onclose = (event: CloseEvent) => {
|
|
757
|
+
if (ws !== this.#ws) return;
|
|
758
|
+
this.#ws = null;
|
|
759
|
+
this.#clearTimers();
|
|
760
|
+
this.#setState('closed');
|
|
761
|
+
|
|
762
|
+
// Clear auth state for clean reconnect
|
|
763
|
+
this.#authenticated = false;
|
|
764
|
+
this.#initMessage = null;
|
|
765
|
+
|
|
766
|
+
if (isTerminalCloseCode(event.code)) {
|
|
767
|
+
this.#intentionallyClosed = true;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
this.#options.onClose(event.code, event.reason);
|
|
771
|
+
|
|
772
|
+
if (!this.#intentionallyClosed) {
|
|
773
|
+
this.#scheduleReconnect();
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#scheduleReconnect(): void {
|
|
779
|
+
if (this.#intentionallyClosed || !this.#options.autoReconnect) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (this.#reconnectAttempts >= this.#options.maxReconnectAttempts) {
|
|
784
|
+
this.#options.onError(
|
|
785
|
+
new CoderHubWebSocketError({
|
|
786
|
+
message: `Exceeded maximum reconnection attempts (${this.#options.maxReconnectAttempts})`,
|
|
787
|
+
code: 'max_reconnects_exceeded',
|
|
788
|
+
sessionId: this.sessionId,
|
|
789
|
+
})
|
|
790
|
+
);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const baseDelay = this.#options.reconnectDelayMs * 2 ** this.#reconnectAttempts;
|
|
795
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
796
|
+
const delay = Math.min(Math.floor(baseDelay * jitter), this.#options.maxReconnectDelayMs);
|
|
797
|
+
|
|
798
|
+
this.#reconnectAttempts++;
|
|
799
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
800
|
+
this.#reconnectTimer = null;
|
|
801
|
+
this.#connectInternal();
|
|
802
|
+
}, delay);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#startHeartbeat(): void {
|
|
806
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
807
|
+
if (!this.isConnected) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const elapsed = Date.now() - this.#lastInboundTimestamp;
|
|
812
|
+
if (elapsed > this.#options.heartbeatTimeoutMs) {
|
|
813
|
+
this.#options.logger.debug('Heartbeat timeout, forcing reconnect');
|
|
814
|
+
this.#ws?.close(1000, 'Heartbeat timeout');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.send({
|
|
819
|
+
type: 'ping',
|
|
820
|
+
timestamp: Date.now(),
|
|
821
|
+
});
|
|
822
|
+
}, this.#options.heartbeatIntervalMs);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
#flushMessageQueue(): void {
|
|
826
|
+
while (this.#messageQueue.length > 0 && this.isConnected) {
|
|
827
|
+
const message = this.#messageQueue.shift()!;
|
|
828
|
+
this.#ws!.send(JSON.stringify(message));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Subscribe to a Coder Hub session via WebSocket using async iteration.
|
|
835
|
+
*
|
|
836
|
+
* Returns an async iterator that yields server messages as they arrive.
|
|
837
|
+
* The connection is automatically managed (auth, reconnection, cleanup).
|
|
838
|
+
*
|
|
839
|
+
* @param options - Configuration for the WebSocket connection
|
|
840
|
+
* @yields Server messages as they arrive
|
|
841
|
+
* @throws {CoderHubWebSocketError} If connection fails or max reconnection attempts exceeded
|
|
842
|
+
*
|
|
843
|
+
* @example Basic usage
|
|
844
|
+
* ```typescript
|
|
845
|
+
* import { subscribeToCoderHub } from '@agentuity/core/coder';
|
|
846
|
+
*
|
|
847
|
+
* for await (const message of subscribeToCoderHub({
|
|
848
|
+
* sessionId: 'session-123',
|
|
849
|
+
* role: 'observer',
|
|
850
|
+
* })) {
|
|
851
|
+
* switch (message.type) {
|
|
852
|
+
* case 'broadcast':
|
|
853
|
+
* console.log('Event:', message.event);
|
|
854
|
+
* break;
|
|
855
|
+
* case 'presence':
|
|
856
|
+
* console.log('Participant:', message.participant);
|
|
857
|
+
* break;
|
|
858
|
+
* }
|
|
859
|
+
* }
|
|
860
|
+
* ```
|
|
861
|
+
*
|
|
862
|
+
* @example With error handling
|
|
863
|
+
* ```typescript
|
|
864
|
+
* try {
|
|
865
|
+
* for await (const message of subscribeToCoderHub({ sessionId: 'session-123' })) {
|
|
866
|
+
* console.log(message);
|
|
867
|
+
* }
|
|
868
|
+
* } catch (err) {
|
|
869
|
+
* if (err instanceof CoderHubWebSocketError) {
|
|
870
|
+
* console.log('WebSocket error:', err.code);
|
|
871
|
+
* }
|
|
872
|
+
* }
|
|
873
|
+
* ```
|
|
874
|
+
*/
|
|
875
|
+
export async function* subscribeToCoderHub(
|
|
876
|
+
options: CoderHubWebSocketOptions
|
|
877
|
+
): AsyncGenerator<ServerMessage, void, unknown> {
|
|
878
|
+
const buffer: ServerMessage[] = [];
|
|
879
|
+
let resolve: (() => void) | null = null;
|
|
880
|
+
let done = false;
|
|
881
|
+
let terminalError: Error | null = null;
|
|
882
|
+
|
|
883
|
+
const wake = () => {
|
|
884
|
+
if (resolve) {
|
|
885
|
+
resolve();
|
|
886
|
+
resolve = null;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const client = new CoderHubWebSocketClient({
|
|
891
|
+
...options,
|
|
892
|
+
onMessage: (message) => {
|
|
893
|
+
buffer.push(message);
|
|
894
|
+
wake();
|
|
895
|
+
},
|
|
896
|
+
onError: (error) => {
|
|
897
|
+
if (
|
|
898
|
+
error instanceof CoderHubWebSocketError &&
|
|
899
|
+
(error.code === 'max_reconnects_exceeded' || error.code === 'auth_failed')
|
|
900
|
+
) {
|
|
901
|
+
terminalError = error;
|
|
902
|
+
done = true;
|
|
903
|
+
wake();
|
|
904
|
+
}
|
|
905
|
+
options.onError?.(error);
|
|
906
|
+
},
|
|
907
|
+
onClose: (code, reason) => {
|
|
908
|
+
if (isTerminalCloseCode(code) || options.autoReconnect === false) {
|
|
909
|
+
done = true;
|
|
910
|
+
}
|
|
911
|
+
wake();
|
|
912
|
+
options.onClose?.(code, reason);
|
|
913
|
+
},
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
client.connect();
|
|
917
|
+
|
|
918
|
+
try {
|
|
919
|
+
while (!done) {
|
|
920
|
+
while (buffer.length > 0) {
|
|
921
|
+
yield buffer.shift()!;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (done) {
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
await new Promise<void>((r) => {
|
|
929
|
+
resolve = r;
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
while (buffer.length > 0) {
|
|
934
|
+
yield buffer.shift()!;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (terminalError) {
|
|
938
|
+
throw terminalError;
|
|
939
|
+
}
|
|
940
|
+
} finally {
|
|
941
|
+
client.close();
|
|
942
|
+
}
|
|
943
|
+
}
|