@agentuity/core 2.0.10 → 2.0.12

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