@agent-relay/events 0.1.0

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.
@@ -0,0 +1,781 @@
1
+ import { SpanKind } from '@opentelemetry/api';
2
+ import { createAgentEvent, createTransportErrorEvent, toAgentEventRecord, } from './envelope.js';
3
+ import { extractTraceContextFromCarrier, injectTraceContextIntoCarrier, initializeRuntimeOtel, withRuntimeSpan, } from './otel.js';
4
+ import { withRetry } from './retry.js';
5
+ const DEFAULT_GATEWAY_URL = 'ws://127.0.0.1:8787/v1/agent-events';
6
+ const READY_FALLBACK_MS = 250;
7
+ const MAX_RECONNECT_DELAY_MS = 30_000;
8
+ /**
9
+ * Opens a normalized event stream over the runtime gateway websocket.
10
+ */
11
+ export function events(options) {
12
+ initializeRuntimeOtel();
13
+ const apiKey = resolveApiKey(options.apiKey);
14
+ const gatewayUrl = resolveGatewayUrl(options.gatewayUrl);
15
+ const webSocketFactory = resolveWebSocketFactory(options.webSocketFactory);
16
+ const expansionCache = new Map();
17
+ let socket = null;
18
+ let reconnectTimer = null;
19
+ let readyFallbackTimer = null;
20
+ let reconnectAttempt = 0;
21
+ let closed = false;
22
+ const pendingRegisterRequests = [];
23
+ const pendingUnregisterRequests = [];
24
+ const pendingOnceRequests = new Map();
25
+ const pendingExpansionRequests = new Map();
26
+ const pendingFileReadRequests = new Map();
27
+ const pendingFileWriteRequests = new Map();
28
+ const pendingFileDeleteRequests = new Map();
29
+ const pendingFileListRequests = new Map();
30
+ const pendingMessageRequests = new Map();
31
+ const pendingApprovalRequests = new Map();
32
+ const pendingLogs = [];
33
+ let resolveReady = null;
34
+ const ready = new Promise((resolve) => {
35
+ resolveReady = resolve;
36
+ });
37
+ const markReady = () => {
38
+ if (!resolveReady) {
39
+ return;
40
+ }
41
+ const resolve = resolveReady;
42
+ resolveReady = null;
43
+ resolve();
44
+ };
45
+ const closeSocket = () => {
46
+ if (readyFallbackTimer) {
47
+ clearTimeout(readyFallbackTimer);
48
+ readyFallbackTimer = null;
49
+ }
50
+ if (!socket) {
51
+ return;
52
+ }
53
+ socket.onopen = null;
54
+ socket.onclose = null;
55
+ socket.onerror = null;
56
+ socket.onmessage = null;
57
+ socket.close();
58
+ socket = null;
59
+ };
60
+ const rejectPendingControls = (error) => {
61
+ while (pendingRegisterRequests.length > 0) {
62
+ pendingRegisterRequests.shift()?.reject(error);
63
+ }
64
+ while (pendingUnregisterRequests.length > 0) {
65
+ pendingUnregisterRequests.shift()?.reject(error);
66
+ }
67
+ for (const [requestId, pending] of pendingOnceRequests.entries()) {
68
+ pending.reject(error);
69
+ pendingOnceRequests.delete(requestId);
70
+ }
71
+ rejectPendingMap(pendingExpansionRequests, error);
72
+ rejectPendingMap(pendingFileReadRequests, error);
73
+ rejectPendingMap(pendingFileWriteRequests, error);
74
+ rejectPendingMap(pendingFileDeleteRequests, error);
75
+ rejectPendingMap(pendingFileListRequests, error);
76
+ rejectPendingMap(pendingMessageRequests, error);
77
+ rejectPendingMap(pendingApprovalRequests, error);
78
+ };
79
+ const sendGatewayMessage = (message) => {
80
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
81
+ throw new Error('Gateway websocket is not connected');
82
+ }
83
+ socket.send(JSON.stringify(message));
84
+ };
85
+ const flushPendingLogs = () => {
86
+ while (pendingLogs.length > 0) {
87
+ const next = pendingLogs[0];
88
+ try {
89
+ sendGatewayMessage({
90
+ type: 'log',
91
+ entry: next,
92
+ });
93
+ pendingLogs.shift();
94
+ }
95
+ catch {
96
+ return;
97
+ }
98
+ }
99
+ };
100
+ const publishLog = (entry) => {
101
+ if (closed) {
102
+ return;
103
+ }
104
+ pendingLogs.push(entry);
105
+ if (pendingLogs.length > 1_000) {
106
+ pendingLogs.splice(0, pendingLogs.length - 1_000);
107
+ }
108
+ flushPendingLogs();
109
+ };
110
+ const dispatchLocally = async (input) => {
111
+ const seed = isAgentEvent(input)
112
+ ? input
113
+ : createAgentEvent(normalizePartialEvent(input, options.workspace), {
114
+ expansionCache: expansionCache,
115
+ });
116
+ try {
117
+ await deliverWithRetry(seed, options.onEvent, options.signal, expansionCache);
118
+ }
119
+ catch (error) {
120
+ await options.onError?.(error, seed);
121
+ throw error;
122
+ }
123
+ };
124
+ const deliverRemote = async (record) => {
125
+ const eventId = record.id ??
126
+ toAgentEventRecord(createAgentEvent(record, {
127
+ expansionCache: expansionCache,
128
+ })).id ??
129
+ globalThis.crypto.randomUUID();
130
+ const expansionLoaders = {
131
+ loadFull: () => handle.requestExpansion(eventId, 'full'),
132
+ loadDiff: () => handle.requestExpansion(eventId, 'diff'),
133
+ loadThread: (threadOptions) => handle.requestExpansion(eventId, 'thread', threadOptions),
134
+ };
135
+ const event = createAgentEvent({ ...record, id: eventId }, {
136
+ expansionCache: expansionCache,
137
+ ...expansionLoaders,
138
+ });
139
+ await withRuntimeSpan('agent.sdk.event.delivery', {
140
+ kind: SpanKind.CONSUMER,
141
+ context: extractTraceContextFromCarrier(record),
142
+ attributes: createEventSpanAttributes(event),
143
+ }, async () => {
144
+ try {
145
+ await deliverWithRetry(event, options.onEvent, options.signal, expansionCache, expansionLoaders);
146
+ sendGatewayMessage({
147
+ type: 'ack',
148
+ eventId: event.id,
149
+ });
150
+ }
151
+ catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ try {
154
+ sendGatewayMessage({
155
+ type: 'nack',
156
+ eventId: event.id,
157
+ ...(message.trim() ? { error: message } : {}),
158
+ ...(isNoRetryError(error) ? { noRetry: true } : {}),
159
+ });
160
+ }
161
+ catch (nackError) {
162
+ await reportTransportError(nackError, 'transport.delivery.nack_failed');
163
+ }
164
+ }
165
+ });
166
+ };
167
+ const reportTransportError = async (error, detail) => {
168
+ await options.onError?.(error, createTransportErrorEvent({
169
+ workspace: options.workspace,
170
+ detail,
171
+ }));
172
+ };
173
+ const scheduleReconnect = () => {
174
+ if (closed || reconnectTimer) {
175
+ return;
176
+ }
177
+ reconnectAttempt += 1;
178
+ const delayMs = Math.min(MAX_RECONNECT_DELAY_MS, 1_000 * 2 ** Math.min(reconnectAttempt - 1, 5));
179
+ reconnectTimer = setTimeout(() => {
180
+ reconnectTimer = null;
181
+ openSocket();
182
+ }, delayMs);
183
+ };
184
+ const handleGatewayMessage = async (message) => {
185
+ switch (message.type) {
186
+ case 'connected':
187
+ case 'auth_ok':
188
+ case 'subscribed':
189
+ reconnectAttempt = 0;
190
+ flushPendingLogs();
191
+ markReady();
192
+ return;
193
+ case 'ping':
194
+ socket?.send(JSON.stringify({ type: 'pong' }));
195
+ return;
196
+ case 'once_result': {
197
+ const requestId = message.requestId?.trim();
198
+ if (!requestId) {
199
+ return;
200
+ }
201
+ const pending = pendingOnceRequests.get(requestId);
202
+ if (!pending) {
203
+ return;
204
+ }
205
+ pendingOnceRequests.delete(requestId);
206
+ pending.resolve(message.acquired);
207
+ return;
208
+ }
209
+ case 'event':
210
+ await deliverRemote(message.event);
211
+ return;
212
+ case 'events':
213
+ for (const record of message.events) {
214
+ await deliverRemote(record);
215
+ }
216
+ return;
217
+ case 'delivery_failed':
218
+ await options.onError?.(new Error(message.error), createAgentEvent(message.event, {
219
+ expansionCache: expansionCache,
220
+ }));
221
+ return;
222
+ case 'registered': {
223
+ const pending = pendingRegisterRequests.shift();
224
+ if (!pending) {
225
+ return;
226
+ }
227
+ pending.resolve({
228
+ ...(message.schedules ? { schedules: message.schedules } : {}),
229
+ ...(message.watches ? { watches: message.watches } : {}),
230
+ ...(message.inbox ? { inbox: message.inbox } : {}),
231
+ });
232
+ return;
233
+ }
234
+ case 'unregistered':
235
+ pendingUnregisterRequests.shift()?.resolve();
236
+ return;
237
+ case 'expand_result':
238
+ resolvePendingMapValue(pendingExpansionRequests, message.requestId, message.expansion);
239
+ return;
240
+ case 'expand_error':
241
+ rejectPendingMapValue(pendingExpansionRequests, message.requestId, formatGatewayError(message.code, message.message));
242
+ return;
243
+ case 'files_read_result':
244
+ resolvePendingMapValue(pendingFileReadRequests, message.requestId, message.file);
245
+ return;
246
+ case 'files_write_result':
247
+ resolvePendingMapValue(pendingFileWriteRequests, message.requestId, undefined);
248
+ return;
249
+ case 'files_delete_result':
250
+ resolvePendingMapValue(pendingFileDeleteRequests, message.requestId, undefined);
251
+ return;
252
+ case 'files_list_result':
253
+ resolvePendingMapValue(pendingFileListRequests, message.requestId, Array.isArray(message.entries) ? message.entries : Array.isArray(message.items) ? message.items : []);
254
+ return;
255
+ case 'files_error':
256
+ rejectRpcRequest([
257
+ pendingFileReadRequests,
258
+ pendingFileWriteRequests,
259
+ pendingFileDeleteRequests,
260
+ pendingFileListRequests,
261
+ ], message.requestId, message.code, message.message);
262
+ return;
263
+ case 'messages_result':
264
+ resolvePendingMapValue(pendingMessageRequests, message.requestId, { id: message.id });
265
+ return;
266
+ case 'messages_error':
267
+ rejectRpcRequest([pendingMessageRequests], message.requestId, message.code, message.message);
268
+ return;
269
+ case 'approval_result':
270
+ resolvePendingMapValue(pendingApprovalRequests, message.requestId, message.approval);
271
+ return;
272
+ case 'approval_error':
273
+ rejectRpcRequest([pendingApprovalRequests], message.requestId, message.code, message.message);
274
+ return;
275
+ case 'error':
276
+ rejectPendingControls(new Error(message.error));
277
+ throw new Error(message.error);
278
+ default: {
279
+ const exhaustive = message;
280
+ throw new Error(`Unsupported gateway message: ${JSON.stringify(exhaustive)}`);
281
+ }
282
+ }
283
+ };
284
+ const openSocket = () => {
285
+ if (closed) {
286
+ return;
287
+ }
288
+ if (!gatewayUrl || !webSocketFactory) {
289
+ void reportTransportError(new Error(gatewayUrl
290
+ ? 'WebSocket is not available in this runtime'
291
+ : 'Event gateway transport is disabled for this runtime'), gatewayUrl ? 'transport.websocket.unavailable' : 'transport.gateway.disabled');
292
+ markReady();
293
+ return;
294
+ }
295
+ closeSocket();
296
+ socket = webSocketFactory(gatewayUrl);
297
+ socket.onopen = () => {
298
+ reconnectAttempt = 0;
299
+ socket?.send(JSON.stringify({
300
+ type: 'subscribe',
301
+ workspace: options.workspace,
302
+ agentId: options.agentId ?? options.workspace,
303
+ apiKey,
304
+ }));
305
+ readyFallbackTimer = setTimeout(markReady, READY_FALLBACK_MS);
306
+ flushPendingLogs();
307
+ };
308
+ socket.onmessage = (message) => {
309
+ try {
310
+ const raw = typeof message.data === 'string' ? message.data : String(message.data);
311
+ const parsed = JSON.parse(raw);
312
+ void handleGatewayMessage(parsed).catch((error) => {
313
+ void reportTransportError(error, 'transport.delivery.error');
314
+ });
315
+ }
316
+ catch (error) {
317
+ void reportTransportError(error, 'transport.parse.error');
318
+ }
319
+ };
320
+ socket.onerror = () => {
321
+ rejectPendingControls(new Error('Gateway websocket error'));
322
+ scheduleReconnect();
323
+ };
324
+ socket.onclose = () => {
325
+ rejectPendingControls(new Error('Gateway websocket closed'));
326
+ closeSocket();
327
+ if (!closed) {
328
+ scheduleReconnect();
329
+ }
330
+ };
331
+ };
332
+ if (options.signal) {
333
+ if (options.signal.aborted) {
334
+ closed = true;
335
+ markReady();
336
+ }
337
+ else {
338
+ options.signal.addEventListener('abort', () => {
339
+ void handle.close();
340
+ }, { once: true });
341
+ }
342
+ }
343
+ const handle = {
344
+ ready,
345
+ close: async () => {
346
+ if (closed) {
347
+ return;
348
+ }
349
+ closed = true;
350
+ if (reconnectTimer) {
351
+ clearTimeout(reconnectTimer);
352
+ reconnectTimer = null;
353
+ }
354
+ rejectPendingControls(new Error('Gateway websocket closed'));
355
+ closeSocket();
356
+ markReady();
357
+ },
358
+ acquireOnce: async (key) => {
359
+ await ready;
360
+ const requestId = globalThis.crypto.randomUUID();
361
+ return await withRuntimeSpan('agent.sdk.ctx.once.acquire', {
362
+ kind: SpanKind.CLIENT,
363
+ attributes: {
364
+ 'relay.request_id': requestId,
365
+ 'relay.once_key': key,
366
+ },
367
+ }, async () => await new Promise((resolve, reject) => {
368
+ pendingOnceRequests.set(requestId, { resolve, reject });
369
+ try {
370
+ sendGatewayMessage(injectTraceContextIntoCarrier({
371
+ type: 'once',
372
+ requestId,
373
+ key,
374
+ }));
375
+ }
376
+ catch (error) {
377
+ pendingOnceRequests.delete(requestId);
378
+ reject(error instanceof Error ? error : new Error(String(error)));
379
+ }
380
+ }));
381
+ },
382
+ releaseOnce: async (key) => {
383
+ await ready;
384
+ await withRuntimeSpan('agent.sdk.ctx.once.release', {
385
+ kind: SpanKind.CLIENT,
386
+ attributes: {
387
+ 'relay.once_key': key,
388
+ },
389
+ }, async () => {
390
+ sendGatewayMessage(injectTraceContextIntoCarrier({
391
+ type: 'once_release',
392
+ key,
393
+ }));
394
+ });
395
+ },
396
+ registerSchedule: async (schedule) => {
397
+ await ready;
398
+ return await withRuntimeSpan('agent.sdk.ctx.schedule.register', {
399
+ kind: SpanKind.CLIENT,
400
+ attributes: createScheduleSpanAttributes(schedule),
401
+ }, async () => await new Promise((resolve, reject) => {
402
+ pendingRegisterRequests.push({
403
+ resolve: (result) => {
404
+ const id = result.schedules?.[0]?.gatewayScheduleId?.trim();
405
+ if (!id) {
406
+ reject(new Error('Gateway did not return a schedule id'));
407
+ return;
408
+ }
409
+ resolve({ id });
410
+ },
411
+ reject,
412
+ });
413
+ try {
414
+ sendGatewayMessage(injectTraceContextIntoCarrier({
415
+ type: 'register',
416
+ schedule,
417
+ }));
418
+ }
419
+ catch (error) {
420
+ pendingRegisterRequests.pop();
421
+ reject(error instanceof Error ? error : new Error(String(error)));
422
+ }
423
+ }));
424
+ },
425
+ registerWatches: async (watch) => {
426
+ await ready;
427
+ return await new Promise((resolve, reject) => {
428
+ pendingRegisterRequests.push({ resolve, reject });
429
+ try {
430
+ sendGatewayMessage({
431
+ type: 'register',
432
+ watch,
433
+ });
434
+ }
435
+ catch (error) {
436
+ pendingRegisterRequests.pop();
437
+ reject(error instanceof Error ? error : new Error(String(error)));
438
+ }
439
+ });
440
+ },
441
+ registerInboxes: async (inbox) => {
442
+ await ready;
443
+ return await new Promise((resolve, reject) => {
444
+ pendingRegisterRequests.push({ resolve, reject });
445
+ try {
446
+ sendGatewayMessage({
447
+ type: 'register',
448
+ inbox,
449
+ });
450
+ }
451
+ catch (error) {
452
+ pendingRegisterRequests.pop();
453
+ reject(error instanceof Error ? error : new Error(String(error)));
454
+ }
455
+ });
456
+ },
457
+ unregisterSchedules: async (scheduleIds) => {
458
+ await ready;
459
+ return await withRuntimeSpan('agent.sdk.ctx.schedule.unregister', {
460
+ kind: SpanKind.CLIENT,
461
+ attributes: {
462
+ ...(scheduleIds?.length ? { 'relay.schedule_ids': scheduleIds.join(',') } : {}),
463
+ },
464
+ }, async () => await new Promise((resolve, reject) => {
465
+ pendingUnregisterRequests.push({ resolve, reject });
466
+ try {
467
+ sendGatewayMessage(injectTraceContextIntoCarrier({
468
+ type: 'unregister',
469
+ ...(scheduleIds ? { scheduleIds } : {}),
470
+ }));
471
+ }
472
+ catch (error) {
473
+ pendingUnregisterRequests.pop();
474
+ reject(error instanceof Error ? error : new Error(String(error)));
475
+ }
476
+ }));
477
+ },
478
+ requestExpansion: async (eventId, level, options) => {
479
+ await ready;
480
+ const requestId = globalThis.crypto.randomUUID();
481
+ return (await runGatewayRpc('agent.sdk.ctx.expand', {
482
+ 'relay.request_id': requestId,
483
+ 'relay.event_id': eventId,
484
+ 'relay.expand_level': level,
485
+ }, pendingExpansionRequests, requestId, {
486
+ type: 'expand',
487
+ requestId,
488
+ eventId,
489
+ level,
490
+ ...(level === 'thread' && options ? { params: options } : {}),
491
+ }, sendGatewayMessage));
492
+ },
493
+ readFile: async (path) => {
494
+ await ready;
495
+ const requestId = globalThis.crypto.randomUUID();
496
+ return await runGatewayRpc('agent.sdk.ctx.files.read', {
497
+ 'relay.request_id': requestId,
498
+ 'relay.file_path': path,
499
+ }, pendingFileReadRequests, requestId, {
500
+ type: 'files_read',
501
+ requestId,
502
+ path,
503
+ }, sendGatewayMessage);
504
+ },
505
+ writeFile: async (path, body, meta) => {
506
+ await ready;
507
+ const requestId = globalThis.crypto.randomUUID();
508
+ await runGatewayRpc('agent.sdk.ctx.files.write', {
509
+ 'relay.request_id': requestId,
510
+ 'relay.file_path': path,
511
+ }, pendingFileWriteRequests, requestId, {
512
+ type: 'files_write',
513
+ requestId,
514
+ path,
515
+ body,
516
+ ...(meta ? { meta } : {}),
517
+ }, sendGatewayMessage);
518
+ },
519
+ deleteFile: async (path) => {
520
+ await ready;
521
+ const requestId = globalThis.crypto.randomUUID();
522
+ await runGatewayRpc('agent.sdk.ctx.files.delete', {
523
+ 'relay.request_id': requestId,
524
+ 'relay.file_path': path,
525
+ }, pendingFileDeleteRequests, requestId, {
526
+ type: 'files_delete',
527
+ requestId,
528
+ path,
529
+ }, sendGatewayMessage);
530
+ },
531
+ listFiles: async (glob) => {
532
+ await ready;
533
+ const requestId = globalThis.crypto.randomUUID();
534
+ return await runGatewayRpc('agent.sdk.ctx.files.list', {
535
+ 'relay.request_id': requestId,
536
+ 'relay.file_glob': glob,
537
+ }, pendingFileListRequests, requestId, {
538
+ type: 'files_list',
539
+ requestId,
540
+ glob,
541
+ }, sendGatewayMessage);
542
+ },
543
+ publishLog,
544
+ postMessage: async (channel, text, opts) => {
545
+ await ready;
546
+ const requestId = globalThis.crypto.randomUUID();
547
+ return await runGatewayRpc('agent.sdk.ctx.messages.post', {
548
+ 'relay.request_id': requestId,
549
+ 'relay.channel': channel,
550
+ }, pendingMessageRequests, requestId, {
551
+ type: 'messages_post',
552
+ requestId,
553
+ channel,
554
+ text,
555
+ ...(opts?.idempotencyKey ? { opts: { idempotencyKey: opts.idempotencyKey } } : {}),
556
+ }, sendGatewayMessage);
557
+ },
558
+ replyMessage: async (threadId, text, opts) => {
559
+ await ready;
560
+ const requestId = globalThis.crypto.randomUUID();
561
+ return await runGatewayRpc('agent.sdk.ctx.messages.reply', {
562
+ 'relay.request_id': requestId,
563
+ 'relay.thread_id': threadId,
564
+ }, pendingMessageRequests, requestId, {
565
+ type: 'messages_reply',
566
+ requestId,
567
+ threadId,
568
+ text,
569
+ ...(opts?.idempotencyKey ? { opts: { idempotencyKey: opts.idempotencyKey } } : {}),
570
+ }, sendGatewayMessage);
571
+ },
572
+ sendDm: async (agentOrUser, text, opts) => {
573
+ await ready;
574
+ const requestId = globalThis.crypto.randomUUID();
575
+ return await runGatewayRpc('agent.sdk.ctx.messages.dm', {
576
+ 'relay.request_id': requestId,
577
+ 'relay.dm_target': agentOrUser,
578
+ }, pendingMessageRequests, requestId, {
579
+ type: 'messages_dm',
580
+ requestId,
581
+ agentOrUser,
582
+ text,
583
+ ...(opts?.idempotencyKey ? { opts: { idempotencyKey: opts.idempotencyKey } } : {}),
584
+ }, sendGatewayMessage);
585
+ },
586
+ awaitApproval: async (approvalId) => {
587
+ await ready;
588
+ const requestId = globalThis.crypto.randomUUID();
589
+ return await runGatewayRpc('agent.sdk.ctx.approval.wait', {
590
+ 'relay.request_id': requestId,
591
+ 'relay.approval_id': approvalId,
592
+ }, pendingApprovalRequests, requestId, {
593
+ type: 'approval_wait',
594
+ requestId,
595
+ approvalId,
596
+ }, sendGatewayMessage);
597
+ },
598
+ trigger: async (event) => {
599
+ await dispatchLocally(event);
600
+ },
601
+ };
602
+ openSocket();
603
+ return handle;
604
+ }
605
+ function resolveApiKey(apiKey) {
606
+ const resolved = apiKey ?? process.env.RELAY_API_KEY;
607
+ if (!resolved) {
608
+ throw new Error('RELAY_API_KEY is required');
609
+ }
610
+ return resolved;
611
+ }
612
+ function resolveGatewayUrl(gatewayUrl) {
613
+ const resolved = gatewayUrl ?? process.env.RELAY_AGENT_EVENTS_URL ?? DEFAULT_GATEWAY_URL;
614
+ const trimmed = resolved.trim();
615
+ return trimmed.length > 0 ? trimmed : null;
616
+ }
617
+ function resolveWebSocketFactory(factory) {
618
+ if (factory) {
619
+ return factory;
620
+ }
621
+ if (typeof globalThis.WebSocket === 'function') {
622
+ return (url) => new globalThis.WebSocket(url);
623
+ }
624
+ return null;
625
+ }
626
+ function isNoRetryError(error) {
627
+ return Boolean(error && typeof error === 'object' && 'name' in error && error.name === 'NoRetry');
628
+ }
629
+ function isAgentEvent(value) {
630
+ return (typeof value.id === 'string' &&
631
+ typeof value.workspace === 'string' &&
632
+ typeof value.type === 'string' &&
633
+ typeof value.occurredAt === 'string' &&
634
+ typeof value.attempt === 'number' &&
635
+ typeof value.resource === 'object' &&
636
+ value.resource !== null &&
637
+ typeof value.summary === 'object' &&
638
+ value.summary !== null &&
639
+ typeof value.expand === 'function');
640
+ }
641
+ function createEventSpanAttributes(event) {
642
+ const attributes = {
643
+ 'relay.workspace': event.workspace,
644
+ 'relay.event_type': event.type,
645
+ 'relay.event_id': event.id,
646
+ };
647
+ if (event.agentId) {
648
+ attributes['relay.agent_id'] = event.agentId;
649
+ }
650
+ if (event.path) {
651
+ attributes['relay.resource_path'] = event.path;
652
+ }
653
+ if (event.resource.kind) {
654
+ attributes['relay.resource_kind'] = event.resource.kind;
655
+ }
656
+ if (event.resource.provider) {
657
+ attributes['relay.provider'] = event.resource.provider;
658
+ }
659
+ if (typeof event.attempt === 'number') {
660
+ attributes['relay.delivery_attempt'] = event.attempt;
661
+ }
662
+ return attributes;
663
+ }
664
+ function normalizePartialEvent(event, workspace) {
665
+ const record = event;
666
+ return {
667
+ id: event.id,
668
+ workspace: event.workspace ?? workspace,
669
+ type: event.type ?? 'startup',
670
+ occurredAt: event.occurredAt,
671
+ attempt: event.attempt,
672
+ resource: event.resource ? { ...event.resource } : undefined,
673
+ summary: event.summary ? { ...event.summary } : undefined,
674
+ digest: event.digest,
675
+ schedule: typeof record.schedule === 'string' ? record.schedule : undefined,
676
+ scheduledFor: typeof record.scheduledFor === 'string' ? record.scheduledFor : undefined,
677
+ reason: record.reason === 'cold-start' || record.reason === 'redeploy' || record.reason === 'manual'
678
+ ? record.reason
679
+ : undefined,
680
+ path: typeof record.path === 'string' ? record.path : undefined,
681
+ watch: typeof record.watch === 'string' ? record.watch : undefined,
682
+ action: record.action === 'created' || record.action === 'updated' || record.action === 'deleted'
683
+ ? record.action
684
+ : undefined,
685
+ agentId: typeof record.agentId === 'string' ? record.agentId : undefined,
686
+ channel: typeof record.channel === 'string' ? record.channel : undefined,
687
+ messageId: typeof record.messageId === 'string' ? record.messageId : undefined,
688
+ threadId: typeof record.threadId === 'string' ? record.threadId : undefined,
689
+ detail: typeof record.detail === 'string' ? record.detail : undefined,
690
+ };
691
+ }
692
+ async function deliverWithRetry(seed, onEvent, signal, expansionCache, expansionLoaders) {
693
+ await withRetry(async (attempt) => {
694
+ const attemptEvent = attempt === seed.attempt
695
+ ? seed
696
+ : createAgentEvent({ ...toAgentEventRecord(seed), attempt }, {
697
+ expansionCache,
698
+ ...expansionLoaders,
699
+ });
700
+ await withRuntimeSpan('agent.sdk.event.handler', {
701
+ attributes: {
702
+ ...createEventSpanAttributes(attemptEvent),
703
+ 'relay.delivery_attempt': attempt,
704
+ },
705
+ }, async () => {
706
+ await onEvent(attemptEvent);
707
+ });
708
+ }, undefined, signal);
709
+ }
710
+ async function runGatewayRpc(spanName, attributes, pending, requestId, message, sendGatewayMessage) {
711
+ return await withRuntimeSpan(spanName, {
712
+ kind: SpanKind.CLIENT,
713
+ attributes,
714
+ }, async () => await awaitGatewayRpc(pending, requestId, injectTraceContextIntoCarrier(message), sendGatewayMessage));
715
+ }
716
+ function rejectPendingMap(pending, error) {
717
+ for (const [requestId, request] of pending.entries()) {
718
+ request.reject(error);
719
+ pending.delete(requestId);
720
+ }
721
+ }
722
+ function resolvePendingMapValue(pending, requestId, value) {
723
+ const normalized = requestId?.trim();
724
+ if (!normalized) {
725
+ return;
726
+ }
727
+ const request = pending.get(normalized);
728
+ if (!request) {
729
+ return;
730
+ }
731
+ pending.delete(normalized);
732
+ request.resolve(value);
733
+ }
734
+ function rejectPendingMapValue(pending, requestId, error) {
735
+ const normalized = requestId?.trim();
736
+ if (!normalized) {
737
+ return;
738
+ }
739
+ const request = pending.get(normalized);
740
+ if (!request) {
741
+ return;
742
+ }
743
+ pending.delete(normalized);
744
+ request.reject(error);
745
+ }
746
+ function rejectRpcRequest(pendingMaps, requestId, code, message) {
747
+ const error = formatGatewayError(code, message);
748
+ for (const pending of pendingMaps) {
749
+ rejectPendingMapValue(pending, requestId, error);
750
+ }
751
+ }
752
+ async function awaitGatewayRpc(pending, requestId, message, sendGatewayMessage) {
753
+ return await new Promise((resolve, reject) => {
754
+ pending.set(requestId, { resolve, reject });
755
+ try {
756
+ sendGatewayMessage(message);
757
+ }
758
+ catch (error) {
759
+ pending.delete(requestId);
760
+ reject(error instanceof Error ? error : new Error(String(error)));
761
+ }
762
+ });
763
+ }
764
+ function formatGatewayError(code, message) {
765
+ return new Error(code?.trim() ? `${code}: ${message}` : message);
766
+ }
767
+ function createScheduleSpanAttributes(schedule) {
768
+ if (typeof schedule === 'string') {
769
+ return { 'relay.schedule': schedule };
770
+ }
771
+ if ('cron' in schedule) {
772
+ return {
773
+ 'relay.schedule': schedule.cron,
774
+ ...(schedule.tz ? { 'relay.schedule_tz': schedule.tz } : {}),
775
+ };
776
+ }
777
+ return {
778
+ 'relay.schedule_at': schedule.at,
779
+ };
780
+ }
781
+ //# sourceMappingURL=transport.js.map