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