@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.
- 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 +393 -41
- package/dist/services/coder/api-reference.js.map +1 -1
- package/dist/services/coder/client.d.ts +44 -2
- package/dist/services/coder/client.d.ts.map +1 -1
- package/dist/services/coder/client.js +89 -3
- 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/discover.d.ts +1 -1
- package/dist/services/coder/discover.js +2 -2
- package/dist/services/coder/discover.js.map +1 -1
- 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 +411 -45
- package/src/services/coder/client.ts +133 -2
- package/src/services/coder/close-codes.ts +83 -0
- package/src/services/coder/discover.ts +2 -2
- 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,676 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Sent Events (SSE) client for observing Coder Hub sessions.
|
|
3
|
+
*
|
|
4
|
+
* SSE provides a unidirectional stream of events from the server, ideal for
|
|
5
|
+
* observers who want to watch session activity without sending commands.
|
|
6
|
+
*
|
|
7
|
+
* @module coder/sse
|
|
8
|
+
*
|
|
9
|
+
* @example Class-based API with callbacks
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { CoderSSEClient } from '@agentuity/core/coder';
|
|
12
|
+
*
|
|
13
|
+
* const client = new CoderSSEClient({
|
|
14
|
+
* apiKey: 'your-api-key',
|
|
15
|
+
* sessionId: 'session-123',
|
|
16
|
+
* onSnapshot: (data) => {
|
|
17
|
+
* console.log('Session state:', data);
|
|
18
|
+
* },
|
|
19
|
+
* onBroadcast: (data) => {
|
|
20
|
+
* console.log('Broadcast event:', data.event, data.data);
|
|
21
|
+
* },
|
|
22
|
+
* onPresence: (data) => {
|
|
23
|
+
* console.log('Participant:', data.event, data.participant);
|
|
24
|
+
* },
|
|
25
|
+
* onError: (err) => {
|
|
26
|
+
* console.error('SSE error:', err);
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* client.connect();
|
|
31
|
+
*
|
|
32
|
+
* // Later: close the connection
|
|
33
|
+
* client.close();
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @example Async iterator API
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import { streamCoderSessionSSE } from '@agentuity/core/coder';
|
|
39
|
+
*
|
|
40
|
+
* const controller = new AbortController();
|
|
41
|
+
*
|
|
42
|
+
* for await (const event of streamCoderSessionSSE({
|
|
43
|
+
* sessionId: 'session-123',
|
|
44
|
+
* signal: controller.signal,
|
|
45
|
+
* })) {
|
|
46
|
+
* console.log('Event:', event.event, event.data);
|
|
47
|
+
*
|
|
48
|
+
* if (event.event === 'broadcast' && event.data.event === 'session_complete') {
|
|
49
|
+
* controller.abort(); // Stop the stream
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
import { z } from 'zod/v4';
|
|
55
|
+
import { StructuredError } from "../../error.js";
|
|
56
|
+
import { APIClient } from "../api.js";
|
|
57
|
+
import { getServiceUrls } from "../config.js";
|
|
58
|
+
import { createMinimalLogger } from "../logger.js";
|
|
59
|
+
import { getEnv } from "../env.js";
|
|
60
|
+
import { discoverUrl } from "./discover.js";
|
|
61
|
+
import { ObserverSseMessageSchema } from "./protocol.js";
|
|
62
|
+
import { normalizeCoderUrl } from "./util.js";
|
|
63
|
+
/**
|
|
64
|
+
* Options for the SSE client (both class-based and async iterator APIs).
|
|
65
|
+
*/
|
|
66
|
+
export const CoderSSEOptionsSchema = z.object({
|
|
67
|
+
/** API key for authentication. Falls back to AGENTUITY_SDK_KEY or AGENTUITY_CLI_KEY env vars. */
|
|
68
|
+
apiKey: z.string().optional().describe('API key for authentication'),
|
|
69
|
+
/** Organization ID for multi-tenant operations */
|
|
70
|
+
orgId: z.string().optional().describe('Organization ID for multi-tenant operations'),
|
|
71
|
+
/** Session ID to observe (required) */
|
|
72
|
+
sessionId: z.string().describe('Session ID to observe'),
|
|
73
|
+
/** Base URL for the Coder Hub. Falls back to AGENTUITY_CODER_URL env var. */
|
|
74
|
+
url: z.string().optional().describe('Base URL for the Coder Hub'),
|
|
75
|
+
/** Region used for Catalyst URL resolution when no explicit URL is provided */
|
|
76
|
+
region: z.string().optional().describe('Region used for Catalyst URL resolution'),
|
|
77
|
+
/** Event filters to subscribe to. Empty subscribes to default observer events. */
|
|
78
|
+
subscribe: z.array(z.string()).optional().describe('Event filters to subscribe to'),
|
|
79
|
+
/** Custom logger implementation */
|
|
80
|
+
logger: z.custom().optional().describe('Custom logger implementation'),
|
|
81
|
+
/** Enable automatic reconnection on disconnect (default: true) */
|
|
82
|
+
reconnect: z.boolean().optional().describe('Enable automatic reconnection'),
|
|
83
|
+
/** Maximum reconnection attempts before giving up (default: 10) */
|
|
84
|
+
maxReconnectAttempts: z.number().optional().describe('Maximum reconnection attempts'),
|
|
85
|
+
/** Initial reconnection delay in milliseconds (default: 1000) */
|
|
86
|
+
reconnectDelayMs: z.number().optional().describe('Initial reconnection delay'),
|
|
87
|
+
/** Maximum reconnection delay in milliseconds (default: 30000) */
|
|
88
|
+
maxReconnectDelayMs: z.number().optional().describe('Maximum reconnection delay'),
|
|
89
|
+
/** AbortSignal to stop the subscription (async iterator only) */
|
|
90
|
+
signal: z.custom().optional().describe('AbortSignal to stop the subscription'),
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Options for the class-based SSE client.
|
|
94
|
+
*
|
|
95
|
+
* Extends the base options with callbacks for each event type.
|
|
96
|
+
*/
|
|
97
|
+
export const CoderSSEClientOptionsSchema = CoderSSEOptionsSchema.extend({
|
|
98
|
+
/** Called when the initial session snapshot is received */
|
|
99
|
+
onSnapshot: z
|
|
100
|
+
.custom()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Callback for snapshot events'),
|
|
103
|
+
/** Called when hydration data (conversation history) is received */
|
|
104
|
+
onHydration: z
|
|
105
|
+
.custom()
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Callback for hydration events'),
|
|
108
|
+
/** Called when presence events (join/leave) occur */
|
|
109
|
+
onPresence: z
|
|
110
|
+
.custom()
|
|
111
|
+
.optional()
|
|
112
|
+
.describe('Callback for presence events'),
|
|
113
|
+
/** Called for broadcast events (session activity updates) */
|
|
114
|
+
onBroadcast: z
|
|
115
|
+
.custom()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe('Callback for broadcast events'),
|
|
118
|
+
/** Called for any SSE event (catch-all) */
|
|
119
|
+
onEvent: z
|
|
120
|
+
.custom()
|
|
121
|
+
.optional()
|
|
122
|
+
.describe('Callback for all events'),
|
|
123
|
+
/** Called when connection is established */
|
|
124
|
+
onOpen: z.custom().optional().describe('Callback when connection opens'),
|
|
125
|
+
/** Called when connection closes */
|
|
126
|
+
onClose: z.custom().optional().describe('Callback when connection closes'),
|
|
127
|
+
/** Called on errors */
|
|
128
|
+
onError: z.custom().optional().describe('Callback on error'),
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* Error type for SSE operations.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* try {
|
|
136
|
+
* for await (const event of streamCoderSessionSSE({ sessionId: 'invalid' })) {
|
|
137
|
+
* // ...
|
|
138
|
+
* }
|
|
139
|
+
* } catch (err) {
|
|
140
|
+
* if (err instanceof CoderSSEError) {
|
|
141
|
+
* console.log('SSE error code:', err.code);
|
|
142
|
+
* }
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export const CoderSSEError = StructuredError('CoderSSEError')();
|
|
147
|
+
async function buildSSEUrl(sessionId, options) {
|
|
148
|
+
let baseUrl = options.url;
|
|
149
|
+
if (!baseUrl) {
|
|
150
|
+
const envUrl = getEnv('AGENTUITY_CODER_URL');
|
|
151
|
+
if (envUrl) {
|
|
152
|
+
baseUrl = normalizeCoderUrl(envUrl);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const region = options.region ?? getEnv('AGENTUITY_REGION') ?? 'usc';
|
|
156
|
+
const catalystUrl = getServiceUrls(region).catalyst;
|
|
157
|
+
const headers = {};
|
|
158
|
+
if (options.orgId) {
|
|
159
|
+
headers['x-agentuity-orgid'] = options.orgId;
|
|
160
|
+
}
|
|
161
|
+
const logger = options.logger ?? createMinimalLogger();
|
|
162
|
+
const catalystClient = new APIClient(catalystUrl, logger, options.apiKey ?? '', {
|
|
163
|
+
headers,
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
baseUrl = await discoverUrl(catalystClient);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
throw new CoderSSEError({
|
|
170
|
+
message: `Failed to discover Coder URL: ${err instanceof Error ? err.message : String(err)}`,
|
|
171
|
+
code: 'connection_failed',
|
|
172
|
+
sessionId,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
baseUrl = baseUrl.replace(/\/$/, '');
|
|
178
|
+
const path = `/api/hub/session/${encodeURIComponent(sessionId)}/events`;
|
|
179
|
+
const params = new URLSearchParams();
|
|
180
|
+
if (options.subscribe && options.subscribe.length > 0) {
|
|
181
|
+
params.set('subscribe', options.subscribe.join(','));
|
|
182
|
+
}
|
|
183
|
+
if (options.apiKey) {
|
|
184
|
+
params.set('api_key', options.apiKey);
|
|
185
|
+
}
|
|
186
|
+
if (options.orgId) {
|
|
187
|
+
params.set('org_id', options.orgId);
|
|
188
|
+
}
|
|
189
|
+
const queryString = params.toString();
|
|
190
|
+
return queryString ? `${baseUrl}${path}?${queryString}` : `${baseUrl}${path}`;
|
|
191
|
+
}
|
|
192
|
+
function getSSEData(event) {
|
|
193
|
+
const msgEvent = event;
|
|
194
|
+
if (typeof msgEvent.data === 'string') {
|
|
195
|
+
return msgEvent.data;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Class-based SSE client for observing Coder Hub sessions.
|
|
201
|
+
*
|
|
202
|
+
* Provides callback-based event handling for session observation via Server-Sent Events.
|
|
203
|
+
* Automatically reconnects on disconnection with exponential backoff.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const client = new CoderSSEClient({
|
|
208
|
+
* apiKey: 'your-api-key',
|
|
209
|
+
* sessionId: 'session-123',
|
|
210
|
+
* onSnapshot: (data) => console.log('Snapshot:', data),
|
|
211
|
+
* onBroadcast: (data) => console.log('Broadcast:', data),
|
|
212
|
+
* onPresence: (data) => console.log('Presence:', data),
|
|
213
|
+
* });
|
|
214
|
+
*
|
|
215
|
+
* client.connect();
|
|
216
|
+
*
|
|
217
|
+
* // Check connection state
|
|
218
|
+
* console.log('State:', client.state);
|
|
219
|
+
* console.log('Connected:', client.isConnected);
|
|
220
|
+
*
|
|
221
|
+
* // Close when done
|
|
222
|
+
* client.close();
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
export class CoderSSEClient {
|
|
226
|
+
#options;
|
|
227
|
+
#state = 'closed';
|
|
228
|
+
#eventSource = null;
|
|
229
|
+
#reconnectAttempts = 0;
|
|
230
|
+
#reconnectTimer = null;
|
|
231
|
+
#intentionallyClosed = false;
|
|
232
|
+
constructor(options) {
|
|
233
|
+
this.#options = {
|
|
234
|
+
sessionId: options.sessionId,
|
|
235
|
+
url: options.url,
|
|
236
|
+
region: options.region ?? getEnv('AGENTUITY_REGION') ?? 'usc',
|
|
237
|
+
apiKey: options.apiKey ?? getEnv('AGENTUITY_SDK_KEY') ?? getEnv('AGENTUITY_CLI_KEY') ?? '',
|
|
238
|
+
orgId: options.orgId ?? '',
|
|
239
|
+
subscribe: options.subscribe,
|
|
240
|
+
logger: options.logger ?? createMinimalLogger(),
|
|
241
|
+
reconnect: options.reconnect ?? true,
|
|
242
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
243
|
+
reconnectDelayMs: options.reconnectDelayMs ?? 1000,
|
|
244
|
+
maxReconnectDelayMs: options.maxReconnectDelayMs ?? 30000,
|
|
245
|
+
onSnapshot: options.onSnapshot,
|
|
246
|
+
onHydration: options.onHydration,
|
|
247
|
+
onPresence: options.onPresence,
|
|
248
|
+
onBroadcast: options.onBroadcast,
|
|
249
|
+
onEvent: options.onEvent,
|
|
250
|
+
onOpen: options.onOpen,
|
|
251
|
+
onClose: options.onClose,
|
|
252
|
+
onError: options.onError,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The current connection state.
|
|
257
|
+
*
|
|
258
|
+
* - `'connecting'` - Initial connection in progress
|
|
259
|
+
* - `'connected'` - Connected and receiving events
|
|
260
|
+
* - `'reconnecting'` - Reconnecting after disconnect
|
|
261
|
+
* - `'closed'` - Connection closed (manually or after max retries)
|
|
262
|
+
*/
|
|
263
|
+
get state() {
|
|
264
|
+
return this.#state;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Whether the client is currently connected and receiving events.
|
|
268
|
+
*/
|
|
269
|
+
get isConnected() {
|
|
270
|
+
return this.#state === 'connected' && this.#eventSource?.readyState === 1;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Establish the SSE connection and start receiving events.
|
|
274
|
+
*
|
|
275
|
+
* If already connected or connecting, this is a no-op.
|
|
276
|
+
* Automatically reconnects on disconnection unless `close()` was called.
|
|
277
|
+
*/
|
|
278
|
+
connect() {
|
|
279
|
+
if (this.#state !== 'closed') {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.#intentionallyClosed = false;
|
|
283
|
+
this.#reconnectAttempts = 0;
|
|
284
|
+
if (this.#reconnectTimer !== null) {
|
|
285
|
+
clearTimeout(this.#reconnectTimer);
|
|
286
|
+
this.#reconnectTimer = null;
|
|
287
|
+
}
|
|
288
|
+
this.#connectInternal();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Close the SSE connection and stop receiving events.
|
|
292
|
+
*
|
|
293
|
+
* After calling `close()`, you can call `connect()` again to reconnect.
|
|
294
|
+
*/
|
|
295
|
+
close() {
|
|
296
|
+
this.#intentionallyClosed = true;
|
|
297
|
+
if (this.#reconnectTimer !== null) {
|
|
298
|
+
clearTimeout(this.#reconnectTimer);
|
|
299
|
+
this.#reconnectTimer = null;
|
|
300
|
+
}
|
|
301
|
+
if (this.#eventSource) {
|
|
302
|
+
this.#eventSource.close();
|
|
303
|
+
this.#eventSource = null;
|
|
304
|
+
}
|
|
305
|
+
this.#state = 'closed';
|
|
306
|
+
this.#options.onClose?.();
|
|
307
|
+
}
|
|
308
|
+
#setState(state) {
|
|
309
|
+
this.#state = state;
|
|
310
|
+
}
|
|
311
|
+
#handleEvent(eventName, typeOverride) {
|
|
312
|
+
this.#eventSource.addEventListener(eventName, (event) => {
|
|
313
|
+
const data = getSSEData(event);
|
|
314
|
+
if (!data)
|
|
315
|
+
return;
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(data);
|
|
318
|
+
const payload = typeOverride ? { type: typeOverride, ...parsed } : parsed;
|
|
319
|
+
const result = ObserverSseMessageSchema.safeParse(payload);
|
|
320
|
+
if (result.success) {
|
|
321
|
+
const semanticEvent = typeOverride || result.data.type;
|
|
322
|
+
const sseEvent = { event: semanticEvent, data: result.data };
|
|
323
|
+
this.#options.onEvent?.(sseEvent);
|
|
324
|
+
if (result.data.type === 'snapshot') {
|
|
325
|
+
this.#options.onSnapshot?.(result.data);
|
|
326
|
+
}
|
|
327
|
+
else if (result.data.type === 'hydration') {
|
|
328
|
+
this.#options.onHydration?.(result.data);
|
|
329
|
+
}
|
|
330
|
+
else if (result.data.type === 'presence') {
|
|
331
|
+
this.#options.onPresence?.(result.data);
|
|
332
|
+
}
|
|
333
|
+
else if (result.data.type === 'broadcast') {
|
|
334
|
+
this.#options.onBroadcast?.(result.data);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const parseError = new CoderSSEError({
|
|
339
|
+
message: `Invalid SSE ${eventName} event format`,
|
|
340
|
+
code: 'parse_error',
|
|
341
|
+
sessionId: this.#options.sessionId,
|
|
342
|
+
});
|
|
343
|
+
this.#options.onError?.(parseError);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
const parseError = new CoderSSEError({
|
|
348
|
+
message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
|
|
349
|
+
code: 'parse_error',
|
|
350
|
+
sessionId: this.#options.sessionId,
|
|
351
|
+
});
|
|
352
|
+
this.#options.onError?.(parseError);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async #connectInternal() {
|
|
357
|
+
if (this.#intentionallyClosed) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.#setState(this.#reconnectAttempts > 0 ? 'reconnecting' : 'connecting');
|
|
361
|
+
let url;
|
|
362
|
+
try {
|
|
363
|
+
url = await buildSSEUrl(this.#options.sessionId, this.#options);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
this.#setState('closed');
|
|
367
|
+
this.#options.onError?.(err);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (this.#intentionallyClosed || this.#state === 'closed') {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Workaround for bun-types EventSource constructor typing issue.
|
|
374
|
+
// The type definitions don't match the runtime signature, so we use
|
|
375
|
+
// a double type assertion to construct EventSource with a URL parameter.
|
|
376
|
+
try {
|
|
377
|
+
const EventSourceCtor = EventSource;
|
|
378
|
+
this.#eventSource = new EventSourceCtor(url);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
this.#setState('closed');
|
|
382
|
+
this.#options.onError?.(new CoderSSEError({
|
|
383
|
+
message: `Failed to create EventSource: ${err instanceof Error ? err.message : String(err)}`,
|
|
384
|
+
code: 'connection_failed',
|
|
385
|
+
sessionId: this.#options.sessionId,
|
|
386
|
+
}));
|
|
387
|
+
this.#scheduleReconnect();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.#eventSource.onerror = () => {
|
|
391
|
+
// Notify caller of transient error before reconnecting
|
|
392
|
+
this.#options.onError?.(new Error('EventSource transient error'));
|
|
393
|
+
if (this.#eventSource) {
|
|
394
|
+
this.#eventSource.close();
|
|
395
|
+
this.#eventSource = null;
|
|
396
|
+
}
|
|
397
|
+
if (this.#intentionallyClosed) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.#scheduleReconnect();
|
|
401
|
+
};
|
|
402
|
+
this.#eventSource.onopen = () => {
|
|
403
|
+
this.#reconnectAttempts = 0;
|
|
404
|
+
this.#setState('connected');
|
|
405
|
+
this.#options.logger.debug('SSE connection established for session %s', this.#options.sessionId);
|
|
406
|
+
this.#options.onOpen?.();
|
|
407
|
+
};
|
|
408
|
+
this.#handleEvent('snapshot', 'snapshot');
|
|
409
|
+
this.#handleEvent('hydration', 'hydration');
|
|
410
|
+
this.#handleEvent('presence', 'presence');
|
|
411
|
+
this.#handleEvent('broadcast', 'broadcast');
|
|
412
|
+
this.#handleEvent('message');
|
|
413
|
+
}
|
|
414
|
+
#scheduleReconnect() {
|
|
415
|
+
if (this.#intentionallyClosed || !this.#options.reconnect) {
|
|
416
|
+
this.#setState('closed');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (this.#reconnectAttempts >= this.#options.maxReconnectAttempts) {
|
|
420
|
+
this.#setState('closed');
|
|
421
|
+
this.#options.onError?.(new CoderSSEError({
|
|
422
|
+
message: `Exceeded maximum reconnection attempts (${this.#options.maxReconnectAttempts})`,
|
|
423
|
+
code: 'max_reconnects_exceeded',
|
|
424
|
+
sessionId: this.#options.sessionId,
|
|
425
|
+
}));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const baseDelay = this.#options.reconnectDelayMs * 2 ** this.#reconnectAttempts;
|
|
429
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
430
|
+
const delay = Math.min(Math.floor(baseDelay * jitter), this.#options.maxReconnectDelayMs);
|
|
431
|
+
this.#reconnectAttempts++;
|
|
432
|
+
this.#setState('reconnecting');
|
|
433
|
+
this.#options.logger.debug('SSE connection lost, reconnecting in %dms (attempt %d)', delay, this.#reconnectAttempts);
|
|
434
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
435
|
+
this.#reconnectTimer = null;
|
|
436
|
+
this.#connectInternal();
|
|
437
|
+
}, delay);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Stream Coder Hub session events via Server-Sent Events (SSE).
|
|
442
|
+
*
|
|
443
|
+
* Returns an async iterator that yields events as they arrive from the server.
|
|
444
|
+
* The connection is automatically managed (reconnection, cleanup).
|
|
445
|
+
*
|
|
446
|
+
* @param options - Configuration for the SSE subscription
|
|
447
|
+
* @yields Events from the session as they arrive
|
|
448
|
+
* @throws {CoderSSEError} If connection fails or max reconnection attempts exceeded
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```typescript
|
|
452
|
+
* import { streamCoderSessionSSE } from '@agentuity/core/coder';
|
|
453
|
+
*
|
|
454
|
+
* // Basic usage
|
|
455
|
+
* for await (const event of streamCoderSessionSSE({
|
|
456
|
+
* sessionId: 'session-123',
|
|
457
|
+
* })) {
|
|
458
|
+
* if (event.event === 'snapshot') {
|
|
459
|
+
* console.log('Session:', event.data.label);
|
|
460
|
+
* } else if (event.event === 'broadcast') {
|
|
461
|
+
* console.log('Event:', event.data.event);
|
|
462
|
+
* }
|
|
463
|
+
* }
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* @example With abort signal
|
|
467
|
+
* ```typescript
|
|
468
|
+
* const controller = new AbortController();
|
|
469
|
+
*
|
|
470
|
+
* // Stop after 60 seconds
|
|
471
|
+
* setTimeout(() => controller.abort(), 60000);
|
|
472
|
+
*
|
|
473
|
+
* for await (const event of streamCoderSessionSSE({
|
|
474
|
+
* sessionId: 'session-123',
|
|
475
|
+
* signal: controller.signal,
|
|
476
|
+
* })) {
|
|
477
|
+
* console.log(event);
|
|
478
|
+
* }
|
|
479
|
+
* ```
|
|
480
|
+
*
|
|
481
|
+
* @example With event filtering
|
|
482
|
+
* ```typescript
|
|
483
|
+
* for await (const event of streamCoderSessionSSE({
|
|
484
|
+
* sessionId: 'session-123',
|
|
485
|
+
* subscribe: ['task_*', 'agent_*'], // Only task and agent events
|
|
486
|
+
* })) {
|
|
487
|
+
* console.log(event);
|
|
488
|
+
* }
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
export async function* streamCoderSessionSSE(options) {
|
|
492
|
+
const logger = options.logger ?? createMinimalLogger();
|
|
493
|
+
const signal = options.signal;
|
|
494
|
+
const reconnect = options.reconnect ?? true;
|
|
495
|
+
const maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
|
|
496
|
+
const reconnectDelayMs = options.reconnectDelayMs ?? 1000;
|
|
497
|
+
const maxReconnectDelayMs = options.maxReconnectDelayMs ?? 30000;
|
|
498
|
+
if (signal?.aborted) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
let eventSource = null;
|
|
502
|
+
let reconnectAttempts = 0;
|
|
503
|
+
const buffer = [];
|
|
504
|
+
const MAX_BUFFER = 1000;
|
|
505
|
+
let resolve = null;
|
|
506
|
+
let done = false;
|
|
507
|
+
let terminalError = null;
|
|
508
|
+
const wake = () => {
|
|
509
|
+
if (resolve) {
|
|
510
|
+
resolve();
|
|
511
|
+
resolve = null;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const cleanup = () => {
|
|
515
|
+
if (eventSource) {
|
|
516
|
+
eventSource.close();
|
|
517
|
+
eventSource = null;
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
const handleSSEEvent = (eventName, typeOverride) => {
|
|
521
|
+
eventSource.addEventListener(eventName, (event) => {
|
|
522
|
+
const data = getSSEData(event);
|
|
523
|
+
if (!data)
|
|
524
|
+
return;
|
|
525
|
+
try {
|
|
526
|
+
const parsed = JSON.parse(data);
|
|
527
|
+
const payload = typeOverride ? { type: typeOverride, ...parsed } : parsed;
|
|
528
|
+
const result = ObserverSseMessageSchema.safeParse(payload);
|
|
529
|
+
if (result.success) {
|
|
530
|
+
if (buffer.length >= MAX_BUFFER) {
|
|
531
|
+
buffer.shift();
|
|
532
|
+
logger.debug('SSE buffer full, dropped oldest event');
|
|
533
|
+
}
|
|
534
|
+
const semanticEvent = typeOverride || result.data.type;
|
|
535
|
+
buffer.push({ event: semanticEvent, data: result.data });
|
|
536
|
+
wake();
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
terminalError = new CoderSSEError({
|
|
540
|
+
message: `Invalid SSE ${eventName} event format`,
|
|
541
|
+
code: 'parse_error',
|
|
542
|
+
sessionId: options.sessionId,
|
|
543
|
+
});
|
|
544
|
+
done = true;
|
|
545
|
+
wake();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
terminalError = new CoderSSEError({
|
|
551
|
+
message: `Failed to parse SSE ${eventName} event: ${err instanceof Error ? err.message : String(err)}`,
|
|
552
|
+
code: 'parse_error',
|
|
553
|
+
sessionId: options.sessionId,
|
|
554
|
+
});
|
|
555
|
+
done = true;
|
|
556
|
+
wake();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
const connect = async () => {
|
|
562
|
+
if (done || signal?.aborted) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
let url;
|
|
566
|
+
try {
|
|
567
|
+
url = await buildSSEUrl(options.sessionId, {
|
|
568
|
+
...options,
|
|
569
|
+
logger,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
terminalError = err;
|
|
574
|
+
done = true;
|
|
575
|
+
wake();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (signal?.aborted) {
|
|
579
|
+
done = true;
|
|
580
|
+
wake();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
// Workaround for bun-types EventSource constructor typing issue (see above).
|
|
584
|
+
try {
|
|
585
|
+
const EventSourceCtor = EventSource;
|
|
586
|
+
eventSource = new EventSourceCtor(url);
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
terminalError = new CoderSSEError({
|
|
590
|
+
message: `Failed to create EventSource: ${err instanceof Error ? err.message : String(err)}`,
|
|
591
|
+
code: 'connection_failed',
|
|
592
|
+
sessionId: options.sessionId,
|
|
593
|
+
});
|
|
594
|
+
done = true;
|
|
595
|
+
wake();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (signal?.aborted) {
|
|
599
|
+
cleanup();
|
|
600
|
+
done = true;
|
|
601
|
+
wake();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
eventSource.onerror = () => {
|
|
605
|
+
cleanup();
|
|
606
|
+
if (signal?.aborted) {
|
|
607
|
+
done = true;
|
|
608
|
+
wake();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (reconnect && reconnectAttempts < maxReconnectAttempts) {
|
|
612
|
+
const baseDelay = reconnectDelayMs * 2 ** reconnectAttempts;
|
|
613
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
614
|
+
const delay = Math.min(Math.floor(baseDelay * jitter), maxReconnectDelayMs);
|
|
615
|
+
reconnectAttempts++;
|
|
616
|
+
logger.debug('SSE connection lost, reconnecting in %dms (attempt %d)', delay, reconnectAttempts);
|
|
617
|
+
setTimeout(() => {
|
|
618
|
+
connect();
|
|
619
|
+
}, delay);
|
|
620
|
+
}
|
|
621
|
+
else if (reconnect) {
|
|
622
|
+
terminalError = new CoderSSEError({
|
|
623
|
+
message: `Exceeded maximum reconnection attempts (${maxReconnectAttempts})`,
|
|
624
|
+
code: 'max_reconnects_exceeded',
|
|
625
|
+
sessionId: options.sessionId,
|
|
626
|
+
});
|
|
627
|
+
done = true;
|
|
628
|
+
wake();
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
done = true;
|
|
632
|
+
wake();
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
eventSource.onopen = () => {
|
|
636
|
+
reconnectAttempts = 0;
|
|
637
|
+
logger.debug('SSE connection established for session %s', options.sessionId);
|
|
638
|
+
};
|
|
639
|
+
handleSSEEvent('snapshot', 'snapshot');
|
|
640
|
+
handleSSEEvent('hydration', 'hydration');
|
|
641
|
+
handleSSEEvent('presence', 'presence');
|
|
642
|
+
handleSSEEvent('broadcast', 'broadcast');
|
|
643
|
+
handleSSEEvent('message');
|
|
644
|
+
};
|
|
645
|
+
const onAbort = () => {
|
|
646
|
+
done = true;
|
|
647
|
+
cleanup();
|
|
648
|
+
wake();
|
|
649
|
+
};
|
|
650
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
651
|
+
await connect();
|
|
652
|
+
try {
|
|
653
|
+
while (!done) {
|
|
654
|
+
while (buffer.length > 0) {
|
|
655
|
+
yield buffer.shift();
|
|
656
|
+
}
|
|
657
|
+
if (done) {
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
await new Promise((r) => {
|
|
661
|
+
resolve = r;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
while (buffer.length > 0) {
|
|
665
|
+
yield buffer.shift();
|
|
666
|
+
}
|
|
667
|
+
if (terminalError) {
|
|
668
|
+
throw terminalError;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
finally {
|
|
672
|
+
signal?.removeEventListener('abort', onAbort);
|
|
673
|
+
cleanup();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=sse.js.map
|