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

Viewing files at commit 89b58109a0e6Commit detailsView latest

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