The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 874239cc591fCommit detailsView latest

packages/sdk/src/index.ts 658 lines · 22.8 KB · typescript
1
import {
2
  DEFAULT_EXTENSION_URI,
3
  canonicalHash,
4
  signObject,
5
  verifyRecordAttestation,
6
  type InteractionEvent,
7
  type KeyPair,
8
  type TrustEnvelope,
9
  type FederatedInteraction,
10
  type AgentResolution,
11
  type InteractionContract,
12
  LiveSessionActivationSchema,
13
  LiveSessionEventSchema,
14
  LiveSessionOfferSchema,
15
  type LiveSessionActivation,
16
  type LiveSessionEvent,
17
  type LiveSessionOffer,
18
  type PublicAgentCard,
19
  type HostedThread,
20
  type HostedMessage,
21
} from '../../protocol/src/index.js';
22
import { createPublicKey, verify } from 'node:crypto';
23
export { createIdentity } from '../../core/src/index.js';
24
export * from '../../protocol/src/index.js';
25
26
export class OpenClaspClient {
27
  constructor(
28
    readonly baseUrl = 'http://localhost:3100/v0.1',
29
    readonly accessToken?: string,
30
  ) {}
31
  private async request<T>(path: string, init?: RequestInit): Promise<T> {
32
    const response = await fetch(`${this.baseUrl}${path}`, {
33
      ...init,
34
      headers: {
35
        ...(init?.body !== undefined && init.body !== null
36
          ? { 'content-type': 'application/json' }
37
          : {}),
38
        ...(this.accessToken ? { authorization: `Bearer ${this.accessToken}` } : {}),
39
        ...init?.headers,
40
      },
41
    });
42
    const body = (await response.json()) as any;
43
    if (!response.ok) throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
44
    return body as T;
45
  }
46
  registerAgent(identity: unknown) {
47
    return this.request('/agents', { method: 'POST', body: JSON.stringify(identity) });
48
  }
49
  getProfile(agentId: string, version: string, taskCategory: string) {
50
    return this.request(
51
      `/profiles/${encodeURIComponent(agentId)}?version=${encodeURIComponent(version)}&taskCategory=${encodeURIComponent(taskCategory)}`,
52
    );
53
  }
54
  saveContract(contract: unknown) {
55
    return this.request('/interactions/contracts', {
56
      method: 'POST',
57
      body: JSON.stringify(contract),
58
    });
59
  }
60
  assess(input: unknown) {
61
    return this.request('/risk/assess', { method: 'POST', body: JSON.stringify(input) });
62
  }
63
  recordEvent(event: unknown) {
64
    return this.request('/events', { method: 'POST', body: JSON.stringify(event) });
65
  }
66
  checkClaim(claim: string) {
67
    return this.request('/claims/check', { method: 'POST', body: JSON.stringify({ claim }) });
68
  }
69
  submitReceipt(receipt: unknown) {
70
    return this.request('/receipts', { method: 'POST', body: JSON.stringify(receipt) });
71
  }
72
  verifyReceipt(receipt: unknown) {
73
    return this.request('/receipts/verify', { method: 'POST', body: JSON.stringify(receipt) });
74
  }
75
  submitFeedback(feedback: unknown) {
76
    return this.request('/feedback', { method: 'POST', body: JSON.stringify(feedback) });
77
  }
78
  setContributionConsent(agentId: string, enabled: boolean) {
79
    return this.request('/contributions/consent', {
80
      method: 'POST',
81
      body: JSON.stringify({ agentId, enabled }),
82
    });
83
  }
84
  getPublicAgentCard(agentId: string): Promise<PublicAgentCard> {
85
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
86
    return fetch(`${root}/agents/${encodeURIComponent(agentId)}/card.json`).then(
87
      async (response) => {
88
        const body = (await response.json()) as any;
89
        if (!response.ok)
90
          throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
91
        return body as PublicAgentCard;
92
      },
93
    );
94
  }
95
  resolveAgent(reference: string): Promise<AgentResolution> {
96
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
97
    return fetch(`${root}/directory/resolve?reference=${encodeURIComponent(reference)}`).then(
98
      async (response) => {
99
        const body = (await response.json()) as any;
100
        if (!response.ok)
101
          throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
102
        return body as AgentResolution;
103
      },
104
    );
105
  }
106
  searchAgents(input: { query?: string; capability?: string; limit?: number } = {}) {
107
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
108
    const search = new URLSearchParams();
109
    if (input.query) search.set('query', input.query);
110
    if (input.capability) search.set('capability', input.capability);
111
    if (input.limit) search.set('limit', String(input.limit));
112
    return fetch(`${root}/directory/search?${search}`).then(async (response) => {
113
      const body = (await response.json()) as any;
114
      if (!response.ok)
115
        throw new Error(body.error ?? `OpenClasp request failed: ${response.status}`);
116
      return body as PublicAgentCard[];
117
    });
118
  }
119
  async verifyPlatformRecord(value: Record<string, unknown>): Promise<boolean> {
120
    const root = this.baseUrl.replace(/\/v0\.1\/?$/, '');
121
    const keyUrl =
122
      typeof value.verification === 'object' &&
123
      value.verification !== null &&
124
      'verificationKeyUrl' in value.verification &&
125
      typeof value.verification.verificationKeyUrl === 'string'
126
        ? value.verification.verificationKeyUrl
127
        : `${root}/.well-known/openclasp-session-key`;
128
    const response = await fetch(keyUrl);
129
    if (!response.ok) return false;
130
    const key = (await response.json()) as { publicKey?: string };
131
    return typeof key.publicKey === 'string' && verifyRecordAttestation(value, key.publicKey);
132
  }
133
  createFederatedInteraction(value: FederatedInteraction) {
134
    return this.request<FederatedInteraction>('/federated-interactions', {
135
      method: 'POST',
136
      body: JSON.stringify(value),
137
    });
138
  }
139
  listFederatedInteractions() {
140
    return this.request<FederatedInteraction[]>('/federated-interactions');
141
  }
142
  getFederatedInteraction(interactionId: string) {
143
    return this.request<FederatedInteraction>(
144
      `/federated-interactions/${encodeURIComponent(interactionId)}`,
145
    );
146
  }
147
  getLiveSession(interactionId: string, agentId: string): Promise<LiveSessionActivation> {
148
    return this.request(
149
      `/federated-interactions/${encodeURIComponent(interactionId)}/session?agentId=${encodeURIComponent(agentId)}`,
150
    );
151
  }
152
  respondToFederatedInteraction(
153
    interactionId: string,
154
    agentId: string,
155
    decision: 'accept' | 'reject',
156
  ) {
157
    return this.request<FederatedInteraction>(
158
      `/federated-interactions/${encodeURIComponent(interactionId)}/respond`,
159
      { method: 'POST', body: JSON.stringify({ agentId, decision }) },
160
    );
161
  }
162
  proposeContractRevision(
163
    interactionId: string,
164
    agentId: string,
165
    contract: InteractionContract,
166
    expectedTermsHash?: string,
167
  ) {
168
    return this.request<FederatedInteraction>(
169
      `/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals`,
170
      {
171
        method: 'POST',
172
        body: JSON.stringify({ agentId, contract, expectedTermsHash }),
173
      },
174
    );
175
  }
176
  respondToContractRevision(
177
    interactionId: string,
178
    revisionId: string,
179
    agentId: string,
180
    decision: 'accept' | 'reject',
181
  ) {
182
    return this.request<FederatedInteraction>(
183
      `/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals/${encodeURIComponent(revisionId)}/respond`,
184
      { method: 'POST', body: JSON.stringify({ agentId, decision }) },
185
    );
186
  }
187
  listHostedThreads(agentId: string): Promise<HostedThread[]> {
188
    return this.request(`/agents/${encodeURIComponent(agentId)}/threads`);
189
  }
190
  getHostedThread(
191
    agentId: string,
192
    threadId: string,
193
  ): Promise<{
194
    thread: HostedThread;
195
    messages: HostedMessage[];
196
    insights: unknown[];
197
  }> {
198
    return this.request(
199
      `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}`,
200
    );
201
  }
202
  sendTemporaryMessage(agentId: string, interactionId: string, content: string) {
203
    return this.request(`/agents/${encodeURIComponent(agentId)}/messages`, {
204
      method: 'POST',
205
      body: JSON.stringify({ interactionId, content }),
206
    });
207
  }
208
  markHostedThreadRead(agentId: string, threadId: string) {
209
    return this.request(
210
      `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}/read`,
211
      { method: 'POST' },
212
    );
213
  }
214
  closeHostedThread(agentId: string, threadId: string) {
215
    return this.request(
216
      `/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}/close`,
217
      { method: 'POST' },
218
    );
219
  }
220
  getRuntimeBootstrap(): Promise<RuntimeBootstrap> {
221
    return this.request('/runtime/bootstrap');
222
  }
223
  connectRuntime(endpoint: string): Promise<RuntimeConnection> {
224
    return this.request('/runtime', {
225
      method: 'PUT',
226
      body: JSON.stringify({ endpoint }),
227
    });
228
  }
229
  disconnectRuntime(): Promise<{ agentId: string; status: 'disabled' }> {
230
    return this.request('/runtime', { method: 'DELETE' });
231
  }
232
  heartbeatRuntime(): Promise<{ status: 'online' | 'offline'; checkedAt: string }> {
233
    return this.request('/runtime/heartbeat', { method: 'POST' });
234
  }
235
}
236
237
export type RuntimeBootstrap = {
238
  agentId: string;
239
  openClaspUrl: string;
240
  runtimeRegistrationEndpoint: string;
241
  protocol: 'A2A/1.0';
242
  controlProtocol: 'OpenClasp/0.1';
243
};
244
245
export type RuntimeConnection = {
246
  agentId: string;
247
  endpoint: string;
248
  a2aEndpoint: string;
249
  status: 'verified';
250
  verifiedAt: string;
251
  verificationKey: string;
252
};
253
254
export function createSignedEvent(
255
  input: Omit<InteractionEvent, 'payloadHash' | 'signature'>,
256
  key: KeyPair,
257
): InteractionEvent {
258
  return signObject(
259
    { ...input, payloadHash: canonicalHash(input.payload) },
260
    key,
261
  ) as InteractionEvent;
262
}
263
264
export function createSignedEnvelope(
265
  input: Omit<TrustEnvelope, 'signature'>,
266
  key: KeyPair,
267
): TrustEnvelope {
268
  return signObject(input, key) as TrustEnvelope;
269
}
270
271
function validRuntimeSignature(
272
  publicKey: string,
273
  requestId: string,
274
  timestamp: string,
275
  body: string,
276
  signature: string,
277
) {
278
  if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60_000) return false;
