@runwayml/avatars-react 0.9.0 → 0.10.0-beta.1

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.
package/README.md CHANGED
@@ -56,6 +56,9 @@ The styles use CSS custom properties for easy customization:
56
56
 
57
57
  See [`examples/`](./examples) for complete working examples:
58
58
  - [`nextjs`](./examples/nextjs) - Next.js App Router
59
+ - [`nextjs-client-events`](./examples/nextjs-client-events) - Client event tools (trivia game)
60
+ - [`nextjs-rpc`](./examples/nextjs-rpc) - Backend RPC + client events (trivia with server-side questions)
61
+ - [`nextjs-rpc-weather`](./examples/nextjs-rpc-weather) - Backend RPC only (weather assistant)
59
62
  - [`nextjs-server-actions`](./examples/nextjs-server-actions) - Next.js with Server Actions
60
63
  - [`react-router`](./examples/react-router) - React Router v7 framework mode
61
64
  - [`express`](./examples/express) - Express + Vite
@@ -150,38 +153,41 @@ import { AvatarCall, AvatarVideo, ControlBar, UserVideo } from '@runwayml/avatar
150
153
 
151
154
  ### Render Props
152
155
 
153
- All components support render props for complete control:
156
+ All display components support render props for complete control. `AvatarVideo` receives a discriminated union with `status`:
154
157
 
155
158
  ```tsx
156
159
  <AvatarVideo>
157
- {({ hasVideo, isConnecting, trackRef }) => (
158
- <div>
159
- {isConnecting && <Spinner />}
160
- {hasVideo && <VideoTrack trackRef={trackRef} />}
161
- </div>
162
- )}
160
+ {(avatar) => {
161
+ switch (avatar.status) {
162
+ case 'connecting': return <Spinner />;
163
+ case 'waiting': return <Placeholder />;
164
+ case 'ready': return <VideoTrack trackRef={avatar.videoTrackRef} />;
165
+ }
166
+ }}
163
167
  </AvatarVideo>
164
168
  ```
165
169
 
166
170
  ### CSS Styling with Data Attributes
167
171
 
168
- Style connection states with CSS:
172
+ Style components with the namespaced `data-avatar-*` attributes:
169
173
 
170
174
  ```tsx
171
175
  <AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect" className="my-avatar" />
172
176
  ```
173
177
 
174
178
  ```css
175
- .my-avatar[data-state="connecting"] {
179
+ /* Style avatar video by connection status */
180
+ [data-avatar-video][data-avatar-status="connecting"] {
176
181
  opacity: 0.5;
177
182
  }
178
183
 
179
- .my-avatar[data-state="error"] {
180
- border: 2px solid red;
184
+ [data-avatar-video][data-avatar-status="ready"] {
185
+ opacity: 1;
181
186
  }
182
187
 
183
- .my-avatar[data-state="connected"] {
184
- border: 2px solid green;
188
+ /* Style control buttons */
189
+ [data-avatar-control][data-avatar-enabled="false"] {
190
+ opacity: 0.5;
185
191
  }
186
192
  ```
187
193
 
@@ -196,9 +202,96 @@ Style connection states with CSS:
196
202
  />
