@relaymessenger/sdk 0.3.6-staging.4 → 0.3.6-staging.40

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.
Files changed (88) hide show
  1. package/README.md +498 -29
  2. package/dist/a2ui.d.ts +79 -0
  3. package/dist/a2ui.d.ts.map +1 -0
  4. package/dist/a2ui.js +99 -0
  5. package/dist/a2ui.js.map +1 -0
  6. package/dist/buttons.d.ts +69 -0
  7. package/dist/buttons.d.ts.map +1 -0
  8. package/dist/buttons.js +141 -0
  9. package/dist/buttons.js.map +1 -0
  10. package/dist/calls/call-room.d.ts +110 -0
  11. package/dist/calls/call-room.d.ts.map +1 -0
  12. package/dist/calls/call-room.js +616 -0
  13. package/dist/calls/call-room.js.map +1 -0
  14. package/dist/calls/engine-werift-video.d.ts +83 -0
  15. package/dist/calls/engine-werift-video.d.ts.map +1 -0
  16. package/dist/calls/engine-werift-video.js +607 -0
  17. package/dist/calls/engine-werift-video.js.map +1 -0
  18. package/dist/calls/engine-werift.d.ts +48 -0
  19. package/dist/calls/engine-werift.d.ts.map +1 -0
  20. package/dist/calls/engine-werift.js +462 -0
  21. package/dist/calls/engine-werift.js.map +1 -0
  22. package/dist/calls/index.d.ts +11 -0
  23. package/dist/calls/index.d.ts.map +1 -0
  24. package/dist/calls/index.js +11 -0
  25. package/dist/calls/index.js.map +1 -0
  26. package/dist/calls/transport.d.ts +466 -0
  27. package/dist/calls/transport.d.ts.map +1 -0
  28. package/dist/calls/transport.js +1409 -0
  29. package/dist/calls/transport.js.map +1 -0
  30. package/dist/calls/video-frame.d.ts +56 -0
  31. package/dist/calls/video-frame.d.ts.map +1 -0
  32. package/dist/calls/video-frame.js +245 -0
  33. package/dist/calls/video-frame.js.map +1 -0
  34. package/dist/calls/video-presets.d.ts +39 -0
  35. package/dist/calls/video-presets.d.ts.map +1 -0
  36. package/dist/calls/video-presets.js +40 -0
  37. package/dist/calls/video-presets.js.map +1 -0
  38. package/dist/calls/video-rtp.d.ts +51 -0
  39. package/dist/calls/video-rtp.d.ts.map +1 -0
  40. package/dist/calls/video-rtp.js +294 -0
  41. package/dist/calls/video-rtp.js.map +1 -0
  42. package/dist/calls/video.d.ts +195 -0
  43. package/dist/calls/video.d.ts.map +1 -0
  44. package/dist/calls/video.js +228 -0
  45. package/dist/calls/video.js.map +1 -0
  46. package/dist/client.d.ts +218 -9
  47. package/dist/client.d.ts.map +1 -1
  48. package/dist/client.js +497 -17
  49. package/dist/client.js.map +1 -1
  50. package/dist/errors.d.ts +11 -0
  51. package/dist/errors.d.ts.map +1 -1
  52. package/dist/errors.js +16 -0
  53. package/dist/errors.js.map +1 -1
  54. package/dist/index.d.ts +9 -4
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +9 -4
  57. package/dist/index.js.map +1 -1
  58. package/dist/links.d.ts +58 -0
  59. package/dist/links.d.ts.map +1 -0
  60. package/dist/links.js +127 -0
  61. package/dist/links.js.map +1 -0
  62. package/dist/operations.d.ts +117 -9
  63. package/dist/operations.d.ts.map +1 -1
  64. package/dist/operations.js +156 -10
  65. package/dist/operations.js.map +1 -1
  66. package/dist/pagination.d.ts +5 -0
  67. package/dist/pagination.d.ts.map +1 -1
  68. package/dist/pagination.js +10 -0
  69. package/dist/pagination.js.map +1 -1
  70. package/dist/payment.d.ts +67 -0
  71. package/dist/payment.d.ts.map +1 -0
  72. package/dist/payment.js +147 -0
  73. package/dist/payment.js.map +1 -0
  74. package/dist/selection.d.ts +46 -0
  75. package/dist/selection.d.ts.map +1 -0
  76. package/dist/selection.js +142 -0
  77. package/dist/selection.js.map +1 -0
  78. package/dist/types.d.ts +1102 -88
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/websocket.d.ts +6 -0
  81. package/dist/websocket.d.ts.map +1 -1
  82. package/dist/websocket.js +29 -10
  83. package/dist/websocket.js.map +1 -1
  84. package/package.json +38 -3
  85. package/dist/calls-room.d.ts +0 -50
  86. package/dist/calls-room.d.ts.map +0 -1
  87. package/dist/calls-room.js +0 -319
  88. package/dist/calls-room.js.map +0 -1
package/README.md CHANGED
@@ -2,6 +2,225 @@
2
2
 
3
3
  TypeScript client for Relay v1.
4
4
 