279
  return verify(
280
    null,
281
    Buffer.from(`${timestamp}.${requestId}.${body}`),
282
    createPublicKey({ key: Buffer.from(publicKey, 'base64url'), type: 'spki', format: 'der' }),
283
    Buffer.from(signature.replace(/^v1=/, ''), 'base64url'),
284
  );
285
}
286
287
export function createOpenClaspRuntimeHandler(input: {
288
  agentId: string;
289
  a2aEndpoint: string;
290
  openClaspUrl?: string;
291
  openClaspVerificationKey?: string;
292
  onSessionOffer: (
293
    offer: LiveSessionOffer,
294
  ) =>
295
    Promise<{ accepted: boolean; sessionId?: string }> | { accepted: boolean; sessionId?: string };
296
  onSessionActivated: (session: LiveSessionActivation) => Promise<void> | void;
297
  loadSession: (interactionId: string) => Promise<LiveSessionActivation | undefined>;
298
  onMessage: (input: {
299
    session: LiveSessionActivation;
300
    requestId: string | number;
301
    message: unknown;
302
  }) => Promise<unknown> | unknown;
303
}) {
304
  let verificationKeyPromise: Promise<string> | undefined;
305
  const verificationKey = () => {
306
    if (input.openClaspVerificationKey) return Promise.resolve(input.openClaspVerificationKey);
307
    return (verificationKeyPromise ??= fetch(
308
      `${(input.openClaspUrl ?? 'https://openclasp.vercel.app').replace(/\/$/, '')}/.well-known/openclasp-session-key`,
309
    )
310
      .then(async (response) => {
311
        if (!response.ok) throw new Error(`OpenClasp key discovery failed: ${response.status}`);
312
        const value = (await response.json()) as { publicKey?: unknown };
313
        if (typeof value.publicKey !== 'string')
314
          throw new Error('Invalid OpenClasp verification key');
315
        return value.publicKey;
316
      })
317
      .catch((error) => {
318
        verificationKeyPromise = undefined;
319
        throw error;
320
      }));
321
  };
322
  return async (request: Request): Promise<Response> => {
323
    const body = await request.text();
324
    let parsed: Record<string, unknown>;
325
    try {
326
      parsed = JSON.parse(body) as Record<string, unknown>;
327
    } catch {
328
      return Response.json({ error: 'invalid_json' }, { status: 400 });
329
    }
330
    if (parsed.type === 'openclasp.runtime.verify')
331
      return Response.json({
332
        type: 'openclasp.runtime.verified',
333
        version: '1',
334
        agentId: parsed.agentId,
335
        challenge: parsed.challenge,
336
        a2aEndpoint: input.a2aEndpoint,
337
      });
338
    if (parsed.type !== 'openclasp.session.offer' && parsed.type !== 'openclasp.session.activation')
339
      return handleDirectMessage(request, parsed, input);
340
    const requestId = request.headers.get('openclasp-request-id') ?? '';
341
    const timestamp = request.headers.get('openclasp-timestamp') ?? '';
342
    const signature = request.headers.get('openclasp-signature') ?? '';
343
    let signatureValid = false;
344
    try {
345
      signatureValid = Boolean(
346
        requestId &&
347
        timestamp &&
348
        signature &&
349
        validRuntimeSignature(await verificationKey(), requestId, timestamp, body, signature),
350
      );
351
    } catch {
352
      return Response.json({ error: 'openclasp_key_unavailable' }, { status: 503 });
353
    }
354
    if (!signatureValid)
355
      return Response.json({ error: 'invalid_openclasp_signature' }, { status: 401 });
356
    if (parsed.type === 'openclasp.session.offer') {
357
      const result = LiveSessionOfferSchema.safeParse(parsed);
358
      if (
359
        !result.success ||
360
        result.data.offerId !== requestId ||
361
        result.data.agentId !== input.agentId
362
      )
363
        return Response.json({ error: 'invalid_session_offer' }, { status: 400 });
364
      const decision = await input.onSessionOffer(result.data);
365
      if (!decision.accepted) return Response.json({ error: 'session_declined' }, { status: 409 });
366
      if (!decision.sessionId)
367
        return Response.json({ error: 'durable_session_id_required' }, { status: 500 });
368
      return Response.json({
369
        type: 'openclasp.session.accepted',
370
        version: '1',
371
        offerId: result.data.offerId,
372
        interactionId: result.data.interactionId,
373
        agentId: input.agentId,
374
        sessionId: decision.sessionId,
375
        a2aEndpoint: input.a2aEndpoint,
376
        expiresAt: result.data.expiresAt,
377
      });
378
    }
379
    const activation = LiveSessionActivationSchema.safeParse(parsed);
380
    if (
381
      !activation.success ||
382
      activation.data.activationId !== requestId ||
383
      activation.data.agentId !== input.agentId
384
    )
385
      return Response.json({ error: 'invalid_session_activation' }, { status: 400 });
386
    await input.onSessionActivated(activation.data);
387
    return Response.json({ accepted: true, activationId: activation.data.activationId });
388
  };
389
}
390
391
type RuntimeHandlerInput = Parameters<typeof createOpenClaspRuntimeHandler>[0];
392
393
export interface RuntimeSessionStore {
394
  get(interactionId: string): Promise<LiveSessionActivation | undefined>;
395
  put(interactionId: string, session: LiveSessionActivation): Promise<void>;
396
}
397
398
export interface AgentRuntimeAdapter {
399
  readonly name: string;
400
  prepareSession(
401
    offer: LiveSessionOffer,
402
  ): Promise<{ accepted: boolean; sessionId?: string }> | { accepted: boolean; sessionId?: string };
403
  activateSession(session: LiveSessionActivation): Promise<void> | void;
404
  receiveMessage(input: {
405
    session: LiveSessionActivation;
406
    requestId: string | number;
407
    message: unknown;
408
  }): Promise<unknown> | unknown;
409
}
410
411
/** In-memory storage is useful for tests only. Production runtimes must use durable storage. */
412
export class MemoryRuntimeSessionStore implements RuntimeSessionStore {
413
  readonly sessions = new Map<string, LiveSessionActivation>();
414
415
  async get(interactionId: string) {
416
    return this.sessions.get(interactionId);
417
  }
418
419
  async put(interactionId: string, session: LiveSessionActivation) {
420
    this.sessions.set(interactionId, session);
421
  }
422
}
423
424
export function createAgentRuntimeConnector(input: {
425
  agentId: string;
426
  a2aEndpoint: string;
427
  adapter: AgentRuntimeAdapter;
428
  sessions: RuntimeSessionStore;
429
  openClaspUrl?: string;
430
  openClaspVerificationKey?: string;
431
}) {
432
  return createOpenClaspRuntimeHandler({
433
    agentId: input.agentId,
434
    a2aEndpoint: input.a2aEndpoint,
435
    ...(input.openClaspUrl ? { openClaspUrl: input.openClaspUrl } : {}),
436
    ...(input.openClaspVerificationKey
437
      ? { openClaspVerificationKey: input.openClaspVerificationKey }
438
      : {}),
439
    async onSessionOffer(offer) {
440
      const decision = await input.adapter.prepareSession(offer);
441
      return decision.accepted
442
        ? { accepted: true, sessionId: decision.sessionId ?? crypto.randomUUID() }
443
        : { accepted: false };
444
    },
445
    async onSessionActivated(session) {
446
      await input.sessions.put(session.interactionId, session);
447
      await input.adapter.activateSession(session);
448
    },
449
    loadSession: (interactionId) => input.sessions.get(interactionId),
450
    onMessage: (message) => input.adapter.receiveMessage(message),
451
  });
452
}
453
454
export class HttpAgentRuntimeAdapter implements AgentRuntimeAdapter {
455
  readonly name = 'http-webhook';
456
  private readonly baseUrl: string;
457
458
  constructor(
459
    baseUrl: string,
460
    private readonly options: {
461
      bearerToken?: string;
462
      timeoutMs?: number;
463
    } = {},
464
  ) {
465
    this.baseUrl = baseUrl.replace(/\/$/, '');
466
  }
467
468
  private async post<T>(path: string, body: unknown): Promise<T> {
469
    const response = await fetch(`${this.baseUrl}${path}`, {
470
      method: 'POST',
471
      headers: {
472
        'content-type': 'application/json',
473
        ...(this.options.bearerToken
474
          ? { authorization: `Bearer ${this.options.bearerToken}` }
475
          : {}),
476
      },
477
      body: JSON.stringify(body),
478
      signal: AbortSignal.timeout(this.options.timeoutMs ?? 30_000),
479
    });
480
    const text = await response.text();
481
    let value: unknown = {};
482
    if (text) {
483
      try {
484
        value = JSON.parse(text);
485
      } catch {
486
        throw new Error(`Agent adapter returned non-JSON from ${path}`);
487
      }
488
    }
489
    if (!response.ok) {
490
      const error = value as { error?: unknown };
491
      throw new Error(
492
        typeof error.error === 'string'
493
          ? error.error
494
          : `Agent adapter ${path} returned HTTP ${response.status}`,
495
      );
496
    }
497
    return value as T;
498
  }
499
500
  async prepareSession(offer: LiveSessionOffer) {
501
    const decision = await this.post<{ accepted?: unknown; sessionId?: unknown }>(
502
      '/openclasp/session-offer',
503
      offer,
504
    );
505
    return {
506
      accepted: decision.accepted === true,
507
      ...(typeof decision.sessionId === 'string' ? { sessionId: decision.sessionId } : {}),
508
    };
509
  }
510
511
  async activateSession(session: LiveSessionActivation) {
512
    await this.post('/openclasp/session-activated', session);
513
  }
514
515
  receiveMessage(input: {
516
    session: LiveSessionActivation;
517
    requestId: string | number;
518
    message: unknown;
519
  }) {
520
    return this.post('/openclasp/message', input);
521
  }
522
}
523
524
function decodeSessionCredential(token: string) {
525
  const [payload, signature] = token.split('.');
526
  if (!payload || !signature) throw new Error('Invalid live-session credential');
527
  return {
528
    payload,
529
    signature,
530
    grant: JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as {
531
      interactionId?: unknown;
532
      senderAgentId?: unknown;
533
      recipientAgentId?: unknown;
534
      expiresAt?: unknown;
535
    },
536
  };
537
}
538
539
async function handleDirectMessage(
540
  request: Request,
541
  parsed: Record<string, unknown>,
542
  input: RuntimeHandlerInput,
543
) {
544
  const rpc = parsed as { jsonrpc?: unknown; id?: unknown; method?: unknown; params?: unknown };
545
  if (
546
    rpc.jsonrpc !== '2.0' ||
547
    (typeof rpc.id !== 'string' && typeof rpc.id !== 'number') ||
548
    rpc.method !== 'message/send'
549
  )
550
    return Response.json({ error: 'invalid_a2a_request' }, { status: 400 });
551
  const authorization = request.headers.get('authorization');
552
  if (!authorization?.startsWith('Bearer '))
553
    return Response.json({ error: 'session_credential_required' }, { status: 401 });
554
  let session: LiveSessionActivation;
555
  try {
556
    const credential = decodeSessionCredential(authorization.slice(7));
557
    if (typeof credential.grant.interactionId !== 'string')
558
      throw new Error('Invalid interaction credential');
559
    const loaded = await input.loadSession(credential.grant.interactionId);
560
    if (!loaded) return Response.json({ error: 'live_session_not_found' }, { status: 404 });
561
    session = loaded;
562
    const valid = verify(
563
      null,
564
      Buffer.from(credential.payload),
565
      createPublicKey({
566
        key: Buffer.from(session.peer.verificationKey, 'base64url'),
567
        type: 'spki',
568
        format: 'der',
569
      }),
570
      Buffer.from(credential.signature, 'base64url'),
571
    );
572
    if (
573
      !valid ||
574
      credential.grant.recipientAgentId !== input.agentId ||
575
      credential.grant.senderAgentId !== session.peer.agentId ||
576
      typeof credential.grant.expiresAt !== 'number' ||
577
      credential.grant.expiresAt <= Date.now() ||
578
      Date.parse(session.expiresAt) <= Date.now()
579
    )
580
      return Response.json({ error: 'invalid_session_credential' }, { status: 401 });
581
  } catch {
582
    return Response.json({ error: 'invalid_session_credential' }, { status: 401 });
583
  }
584
  try {
585
    const result = await input.onMessage({ session, requestId: rpc.id, message: rpc.params });
586
    return Response.json({ jsonrpc: '2.0', id: rpc.id, result });
587
  } catch (error) {
588
    return Response.json(
589
      {
590
        jsonrpc: '2.0',
591
        id: rpc.id,
592
        error: { code: -32000, message: error instanceof Error ? error.message : 'Agent failed' },
593
      },
594
      { status: 500 },
595
    );
596
  }
597
}
598
599
export async function sendOpenClaspDirectMessage(
600
  session: LiveSessionActivation,
601
  payload: unknown,
602
  requestId = crypto.randomUUID(),
603
) {
604
  const message =
605
    payload && typeof payload === 'object'
606
      ? {
607
          ...(payload as Record<string, unknown>),
608
          metadata: {
609
            ...(((payload as Record<string, unknown>).metadata as Record<string, unknown>) ?? {}),
610
            [DEFAULT_EXTENSION_URI]: {
611
              interactionId: session.interactionId,
612
              termsHash: session.contractHash,
613
              initiatorAgentId:
614
                session.role === 'initiator' ? session.agentId : session.peer.agentId,
615
              responderAgentId:
616
                session.role === 'responder' ? session.agentId : session.peer.agentId,
617
            },
618
          },
619
        }
620
      : payload;
621
  const response = await fetch(session.peer.endpoint, {
622
    method: 'POST',
623
    headers: {
624
      authorization: `Bearer ${session.peer.bearerToken}`,
625
      'content-type': 'application/json',
626
      'A2A-Extensions': DEFAULT_EXTENSION_URI,
627
    },
628
    body: JSON.stringify({
629
      jsonrpc: '2.0',
630
      id: requestId,
631
      method: 'message/send',
632
      params: { message },
633
    }),
634
  });
635
  const result = (await response.json()) as unknown;
636
  if (!response.ok) throw new Error(`Direct agent request failed with HTTP ${response.status}`);
637
  return result;
638
}
639
640
export async function reportOpenClaspSessionEvent(
641
  session: LiveSessionActivation,
642
  event: LiveSessionEvent,
643
) {
644
  const value = LiveSessionEventSchema.parse(event);
645
  if (value.interactionId !== session.interactionId || value.agentId !== session.agentId)
646
    throw new Error('Session event does not match the active session');
647
  const response = await fetch(session.reporting.endpoint, {
648
    method: 'POST',
649
    headers: {
650
      authorization: `Bearer ${session.reporting.bearerToken}`,
651
      'content-type': 'application/json',
652
    },
653
    body: JSON.stringify(value),
654
  });
655
  const result = (await response.json()) as unknown;
656
  if (!response.ok) throw new Error(`OpenClasp event report failed with HTTP ${response.status}`);
657
  return result;
658
}