realtime-avatar 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2398,11 +2398,11 @@ function useRealtimeSession(input) {
2398
2398
  requestGracefulClose();
2399
2399
  lifecycle.reset();
2400
2400
  }, [lifecycle, requestGracefulClose]);
2401
- const performClip = useCallback(
2402
- (clipId, opts) => {
2401
+ const performAction = useCallback(
2402
+ (actionId, opts) => {
2403
2403
  const requestId = newTurnId();
2404
2404
  const publish = dataPublisherRef.current;
2405
- const trimmed = clipId.trim();
2405
+ const trimmed = actionId.trim();
2406
2406
  if (!publish || !trimmed) {
2407
2407
  const result = { requestId, accepted: false, reason: "not_connected" };
2408
2408
  cbRef.current.onClipResult?.(result);
@@ -2417,10 +2417,9 @@ function useRealtimeSession(input) {
2417
2417
  }, opts?.timeoutMs ?? 5e3);
2418
2418
  pendingClipsRef.current.set(requestId, { resolve, timer });
2419
2419
  publish({
2420
- kind: "clip_request",
2420
+ kind: "action_request",
2421
2421
  request_id: requestId,
2422
- clip_id: trimmed,
2423
- ...typeof opts?.holdSeconds === "number" ? { hold_seconds: opts.holdSeconds } : {}
2422
+ action_id: trimmed
2424
2423
  });
2425
2424
  });
2426
2425
  },
@@ -2465,7 +2464,7 @@ function useRealtimeSession(input) {
2465
2464
  retryTurn,
2466
2465
  end,
2467
2466
  behavior,
2468
- performClip,
2467
+ performAction,
2469
2468
  onLifecycleData,
2470
2469
  registerDataPublisher,
2471
2470
  registerTurnSender,
@@ -2487,7 +2486,7 @@ function useRealtimeSession(input) {
2487
2486
  retryTurn,
2488
2487
  end,
2489
2488
  behavior,
2490
- performClip,
2489
+ performAction,
2491
2490
  onLifecycleData,
2492
2491
  registerDataPublisher,
2493
2492
  registerTurnSender,
package/dist/react.d.ts CHANGED
@@ -1,10 +1,25 @@
1
+ import { RegisteredTool, ToolRegistration } from './tools.js';
1
2
  import { ReactNode } from 'react';
2
- import { g as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-DlYBC6cp.js';
3
- export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, aK as AvatarVideoSurface, aL as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, h as CallTranscriptSegment, i as CartesiaTtsModel, j as ClipResult, k as ClosingTurnResult, l as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, m as DEFAULT_CREDITS_LOW_LEAD_SECONDS, n as DEFAULT_GOVERNOR_CONFIG, o as DEFAULT_GRACE_CEILING_SECONDS, p as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, q as DEFAULT_IDLE_SECONDS, r as DEFAULT_IDLE_WARN_LEAD_SECONDS, s as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, t as EndedEvent, u as ExtendResult, F as FishTtsModel, v as FreezeReadingFn, G as Governor, w as GovernorAction, x as GovernorConfig, y as GovernorSignal, z as GovernorState, H as GraceWindowClosedEvent, I as GraceWindowOpenEvent, J as GraceWindowState, K as IdleWarningEvent, M as InboundRtpCursor, N as InboundRtpReading, O as KnownBehaviorState, P as LLMProvider, Q as LLMSelection, R as LiveKitAvatarGrantState, S as LiveKitAvatarGrantStatus, T as LiveKitCapacityState, U as LiveKitConnectionStatus, V as LiveKitSessionRequest, W as LiveKitSessionStartResult, aM as LivePlaybackKeeper, X as MAX_RECONNECT_ATTEMPTS, Y as MAX_SESSION_INSTRUCTIONS_CHARS, aN as MIC_LEASE_ENDED_TIMEOUT_MS, aO as MicLease, aP as PlayableVideoElement, Z as ProxyClientOptions, _ as QualityCap, $ as RECONNECT_BACKOFF_MS, aQ as RealtimeAvatarCapacityError, aR as RealtimeAvatarLiveKitRoom, aS as RealtimeAvatarLiveKitRoomProps, a0 as RealtimeAvatarRequestOptions, a1 as RealtimeSessionApi, a2 as RealtimeSessionMedia, a3 as RealtimeSessionRoomSinks, a4 as ReconnectPolicy, a5 as ReconnectingEvent, a6 as RecoveryState, a7 as RetryStep, a8 as SessionBehavior, a9 as SessionClip, aa as SessionClocks, ab as SessionEndReason, ac as SessionLifecycleApi, ad as SessionLifecyclePhase, ae as SessionLifecyclePhaseKind, af as SessionLifecycleRoomBridge, ag as SessionLifecycleRoomBridgeProps, ah as SurfaceLayers, ai as TurnState, aj as TurnTimeoutEvent, ak as UseAvatarQualityGovernorInput, al as UseLiveKitAvatarGrantInput, am as UseRealtimeSessionInput, an as UseSessionLifecycleInput, ao as VoiceSpec, ap as VoiceSpecInput, aq as capacityErrorFromBusy, ar as capacityStateFromGrant, as as createProxyClient, au as knownBehaviorStates, av as mapTurnState, aw as readInboundRtp, ax as sessionBehaviorSchema, ay as sessionClipSchema, aT as shouldReplayPendingTurn, az as splitCallTranscript, aA as useAvatarAdaptivePlayoutDelay, aB as useAvatarPlayoutDelay, aC as useAvatarQualityGovernor, aD as useCallTranscript, aE as useLiveKitAvatarGrant, aG as useMicLease, aH as useRealtimeSession, aI as useReleaseMicLeaseOnTrackEnded, aJ as useSessionLifecycle } from './proxy-client-DlYBC6cp.js';
3
+ import { g as AvatarSessionClient, A as AvatarVideoFit } from './proxy-client-B6OnXpaC.js';
4
+ export { a as AdaptivePlayoutController, b as AdaptivePlayoutDecision, c as AdaptivePlayoutOptions, d as AdaptivePlayoutSample, e as ApproachingEndEvent, f as ApproachingEndReason, aK as AvatarVideoSurface, aL as AvatarVideoSurfaceProps, B as BehaviorSnapshot, C as CallTranscript, h as CallTranscriptSegment, i as CartesiaTtsModel, j as ClipResult, k as ClosingTurnResult, l as CreditsLowEvent, D as DEFAULT_APPROACHING_END_LEAD_SECONDS, m as DEFAULT_CREDITS_LOW_LEAD_SECONDS, n as DEFAULT_GOVERNOR_CONFIG, o as DEFAULT_GRACE_CEILING_SECONDS, p as DEFAULT_GRACE_WINDOW_LEAD_SECONDS, q as DEFAULT_IDLE_SECONDS, r as DEFAULT_IDLE_WARN_LEAD_SECONDS, s as DEFAULT_TURN_TIMEOUT_SECONDS, E as EndReason, t as EndedEvent, u as ExtendResult, F as FishTtsModel, v as FreezeReadingFn, G as Governor, w as GovernorAction, x as GovernorConfig, y as GovernorSignal, z as GovernorState, H as GraceWindowClosedEvent, I as GraceWindowOpenEvent, J as GraceWindowState, K as IdleWarningEvent, M as InboundRtpCursor, N as InboundRtpReading, O as KnownBehaviorState, P as LLMProvider, Q as LLMSelection, R as LiveKitAvatarGrantState, S as LiveKitAvatarGrantStatus, T as LiveKitCapacityState, U as LiveKitConnectionStatus, V as LiveKitSessionRequest, W as LiveKitSessionStartResult, aM as LivePlaybackKeeper, X as MAX_RECONNECT_ATTEMPTS, Y as MAX_SESSION_INSTRUCTIONS_CHARS, aN as MIC_LEASE_ENDED_TIMEOUT_MS, aO as MicLease, aP as PlayableVideoElement, Z as ProxyClientOptions, _ as QualityCap, $ as RECONNECT_BACKOFF_MS, aQ as RealtimeAvatarCapacityError, aR as RealtimeAvatarLiveKitRoom, aS as RealtimeAvatarLiveKitRoomProps, a0 as RealtimeAvatarRequestOptions, a1 as RealtimeSessionApi, a2 as RealtimeSessionMedia, a3 as RealtimeSessionRoomSinks, a4 as ReconnectPolicy, a5 as ReconnectingEvent, a6 as RecoveryState, a7 as RetryStep, a8 as SessionBehavior, a9 as SessionClip, aa as SessionClocks, ab as SessionEndReason, ac as SessionLifecycleApi, ad as SessionLifecyclePhase, ae as SessionLifecyclePhaseKind, af as SessionLifecycleRoomBridge, ag as SessionLifecycleRoomBridgeProps, ah as SurfaceLayers, ai as TurnState, aj as TurnTimeoutEvent, ak as UseAvatarQualityGovernorInput, al as UseLiveKitAvatarGrantInput, am as UseRealtimeSessionInput, an as UseSessionLifecycleInput, ao as VoiceSpec, ap as VoiceSpecInput, aq as capacityErrorFromBusy, ar as capacityStateFromGrant, as as createProxyClient, au as knownBehaviorStates, av as mapTurnState, aw as readInboundRtp, ax as sessionBehaviorSchema, ay as sessionClipSchema, aT as shouldReplayPendingTurn, az as splitCallTranscript, aA as useAvatarAdaptivePlayoutDelay, aB as useAvatarPlayoutDelay, aC as useAvatarQualityGovernor, aD as useCallTranscript, aE as useLiveKitAvatarGrant, aG as useMicLease, aH as useRealtimeSession, aI as useReleaseMicLeaseOnTrackEnded, aJ as useSessionLifecycle } from './proxy-client-B6OnXpaC.js';
4
5
  export { D as DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS } from './playout-delay-DWOc8iCl.js';
6
+ import 'zod';
5
7
  import '@livekit/components-react';
6
8
  import 'livekit-client';
7
- import 'zod';
9
+
10
+ declare function useCharacterTools(tools: Record<string, RegisteredTool>, options?: {
11
+ onResult?: (event: {
12
+ name: string;
13
+ callId: string;
14
+ ok: boolean;
15
+ error?: string;
16
+ }) => void;
17
+ }): {
18
+ status: "idle" | "registering" | "ready" | "error";
19
+ registered: string[];
20
+ rejected: ToolRegistration["rejected"];
21
+ error?: string;
22
+ };
8
23
 
9
24
  /**
10
25
  * The CONNECT surface (client half) — one component that puts a live character on screen.
@@ -105,4 +120,4 @@ declare function useAvatarCall(props: AvatarCallProps): {
105
120
  /** One component: a live character on screen. */
106
121
  declare function AvatarCall(props: AvatarCallProps): ReactNode;
107
122
 
108
- export { AvatarCall, type AvatarCallEndReason, type AvatarCallHandle, type AvatarCallProps, type AvatarCallStatus, AvatarSessionClient, AvatarVideoFit, useAvatarCall };
123
+ export { AvatarCall, type AvatarCallEndReason, type AvatarCallHandle, type AvatarCallProps, type AvatarCallStatus, AvatarSessionClient, AvatarVideoFit, useAvatarCall, useCharacterTools };
package/dist/react.js CHANGED
@@ -1,10 +1,204 @@
1
1
  "use client";
2
2
  import { useRef, useState, useEffect, useCallback, useMemo, createElement, Fragment } from 'react';
3
- import { useTranscriptions, useLocalParticipant, RoomAudioRenderer, LiveKitRoom, useMaybeRoomContext, useVoiceAssistant, useConnectionState, VideoTrack, useRoomContext, useChat } from '@livekit/components-react';
4
- import { ConnectionQuality, RoomEvent, ConnectionState, TrackEvent, Track, VideoQuality, ConnectionError, ConnectionErrorReason, DisconnectReason, Room } from 'livekit-client';
3
+ import { useRoomContext, useConnectionState, useTranscriptions, useLocalParticipant, RoomAudioRenderer, LiveKitRoom, useMaybeRoomContext, useVoiceAssistant, VideoTrack, useChat } from '@livekit/components-react';
4
+ import { ConnectionState, ConnectionQuality, RoomEvent, TrackEvent, Track, VideoQuality, ConnectionError, ConnectionErrorReason, DisconnectReason, Room } from 'livekit-client';
5
5
  import { z } from 'zod';
6
6
 
7
- // ../client/src/react/avatar-call.ts
7
+ // ../client/src/react/use-character-tools.ts
8
+ var invocationSchema = z.object({
9
+ name: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
10
+ args: z.string().max(8192),
11
+ call_id: z.string().min(1).max(128)
12
+ }).strict();
13
+ var registrationSchema = z.object({
14
+ accepted: z.array(z.string()),
15
+ rejected: z.array(z.object({ name: z.string(), reason: z.string() })).default([])
16
+ });
17
+ var MAX_TOOLS = 32;
18
+ var MAX_MANIFEST_BYTES = 12e3;
19
+ var MAX_RESULT_CHARS = 8192;
20
+ var TOOL_DEADLINE_MS = 2500;
21
+ var REGISTER_METHOD = "rta.tools.register";
22
+ var INVOKE_METHOD = "rta.tool.invoke";
23
+ var NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
24
+ function candidateIdentities(room) {
25
+ const all = Array.from(room.remoteParticipants.values(), (p) => p.identity).filter(Boolean);
26
+ const agents = all.filter((id) => /^agent[-_]/i.test(id));
27
+ return [...agents, ...all.filter((id) => !agents.includes(id))];
28
+ }
29
+ function methodMissing(error) {
30
+ const code = error?.code;
31
+ if (code === "UNSUPPORTED_METHOD" || code === "RECIPIENT_NOT_FOUND" || code === 1401 || code === 1402) {
32
+ return true;
33
+ }
34
+ const message = error instanceof Error ? error.message : String(error ?? "");
35
+ return /not supported|unsupported_method|recipient_not_found|recipient not found/i.test(message);
36
+ }
37
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
38
+ function buildManifest(tools) {
39
+ const descriptors = [];
40
+ const rejected = [];
41
+ for (const [name, tool] of Object.entries(tools)) {
42
+ if (!NAME_RE.test(name)) {
43
+ rejected.push({ name, reason: "name must match [a-zA-Z0-9_-]{1,64}" });
44
+ continue;
45
+ }
46
+ if (!tool?.description) {
47
+ rejected.push({ name, reason: "description is required \u2014 it is how she knows when to call it" });
48
+ continue;
49
+ }
50
+ if (typeof tool.execute !== "function") {
51
+ rejected.push({ name, reason: "execute must be a function" });
52
+ continue;
53
+ }
54
+ if (descriptors.length >= MAX_TOOLS) {
55
+ rejected.push({ name, reason: `over the ${MAX_TOOLS}-tool limit for one session` });
56
+ continue;
57
+ }
58
+ descriptors.push({ name, description: tool.description, parameters: tool.parameters ?? {} });
59
+ }
60
+ return { descriptors, rejected };
61
+ }
62
+ async function attachAvatarTools(room, tools, options = {}) {
63
+ const { descriptors, rejected } = buildManifest(tools);
64
+ const payload = JSON.stringify({ tools: descriptors });
65
+ if (payload.length > MAX_MANIFEST_BYTES) {
66
+ throw new Error(
67
+ `tool manifest is ${payload.length} bytes, over the ${MAX_MANIFEST_BYTES} limit \u2014 shorten descriptions or register fewer tools`
68
+ );
69
+ }
70
+ options.signal?.throwIfAborted();
71
+ let disposed = false;
72
+ let activeCaller;
73
+ const inFlight = /* @__PURE__ */ new Set();
74
+ const dispose = () => {
75
+ if (disposed) return;
76
+ disposed = true;
77
+ room.localParticipant.unregisterRpcMethod?.(INVOKE_METHOD);
78
+ for (const call of inFlight) call.abort();
79
+ options.signal?.removeEventListener("abort", dispose);
80
+ };
81
+ options.signal?.addEventListener("abort", dispose, { once: true });
82
+ room.localParticipant.registerRpcMethod(INVOKE_METHOD, async ({ payload: payload2, callerIdentity }) => {
83
+ let name = "", callId = "";
84
+ try {
85
+ if (disposed) throw new Error("Tool registration was disposed");
86
+ if (callerIdentity && callerIdentity !== activeCaller) throw new Error("Unregistered tool caller");
87
+ const req = invocationSchema.parse(JSON.parse(payload2));
88
+ name = req.name;
89
+ callId = req.call_id;
90
+ const tool = tools[name];
91
+ if (!tool) return JSON.stringify({ ok: false, call_id: callId, error: `no tool named ${name}` });
92
+ const args = z.record(z.string(), z.unknown()).parse(req.args ? JSON.parse(req.args) : {});
93
+ options.onInvoke?.(name, args);
94
+ const run = tool.execute;
95
+ const controller = new AbortController();
96
+ inFlight.add(controller);
97
+ const timer = setTimeout(() => controller.abort(), TOOL_DEADLINE_MS);
98
+ let result;
99
+ try {
100
+ result = await run(args, { signal: controller.signal, callId });
101
+ controller.signal.throwIfAborted();
102
+ } finally {
103
+ inFlight.delete(controller);
104
+ clearTimeout(timer);
105
+ }
106
+ const encoded = typeof result === "string" ? result : JSON.stringify(result ?? null);
107
+ if (encoded.length > MAX_RESULT_CHARS) {
108
+ return JSON.stringify({
109
+ ok: false,
110
+ call_id: callId,
111
+ error: `result was ${encoded.length} chars, over the ${MAX_RESULT_CHARS} limit`
112
+ });
113
+ }
114
+ options.onResult?.({ name, callId, ok: true });
115
+ return JSON.stringify({ ok: true, call_id: callId, result: encoded });
116
+ } catch (error) {
117
+ options.onResult?.({ name, callId, ok: false, error: error instanceof Error ? error.message : "tool failed" });
118
+ return JSON.stringify({
119
+ ok: false,
120
+ call_id: callId,
121
+ error: error instanceof Error ? error.message : "tool failed"
122
+ });
123
+ }
124
+ });
125
+ const deadline = Date.now() + Math.max(0, options.timeoutMs ?? 8e3);
126
+ let lastError = null;
127
+ let sawCandidate = false;
128
+ try {
129
+ for (; ; ) {
130
+ options.signal?.throwIfAborted();
131
+ for (const identity of candidateIdentities(room)) {
132
+ sawCandidate = true;
133
+ activeCaller = identity;
134
+ try {
135
+ const raw = await room.localParticipant.performRpc({
136
+ destinationIdentity: identity,
137
+ method: REGISTER_METHOD,
138
+ payload
139
+ });
140
+ options.signal?.throwIfAborted();
141
+ const result = registrationSchema.parse(JSON.parse(raw));
142
+ return {
143
+ accepted: result.accepted,
144
+ rejected: [...rejected, ...result.rejected],
145
+ dispose
146
+ };
147
+ } catch (error) {
148
+ activeCaller = void 0;
149
+ lastError = error;
150
+ if (!methodMissing(error)) throw error;
151
+ }
152
+ }
153
+ if (Date.now() >= deadline) break;
154
+ await sleep(200);
155
+ }
156
+ throw new Error(
157
+ sawCandidate ? `no participant accepted the tool manifest \u2014 either this session was not minted with the \`client_tools\` capability (a server-side grant), or the agent never armed registration within ${options.timeoutMs ?? 8e3}ms` + (lastError instanceof Error ? ` (last error: ${lastError.message})` : "") : "no remote participant joined the room before the timeout \u2014 nothing to register with"
158
+ );
159
+ } catch (error) {
160
+ dispose();
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ // ../client/src/react/use-character-tools.ts
166
+ function useCharacterTools(tools, options = {}) {
167
+ const room = useRoomContext();
168
+ const connection = useConnectionState();
169
+ const callback = useRef(options.onResult);
170
+ callback.current = options.onResult;
171
+ const [state, setState] = useState({ status: "idle", registered: [], rejected: [] });
172
+ useEffect(() => {
173
+ if (connection !== ConnectionState.Connected) {
174
+ setState({ status: "idle", registered: [], rejected: [] });
175
+ return;
176
+ }
177
+ const abort = new AbortController();
178
+ setState({ status: "registering", registered: [], rejected: [] });
179
+ void attachAvatarTools(room, tools, {
180
+ signal: abort.signal,
181
+ onResult: (event) => {
182
+ if (!abort.signal.aborted) callback.current?.(event);
183
+ }
184
+ }).then((result) => {
185
+ if (abort.signal.aborted) {
186
+ result.dispose();
187
+ return;
188
+ }
189
+ setState({ status: "ready", registered: result.accepted, rejected: result.rejected });
190
+ }).catch((error) => {
191
+ if (!abort.signal.aborted) setState({
192
+ status: "error",
193
+ registered: [],
194
+ rejected: [],
195
+ error: error instanceof Error ? error.message : "Tool registration failed"
196
+ });
197
+ });
198
+ return () => abort.abort();
199
+ }, [room, tools, connection]);
200
+ return state;
201
+ }
8
202
  var holder = null;
9
203
  var queue = [];
10
204
  var pendingEnded = null;
@@ -1027,6 +1221,8 @@ function AvatarVideoSurface(props) {
1027
1221
  onPlayoutDelayChange
1028
1222
  } = props;
1029
1223
  const testId = props["data-testid"];
1224
+ const visibilityCallback = useRef(props.onLiveVisibilityChange);
1225
+ visibilityCallback.current = props.onLiveVisibilityChange;
1030
1226
  const { videoTrack, audioTrack } = useVoiceAssistant();
1031
1227
  useAvatarPlayoutDelay(videoTrack, audioTrack);
1032
1228
  const playoutDelaySeconds = useAvatarAdaptivePlayoutDelay(
@@ -1069,6 +1265,9 @@ function AvatarVideoSurface(props) {
1069
1265
  liveWanted,
1070
1266
  connected && !networkStalled ? idleReturnDelayMs : 0
1071
1267
  );
1268
+ useEffect(() => {
1269
+ visibilityCallback.current?.(showLive);
1270
+ }, [showLive]);
1072
1271
  const liveDims = videoTrack?.publication?.dimensions ?? null;
1073
1272
  const idleVideoRef = useRef(null);
1074
1273
  const boxAspect = aspectRatio ?? null;
@@ -2678,11 +2877,11 @@ function useRealtimeSession(input) {
2678
2877
  requestGracefulClose();
2679
2878
  lifecycle.reset();
2680
2879
  }, [lifecycle, requestGracefulClose]);
2681
- const performClip = useCallback(
2682
- (clipId, opts) => {
2880
+ const performAction = useCallback(
2881
+ (actionId, opts) => {
2683
2882
  const requestId = newTurnId();
2684
2883
  const publish = dataPublisherRef.current;
2685
- const trimmed = clipId.trim();
2884
+ const trimmed = actionId.trim();
2686
2885
  if (!publish || !trimmed) {
2687
2886
  const result = { requestId, accepted: false, reason: "not_connected" };
2688
2887
  cbRef.current.onClipResult?.(result);
@@ -2697,10 +2896,9 @@ function useRealtimeSession(input) {
2697
2896
  }, opts?.timeoutMs ?? 5e3);
2698
2897
  pendingClipsRef.current.set(requestId, { resolve, timer });
2699
2898
  publish({
2700
- kind: "clip_request",
2899
+ kind: "action_request",
2701
2900
  request_id: requestId,
2702
- clip_id: trimmed,
2703
- ...typeof opts?.holdSeconds === "number" ? { hold_seconds: opts.holdSeconds } : {}
2901
+ action_id: trimmed
2704
2902
  });
2705
2903
  });
2706
2904
  },
@@ -2745,7 +2943,7 @@ function useRealtimeSession(input) {
2745
2943
  retryTurn,
2746
2944
  end,
2747
2945
  behavior,
2748
- performClip,
2946
+ performAction,
2749
2947
  onLifecycleData,
2750
2948
  registerDataPublisher,
2751
2949
  registerTurnSender,
@@ -2767,7 +2965,7 @@ function useRealtimeSession(input) {
2767
2965
  retryTurn,
2768
2966
  end,
2769
2967
  behavior,
2770
- performClip,
2968
+ performAction,
2771
2969
  onLifecycleData,
2772
2970
  registerDataPublisher,
2773
2971
  registerTurnSender,
@@ -2931,4 +3129,4 @@ function createProxyClient(options) {
2931
3129
  };
2932
3130
  }
2933
3131
 
2934
- export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, MIC_LEASE_ENDED_TIMEOUT_MS, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
3132
+ export { AdaptivePlayoutController, AvatarCall, AvatarVideoSurface, DEFAULT_APPROACHING_END_LEAD_SECONDS, DEFAULT_AVATAR_PLAYOUT_DELAY_SECONDS, DEFAULT_CREDITS_LOW_LEAD_SECONDS, DEFAULT_GOVERNOR_CONFIG, DEFAULT_GRACE_CEILING_SECONDS, DEFAULT_GRACE_WINDOW_LEAD_SECONDS, DEFAULT_IDLE_SECONDS, DEFAULT_IDLE_WARN_LEAD_SECONDS, DEFAULT_TURN_TIMEOUT_SECONDS, MAX_RECONNECT_ATTEMPTS, MAX_SESSION_INSTRUCTIONS_CHARS, MIC_LEASE_ENDED_TIMEOUT_MS, RECONNECT_BACKOFF_MS, RealtimeAvatarCapacityError, RealtimeAvatarLiveKitRoom, SessionLifecycleRoomBridge, capacityErrorFromBusy, capacityStateFromGrant, createProxyClient, knownBehaviorStates, mapTurnState, readInboundRtp, sessionBehaviorSchema, sessionClipSchema, shouldReplayPendingTurn, splitCallTranscript, useAvatarAdaptivePlayoutDelay, useAvatarCall, useAvatarPlayoutDelay, useAvatarQualityGovernor, useCallTranscript, useCharacterTools, useLiveKitAvatarGrant, useMicLease, useRealtimeSession, useReleaseMicLeaseOnTrackEnded, useSessionLifecycle };
package/dist/server.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, RealtimeAvatarOptions, StartCallOptions, verifyTranscript } from './index.js';
2
- export { i as Asset, h as AssetKind, A as Avatar, m as AvatarClip, c as AvatarSourceSwap, b as AvatarUpdate, n as CallConnection, C as CallMode, a as CallPolicy, o as CallQueued, d as ClipDeclaration, f as ClipLibrary, p as ClipLibraryPlan, e as ClipLibraryUpdate, q as ClipSource, g as ClipSyncResult, r as ContextMessage, l as CreditBalance, E as EndCallOptions, s as EndCallReason, j as ListSessionsOptions, L as LoopRedirect, S as StartCallResult, T as TranscriptPayload, k as UsageSession, U as UsageSessionPage, V as VideoPolicy, t as VideoState, u as isQueued } from './types-D_aIKv4P.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, RealtimeAvatarOptions, StartCallOptions, clipLibraryDeclarationSchema, verifyTranscript } from './index.js';
2
+ export { h as Asset, g as AssetKind, A as Avatar, l as AvatarClip, c as AvatarSourceSwap, b as AvatarUpdate, m as CallConnection, C as CallMode, a as CallPolicy, n as CallQueued, o as ClipDeclaration, f as ClipLibrary, d as ClipLibraryDeclaration, p as ClipLibraryPlan, e as ClipLibraryUpdate, q as ClipSource, r as ContextMessage, k as CreditBalance, E as EndCallOptions, s as EndCallReason, i as ListSessionsOptions, L as LoopRedirect, S as StartCallResult, T as TranscriptPayload, j as UsageSession, U as UsageSessionPage, V as VideoPolicy, t as isQueued } from './types-BgYFwlSn.js';
3
+ import 'zod';
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, isQueued, verifyTranscript } from './chunk-F7E4HMBW.js';
1
+ export { RealtimeAvatar, RealtimeAvatarError, RealtimeAvatarHttpError, clipLibraryDeclarationSchema, isQueued, verifyTranscript } from './chunk-5UEMZN6J.js';
@@ -1,5 +1,5 @@
1
- import { P as ProxyConfig } from './types-QoMHpR1h.js';
2
- import './types-D_aIKv4P.js';
1
+ import { P as ProxyConfig } from './types-zQtYeR_l.js';
2
+ import './types-BgYFwlSn.js';
3
3
 
4
4
  /**
5
5
  * TanStack Start. Mount at `routes/api/realtime-avatar/$.ts` — the trailing `$` is Start's
@@ -1,5 +1,5 @@
1
- import { createProxyHandler } from './chunk-FTAD423K.js';
2
- import './chunk-F7E4HMBW.js';
1
+ import { createProxyHandler } from './chunk-UJQBYKPI.js';
2
+ import './chunk-5UEMZN6J.js';
3
3
 
4
4
  // src/tanstack-start.ts
5
5
  function realtimeAvatarServerRoute(config) {
package/dist/tools.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * The browser tool plane, without React.
3
5
  *
@@ -13,6 +15,17 @@
13
15
  * minted with the `client_tools` capability, which your server decides. If registration
14
16
  * reports the method is missing, the mint did not grant it — the fix is server-side.
15
17
  */
18
+
19
+ declare function defineAvatarTool<I extends z.ZodType, O extends z.ZodType>(definition: {
20
+ description: string;
21
+ inputSchema: I;
22
+ outputSchema: O;
23
+ execute: (input: z.output<I>, context: ToolContext) => z.input<O> | Promise<z.input<O>>;
24
+ }): {
25
+ description: string;
26
+ parameters: z.core.ZodStandardJSONSchemaPayload<I>;
27
+ execute(input: unknown, context: ToolContext): Promise<z.output<O>>;
28
+ };
16
29
  /** Caps the wire enforces. Exceeding them is a rejection, not a truncation. */
17
30
  declare const MAX_TOOLS = 32;
18
31
  declare const MAX_MANIFEST_BYTES = 12000;
@@ -60,13 +73,16 @@ interface ToolRegistration {
60
73
  name: string;
61
74
  reason: string;
62
75
  }>;
76
+ dispose: () => void;
63
77
  }
64
78
  /** The subset of a LiveKit Room this needs — kept structural so it is trivial to fake. */
65
79
  interface RoomLike {
66
80
  localParticipant: {
67
81
  registerRpcMethod(method: string, handler: (data: {
68
82
  payload: string;
83
+ callerIdentity?: string;
69
84
  }) => Promise<string>): void;
85
+ unregisterRpcMethod?(method: string): void;
70
86
  performRpc(options: {
71
87
  destinationIdentity: string;
72
88
  method: string;
@@ -104,8 +120,15 @@ interface RoomLike {
104
120
  */
105
121
  declare function attachAvatarTools(room: RoomLike, tools: Record<string, RegisteredTool>, options?: {
106
122
  onInvoke?: (name: string, args: unknown) => void;
123
+ onResult?: (event: {
124
+ name: string;
125
+ callId: string;
126
+ ok: boolean;
127
+ error?: string;
128
+ }) => void;
129
+ signal?: AbortSignal;
107
130
  /** How long to keep waiting for the agent to arm its side. Default 8000ms. */
108
131
  timeoutMs?: number;
109
132
  }): Promise<ToolRegistration>;
110
133
 
111
- export { type AvatarTool, MAX_MANIFEST_BYTES, MAX_RESULT_CHARS, MAX_TOOLS, type RegisteredTool, TOOL_DEADLINE_MS, type ToolContext, type ToolRegistration, attachAvatarTools };
134
+ export { type AvatarTool, MAX_MANIFEST_BYTES, MAX_RESULT_CHARS, MAX_TOOLS, type RegisteredTool, TOOL_DEADLINE_MS, type ToolContext, type ToolRegistration, attachAvatarTools, defineAvatarTool };
package/dist/tools.js CHANGED
@@ -1,4 +1,28 @@
1
+ import { z } from 'zod';
2
+
1
3
  // ../tools/src/index.ts
4
+ function defineAvatarTool(definition) {
5
+ return {
6
+ description: definition.description,
7
+ parameters: z.toJSONSchema(definition.inputSchema, { io: "input" }),
8
+ async execute(input, context) {
9
+ context.signal.throwIfAborted();
10
+ const args = definition.inputSchema.parse(input);
11
+ const result = await definition.execute(args, context);
12
+ context.signal.throwIfAborted();
13
+ return definition.outputSchema.parse(result);
14
+ }
15
+ };
16
+ }
17
+ var invocationSchema = z.object({
18
+ name: z.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
19
+ args: z.string().max(8192),
20
+ call_id: z.string().min(1).max(128)
21
+ }).strict();
22
+ var registrationSchema = z.object({
23
+ accepted: z.array(z.string()),
24
+ rejected: z.array(z.object({ name: z.string(), reason: z.string() })).default([])
25
+ });
2
26
  var MAX_TOOLS = 32;
3
27
  var MAX_MANIFEST_BYTES = 12e3;
4
28
  var MAX_RESULT_CHARS = 8192;
@@ -52,23 +76,40 @@ async function attachAvatarTools(room, tools, options = {}) {
52
76
  `tool manifest is ${payload.length} bytes, over the ${MAX_MANIFEST_BYTES} limit \u2014 shorten descriptions or register fewer tools`
53
77
  );
54
78
  }
55
- room.localParticipant.registerRpcMethod(INVOKE_METHOD, async ({ payload: payload2 }) => {
79
+ options.signal?.throwIfAborted();
80
+ let disposed = false;
81
+ let activeCaller;
82
+ const inFlight = /* @__PURE__ */ new Set();
83
+ const dispose = () => {
84
+ if (disposed) return;
85
+ disposed = true;
86
+ room.localParticipant.unregisterRpcMethod?.(INVOKE_METHOD);
87
+ for (const call of inFlight) call.abort();
88
+ options.signal?.removeEventListener("abort", dispose);
89
+ };
90
+ options.signal?.addEventListener("abort", dispose, { once: true });
91
+ room.localParticipant.registerRpcMethod(INVOKE_METHOD, async ({ payload: payload2, callerIdentity }) => {
56
92
  let name = "", callId = "";
57
93
  try {
58
- const req = JSON.parse(payload2);
94
+ if (disposed) throw new Error("Tool registration was disposed");
95
+ if (callerIdentity && callerIdentity !== activeCaller) throw new Error("Unregistered tool caller");
96
+ const req = invocationSchema.parse(JSON.parse(payload2));
59
97
  name = req.name;
60
98
  callId = req.call_id;
61
99
  const tool = tools[name];
62
100
  if (!tool) return JSON.stringify({ ok: false, call_id: callId, error: `no tool named ${name}` });
63
- const args = req.args ? JSON.parse(req.args) : {};
101
+ const args = z.record(z.string(), z.unknown()).parse(req.args ? JSON.parse(req.args) : {});
64
102
  options.onInvoke?.(name, args);
65
103
  const run = tool.execute;
66
104
  const controller = new AbortController();
105
+ inFlight.add(controller);
67
106
  const timer = setTimeout(() => controller.abort(), TOOL_DEADLINE_MS);
68
107
  let result;
69
108
  try {
70
109
  result = await run(args, { signal: controller.signal, callId });
110
+ controller.signal.throwIfAborted();
71
111
  } finally {
112
+ inFlight.delete(controller);
72
113
  clearTimeout(timer);
73
114
  }
74
115
  const encoded = typeof result === "string" ? result : JSON.stringify(result ?? null);
@@ -79,8 +120,10 @@ async function attachAvatarTools(room, tools, options = {}) {
79
120
  error: `result was ${encoded.length} chars, over the ${MAX_RESULT_CHARS} limit`
80
121
  });
81
122
  }
123
+ options.onResult?.({ name, callId, ok: true });
82
124
  return JSON.stringify({ ok: true, call_id: callId, result: encoded });
83
125
  } catch (error) {
126
+ options.onResult?.({ name, callId, ok: false, error: error instanceof Error ? error.message : "tool failed" });
84
127
  return JSON.stringify({
85
128
  ok: false,
86
129
  call_id: callId,
@@ -91,31 +134,41 @@ async function attachAvatarTools(room, tools, options = {}) {
91
134
  const deadline = Date.now() + Math.max(0, options.timeoutMs ?? 8e3);
92
135
  let lastError = null;
93
136
  let sawCandidate = false;
94
- for (; ; ) {
95
- for (const identity of candidateIdentities(room)) {
96
- sawCandidate = true;
97
- try {
98
- const raw = await room.localParticipant.performRpc({
99
- destinationIdentity: identity,
100
- method: REGISTER_METHOD,
101
- payload
102
- });
103
- const result = JSON.parse(raw);
104
- return {
105
- accepted: result.accepted ?? [],
106
- rejected: [...rejected, ...result.rejected ?? []]
107
- };
108
- } catch (error) {
109
- lastError = error;
110
- if (!methodMissing(error)) throw error;
137
+ try {
138
+ for (; ; ) {
139
+ options.signal?.throwIfAborted();
140
+ for (const identity of candidateIdentities(room)) {
141
+ sawCandidate = true;
142
+ activeCaller = identity;
143
+ try {
144
+ const raw = await room.localParticipant.performRpc({
145
+ destinationIdentity: identity,
146
+ method: REGISTER_METHOD,
147
+ payload
148
+ });
149
+ options.signal?.throwIfAborted();
150
+ const result = registrationSchema.parse(JSON.parse(raw));
151
+ return {
152
+ accepted: result.accepted,
153
+ rejected: [...rejected, ...result.rejected],
154
+ dispose
155
+ };
156
+ } catch (error) {
157
+ activeCaller = void 0;
158
+ lastError = error;
159
+ if (!methodMissing(error)) throw error;
160
+ }
111
161
  }
162
+ if (Date.now() >= deadline) break;
163
+ await sleep(200);
112
164
  }
113
- if (Date.now() >= deadline) break;
114
- await sleep(200);
165
+ throw new Error(
166
+ sawCandidate ? `no participant accepted the tool manifest \u2014 either this session was not minted with the \`client_tools\` capability (a server-side grant), or the agent never armed registration within ${options.timeoutMs ?? 8e3}ms` + (lastError instanceof Error ? ` (last error: ${lastError.message})` : "") : "no remote participant joined the room before the timeout \u2014 nothing to register with"
167
+ );
168
+ } catch (error) {
169
+ dispose();
170
+ throw error;
115
171
  }
116
- throw new Error(
117
- sawCandidate ? `no participant accepted the tool manifest \u2014 either this session was not minted with the \`client_tools\` capability (a server-side grant), or the agent never armed registration within ${options.timeoutMs ?? 8e3}ms` + (lastError instanceof Error ? ` (last error: ${lastError.message})` : "") : "no remote participant joined the room before the timeout \u2014 nothing to register with"
118
- );
119
172
  }
120
173
 
121
- export { MAX_MANIFEST_BYTES, MAX_RESULT_CHARS, MAX_TOOLS, TOOL_DEADLINE_MS, attachAvatarTools };
174
+ export { MAX_MANIFEST_BYTES, MAX_RESULT_CHARS, MAX_TOOLS, TOOL_DEADLINE_MS, attachAvatarTools, defineAvatarTool };