@agentsquared/cli 1.0.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,857 @@
1
+ import { randomRequestId } from '../shared/primitives.mjs'
2
+ import { createConnectTicket, getAgentCard, introspectConnectTicket, postOnline, reportSession } from './relay_http.mjs'
3
+ import { currentPeerConnection, dialProtocol, openStreamOnExistingConnection, readJsonMessage, waitForPublishedTransport, writeLine } from './libp2p.mjs'
4
+
5
+ const RELAY_RECOVERY_RETRY_DELAY_MS = 1500
6
+ const RESPONSE_ACK_TIMEOUT_MS = 3 * 1000
7
+ const TURN_RECEIPT_TIMEOUT_MS = 20 * 1000
8
+ const TURN_RESPONSE_TIMEOUT_MS = 210 * 1000
9
+
10
+ export function buildJsonRpcEnvelope({ id, method, message, metadata = {} }) {
11
+ return {
12
+ jsonrpc: '2.0',
13
+ id: id ?? randomRequestId('a2a'),
14
+ method,
15
+ params: {
16
+ message,
17
+ metadata
18
+ }
19
+ }
20
+ }
21
+
22
+ function buildJsonRpcAck(id) {
23
+ return {
24
+ jsonrpc: '2.0',
25
+ id,
26
+ result: {
27
+ ack: true
28
+ }
29
+ }
30
+ }
31
+
32
+ function buildJsonRpcReceipt(id) {
33
+ return {
34
+ jsonrpc: '2.0',
35
+ id,
36
+ result: {
37
+ received: true
38
+ }
39
+ }
40
+ }
41
+
42
+ function isJsonRpcAck(message, id = '') {
43
+ return Boolean(
44
+ message
45
+ && typeof message === 'object'
46
+ && `${message.jsonrpc ?? ''}`.trim() === '2.0'
47
+ && `${message.id ?? ''}`.trim() === `${id ?? ''}`.trim()
48
+ && message.result
49
+ && typeof message.result === 'object'
50
+ && message.result.ack === true
51
+ )
52
+ }
53
+
54
+ function isJsonRpcReceipt(message, id = '') {
55
+ return Boolean(
56
+ message
57
+ && typeof message === 'object'
58
+ && `${message.jsonrpc ?? ''}`.trim() === '2.0'
59
+ && `${message.id ?? ''}`.trim() === `${id ?? ''}`.trim()
60
+ && message.result
61
+ && typeof message.result === 'object'
62
+ && message.result.received === true
63
+ )
64
+ }
65
+
66
+ async function sendResponseAck(stream, requestId) {
67
+ if (!stream || !`${requestId ?? ''}`.trim()) {
68
+ return
69
+ }
70
+ try {
71
+ await writeLine(stream, JSON.stringify(buildJsonRpcAck(requestId)))
72
+ } catch {
73
+ // best-effort only; the response itself was already received locally
74
+ }
75
+ }
76
+
77
+ async function sendRequestReceipt(stream, requestId) {
78
+ if (!stream || !`${requestId ?? ''}`.trim()) {
79
+ return
80
+ }
81
+ await writeLine(stream, JSON.stringify(buildJsonRpcReceipt(requestId)))
82
+ }
83
+
84
+ async function waitForOptionalAck(stream, requestId, timeoutMs = RESPONSE_ACK_TIMEOUT_MS) {
85
+ if (!stream || !`${requestId ?? ''}`.trim()) {
86
+ return false
87
+ }
88
+ try {
89
+ const ack = await Promise.race([
90
+ readJsonMessage(stream),
91
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
92
+ ])
93
+ return isJsonRpcAck(ack, requestId)
94
+ } catch {
95
+ return false
96
+ }
97
+ }
98
+
99
+ async function readMessageWithTimeout(readMessageFn, stream, timeoutMs, label) {
100
+ try {
101
+ return await Promise.race([
102
+ readMessageFn(stream),
103
+ new Promise((_, reject) => setTimeout(() => {
104
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`))
105
+ }, timeoutMs))
106
+ ])
107
+ } catch (error) {
108
+ throw error
109
+ }
110
+ }
111
+
112
+ function inferPostDispatchFailureKind(error = null) {
113
+ const explicit = `${error?.a2FailureKind ?? ''}`.trim()
114
+ if (explicit) {
115
+ return explicit
116
+ }
117
+ const lower = `${error?.message ?? ''}`.trim().toLowerCase()
118
+ if (!lower) {
119
+ return ''
120
+ }
121
+ if (lower.includes('turn response timed out after')) {
122
+ return 'post-dispatch-response-timeout'
123
+ }
124
+ if (lower === 'empty json message') {
125
+ return 'post-dispatch-empty-response'
126
+ }
127
+ if (
128
+ lower.includes('stream that is closed')
129
+ || lower.includes('stream closed before drain')
130
+ || lower.includes('stream reset')
131
+ || lower.includes('connection reset')
132
+ || lower.includes('connection closed')
133
+ ) {
134
+ return 'post-dispatch-stream-closed'
135
+ }
136
+ return 'post-dispatch-response-unconfirmed'
137
+ }
138
+
139
+ export async function currentTransport(node, binding, options = {}) {
140
+ return waitForPublishedTransport(node, binding, options)
141
+ }
142
+
143
+ export async function publishGatewayPresence(apiBase, agentId, bundle, node, binding, activitySummary, {
144
+ availabilityStatus = 'available',
145
+ requireRelayReservation = false
146
+ } = {}) {
147
+ const transport = await currentTransport(node, binding, { requireRelayReservation })
148
+ return postOnline(apiBase, agentId, bundle, {
149
+ availabilityStatus,
150
+ activitySummary,
151
+ peerId: transport.peerId,
152
+ listenAddrs: transport.listenAddrs,
153
+ relayAddrs: transport.relayAddrs,
154
+ supportedBindings: transport.supportedBindings,
155
+ a2aProtocolVersion: transport.a2aProtocolVersion,
156
+ streamProtocol: transport.streamProtocol
157
+ })
158
+ }
159
+
160
+ export async function openDirectPeerSession({
161
+ apiBase,
162
+ agentId,
163
+ bundle,
164
+ node,
165
+ binding,
166
+ targetAgentId,
167
+ skillName,
168
+ method,
169
+ message,
170
+ metadata = null,
171
+ activitySummary,
172
+ report,
173
+ sessionStore = null,
174
+ allowTrustedReuse = true,
175
+ _deps = null
176
+ }) {
177
+ const deps = {
178
+ currentPeerConnectionFn: currentPeerConnection,
179
+ exchangeOverTransportFn: exchangeOverTransport,
180
+ currentTransportFn: currentTransport,
181
+ createConnectTicketWithRecoveryFn: createConnectTicketWithRecovery,
182
+ ...(_deps && typeof _deps === 'object' ? _deps : {})
183
+ }
184
+ const metadataPayload = metadata && typeof metadata === 'object' ? metadata : {}
185
+ const conversationKey = `${metadataPayload.conversationKey ?? ''}`.trim()
186
+ if (!conversationKey) {
187
+ throw Object.assign(new Error('conversationKey is required for outbound AgentSquared conversations'), { code: 400 })
188
+ }
189
+ // Each AgentSquared turn always opens a fresh libp2p stream.
190
+ // When a trusted peer session is available, we only reuse the underlying
191
+ // peer connection and session metadata, not the previous stream itself.
192
+ const cachedSession = sessionStore?.trustedSessionByConversation?.(conversationKey)
193
+ ?? null
194
+ const liveConnection = cachedSession?.remotePeerId ? deps.currentPeerConnectionFn(node, cachedSession.remotePeerId) : null
195
+
196
+ let ticket = null
197
+ let peerSessionId = `${cachedSession?.peerSessionId ?? ''}`.trim()
198
+ let targetTransport = null
199
+ let reusedPeerConnection = false
200
+ let ambiguousTrustedDispatchError = null
201
+ const reusableTransport = allowTrustedReuse && cachedSession
202
+ ? mergeTargetTransport({
203
+ primary: cachedSession.remoteTransport,
204
+ secondary: cachedSession?.remotePeerId
205
+ ? {
206
+ peerId: cachedSession.remotePeerId,
207
+ streamProtocol: binding.streamProtocol
208
+ }
209
+ : null,
210
+ streamProtocol: binding.streamProtocol
211
+ })
212
+ : null
213
+ const requestId = randomRequestId('a2a')
214
+ const buildRequest = ({
215
+ relayConnectTicket = '',
216
+ peerSessionId: nextPeerSessionId = ''
217
+ } = {}) => buildJsonRpcEnvelope({
218
+ id: requestId,
219
+ method,
220
+ message,
221
+ metadata: {
222
+ ...metadataPayload,
223
+ relayConnectTicket,
224
+ peerSessionId: `${nextPeerSessionId ?? ''}`.trim(),
225
+ skillHint: `${skillName ?? ''}`.trim(),
226
+ from: agentId,
227
+ to: targetAgentId
228
+ }
229
+ })
230
+
231
+ if (reusableTransport?.peerId && reusableTransport?.streamProtocol && liveConnection) {
232
+ try {
233
+ sessionStore?.touchTrustedSession?.(cachedSession.peerSessionId)
234
+ const reusedResponse = await deps.exchangeOverTransportFn({
235
+ node,
236
+ transport: reusableTransport,
237
+ request: buildRequest({
238
+ relayConnectTicket: '',
239
+ peerSessionId
240
+ }),
241
+ reuseExistingConnection: true
242
+ })
243
+ targetTransport = reusableTransport
244
+ reusedPeerConnection = true
245
+
246
+ if (peerSessionId && targetTransport?.peerId) {
247
+ sessionStore?.rememberTrustedSession?.({
248
+ peerSessionId,
249
+ conversationKey,
250
+ remoteAgentId: targetAgentId,
251
+ remotePeerId: targetTransport.peerId,
252
+ remoteTransport: targetTransport,
253
+ skillHint: `${skillName ?? ''}`.trim()
254
+ })
255
+ }
256
+
257
+ return {
258
+ ticket,
259
+ peerSessionId,
260
+ response: reusedResponse,
261
+ sessionReport: null,
262
+ reusedPeerConnection,
263
+ // Backward-compatible alias for existing callers.
264
+ reusedSession: reusedPeerConnection
265
+ }
266
+ } catch (error) {
267
+ if (!error?.a2DeliveryStatusKnown && `${error?.a2DispatchStage ?? ''}` === 'post-dispatch') {
268
+ ambiguousTrustedDispatchError = error
269
+ } else if (!isTrustedSessionRetryable(error)) {
270
+ throw error
271
+ }
272
+ }
273
+ }
274
+
275
+ let response
276
+ try {
277
+ const transport = await deps.currentTransportFn(node, binding, { requireRelayReservation: true })
278
+ const relayAttempt = await deps.createConnectTicketWithRecoveryFn({
279
+ apiBase,
280
+ agentId,
281
+ bundle,
282
+ node,
283
+ binding,
284
+ targetAgentId,
285
+ skillName,
286
+ transport,
287
+ cachedTransport: cachedSession?.remoteTransport ?? null
288
+ })
289
+ ticket = relayAttempt.ticket
290
+ targetTransport = relayAttempt.targetTransport
291
+ peerSessionId = peerSessionId || parseConnectTicketId(ticket.ticket) || randomRequestId('peer')
292
+
293
+ response = await deps.exchangeOverTransportFn({
294
+ node,
295
+ transport: targetTransport,
296
+ request: buildRequest({
297
+ relayConnectTicket: ticket?.ticket ?? '',
298
+ peerSessionId
299
+ })
300
+ })
301
+ } catch (error) {
302
+ if (ambiguousTrustedDispatchError && !ambiguousTrustedDispatchError.a2DeliveryStatusKnown) {
303
+ const followUpDetail = `${error?.message ?? ''}`.trim()
304
+ if (followUpDetail && !ambiguousTrustedDispatchError.message.includes(followUpDetail)) {
305
+ ambiguousTrustedDispatchError.message = `${ambiguousTrustedDispatchError.message} Fresh relay retry also failed: ${followUpDetail}`
306
+ }
307
+ throw ambiguousTrustedDispatchError
308
+ }
309
+ throw error
310
+ }
311
+
312
+ if (peerSessionId && targetTransport?.peerId) {
313
+ sessionStore?.rememberTrustedSession?.({
314
+ peerSessionId,
315
+ conversationKey,
316
+ remoteAgentId: targetAgentId,
317
+ remotePeerId: targetTransport.peerId,
318
+ remoteTransport: targetTransport,
319
+ skillHint: `${skillName ?? ''}`.trim()
320
+ })
321
+ }
322
+
323
+ let sessionReport = null
324
+ if (report && ticket?.ticket) {
325
+ sessionReport = await reportSession(apiBase, agentId, bundle, {
326
+ ticket: ticket.ticket,
327
+ taskId: report.taskId,
328
+ status: report.status ?? 'completed',
329
+ summary: report.summary,
330
+ publicSummary: report.publicSummary ?? ''
331
+ }, await bestEffortCurrentTransport(node, binding))
332
+ }
333
+
334
+ return {
335
+ ticket,
336
+ peerSessionId,
337
+ response,
338
+ sessionReport,
339
+ reusedPeerConnection,
340
+ reusedSession: reusedPeerConnection
341
+ }
342
+ }
343
+
344
+ export async function createConnectTicketWithRecovery({
345
+ apiBase,
346
+ agentId,
347
+ bundle,
348
+ node,
349
+ binding,
350
+ targetAgentId,
351
+ skillName,
352
+ transport,
353
+ cachedTransport = null,
354
+ republishPresence = null,
355
+ retryDelayMs = RELAY_RECOVERY_RETRY_DELAY_MS
356
+ }) {
357
+ const attempt = async () => {
358
+ const latestAgentCard = await bestEffortAgentCard(apiBase, agentId, bundle, targetAgentId, transport)
359
+ const ticket = await createConnectTicket(apiBase, agentId, bundle, targetAgentId, skillName, transport)
360
+ const targetTransport = mergeTargetTransport({
361
+ primary: latestAgentCard?.preferredTransport ?? null,
362
+ secondary: ticket.targetTransport ?? ticket.agentCard?.preferredTransport ?? null,
363
+ tertiary: cachedTransport,
364
+ streamProtocol: binding.streamProtocol
365
+ })
366
+ return { ticket, targetTransport }
367
+ }
368
+
369
+ try {
370
+ return await attempt()
371
+ } catch (error) {
372
+ if (!isRelayPresenceRetryable(error)) {
373
+ throw error
374
+ }
375
+ if (typeof republishPresence === 'function') {
376
+ await republishPresence(error)
377
+ } else {
378
+ await bestEffortRepublishPresence(apiBase, agentId, bundle, node, binding)
379
+ }
380
+ await sleep(retryDelayMs)
381
+ return attempt()
382
+ }
383
+ }
384
+
385
+ export async function attachInboundRouter({
386
+ apiBase,
387
+ agentId,
388
+ bundle,
389
+ node,
390
+ binding,
391
+ handler,
392
+ sessionStore
393
+ }) {
394
+ node.handle(binding.streamProtocol, async (eventOrStream, maybeConnection) => {
395
+ const { stream, connection } = normalizeInboundStreamContext(eventOrStream, maybeConnection)
396
+ const remotePeerId = connection?.remotePeer?.toString?.()
397
+ ?? stream?.stat?.connection?.remotePeer?.toString?.()
398
+ ?? ''
399
+ let request = null
400
+ let peerSessionId = ''
401
+ let receiptSent = false
402
+ try {
403
+ request = await readJsonMessage(stream)
404
+ const metadata = request?.params?.metadata ?? {}
405
+ const conversationKey = `${metadata.conversationKey ?? ''}`.trim()
406
+ if (!conversationKey) {
407
+ await writeLine(stream, JSON.stringify({
408
+ jsonrpc: '2.0',
409
+ id: request?.id ?? randomRequestId('invalid'),
410
+ error: { code: 400, message: 'conversationKey is required for inbound AgentSquared conversations' }
411
+ }))
412
+ return
413
+ }
414
+ const relayConnectTicket = `${metadata.relayConnectTicket ?? ''}`.trim()
415
+ const requestedPeerSessionId = `${metadata.peerSessionId ?? ''}`.trim()
416
+ let ticketView = null
417
+ peerSessionId = requestedPeerSessionId
418
+ let remoteAgentId = `${metadata.from ?? ''}`.trim()
419
+ let suggestedSkill = `${metadata.skillHint ?? ''}`.trim()
420
+
421
+ if (relayConnectTicket) {
422
+ ticketView = await introspectConnectTicket(
423
+ apiBase,
424
+ agentId,
425
+ bundle,
426
+ relayConnectTicket,
427
+ await bestEffortCurrentTransport(node, binding)
428
+ )
429
+ peerSessionId = peerSessionId || ticketView.ticketId
430
+ remoteAgentId = remoteAgentId || ticketView.initiatorAgentId
431
+ suggestedSkill = suggestedSkill || `${ticketView.skillName ?? ''}`.trim()
432
+ const remoteTransport = buildInboundRemoteTransport({
433
+ connection,
434
+ remotePeerId,
435
+ binding
436
+ })
437
+ sessionStore?.rememberTrustedSession?.({
438
+ peerSessionId,
439
+ conversationKey,
440
+ remoteAgentId,
441
+ remotePeerId,
442
+ remoteTransport,
443
+ ticketView,
444
+ skillHint: suggestedSkill
445
+ })
446
+ } else {
447
+ const trustedSession = sessionStore?.trustedSessionById?.(peerSessionId)
448
+ if (!trustedSession || trustedSession.remotePeerId !== remotePeerId) {
449
+ await writeLine(stream, JSON.stringify({
450
+ jsonrpc: '2.0',
451
+ id: request?.id ?? randomRequestId('invalid'),
452
+ error: { code: 401, message: 'relayConnectTicket or a trusted peerSessionId is required' }
453
+ }))
454
+ return
455
+ }
456
+ sessionStore?.touchTrustedSession?.(trustedSession.peerSessionId)
457
+ if (conversationKey) {
458
+ sessionStore?.rememberTrustedSession?.({
459
+ peerSessionId: trustedSession.peerSessionId,
460
+ conversationKey,
461
+ remoteAgentId: trustedSession.remoteAgentId,
462
+ remotePeerId: trustedSession.remotePeerId,
463
+ remoteTransport: trustedSession.remoteTransport,
464
+ ticketView: trustedSession.ticketView,
465
+ skillHint: trustedSession.skillHint
466
+ })
467
+ }
468
+ remoteAgentId = remoteAgentId || trustedSession.remoteAgentId
469
+ ticketView = trustedSession.ticketView ?? null
470
+ suggestedSkill = suggestedSkill || trustedSession.skillHint || 'friend-im'
471
+ }
472
+
473
+ const cachedHandledResponse = sessionStore?.handledRequestResponse?.(peerSessionId, request?.id)
474
+ if (cachedHandledResponse) {
475
+ await sendRequestReceipt(stream, request?.id)
476
+ receiptSent = true
477
+ await writeLine(stream, JSON.stringify(cachedHandledResponse))
478
+ await waitForOptionalAck(stream, request?.id)
479
+ return
480
+ }
481
+
482
+ const inbound = await sessionStore.enqueueInbound({
483
+ request,
484
+ ticketView,
485
+ remotePeerId,
486
+ remoteAgentId,
487
+ peerSessionId,
488
+ suggestedSkill,
489
+ defaultSkill: 'friend-im'
490
+ })
491
+ await sendRequestReceipt(stream, request?.id)
492
+ receiptSent = true
493
+ const result = await inbound.responsePromise
494
+ const finalResult = typeof result === 'object' && result != null
495
+ ? {
496
+ ...result,
497
+ metadata: {
498
+ ...(result.metadata ?? {}),
499
+ peerSessionId
500
+ }
501
+ }
502
+ : { value: result, metadata: { peerSessionId } }
503
+ await writeLine(stream, JSON.stringify({
504
+ jsonrpc: '2.0',
505
+ id: request.id,
506
+ result: finalResult
507
+ }))
508
+ sessionStore?.rememberHandledRequest?.({
509
+ peerSessionId,
510
+ requestId: request?.id,
511
+ response: {
512
+ jsonrpc: '2.0',
513
+ id: request.id,
514
+ result: finalResult
515
+ }
516
+ })
517
+ await waitForOptionalAck(stream, request?.id)
518
+ } catch (error) {
519
+ const errorResponse = {
520
+ jsonrpc: '2.0',
521
+ id: request?.id ?? randomRequestId('error'),
522
+ error: { code: Number.parseInt(`${error.code ?? 500}`, 10) || 500, message: error.message }
523
+ }
524
+ if (peerSessionId && request?.id) {
525
+ sessionStore?.rememberHandledRequest?.({
526
+ peerSessionId,
527
+ requestId: request.id,
528
+ response: errorResponse
529
+ })
530
+ }
531
+ if (request?.id && !receiptSent) {
532
+ await sendRequestReceipt(stream, request?.id)
533
+ receiptSent = true
534
+ }
535
+ await writeLine(stream, JSON.stringify(errorResponse))
536
+ await waitForOptionalAck(stream, request?.id)
537
+ } finally {
538
+ await stream.close()
539
+ }
540
+ }, { runOnLimitedConnection: true })
541
+ }
542
+
543
+ function normalizeInboundStreamContext(eventOrStream, maybeConnection) {
544
+ if (maybeConnection) {
545
+ return {
546
+ stream: eventOrStream,
547
+ connection: maybeConnection
548
+ }
549
+ }
550
+
551
+ return {
552
+ stream: eventOrStream?.stream ?? eventOrStream,
553
+ connection: eventOrStream?.connection ?? null
554
+ }
555
+ }
556
+
557
+ async function bestEffortRepublishPresence(apiBase, agentId, bundle, node, binding) {
558
+ try {
559
+ await publishGatewayPresence(apiBase, agentId, bundle, node, binding, 'Refreshing relay presence after a transient delivery failure.', {
560
+ requireRelayReservation: true
561
+ })
562
+ } catch {
563
+ // best-effort only; retry path will still surface the original relay error if recovery did not help
564
+ }
565
+ }
566
+
567
+ async function bestEffortCurrentTransport(node, binding) {
568
+ try {
569
+ return await currentTransport(node, binding)
570
+ } catch {
571
+ return null
572
+ }
573
+ }
574
+
575
+ export async function exchangeOverTransport({
576
+ node,
577
+ transport,
578
+ request,
579
+ reuseExistingConnection = false,
580
+ openStreamFn = openTransportStream,
581
+ writeLineFn = writeLine,
582
+ readMessageFn = readJsonMessage,
583
+ turnReceiptTimeoutMs = TURN_RECEIPT_TIMEOUT_MS,
584
+ turnResponseTimeoutMs = TURN_RESPONSE_TIMEOUT_MS
585
+ }) {
586
+ let lastError = null
587
+ for (let attempt = 0; attempt < 2; attempt += 1) {
588
+ let stream = null
589
+ let dispatchStage = 'pre-dispatch'
590
+ let receiptConfirmed = false
591
+ try {
592
+ stream = await openStreamFn(node, transport, {
593
+ reuseExistingConnection,
594
+ allowDialFallback: attempt > 0
595
+ })
596
+ await writeLineFn(stream, JSON.stringify(request))
597
+ const firstMessage = await readMessageWithTimeout(
598
+ readMessageFn,
599
+ stream,
600
+ turnReceiptTimeoutMs,
601
+ 'request receipt'
602
+ )
603
+ let response = firstMessage
604
+ if (isJsonRpcReceipt(firstMessage, request?.id)) {
605
+ receiptConfirmed = true
606
+ dispatchStage = 'post-dispatch'
607
+ response = await readMessageWithTimeout(
608
+ readMessageFn,
609
+ stream,
610
+ turnResponseTimeoutMs,
611
+ 'turn response'
612
+ )
613
+ } else if (isJsonRpcAck(firstMessage, request?.id)) {
614
+ const error = new Error('unexpected response acknowledgement before request receipt')
615
+ error.a2DeliveryStatusKnown = true
616
+ throw error
617
+ } else {
618
+ receiptConfirmed = true
619
+ dispatchStage = 'post-dispatch'
620
+ }
621
+ await sendResponseAck(stream, request?.id)
622
+ if (response.error) {
623
+ throw buildJsonRpcError(response.error)
624
+ }
625
+ return response
626
+ } catch (error) {
627
+ error.a2DispatchStage = error.a2DispatchStage || dispatchStage
628
+ if (shouldRetryBeforeReceipt(error, attempt, receiptConfirmed)) {
629
+ lastError = error
630
+ continue
631
+ }
632
+ if (shouldRetryEmptyPostDispatch(error, attempt)) {
633
+ lastError = error
634
+ continue
635
+ }
636
+ if (lastError && `${lastError?.a2DispatchStage ?? ''}` === 'post-dispatch' && !lastError?.a2DeliveryStatusKnown) {
637
+ lastError.a2FailureKind = inferPostDispatchFailureKind(lastError)
638
+ if (!/delivery status is unknown/i.test(`${lastError.message ?? ''}`)) {
639
+ lastError.message = `delivery status is unknown after the request was dispatched: ${lastError.message ?? 'response could not be confirmed'}`
640
+ }
641
+ throw lastError
642
+ }
643
+ if (dispatchStage === 'post-dispatch' && !error.a2DeliveryStatusKnown) {
644
+ error.a2DeliveryStatusKnown = false
645
+ error.a2FailureKind = inferPostDispatchFailureKind(error)
646
+ if (!/delivery status is unknown/i.test(`${error.message ?? ''}`)) {
647
+ error.message = `delivery status is unknown after the request was dispatched: ${error.message ?? 'response could not be confirmed'}`
648
+ }
649
+ }
650
+ throw error
651
+ } finally {
652
+ await stream?.close?.()
653
+ }
654
+ }
655
+
656
+ if (lastError) {
657
+ if (!lastError.a2DeliveryStatusKnown) {
658
+ lastError.a2DeliveryStatusKnown = false
659
+ }
660
+ lastError.a2FailureKind = inferPostDispatchFailureKind(lastError)
661
+ if (!/delivery status is unknown/i.test(`${lastError.message ?? ''}`)) {
662
+ lastError.message = `delivery status is unknown after the request was dispatched: ${lastError.message ?? 'response could not be confirmed'}`
663
+ }
664
+ throw lastError
665
+ }
666
+
667
+ throw new Error('delivery status is unknown after the request was dispatched: response could not be confirmed')
668
+ }
669
+
670
+ async function openTransportStream(node, transport, {
671
+ reuseExistingConnection = false,
672
+ allowDialFallback = false
673
+ } = {}) {
674
+ if (!reuseExistingConnection) {
675
+ return dialProtocol(node, transport, { requireDirect: false })
676
+ }
677
+ try {
678
+ return await openStreamOnExistingConnection(node, transport)
679
+ } catch (error) {
680
+ if (!allowDialFallback) {
681
+ throw error
682
+ }
683
+ }
684
+ return dialProtocol(node, transport, { requireDirect: false })
685
+ }
686
+
687
+ function shouldRetryEmptyPostDispatch(error, attempt) {
688
+ if (attempt > 0) {
689
+ return false
690
+ }
691
+ if (`${error?.a2DispatchStage ?? ''}` !== 'post-dispatch') {
692
+ return false
693
+ }
694
+ return `${error?.message ?? ''}`.trim().toLowerCase() === 'empty json message'
695
+ }
696
+
697
+ function shouldRetryBeforeReceipt(error, attempt, receiptConfirmed) {
698
+ if (attempt > 0 || receiptConfirmed) {
699
+ return false
700
+ }
701
+ if (`${error?.a2DispatchStage ?? ''}` !== 'pre-dispatch') {
702
+ return false
703
+ }
704
+ const lower = `${error?.message ?? ''}`.trim().toLowerCase()
705
+ return (
706
+ lower === 'empty json message'
707
+ || lower.includes('request receipt timed out after')
708
+ || lower.includes('stream that is closed')
709
+ || lower.includes('stream closed before drain')
710
+ || lower.includes('stream reset')
711
+ || lower.includes('connection reset')
712
+ || lower.includes('connection closed')
713
+ || lower.includes('no existing peer connection is available')
714
+ )
715
+ }
716
+
717
+ function buildJsonRpcError(error = {}) {
718
+ const out = new Error(`${error.message ?? 'remote peer returned an error'}`)
719
+ out.code = Number.parseInt(`${error.code ?? 500}`, 10) || 500
720
+ return out
721
+ }
722
+
723
+ async function bestEffortAgentCard(apiBase, agentId, bundle, targetAgentId, transport) {
724
+ try {
725
+ return await getAgentCard(apiBase, agentId, bundle, targetAgentId, transport)
726
+ } catch {
727
+ return null
728
+ }
729
+ }
730
+
731
+ function isTrustedSessionRetryable(error) {
732
+ const message = `${error?.message ?? ''}`.trim()
733
+ const lower = message.toLowerCase()
734
+ const code = Number.parseInt(`${error?.code ?? 0}`, 10) || 0
735
+ if (code === 401 || message.includes('relayConnectTicket or a trusted peerSessionId is required')) {
736
+ return true
737
+ }
738
+ if (`${error?.a2DispatchStage ?? ''}` !== 'pre-dispatch') {
739
+ return false
740
+ }
741
+ return [
742
+ 'target transport is missing dialaddrs',
743
+ 'target transport is missing peerid',
744
+ 'target transport is missing streamprotocol',
745
+ 'no connection was available',
746
+ 'no existing peer connection is available',
747
+ 'direct p2p upgrade did not complete',
748
+ 'connection refused',
749
+ 'connection reset',
750
+ 'connection closed',
751
+ 'stream reset',
752
+ 'stream closed before drain',
753
+ 'stream that is closed',
754
+ 'empty json message',
755
+ 'request receipt timed out after',
756
+ 'the operation was aborted',
757
+ 'already aborted',
758
+ 'dial timeout',
759
+ 'timed out'
760
+ ].some((pattern) => lower.includes(pattern))
761
+ }
762
+
763
+ function isRelayPresenceRetryable(error) {
764
+ const message = `${error?.message ?? ''}`.trim().toLowerCase()
765
+ return (
766
+ message.startsWith('409 target agent is not currently online') ||
767
+ message.startsWith('409 target agent presence is invalid or stale') ||
768
+ message.startsWith('409 target agent has not published a current peer identity for direct p2p contact') ||
769
+ message.startsWith('409 target agent has not published a current relay reservation or public direct dial address for p2p contact')
770
+ )
771
+ }
772
+
773
+ function sleep(ms) {
774
+ return new Promise((resolve) => setTimeout(resolve, ms))
775
+ }
776
+
777
+ function mergeTargetTransport({
778
+ primary = null,
779
+ secondary = null,
780
+ tertiary = null,
781
+ streamProtocol = ''
782
+ } = {}) {
783
+ const sources = [primary, secondary, tertiary].filter((value) => value && typeof value === 'object')
784
+ const peerId = firstNonEmpty(sources.map((value) => value.peerId))
785
+ const protocol = firstNonEmpty(sources.map((value) => value.streamProtocol).concat(streamProtocol))
786
+ const dialAddrs = unique(
787
+ sources.flatMap((value) => value.dialAddrs ?? [])
788
+ )
789
+ const listenAddrs = unique(
790
+ sources.flatMap((value) => value.listenAddrs ?? [])
791
+ )
792
+ const relayAddrs = unique(
793
+ sources.flatMap((value) => value.relayAddrs ?? [])
794
+ )
795
+ const supportedBindings = unique(
796
+ sources.flatMap((value) => value.supportedBindings ?? [])
797
+ )
798
+ const a2aProtocolVersion = firstNonEmpty(sources.map((value) => value.a2aProtocolVersion))
799
+
800
+ if (!peerId || !protocol) {
801
+ return null
802
+ }
803
+
804
+ return {
805
+ peerId,
806
+ streamProtocol: protocol,
807
+ dialAddrs,
808
+ listenAddrs,
809
+ relayAddrs,
810
+ supportedBindings,
811
+ a2aProtocolVersion
812
+ }
813
+ }
814
+
815
+ function buildInboundRemoteTransport({
816
+ connection,
817
+ remotePeerId,
818
+ binding
819
+ } = {}) {
820
+ const remoteAddr = cleanAddr(connection?.remoteAddr?.toString?.())
821
+ const dialAddrs = unique(remoteAddr ? [remoteAddr] : [])
822
+ return {
823
+ peerId: `${remotePeerId ?? ''}`.trim(),
824
+ streamProtocol: `${binding?.streamProtocol ?? ''}`.trim(),
825
+ dialAddrs,
826
+ listenAddrs: dialAddrs
827
+ }
828
+ }
829
+
830
+ function cleanAddr(value) {
831
+ return `${value ?? ''}`.trim()
832
+ }
833
+
834
+ function firstNonEmpty(values = []) {
835
+ for (const value of values) {
836
+ const cleaned = `${value ?? ''}`.trim()
837
+ if (cleaned) {
838
+ return cleaned
839
+ }
840
+ }
841
+ return ''
842
+ }
843
+
844
+ function unique(values = []) {
845
+ return [...new Set(values.map((value) => `${value}`.trim()).filter(Boolean))]
846
+ }
847
+
848
+ function parseConnectTicketId(token) {
849
+ const parts = `${token ?? ''}`.trim().split('.')
850
+ if (parts.length < 2) return ''
851
+ try {
852
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'))
853
+ return `${payload.tid ?? payload.jti ?? ''}`.trim()
854
+ } catch {
855
+ return ''
856
+ }
857
+ }