197
203
  ```
198
204
 
205
+ ## Webcam & Screen Sharing
206
+
207
+ The avatar can see your webcam feed or screen share, enabling visual interactions — show a plant for identification, [hold up a Pokémon card for trivia](https://x.com/technofantasyy/status/2031124673552097412), get [real-time coaching while you play a game](https://x.com/iamneubert/status/2031160102452081046), walk through a presentation, or ask for feedback on a design you're working on.
208
+
209
+ **Compatibility:** Webcam and screen sharing are supported by all preset avatars and custom avatars that use a preset voice. Custom avatars with a custom voice do not support webcam or screen sharing.
210
+
211
+ ### Webcam
212
+
213
+ The webcam is enabled by default. The `video` prop controls whether the camera activates on connect, and the `<UserVideo>` component renders the local camera feed:
214
+
215
+ ```tsx
216
+ <AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect">
217
+ <AvatarVideo />
218
+ <UserVideo />
219
+ <ControlBar />
220
+ </AvatarCall>
221
+ ```
222
+
223
+ To disable the webcam, set `video={false}`:
224
+
225
+ ```tsx
226
+ <AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect" video={false} />
227
+ ```
228
+
229
+ ### Screen Sharing
230
+
231
+ Enable the screen share button by passing `showScreenShare` to `ControlBar`, and use `<ScreenShareVideo>` to display the shared content:
232
+
233
+ ```tsx
234
+ <AvatarCall avatarId="music-superstar" connectUrl="/api/avatar/connect">
235
+ <AvatarVideo />
236
+ <ScreenShareVideo />
237
+ <ControlBar showScreenShare />
238
+ </AvatarCall>
239
+ ```
240
+
241
+ You can also start screen sharing automatically by passing a pre-captured stream via `initialScreenStream`. This is useful when you want to prompt the user for screen share permission before the session connects:
242
+
243
+ ```tsx
244
+ function ScreenShareCall() {
245
+ const [stream, setStream] = useState<MediaStream | null>(null);
246
+
247
+ async function startWithScreenShare() {
248
+ const mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
249
+ setStream(mediaStream);
250
+ }
251
+
252
+ if (!stream) {
253
+ return <button onClick={startWithScreenShare}>Share Screen & Start Call</button>;
254
+ }
255
+
256
+ return (
257
+ <AvatarCall
258
+ avatarId="music-superstar"
259
+ connectUrl="/api/avatar/connect"
260
+ initialScreenStream={stream}
261
+ >
262
+ <AvatarVideo />
263
+ <ScreenShareVideo />
264
+ <ControlBar showScreenShare />
265
+ </AvatarCall>
266
+ );
267
+ }
268
+ ```
269
+
270
+ ### Programmatic Control
271
+
272
+ Use the `useLocalMedia` hook for full programmatic control over camera and screen sharing:
273
+
274
+ ```tsx
275
+ function MediaControls() {
276
+ const {
277
+ isCameraEnabled,
278
+ isScreenShareEnabled,
279
+ toggleCamera,
280
+ toggleScreenShare,
281
+ } = useLocalMedia();
282
+
283
+ return (
284
+ <div>
285
+ <button onClick={toggleCamera}>{isCameraEnabled ? 'Hide Camera' : 'Show Camera'}</button>
286
+ <button onClick={toggleScreenShare}>{isScreenShareEnabled ? 'Stop Sharing' : 'Share Screen'}</button>
287
+ </div>
288
+ );
289
+ }
290
+ ```
291
+
199
292
  ## Hooks
200
293
 
201
- Use hooks for custom components within an `AvatarCall` or `AvatarSession`:
294
+ Use hooks for custom components within an `AvatarCall` or `AvatarSession`. Also available: `useClientEvent` and `useClientEvents` for [client events](#client-events), and `useTranscription` for real-time transcription.
202
295
 
203
296
  ### useAvatarSession
204
297
 
@@ -233,13 +326,14 @@ function CustomAvatar() {
233
326
 
234
327
  ### useLocalMedia
235
328
 
236
- Control local camera and microphone:
329
+ Control local camera, microphone, and screen sharing:
237
330
 
238
331
  ```tsx