5
+ ## Selection
6
+
7
+ The SDK exposes `SelectionPart`, `SelectionPartResponse` (including read-only
8
+ viewer-relative `has_responded`), and `SelectionResponsePart` through the
9
+ existing message and event unions.
10
+
11
+ ```ts
12
+ import { partsWithSelection } from "@relaymessenger/sdk";
13
+
14
+ const parts = partsWithSelection("I can go deep on any of these.", {
15
+ type: "selection",
16
+ title: "Topics",
17
+ options: [
18
+ { value: "research", label: "Research" },
19
+ { value: "design", label: "Design" },
20
+ ],
21
+ });
22
+ // Send with relay.chats.messages.send(chatId, { message: { parts, idempotency_key } }).
23
+ ```
24
+
25
+ The question goes in `title`: trimmed, 1 to 60 characters, a few words such as
26
+ "Pizza toppings". It is the card's title in the chat and the sheet's title. The
27
+ text part is optional; when present it is an ordinary chat bubble above the
28
+ card, and `partsWithSelection(undefined, selection)` sends the selection alone.
29
+
30
+ `selectionPart` validates a complete part (`type` may be left out), returning a
31
+ normalized part or an error string. The title is limited to 60 characters,
32
+ options to 25, trimmed labels to 80 characters, and explicit case-sensitive
33
+ ASCII token values to 100. Unknown fields, duplicate values, a missing or blank
34
+ title, and blank labels are rejected.
35
+
36
+ Selection inherits existing Chat membership rules: at most one human user,
37
+ with multiple agents allowed. Only the human user can respond; agents cannot.
38
+ The durable per-user response claim applies across that user's devices and
39
+ idempotency keys, without expanding group membership.
40
+
41
+ `answerMessages` accepts a `selection` fenced JSON block containing
42
+ `{"title": "...", "options": [...]}`; the words outside it, if any, become the
43
+ text above the card. It keeps invalid blocks as text with an error and never combines a
44
+ selection with buttons. Existing buttons retain their behavior.
45
+
46
+ The person opens the prompt, checks any number of options and submits them
47
+ once; checking sends nothing and only the submit does. A person answers a given
48
+ selection once, and reopening it afterwards shows what they chose without
49
+ letting them change it.
50
+
51
+ The user's new reply is exactly literal `• ` + each selected source label
52
+ joined with `\n`, then
53
+ `{ type: "selection_response", selected_values: ["research", "design"] }`,
54
+ in source-option order, with explicit `reply_to.message_id` and `part_index`.
55
+ The user client keeps its existing outgoing idempotency identity for retries.
56
+ The server also accepts exact legacy comma-joined source labels only for
57
+ compatibility. iOS may draw a checkmark in place of each bullet and repeat the
58
+ prompt's title, as presentation only, but portable text stays bulleted.
59
+ Metadata has no additional display text. Use `selected_values` and the source
60
+ reply target to dispatch your own handler, rather than splitting labels.
61
+ Signed webhook `unwrap` and WebSocket `onEvent` default types expose this
62
+ metadata after narrowing to `message.received`.
63
+
64
+ The CLI, Pi, OpenClaw, the Claude Code channel, MCP, and the Chat SDK adapter
65
+ include selection guidance and structured inbound discovery.
66
+ `selectionReply(parts, replyTo)` discovers values and the explicit source target.
67
+
68
+ ## Payment
69
+
70
+ Ask a person to pay in two steps. Create a payment request on your
71
+ organization's connected Stripe account, then send its `checkout_url` as a
72
+ `payment` part, the only part of its Message. The card reads its amount and
73
+ title from the request. The money settles to your Stripe account.
74
+
75
+ ```ts
76
+ const request = await relay.paymentRequests.create(
77
+ {
78
+ amount: 2400, // minor units
79
+ currency: "usd",
80
+ description: "House blend, 250 g",
81
+ category: "physical_goods", // or "digital_goods", "donation"
82
+ },
83
+ { idempotencyKey: `order-${orderId}` },
84
+ );
85
+ await relay.chats.messages.send(chatId, {
86
+ message: {
87
+ parts: [{ type: "payment", checkout_url: request.checkout_url }],
88
+ idempotency_key: crypto.randomUUID(),
89
+ },
90
+ });
91
+ ```
92
+
93
+ For a subscription, pass `mode: "subscription"` and a recurring `price_id`
94
+ instead of `amount` and `currency`. `relay.paymentRequests.list`, `retrieve`
95
+ and `cancel` read and cancel requests. The status moves only on Stripe's word
96
+ or your cancel, and arrives as `payment.succeeded`, `payment.canceled` or
97
+ `payment.expired`; a paid request also adds a `payment_receipt` message from
98
+ the payer, which arrives as `message.received`.
99
+
100
+ A model inside a bridge never holds the Relay token, so it gives the request's
101
+ fields and the bridge creates it. `answerMessages` lifts one `payment` fenced
102
+ JSON block (`description`, `category`, `amount` and `currency`, or
103
+ `mode: "subscription"` with `price_id`) out of the words and returns it as
104
+ `payment`, checked by `paymentRequestFields`; `createPaymentPart` creates the
105
+ request on the card Message's own idempotency key and returns the `payment`
106
+ part to send last. A second payment, or a payment beside buttons or a
107
+ selection, stays text with an error. `PAYMENT_GUIDANCE` is the text the CLI,
108
+ Pi, OpenClaw, MCP and the Claude Code channel carry; the text bridges also
109
+ carry `PAYMENT_BLOCK_INSTRUCTION`.
110
+
111
+ ## Location
112
+
113
+ Ask the person in a one-to-one chat to share their location, then read it.
114
+ The request puts a `location_request` Message from your agent in the chat;
115
+ the person chooses whether to share and for how long.
116
+
117
+ ```ts
118
+ await relay.chats.location.request(chatId);
119
+
120
+ // After `location.sharing.started` arrives:
121
+ const { data } = await relay.chats.location.retrieve(chatId);
122
+ for (const feature of data.features) {
123
+ const [longitude, latitude] = feature.geometry.coordinates;
124
+ console.log(feature.properties.handle, latitude, longitude, feature.properties.updated_at);
125
+ }
126
+ ```
127
+
128
+ `data` is a GeoJSON FeatureCollection, longitude first; `features` is empty
129
+ when nobody is sharing. `location.sharing.started` and
130
+ `location.sharing.stopped` fire when a share begins or ends, never when the
131
+ position moves, so read again when you need the latest position. A request
132
+ returns 409 in a group chat, in a chat with no person, or while the person is
133
+ already sharing, and 429 with `Retry-After` after one request in the same chat
134
+ in the last 60 seconds. The person's card arrives as a `location` part that
135
+ carries the share's state, never its position.
136
+
137
+ ## Cards (A2UI)
138
+
139
+ A card is an [A2UI v0.9.1](https://a2ui.org) surface, sent as a `data` part:
140
+ `{ type: "data", media_type: "application/a2ui+json", data: [A2UI messages] }`.
141
+ Send the card, read the tap, then change the same card in place.
142
+
143
+ ```ts
144
+ import Relay, {
145
+ A2UI_BASIC_CATALOG_ID,
146
+ readA2uiAction,
147
+ sendA2uiSurface,
148
+ updateA2uiSurface,
149
+ } from "@relaymessenger/sdk";
150
+
151
+ const relay = new Relay({
152
+ apiKey: process.env.RELAY_AGENT_TOKEN!,
153
+ webhookSecret: process.env.RELAY_WEBHOOK_SECRET!,
154
+ });
155
+
156
+ // 1. Send a card with a button.
157
+ await sendA2uiSurface(relay, chatId, {
158
+ surfaceId: "order-1042",
159
+ catalogId: A2UI_BASIC_CATALOG_ID,
160
+ components: [
161
+ { id: "root", component: "Card", child: "body" },
162
+ { id: "body", component: "Column", children: ["title", "status", "confirm"] },
163
+ { id: "title", component: "Text", text: "Oat latte, large", variant: "h3" },
164
+ { id: "status", component: "Text", text: { path: "/status" } },
165
+ { id: "confirm_label", component: "Text", text: "Confirm order" },
166
+ {
167
+ id: "confirm",
168
+ component: "Button",
169
+ child: "confirm_label",
170
+ variant: "primary",
171
+ action: { event: { name: "confirm_order", context: { order: "1042" } } },
172
+ },
173
+ ],
174
+ dataModel: { status: "Waiting for you" },
175
+ }, { idempotency_key: "order-1042-card" });
176
+
177
+ // 2. The tap arrives as message.received.
178
+ const event = relay.webhooks.unwrap(rawBody, { headers });
179
+ const tap = readA2uiAction(event);
180
+ if (tap?.action.name === "confirm_order" && event.event_type === "message.received") {
181
+ // 3. Change the same card to its done state. No new Message is added.
182
+ await updateA2uiSurface(relay, event.data.chat.id, tap.action.surfaceId, {
183
+ components: [
184
+ { id: "body", component: "Column", children: ["title", "status"] },
185
+ ],
186
+ dataModel: { path: "/status", value: `Confirmed, order ${tap.action.context.order}` },
187
+ });
188
+ }
189
+ ```
190
+
191
+ `sendA2uiSurface` sends `createSurface`, `updateComponents` and, with
192
+ `dataModel`, `updateDataModel` in one data part. `updateA2uiSurface` sends
193
+ `updateComponents` and `updateDataModel` for a surface already in the chat;
194
+ components replace their namesakes by `id`. A surface's first
195
+ `updateComponents` holds the component with the id `root`. `deleteA2uiSurface` sends
196
+ `deleteSurface`; when every surface of a Message is deleted, the Message reads
197
+ back with no parts and a non-null `unsent_at`. Each takes the rest of the
198
+ Message as its fourth argument: `text` becomes a text part before the card,
199
+ and `reply_to`, `idempotency_key`, `silent` and `metadata` pass through.
200
+ `a2uiPart` wraps any list of A2UI messages in a data part for
201
+ `relay.chats.messages.send`.
202
+
203
+ Relay applies each A2UI message on its own, checked against A2UI's schemas and
204
+ the catalog the surface names; a component, property or value the catalog does
205
+ not define is refused. The messages it could not apply come back in the
206
+ response's `a2ui_errors`: each gives `part_index` and `data_index` in your
207
+ request and `a2ui_message`, A2UI's own `error` message, whose `path` points
208
+ inside the failing message's body. A send that applies nothing throws a
209
+ `RelayAPIError` (404 unknown or deleted surface, 409 a `surfaceId` already live
210
+ in the chat, 422 anything else) whose `body.a2ui_errors` lists each.
211
+
212
+ Every `message.received` carries `metadata.a2uiClientCapabilities`: the
213
+ catalogs Relay's app draws, in order of preference, `RELAY_A2UI_CATALOG_ID`
214
+ (every basic catalog component and function, plus `PaymentRequest`) and
215
+ `A2UI_BASIC_CATALOG_ID`. When a surface sets `sendDataModel: true`, each tap
216
+ also carries that surface's data model; `readA2uiAction` returns it as
217
+ `dataModel`. A tap reaches only the person who tapped and the agent that
218
+ created the surface. Only an agent sends `createSurface`, `updateComponents`,
219
+ `updateDataModel` and `deleteSurface`; any agent in the chat may update any
220
+ surface in it.
221
+
222
+ ## Chat permissions
223
+
5
224
  Relay Chats support one human user with one or more agents. Contacts, Handles,
6
225
  and Participants remain generic API types, but selectable participants are
7
226
  agents. Agent-to-agent Chats remain supported. This Agent SDK does not expose phone address-book syncing, mutual
@@ -65,6 +284,46 @@ await relay.chats.messages.send(chatId, {
65
284
  });
66
285
  ```
67
286
 
287
+ ## Show task activity
288
+
289
+ Start activity when the work starts. Image generation uses `🖼️ Generating image`;
290
+ voice-note generation uses `🎙️ Generating voice note`.
291
+
292
+ ```ts
293
+ const state = await relay.chats.setActivity(chatId, {
294
+ text: "Generating image",
295
+ emoji: "🖼️",
296
+ });
297
+ const activityId = state.activity!.id;
298
+ ```
299
+
300
+ Renew every 60 seconds only while that task remains active. The same ID guards
301
+ updates against replacing a newer task; a replaced or cleared ID returns 409.
302
+ Each successful update renews the 90-second safety lease:
303
+
304
+ ```ts
305
+ await relay.chats.setActivity(chatId, {
306
+ text: "Generating image",
307
+ emoji: "🖼️",
308
+ activity_id: activityId,
309
+ });
310
+ ```
311
+
312
+ Clear when work completes, fails, or is cancelled. Guard cleanup with the task
313
+ ID so an old task cannot clear a newer one. Missing or replaced activity is a
314
+ successful no-op:
315
+
316
+ ```ts
317
+ await relay.chats.clearActivity(chatId, { activity_id: activityId });
318
+ const current = await relay.chats.getActivity(chatId);
319
+ ```
320
+
321
+ GET reads your own state. Text allows 1–21 visible characters and at most
322
+ 1024 UTF-8 bytes; `emoji` is one Unicode emoji or `null`. Response `version`
323
+ is a string and `activity` is an object or `null`. Chat handles may also carry
324
+ `activity_version` and `activity`. Typing remains `chats.startTyping` and
325
+ `chats.stopTyping`; activity does not add an agent event or polling transport.
326
+
68
327
  ## Share your Contact Card
69
328
 
70
329
  ```ts
@@ -107,9 +366,19 @@ Available resource methods:
107
366
  - `webhookEvents.list`
108
367
  - `webhookSubscriptions.create`, `retrieve`, `update`, `list`, `delete`
109
368
  - `contactCard.create`, `retrieve`, `update`
369
+ - `contacts.lookup({ handle })`
110
370
  - `blockedHandles.list`, `block`, `unblock`
371
+ - `access.list`, `access.set(handle, { rule })`, `access.remove(handle)`: the
372
+ agent's Always Allow (`allow`) and Never Allow (`deny`) lists
111
373
  - `websocket.run`
112
374
 
375
+ A handle on Always Allow may start a Chat with the agent whatever its
376
+ organization set under "Available to" in Relay Console; a handle on Never
377
+ Allow may not. People in the agent's organization always get through and are
378
+ on neither list. `access.set` answers 404 (`code === 2001`) when no contact
379
+ has that handle, and 409 (`code === 2032`) for the agent itself, or its owner
380
+ on Never Allow.
381
+
113
382
  `chats.participants.add` and `chats.participants.remove` keep their generic
114
383
  names. Use an agent Handle when adding a participant; do not build a human
115
384
  participant picker. Agent-initiated Chat creation and Messages to users remain
@@ -179,48 +448,248 @@ attachment rules are at <https://docs.relayapp.im>.
179
448
  ## Individual audio Calls (staging)
180
449
 
181
450
  Calls join one user and one agent in an existing individual Chat. Use
182
- `relay.calls.create(chatId, { to: [handle], mode: "audio" }, { idempotencyKey })`;
451
+ `relay.calls.create(chatId, { to: [handle] }, { idempotencyKey })`;
183
452
  keep the same key and body when retrying an uncertain create response.
184
453
 
185
- `relay.calls` also exposes `retrieve`, `list`, `accept`, `decline`, and `end`.
186
- Receive typed `call.created`, `call.updated`, and `call.ended` events through
187
- the existing signed Webhook or Agent WebSocket.
454
+ `relay.calls` also exposes `retrieve`, `list`, `room`, and `end`. Receive typed
455
+ `call.created`, `call.updated`, and `call.ended` events through the existing
456
+ signed Webhook or acknowledged Agent WebSocket. Agents do not configure a call
457
+ URL; receiving `call.created` and joining that Call's room is the answer path.
188
458
 
189
- Every participant holds one socket to the Call room. `relay.calls.room(callId)`
190
- opens `GET /v1/calls/{callId}/room` with the same bearer as every REST route,
191
- sends `join`, and keeps `room.state` at the latest `roomState` the room pushed
192
- after every change. Nothing is polled.
459
+ Every participant holds one signaling socket to the Call room.
460
+ `relay.calls.room(callId)` targets `GET /v1/calls/{callId}/room` with the same
461
+ bearer as every REST route. Register handlers, call `connect()`, and the SDK
462
+ sends `join`; `room.state` then tracks the latest `roomState`. Nothing is polled
463
+ and no audio bytes pass through this WebSocket.
193
464
 
194
465
  ```ts
195
466
  const room = relay.calls.room(event.data.call.id);
196
- room.on("roomState", ({ call, participants, media }) => {
197
- // `media` (the audio-socket url and short-lived token) is present only on
198
- // the agent's own socket. Open it, then accept.
199
- if (call.status === "ringing" && media) room.accept();
467
+ room.on("roomState", ({ call, participants }) => {
468
+ // Each participant has track: "audio" | null, muted and connected state.
469
+ });
470
+ room.on("answer", ({ session_description }) => {
471
+ // Apply Relay's answer to the local WebRTC publication.
200
472
  });
201
473
  room.on("offer", ({ session_description, track }) => {
202
- // The room started pulling the other side's track; answer it.
474
+ // Relay subscribed this participant to the other side's `audio` track.
475
+ // Apply the offer, create a WebRTC answer, gather ICE, then send it:
203
476
  room.send({ type: "answer", session_description: { type: "answer", sdp } });
204
477
  });
205
478
  room.on("ended", ({ reason }) => { /* the socket closes right after */ });
206
- room.connected();
207
- room.userUpdate({ muted: true });
208
- room.end();
479
+ await room.connect(); // sends { type: "join" }; a callee answers here
480
+ ```
481
+
482
+ The stable client frames are `join`, `offer`, `answer`, `connected`,
483
+ `userUpdate`, `end`, and `heartbeat`. A publishing `offer` has exactly one
484
+ `tracks` entry, `{ mid, name: "audio" }`. Stable server frames are `roomState`,
485
+ `answer`, `offer` with `track: "audio"`, `ended`, and `error`; every
486
+ `roomState.participants[]` item reports `track: "audio" | null`. The SDK sends
487
+ `heartbeat` every 15 seconds until `close()`. `connected()`, `userUpdate()` and
488
+ `end()` are convenience methods for those frames, and `reconnect()` replaces
489
+ the signaling socket without first dropping the old one.
490
+
491
+ WebRTC carries the audio. Relay's server owns SFU/provider details, so clients
492
+ only exchange SDP in the room protocol and never receive Cloudflare
493
+ credentials, session IDs, or ICE-provider configuration. Node agents that want
494
+ this SDP/ICE and PCM plumbing handled for them use `RelayCallTransport` from
495
+ `@relaymessenger/sdk/calls`, below; `@relaymessenger/livekit` plugs it into
496
+ LiveKit Agents audio input and output.
497
+
498
+ ### Join a Call as the agent
499
+
500
+ `@relaymessenger/sdk/calls` owns the agent's WebRTC peer, so your code never
501
+ handles SDP, ICE, or SFU credentials. It needs WebRTC packages that
502
+ `@relaymessenger/sdk` lists as optional peer dependencies; install them only if
503
+ your agent joins Calls (`node-webcodecs` only for video):
504
+
505
+ ```sh
506
+ npm install @relaymessenger/sdk werift @evan/opus rtp-packet node-webcodecs
507
+ ```
508
+
509
+ ```ts
510
+ import Relay from "@relaymessenger/sdk";
511
+ import { RelayCallTransport } from "@relaymessenger/sdk/calls";
512
+
513
+ const relay = new Relay({ apiKey: process.env.RELAY_AGENT_TOKEN! });
514
+ const transport = new RelayCallTransport({ relay, callId });
515
+
516
+ transport.on("audio", ({ samples, sampleRate, channelCount }) => {
517
+ // Interleaved signed PCM16 from the remote Relay participant.
518
+ });
519
+ // Inbound audio is 48 kHz stereo unless you pass
520
+ // `inboundAudio: { sampleRate: 8000 | 12000 | 16000 | 24000 | 48000, channelCount: 1 | 2 }`;
521
+ // the `"wrtc"` engine accepts only the default.
522
+
523
+ await transport.connect();
524
+ await transport.writeAudio({
525
+ samples,
526
+ sampleRate: 48_000,
527
+ channelCount: 1,
528
+ });
529
+ await transport.waitForPlayout();
530
+ ```
531
+
532
+ `writeAudio()` resolves once its 10 ms slices are queued, `queuedAudioMs()` is
533
+ what has not left yet, and `waitForPlayout()` resolves when the queue is empty
534
+ and the pump is idle (early on `clearAudio()`). Until the other participant is
535
+ receiving the agent's audio (`subscribed`: its `roomState` entry lists `audio`
536
+ in `receiving`), written audio is held, in order, and the track keeps sending
537
+ silence; then it plays from the start, so a greeting that starts early is not
538
+ cut. A restart holds again until the new session is received. `clearAudio()`
539
+ drops held audio too.
540
+
541
+ ### Video
542
+
543
+ Relay calls carry video whenever a camera is on. The video API copies
544
+ LiveKit's `@livekit/rtc-node` names: `VideoSource`, `LocalVideoTrack`,
545
+ `VideoFrame`, `VideoBufferType`, `VideoStream`. Video needs the default
546
+ `"werift"` engine and the optional dependency `node-webcodecs` (prebuilt for
547
+ macOS arm64 and Linux x64/arm64).
548
+
549
+ Send a video feed:
550
+
551
+ ```ts
552
+ import {
553
+ LocalVideoTrack,
554
+ VideoBufferType,
555
+ VideoFrame,
556
+ VideoSource,
557
+ } from "@relaymessenger/sdk/calls";
558
+
559
+ await transport.connect();
560
+
561
+ const source = new VideoSource(1280, 720);
562
+ const track = LocalVideoTrack.createVideoTrack("camera", source);
563
+ await transport.publishTrack(track);
564
+
565
+ // RGBA, BGRA or I420 bytes, tightly packed, 30 frames a second.
566
+ source.captureFrame(new VideoFrame(rgba, 1280, 720, VideoBufferType.RGBA));
567
+
568
+ // Camera off, then on again; the track stays negotiated.
569
+ await transport.unpublishTrack(track);
570
+ await transport.publishTrack(track);
571
+ ```
572
+
573
+ Send frames up to 1920x1080 at 30 fps. Without `videoEncoding`, each frame
574
+ size gets [LiveKit's camera preset](https://github.com/livekit/client-sdk-js/blob/5cadc938236033fb58b72696bdb3c351adbbe587/src/room/track/options.ts#L507-L532): 3 Mbps at 30 fps for 1920x1080,
575
+ 1.7 Mbps at 30 fps for 1280x720 and 450 kbps at 20 fps for 640x360. Pass
576
+ `videoEncoding: { maxBitrate, maxFramerate }` to set your own.
577
+
578
+ Receive the other participant's video:
579
+
580
+ ```ts
581
+ import { VideoBufferType, VideoStream } from "@relaymessenger/sdk/calls";
582
+
583
+ transport.on("trackSubscribed", async (track) => {
584
+ const stream = new VideoStream(track, { format: VideoBufferType.RGBA, capacity: 2 });
585
+ for await (const { frame, timestampUs } of stream) {
586
+ // frame.data is width x height x 4 bytes of RGBA.
587
+ }
588
+ });
589
+ transport.on("remoteVideo", (on) => {
590
+ // The other participant's camera started or stopped sending.
591
+ });
592
+ ```
593
+
594
+ Frames are decoded only while a `VideoStream` is open. `transport.videoStats()`
595
+ reports frames, packets, keyframes and decode errors in both directions.
596
+
597
+ ### ICE servers, TURN, and diagnostics
598
+
599
+ By default the peer uses the servers the Call room sends after it joins:
600
+ Cloudflare's STUN server and TURN credentials that Relay mints for the call.
601
+ An agent in a container or behind a firewall that blocks outbound UDP connects
602
+ through TURN with no setup. Each restart uses the room's latest servers. A room
603
+ that sends none falls back to Cloudflare's STUN server
604
+ (`stun:stun.cloudflare.com:3478`), as Cloudflare's own Realtime echo example
605
+ does.
606
+
607
+ The offer leaves as soon as the first local candidate exists, without waiting
608
+ for ICE gathering to finish: the SFU is ICE-lite and learns the agent's address
609
+ from its connectivity checks, TURN relay checks included.
610
+
611
+ To use your own servers instead, pass `iceServers` in the standard
612
+ `RTCIceServer` shape and, if every path must go through TURN,
613
+ `iceTransportPolicy: "relay"`. Your value replaces the room's. Both options are
614
+ accepted by `RelayCallTransport` and by `RelayLiveKitCall.connect()` in
615
+ `@relaymessenger/livekit`:
616
+
617
+ ```ts
618
+ const transport = new RelayCallTransport({
619
+ relay,
620
+ callId,
621
+ iceServers: [
622
+ {
623
+ urls: [
624
+ "turn:turn.example.com:3478?transport=udp",
625
+ "turn:turn.example.com:3478?transport=tcp",
626
+ "turns:turn.example.com:5349?transport=tcp",
627
+ ],
628
+ username: process.env.TURN_USERNAME!,
629
+ credential: process.env.TURN_CREDENTIAL!,
630
+ },
631
+ ],
632
+ iceTransportPolicy: "all",
633
+ });
209
634
  ```
210
635
 
211
- `room.send(frame)` takes any client frame from the contract (`join`, `offer`,
212
- `answer`, `userUpdate`, `accept`, `decline`, `end`, `connected`, `heartbeat`);
213
- `accept()`, `decline()`, `end()`, `connected()` and `userUpdate()` send the
214
- matching frame. The client sends `heartbeat` every 15 s until `close()`. An
215
- invalid room frame closes the socket 4400 and fires `error`.
216
-
217
- The agent's audio flows on the media socket named in `roomState.media`, with
218
- `Authorization: Bearer <media.token>`: raw PCM16 little-endian, 48 kHz stereo
219
- binary frames. The server sends JSON `start` with the format, then `ready`.
220
- Send `{"type":"clear"}` to discard unsent speech. An `ended` frame terminates
221
- the media socket. Both sockets are separate from `relay.websocket.run`, which
222
- carries durable events. The SDK exposes no media-provider credentials, session
223
- IDs, model configuration, or audio generation.
636
+ `iceServers` may also be a function; it is called before every peer
637
+ connection, so it can mint fresh TURN credentials for each restart. The room's
638
+ servers are on `room.iceServers` and its `iceServers` event.
639
+
640
+ `connect()` resolves when media first reaches `connected`. It has no overall
641
+ deadline: when an SFU session is not `connected` within
642
+ `sessionConnectTimeoutMs` (5 seconds by default) of its answer, becomes
643
+ `failed`, or stays `disconnected` for 7 seconds, the transport closes that
644
+ peer, waits 250 ms (x1.1 per further attempt, at most 10 s), and publishes
645
+ from a new peer on a new session, for as long as the Call is ringing or in
646
+ progress. Outgoing audio keeps flowing into the new peer. `connect()` rejects
647
+ only when the Call ends, the room or transport closes, the room reports an
648
+ error, or the `signal` passed to it aborts. Each replacement emits
649
+ `restarted` with the reason and the replaced session's summary, for example
650
+ `local: host 2, srflx 0, relay 0; remote: udp 1473; states: new→complete 0.2s, connecting 0.3s, no connected; …`,
651
+ and `diagnostics().restarts` counts them. `mediaConnectTimeoutMs` and
652
+ `connectionTimeoutMs` are deprecated names for `sessionConnectTimeoutMs`.
653
+
654
+ `transport.waitForPeerAudio(timeoutMs)` resolves once the person's audio has
655
+ arrived and the room shows them connected (the transport's `peerAudio` event);
656
+ start the agent's session after it so the first words are heard.
657
+
658
+ The same facts are available at any time from `transport.diagnostics()`: local candidate counts by type, the remote
659
+ candidates' transport and port (never their address), the ICE gathering, ICE
660
+ connection and peer connection state changes with their offsets from
661
+ `connect()`, packet counts in both directions (`inbound`: RTP received, Opus
662
+ decode failures, PCM frames delivered, first and last packet offsets, packets
663
+ in the last 5 s; `outbound`: PCM frames accepted, Opus packets, RTP written
664
+ with the caller's audio, RTP written with silence, first and last packet
665
+ offsets, packets in the last 5 s, paced queue size, pacer state), the room frames seen (`roomState` count, pull `offer` count,
666
+ `ended` reason, `error` messages), and the one-line `summary`, for example
667
+ `…; in: 1234 rtp, 0 bad, 1234 frames, first 0.9s last 41.2s, 250/5s; out: 2600 frames, 1300 opus, 1300 rtp, silence 700, first 1.1s last 41.0s, 250/5s, queue 0, pacer alive; room: 3 roomState, 1 offer`.
668
+ Packet counts come from the `werift` engine; `wrtc` reports zero packets and
669
+ `pacer n/a`.
670
+
671
+ Like a live microphone, the `werift` engine's published track sends one Opus
672
+ packet every 20 ms from the moment media connects until the transport closes,
673
+ paced by the monotonic clock: a timer that fires late sends every packet due
674
+ by then, so the wire carries exactly 50 packets a second; a stall longer than
675
+ 200 ms restarts the clock instead of bursting. The track carries
676
+ the caller's audio when some is queued, Opus silence otherwise. Cloudflare's
677
+ SFU will not let the person's side pull a track that has carried no RTP, so a
678
+ silent agent track would never be heard. Silence never counts toward
679
+ `queuedAudioMs()` or `waitForPlayout()`. The `wrtc` engine sends only the
680
+ audio written to it; write silence yourself if you use it.
681
+
682
+ `onWarning` is called once per call, with the summary, when outbound audio is
683
+ queued but no RTP packet has been written for 2 s while media is connected.
684
+ Nothing is restarted; the callback exists so the failing direction is named in
685
+ the agent's logs.
686
+
687
+ `RelayCallTransport` consumes the SDK's `CallRoom`; it does not duplicate the
688
+ room protocol. The default engine is `werift` (pure TypeScript WebRTC) with
689
+ `@evan/opus` (prebuilt Opus, WASM fallback), so no native WebRTC binding is
690
+ loaded. Pass `engine: "wrtc"` to use the optional `@roamhq/wrtc` binding
691
+ instead. The transport boundary stays provider-neutral; `@relaymessenger/livekit`
692
+ adapts it to LiveKit Agents.
224
693
 
225
694
  ## Webhooks
226
695
 
package/dist/a2ui.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type { Relay } from "./client.js";
2
+ import type { A2uiAction, A2uiComponent, A2uiCreateSurfaceMessage, A2uiMessage, DataPart, MessageContent, MessageSendResponse, MessageWebhookData, RelayWebhookEvent, RequestOptions } from "./types.js";
3
+ /** The `media_type` of a data part that carries A2UI messages. */
4
+ export declare const A2UI_MEDIA_TYPE = "application/a2ui+json";
5
+ /** The A2UI version every message these helpers write carries. */
6
+ export declare const A2UI_VERSION = "v0.9.1";
7
+ /** A2UI v0.9.1's basic catalog, by the `catalogId` its catalog file declares. */
8
+ export declare const A2UI_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json";
9
+ /** Relay's catalog: every basic catalog component and function, plus `PaymentRequest`. */
10
+ export declare const RELAY_A2UI_CATALOG_ID = "https://relayapp.im/a2ui/catalog/v1";
11
+ /** A card: the surface to create, its components, and, optionally, its first data model. */
12
+ export interface A2uiSurface {
13
+ surfaceId: string;
14
+ /** A catalog Relay draws: `A2UI_BASIC_CATALOG_ID` or `RELAY_A2UI_CATALOG_ID`. */
15
+ catalogId: string;
16
+ /** Every component of the card; one has the id `root`. The catalog defines each component and property. */
17
+ components: A2uiComponent[];
18
+ /** The whole data model, sent as an `updateDataModel` with no `path`. */
19
+ dataModel?: Record<string, unknown>;
20
+ theme?: A2uiCreateSurfaceMessage["createSurface"]["theme"];
21
+ sendDataModel?: boolean;
22
+ }
23
+ /**
24
+ * A change to a card that is already in the chat: components to add or
25
+ * replace by id, and a data model value to set at `path` (a JSON Pointer; no
26
+ * `path` is the whole data model; no `value` removes the key at `path`).
27
+ */
28
+ export interface A2uiSurfaceUpdate {
29
+ components?: A2uiComponent[];
30
+ dataModel?: {
31
+ path?: string;
32
+ value?: unknown;
33
+ };
34
+ }
35
+ /**
36
+ * The rest of the Message beside the data part. `text` becomes a text part
37
+ * before it; the chat list and notification show it. With no text, they show
38
+ * the card's first `Text` component. A text beside an update is a new Message,
39
+ * and the update still changes the card in place.
40
+ */
41
+ export type A2uiSendOptions = Omit<MessageContent, "parts"> & {
42
+ text?: string;
43
+ };
44
+ /** A tap read out of `message.received`. */
45
+ export interface A2uiTap {
46
+ action: A2uiAction;
47
+ /** The tapped surface's data model, present when the surface set `sendDataModel`. */
48
+ dataModel?: Record<string, unknown>;
49
+ }
50
+ /** Wraps A2UI messages in the data part Relay carries them in. */
51
+ export declare const a2uiPart: (messages: A2uiMessage[]) => DataPart;
52
+ /**
53
+ * Sends a new card into a chat: `createSurface`, `updateComponents` and, when
54
+ * `dataModel` is given, `updateDataModel`, in one data part. Messages Relay
55
+ * could not apply come back in `a2ui_errors` (`A2uiFailure`); a send it could
56
+ * not apply at all throws a `RelayAPIError` whose `body.a2ui_errors` lists each.
57
+ */
58
+ export declare const sendA2uiSurface: (client: Pick<Relay, "chats">, chatID: string, surface: A2uiSurface, message?: A2uiSendOptions, options?: RequestOptions) => Promise<MessageSendResponse>;
59
+ /**
60
+ * Changes a card already in the chat, in place, for every member. With no
61
+ * `text`, the send adds no Message and returns the card's own Message. Any
62
+ * agent in the chat may update any surface in it.
63
+ */
64
+ export declare const updateA2uiSurface: (client: Pick<Relay, "chats">, chatID: string, surfaceId: string, update: A2uiSurfaceUpdate, message?: A2uiSendOptions, options?: RequestOptions) => Promise<MessageSendResponse>;
65
+ /**
66
+ * Removes a card for everyone. When every surface a Message draws is deleted,
67
+ * the Message reads back with no parts and a non-null `unsent_at`. The
68
+ * `surfaceId` may be created again afterwards.
69
+ */
70
+ export declare const deleteA2uiSurface: (client: Pick<Relay, "chats">, chatID: string, surfaceId: string, message?: A2uiSendOptions, options?: RequestOptions) => Promise<MessageSendResponse>;
71
+ /**
72
+ * Reads the first A2UI `action` out of a `message.received` event (a signed
73
+ * webhook or a WebSocket event), or out of its `data`. Returns null for any
74
+ * other event and for a Message that carries no tap. When the surface set
75
+ * `sendDataModel`, `dataModel` is that surface's model from
76
+ * `metadata.a2uiClientDataModel`.
77
+ */
78
+ export declare const readA2uiAction: (event: RelayWebhookEvent | MessageWebhookData) => A2uiTap | null;
79
+ //# sourceMappingURL=a2ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a2ui.d.ts","sourceRoot":"","sources":["../src/a2ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,wBAAwB,EACxB,WAAW,EAEX,QAAQ,EACR,cAAc,EAEd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,kEAAkE;AAClE,eAAO,MAAM,eAAe,0BAA0B,CAAC;AAEvD,kEAAkE;AAClE,eAAO,MAAM,YAAY,WAAW,CAAC;AAErC,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,oEAAoE,CAAC;AAEvG,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,wCAAwC,CAAC;AAE3E,4FAA4F;AAC5F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,2GAA2G;IAC3G,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,wBAAwB,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3D,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAChD;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,4CAA4C;AAC5C,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,qFAAqF;IACrF,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,kEAAkE;AAClE,eAAO,MAAM,QAAQ,GAAI,UAAU,WAAW,EAAE,KAAG,QAIjD,CAAC;AAeH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAC5B,QAAQ,MAAM,EACd,SAAS,WAAW,EACpB,UAAU,eAAe,EACzB,UAAU,cAAc,KACvB,OAAO,CAAC,mBAAmB,CAkB7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAC5B,QAAQ,MAAM,EACd,WAAW,MAAM,EACjB,QAAQ,iBAAiB,EACzB,UAAU,eAAe,EACzB,UAAU,cAAc,KACvB,OAAO,CAAC,mBAAmB,CAU7B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,EAC5B,QAAQ,MAAM,EACd,WAAW,MAAM,EACjB,UAAU,eAAe,EACzB,UAAU,cAAc,KACvB,OAAO,CAAC,mBAAmB,CACqE,CAAC;AAEpG;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,iBAAiB,GAAG,kBAAkB,KAC5C,OAAO,GAAG,IAkBZ,CAAC"}