239
332
  function MediaControls() {
240
333
  const {
241
334
  isMicEnabled,
242
335
  isCameraEnabled,
336
+ isScreenShareEnabled,
243
337
  toggleMic,
244
338
  toggleCamera,
245
339
  toggleScreenShare,
@@ -249,11 +343,53 @@ function MediaControls() {
249
343
  <div>
250
344
  <button onClick={toggleMic}>{isMicEnabled ? 'Mute' : 'Unmute'}</button>
251
345
  <button onClick={toggleCamera}>{isCameraEnabled ? 'Hide' : 'Show'}</button>
346
+ <button onClick={toggleScreenShare}>{isScreenShareEnabled ? 'Stop Sharing' : 'Share Screen'}</button>
252
347
  </div>
253
348
  );
254
349
  }
255
350
  ```
256
351
 
352
+ ## Client Events
353
+
354
+ > **Compatibility:** Client events (tool calling) are supported on avatars that use a **preset voice**. Custom voice avatars do not currently support client events.
355
+
356
+ Avatars can trigger UI events via tool calls sent over the data channel. Define tools, pass them when creating a session, and subscribe on the client:
357
+
358
+ ```ts
359
+ // lib/tools.ts — shared between server and client
360
+ import { clientTool, type ClientEventsFrom } from '@runwayml/avatars-react/api';
361
+
362
+ export const showCaption = clientTool('show_caption', {
363
+ description: 'Display a caption overlay',
364
+ args: {} as { text: string },
365
+ });
366
+
367
+ export const tools = [showCaption];
368
+ export type MyEvent = ClientEventsFrom<typeof tools>;
369
+ ```
370
+
371
+ ```ts
372
+ // Server — pass tools when creating the session
373
+ const { id } = await client.realtimeSessions.create({
374
+ model: 'gwm1_avatars',
375
+ avatar: { type: 'custom', avatarId: '...' },
376
+ tools,
377
+ });
378
+ ```
379
+
380
+ ```tsx
381
+ // Client — subscribe to events inside AvatarCall
382
+ import { useClientEvent } from '@runwayml/avatars-react';
383
+ import type { MyEvent } from '@/lib/tools';
384
+
385
+ function CaptionOverlay() {
386
+ const caption = useClientEvent<MyEvent, 'show_caption'>('show_caption');
387
+ return caption ? <p>{caption.text}</p> : null;
388
+ }
389
+ ```
390
+
391
+ See the [`nextjs-client-events`](./examples/nextjs-client-events) example for a full working demo.
392
+
257
393
  ## Advanced: AvatarSession
258
394
 
259
395
  For full control over session management, use `AvatarSession` directly with pre-fetched credentials:
@@ -285,7 +421,7 @@ function AdvancedUsage({ credentials }) {
285
421
  | `AvatarSession` | Low-level wrapper that requires credentials |
286
422
  | `AvatarVideo` | Renders the remote avatar video |
287
423
  | `UserVideo` | Renders the local user's camera |
288
- | `ControlBar` | Media control buttons (mic, camera, end call) |
424
+ | `ControlBar` | Media control buttons (mic, camera, screen share, end call) |
289
425
  | `ScreenShareVideo` | Renders screen share content |
290
426
  | `AudioRenderer` | Handles avatar audio playback |
291
427
 
package/dist/api.cjs CHANGED
@@ -1,5 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ // src/tools.ts
4
+ function clientTool(name, config) {
5
+ return {
6
+ type: "client_event",
7
+ name,
8
+ description: config.description
9
+ };
10
+ }
11
+
3
12
  // src/api/config.ts
4
13
  var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
5
14
 
@@ -23,6 +32,7 @@ async function consumeSession(options) {
23
32
  return response.json();
24
33
  }
25
34
 
35
+ exports.clientTool = clientTool;
26
36
  exports.consumeSession = consumeSession;
27
37
  //# sourceMappingURL=api.cjs.map
28
38
  //# sourceMappingURL=api.cjs.map
package/dist/api.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";;;AAAO,IAAM,gBAAA,GAAmB,8BAAA;;;ACGhC,eAAsB,eACpB,OAAA,EACiC;AACjC,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,OAAA,GAAU,kBAAiB,GAAI,OAAA;AAE9D,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,sBAAA,EAAyB,SAAS,CAAA,QAAA,CAAA;AACxD,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAChC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,UAAU,UAAU,CAAA;AAAA;AACrC,GACD,CAAA;AAED,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB","file":"api.cjs","sourcesContent":["export const DEFAULT_BASE_URL = 'https://api.dev.runwayml.com';\n","import type { ConsumeSessionOptions, ConsumeSessionResponse } from '../types';\nimport { DEFAULT_BASE_URL } from './config';\n\nexport async function consumeSession(\n options: ConsumeSessionOptions,\n): Promise<ConsumeSessionResponse> {\n const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;\n\n const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${sessionKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to consume session: ${response.status} ${errorText}`,\n );\n }\n\n return response.json();\n}\n"]}
1
+ {"version":3,"sources":["../src/tools.ts","../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";;;AAwDO,SAAS,UAAA,CACd,MACA,MAAA,EAC2B;AAC3B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,cAAA;AAAA,IACN,IAAA;AAAA,IACA,aAAa,MAAA,CAAO;AAAA,GACtB;AACF;;;ACjEO,IAAM,gBAAA,GAAmB,8BAAA;;;ACGhC,eAAsB,eACpB,OAAA,EACiC;AACjC,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,OAAA,GAAU,kBAAiB,GAAI,OAAA;AAE9D,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,sBAAA,EAAyB,SAAS,CAAA,QAAA,CAAA;AACxD,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAChC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,UAAU,UAAU,CAAA;AAAA;AACrC,GACD,CAAA;AAED,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB","file":"api.cjs","sourcesContent":["import type { ClientEvent } from './types';\n\n/**\n * A standalone client tool definition. Composable — combine into arrays\n * and derive event types with `ClientEventsFrom`.\n *\n * At runtime this is just `{ type, name, description }`. The `Args` generic\n * is phantom — it only exists at the TypeScript level for type narrowing.\n */\nexport interface ClientToolDef<Name extends string = string, Args = unknown> {\n readonly type: 'client_event';\n readonly name: Name;\n readonly description: string;\n /** @internal phantom field — always `undefined` at runtime */\n readonly _args?: Args;\n}\n\n/**\n * Derive a discriminated union of ClientEvent types from an array of tools.\n *\n * @example\n * ```typescript\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> =\n T[number] extends infer U\n ? U extends ClientToolDef<infer Name, infer Args>\n ? ClientEvent<Name, Args>\n : never\n : never;\n\n/**\n * Define a single client tool.\n *\n * Returns a standalone object that can be composed into arrays and passed\n * to `realtimeSessions.create({ tools })`.\n *\n * @example\n * ```typescript\n * const showQuestion = clientTool('show_question', {\n * description: 'Display a trivia question',\n * args: {} as { question: string; options: Array<string> },\n * });\n *\n * const playSound = clientTool('play_sound', {\n * description: 'Play a sound effect',\n * args: {} as { sound: 'correct' | 'incorrect' },\n * });\n *\n * // Combine and derive types\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport function clientTool<Name extends string, Args>(\n name: Name,\n config: { description: string; args: Args },\n): ClientToolDef<Name, Args> {\n return {\n type: 'client_event',\n name,\n description: config.description,\n };\n}\n","export const DEFAULT_BASE_URL = 'https://api.dev.runwayml.com';\n","import type { ConsumeSessionOptions, ConsumeSessionResponse } from '../types';\nimport { DEFAULT_BASE_URL } from './config';\n\nexport async function consumeSession(\n options: ConsumeSessionOptions,\n): Promise<ConsumeSessionResponse> {\n const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;\n\n const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${sessionKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to consume session: ${response.status} ${errorText}`,\n );\n }\n\n return response.json();\n}\n"]}
package/dist/api.d.cts CHANGED
@@ -20,7 +20,80 @@ interface ConsumeSessionOptions {
20
20
  /** Optional base URL for the Runway API (defaults to production) */
21
21
  baseUrl?: string;
22
22
  }
23
+ /**
24
+ * Client event received from the avatar via the data channel.
25
+ * These are fire-and-forget events triggered by the avatar model.
26
+ *
27
+ * @typeParam T - The tool name (defaults to string for untyped usage)
28
+ * @typeParam A - The args type (defaults to Record<string, unknown>)
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Untyped usage
33
+ * const event: ClientEvent = { type: 'client_event', tool: 'show_caption', args: { text: 'Hello' } };
34
+ *
35
+ * // Typed usage with discriminated union
36
+ * type MyEvent = ClientEvent<'show_caption', { text: string }>;
37
+ * ```
38
+ */
39
+ interface ClientEvent<T extends string = string, A = Record<string, unknown>> {
40
+ type: 'client_event';
41
+ tool: T;
42
+ args: A;
43
+ }
44
+
45
+ /**
46
+ * A standalone client tool definition. Composable — combine into arrays
47
+ * and derive event types with `ClientEventsFrom`.
48
+ *
49
+ * At runtime this is just `{ type, name, description }`. The `Args` generic
50
+ * is phantom — it only exists at the TypeScript level for type narrowing.
51
+ */
52
+ interface ClientToolDef<Name extends string = string, Args = unknown> {
53
+ readonly type: 'client_event';
54
+ readonly name: Name;
55
+ readonly description: string;
56
+ /** @internal phantom field — always `undefined` at runtime */
57
+ readonly _args?: Args;
58
+ }
59
+ /**
60
+ * Derive a discriminated union of ClientEvent types from an array of tools.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const tools = [showQuestion, playSound];
65
+ * type MyEvent = ClientEventsFrom<typeof tools>;
66
+ * ```
67
+ */
68
+ type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> = T[number] extends infer U ? U extends ClientToolDef<infer Name, infer Args> ? ClientEvent<Name, Args> : never : never;
69
+ /**
70
+ * Define a single client tool.
71
+ *
72
+ * Returns a standalone object that can be composed into arrays and passed
73
+ * to `realtimeSessions.create({ tools })`.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const showQuestion = clientTool('show_question', {
78
+ * description: 'Display a trivia question',
79
+ * args: {} as { question: string; options: Array<string> },
80
+ * });
81
+ *
82
+ * const playSound = clientTool('play_sound', {
83
+ * description: 'Play a sound effect',
84
+ * args: {} as { sound: 'correct' | 'incorrect' },
85
+ * });
86
+ *
87
+ * // Combine and derive types
88
+ * const tools = [showQuestion, playSound];
89
+ * type MyEvent = ClientEventsFrom<typeof tools>;
90
+ * ```
91
+ */
92
+ declare function clientTool<Name extends string, Args>(name: Name, config: {
93
+ description: string;
94
+ args: Args;
95
+ }): ClientToolDef<Name, Args>;
23
96
 
24
97
  declare function consumeSession(options: ConsumeSessionOptions): Promise<ConsumeSessionResponse>;
25
98
 
26
- export { type ConsumeSessionOptions, type ConsumeSessionResponse, consumeSession };
99
+ export { type ClientEvent, type ClientEventsFrom, type ClientToolDef, type ConsumeSessionOptions, type ConsumeSessionResponse, clientTool, consumeSession };
package/dist/api.d.ts CHANGED
@@ -20,7 +20,80 @@ interface ConsumeSessionOptions {
20
20
  /** Optional base URL for the Runway API (defaults to production) */
21
21
  baseUrl?: string;
22
22
  }
23
+ /**
24
+ * Client event received from the avatar via the data channel.
25
+ * These are fire-and-forget events triggered by the avatar model.
26
+ *
27
+ * @typeParam T - The tool name (defaults to string for untyped usage)
28
+ * @typeParam A - The args type (defaults to Record<string, unknown>)
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Untyped usage
33
+ * const event: ClientEvent = { type: 'client_event', tool: 'show_caption', args: { text: 'Hello' } };
34
+ *
35
+ * // Typed usage with discriminated union
36
+ * type MyEvent = ClientEvent<'show_caption', { text: string }>;
37
+ * ```
38
+ */
39
+ interface ClientEvent<T extends string = string, A = Record<string, unknown>> {
40
+ type: 'client_event';
41
+ tool: T;
42
+ args: A;
43
+ }
44
+
45
+ /**
46
+ * A standalone client tool definition. Composable — combine into arrays
47
+ * and derive event types with `ClientEventsFrom`.
48
+ *
49
+ * At runtime this is just `{ type, name, description }`. The `Args` generic
50
+ * is phantom — it only exists at the TypeScript level for type narrowing.
51
+ */
52
+ interface ClientToolDef<Name extends string = string, Args = unknown> {
53
+ readonly type: 'client_event';
54
+ readonly name: Name;
55
+ readonly description: string;
56
+ /** @internal phantom field — always `undefined` at runtime */
57
+ readonly _args?: Args;
58
+ }
59
+ /**
60
+ * Derive a discriminated union of ClientEvent types from an array of tools.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const tools = [showQuestion, playSound];
65
+ * type MyEvent = ClientEventsFrom<typeof tools>;
66
+ * ```
67
+ */
68
+ type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> = T[number] extends infer U ? U extends ClientToolDef<infer Name, infer Args> ? ClientEvent<Name, Args> : never : never;
69
+ /**
70
+ * Define a single client tool.
71
+ *
72
+ * Returns a standalone object that can be composed into arrays and passed
73
+ * to `realtimeSessions.create({ tools })`.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const showQuestion = clientTool('show_question', {
78
+ * description: 'Display a trivia question',
79
+ * args: {} as { question: string; options: Array<string> },
80
+ * });
81
+ *
82
+ * const playSound = clientTool('play_sound', {
83
+ * description: 'Play a sound effect',
84
+ * args: {} as { sound: 'correct' | 'incorrect' },
85
+ * });
86
+ *
87
+ * // Combine and derive types
88
+ * const tools = [showQuestion, playSound];
89
+ * type MyEvent = ClientEventsFrom<typeof tools>;
90
+ * ```
91
+ */
92
+ declare function clientTool<Name extends string, Args>(name: Name, config: {
93
+ description: string;
94
+ args: Args;
95
+ }): ClientToolDef<Name, Args>;
23
96
 
24
97
  declare function consumeSession(options: ConsumeSessionOptions): Promise<ConsumeSessionResponse>;
25
98
 
26
- export { type ConsumeSessionOptions, type ConsumeSessionResponse, consumeSession };
99
+ export { type ClientEvent, type ClientEventsFrom, type ClientToolDef, type ConsumeSessionOptions, type ConsumeSessionResponse, clientTool, consumeSession };
package/dist/api.js CHANGED
@@ -1,3 +1,12 @@
1
+ // src/tools.ts
2
+ function clientTool(name, config) {
3
+ return {
4
+ type: "client_event",
5
+ name,
6
+ description: config.description
7
+ };
8
+ }
9
+
1
10
  // src/api/config.ts
2
11
  var DEFAULT_BASE_URL = "https://api.dev.runwayml.com";
3
12
 
@@ -21,6 +30,6 @@ async function consumeSession(options) {
21
30
  return response.json();
22
31
  }
23
32
 
24
- export { consumeSession };
33
+ export { clientTool, consumeSession };
25
34
  //# sourceMappingURL=api.js.map
26
35
  //# sourceMappingURL=api.js.map
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";AAAO,IAAM,gBAAA,GAAmB,8BAAA;;;ACGhC,eAAsB,eACpB,OAAA,EACiC;AACjC,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,OAAA,GAAU,kBAAiB,GAAI,OAAA;AAE9D,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,sBAAA,EAAyB,SAAS,CAAA,QAAA,CAAA;AACxD,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAChC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,UAAU,UAAU,CAAA;AAAA;AACrC,GACD,CAAA;AAED,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB","file":"api.js","sourcesContent":["export const DEFAULT_BASE_URL = 'https://api.dev.runwayml.com';\n","import type { ConsumeSessionOptions, ConsumeSessionResponse } from '../types';\nimport { DEFAULT_BASE_URL } from './config';\n\nexport async function consumeSession(\n options: ConsumeSessionOptions,\n): Promise<ConsumeSessionResponse> {\n const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;\n\n const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${sessionKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to consume session: ${response.status} ${errorText}`,\n );\n }\n\n return response.json();\n}\n"]}
1
+ {"version":3,"sources":["../src/tools.ts","../src/api/config.ts","../src/api/consume.ts"],"names":[],"mappings":";AAwDO,SAAS,UAAA,CACd,MACA,MAAA,EAC2B;AAC3B,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,cAAA;AAAA,IACN,IAAA;AAAA,IACA,aAAa,MAAA,CAAO;AAAA,GACtB;AACF;;;ACjEO,IAAM,gBAAA,GAAmB,8BAAA;;;ACGhC,eAAsB,eACpB,OAAA,EACiC;AACjC,EAAA,MAAM,EAAE,SAAA,EAAW,UAAA,EAAY,OAAA,GAAU,kBAAiB,GAAI,OAAA;AAE9D,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,sBAAA,EAAyB,SAAS,CAAA,QAAA,CAAA;AACxD,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,IAChC,MAAA,EAAQ,MAAA;AAAA,IACR,OAAA,EAAS;AAAA,MACP,cAAA,EAAgB,kBAAA;AAAA,MAChB,aAAA,EAAe,UAAU,UAAU,CAAA;AAAA;AACrC,GACD,CAAA;AAED,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,2BAAA,EAA8B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,SAAS,CAAA;AAAA,KAC5D;AAAA,EACF;AAEA,EAAA,OAAO,SAAS,IAAA,EAAK;AACvB","file":"api.js","sourcesContent":["import type { ClientEvent } from './types';\n\n/**\n * A standalone client tool definition. Composable — combine into arrays\n * and derive event types with `ClientEventsFrom`.\n *\n * At runtime this is just `{ type, name, description }`. The `Args` generic\n * is phantom — it only exists at the TypeScript level for type narrowing.\n */\nexport interface ClientToolDef<Name extends string = string, Args = unknown> {\n readonly type: 'client_event';\n readonly name: Name;\n readonly description: string;\n /** @internal phantom field — always `undefined` at runtime */\n readonly _args?: Args;\n}\n\n/**\n * Derive a discriminated union of ClientEvent types from an array of tools.\n *\n * @example\n * ```typescript\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport type ClientEventsFrom<T extends ReadonlyArray<ClientToolDef>> =\n T[number] extends infer U\n ? U extends ClientToolDef<infer Name, infer Args>\n ? ClientEvent<Name, Args>\n : never\n : never;\n\n/**\n * Define a single client tool.\n *\n * Returns a standalone object that can be composed into arrays and passed\n * to `realtimeSessions.create({ tools })`.\n *\n * @example\n * ```typescript\n * const showQuestion = clientTool('show_question', {\n * description: 'Display a trivia question',\n * args: {} as { question: string; options: Array<string> },\n * });\n *\n * const playSound = clientTool('play_sound', {\n * description: 'Play a sound effect',\n * args: {} as { sound: 'correct' | 'incorrect' },\n * });\n *\n * // Combine and derive types\n * const tools = [showQuestion, playSound];\n * type MyEvent = ClientEventsFrom<typeof tools>;\n * ```\n */\nexport function clientTool<Name extends string, Args>(\n name: Name,\n config: { description: string; args: Args },\n): ClientToolDef<Name, Args> {\n return {\n type: 'client_event',\n name,\n description: config.description,\n };\n}\n","export const DEFAULT_BASE_URL = 'https://api.dev.runwayml.com';\n","import type { ConsumeSessionOptions, ConsumeSessionResponse } from '../types';\nimport { DEFAULT_BASE_URL } from './config';\n\nexport async function consumeSession(\n options: ConsumeSessionOptions,\n): Promise<ConsumeSessionResponse> {\n const { sessionId, sessionKey, baseUrl = DEFAULT_BASE_URL } = options;\n\n const url = `${baseUrl}/v1/realtime_sessions/${sessionId}/consume`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${sessionKey}`,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Failed to consume session: ${response.status} ${errorText}`,\n );\n }\n\n return response.json();\n}\n"]}