realtime-avatar 0.9.0 → 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-DDqYRz-_.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-DDqYRz-_.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;
@@ -978,6 +1172,33 @@ function useAvatarQualityGovernor(input) {
978
1172
  }, [enabled, room, videoTrack, freezeReading, config, tickMs]);
979
1173
  }
980
1174
 
1175
+ // ../client/src/react/frame-recovery.ts
1176
+ var FrameRecovery = class {
1177
+ constructor(stallAfterMs) {
1178
+ this.stallAfterMs = stallAfterMs;
1179
+ }
1180
+ stallAfterMs;
1181
+ lastFrameAtMs = null;
1182
+ stableSinceMs = null;
1183
+ ready = true;
1184
+ stall() {
1185
+ this.ready = false;
1186
+ this.stableSinceMs = null;
1187
+ }
1188
+ frame(nowMs) {
1189
+ const gapMs = this.lastFrameAtMs === null ? 0 : nowMs - this.lastFrameAtMs;
1190
+ if (this.lastFrameAtMs !== null && gapMs > this.stallAfterMs) this.stall();
1191
+ if (!this.ready) {
1192
+ if (this.stableSinceMs === null || gapMs > Math.min(350, this.stallAfterMs)) {
1193
+ this.stableSinceMs = nowMs;
1194
+ }
1195
+ this.ready = nowMs - this.stableSinceMs >= 500;
1196
+ }
1197
+ this.lastFrameAtMs = nowMs;
1198
+ return this.ready;
1199
+ }
1200
+ };
1201
+
981
1202
  // ../client/src/react/avatar-video-surface.ts
982
1203
  var DEFAULT_AVATAR_FRAME_STALL_MS = 800;
983
1204
  var AVATAR_FRAME_GAP_FREEZE_FLOOR_MS = 100;
@@ -1000,6 +1221,8 @@ function AvatarVideoSurface(props) {
1000
1221
  onPlayoutDelayChange
1001
1222
  } = props;
1002
1223
  const testId = props["data-testid"];
1224
+ const visibilityCallback = useRef(props.onLiveVisibilityChange);
1225
+ visibilityCallback.current = props.onLiveVisibilityChange;
1003
1226
  const { videoTrack, audioTrack } = useVoiceAssistant();
1004
1227
  useAvatarPlayoutDelay(videoTrack, audioTrack);
1005
1228
  const playoutDelaySeconds = useAvatarAdaptivePlayoutDelay(
@@ -1042,10 +1265,18 @@ function AvatarVideoSurface(props) {
1042
1265
  liveWanted,
1043
1266
  connected && !networkStalled ? idleReturnDelayMs : 0
1044
1267
  );
1268
+ useEffect(() => {
1269
+ visibilityCallback.current?.(showLive);
1270
+ }, [showLive]);
1045
1271
  const liveDims = videoTrack?.publication?.dimensions ?? null;
1046
1272
  const idleVideoRef = useRef(null);
1047
1273
  const boxAspect = aspectRatio ?? null;
1048
- useIdleWhileResting(idleVideoRef, idleVideoUrl, showLive);
1274
+ useIdleWhileResting(
1275
+ idleVideoRef,
1276
+ idleVideoUrl,
1277
+ showLive,
1278
+ !live || connectionState === ConnectionState.Disconnected
1279
+ );
1049
1280
  useLiveResumeOnProducing(liveWrapRef, trackProducing);
1050
1281
  const fitClass = fit === "cover" ? AVATAR_VIDEO_FIT_COVER : AVATAR_VIDEO_FIT_CONTAIN;
1051
1282
  const liveLabel = showLive && liveDims && liveDims.width > 0 && liveDims.height > 0 ? `${liveDims.width}\xD7${liveDims.height}` : null;
@@ -1177,6 +1408,7 @@ function isFrameFreezeInhibited(snapshot) {
1177
1408
  }
1178
1409
  function useLiveFrameFlow(wrapRef, trackProducing, trackIdentity, stallAfterMs) {
1179
1410
  const boundedStallMs = normalizeFrameStallMs(stallAfterMs);
1411
+ const recovery = useMemo(() => new FrameRecovery(boundedStallMs), [trackIdentity, boundedStallMs]);
1180
1412
  const [flowing, setFlowing] = useState(false);
1181
1413
  const [seenFrame, setSeenFrame] = useState(false);
1182
1414
  const flowingRef = useRef(false);
@@ -1206,6 +1438,7 @@ function useLiveFrameFlow(wrapRef, trackProducing, trackIdentity, stallAfterMs)
1206
1438
  const markFrame = () => {
1207
1439
  const previous = sampleRef.current;
1208
1440
  const now = Date.now();
1441
+ lastCurrentTime = video?.currentTime ?? lastCurrentTime;
1209
1442
  const firstFrame = !previous.seenFrame;
1210
1443
  sampleRef.current = {
1211
1444
  seenFrame: true,
@@ -1219,9 +1452,10 @@ function useLiveFrameFlow(wrapRef, trackProducing, trackIdentity, stallAfterMs)
1219
1452
  resumePending: false
1220
1453
  };
1221
1454
  if (firstFrame) setSeenFrame(true);
1222
- if (!flowingRef.current) {
1223
- flowingRef.current = true;
1224
- setFlowing(true);
1455
+ const nextFlowing = recovery.frame(now);
1456
+ if (flowingRef.current !== nextFlowing) {
1457
+ flowingRef.current = nextFlowing;
1458
+ setFlowing(nextFlowing);
1225
1459
  }
1226
1460
  };
1227
1461
  const cancelFrameCallback = () => {
@@ -1263,12 +1497,14 @@ function useLiveFrameFlow(wrapRef, trackProducing, trackIdentity, stallAfterMs)
1263
1497
  lastCurrentTime = video.currentTime;
1264
1498
  markFrame();
1265
1499
  }
1266
- const nextFlowing = isFrameFlowingAt({
1500
+ const frameFresh = isFrameFlowingAt({
1267
1501
  seenFrame: sampleRef.current.seenFrame,
1268
1502
  lastFrameAtMs: sampleRef.current.lastFrameAtMs,
1269
1503
  nowMs: Date.now(),
1270
1504
  stallAfterMs: boundedStallMs
1271
1505
  });
1506
+ if (!frameFresh && sampleRef.current.seenFrame) recovery.stall();
1507
+ const nextFlowing = frameFresh && recovery.ready;
1272
1508
  if (flowingRef.current !== nextFlowing) {
1273
1509
  flowingRef.current = nextFlowing;
1274
1510
  setFlowing(nextFlowing);
@@ -1281,7 +1517,7 @@ function useLiveFrameFlow(wrapRef, trackProducing, trackIdentity, stallAfterMs)
1281
1517
  stopObserving();
1282
1518
  cancelFrameCallback();
1283
1519
  };
1284
- }, [wrapRef, trackProducing, trackIdentity, boundedStallMs]);
1520
+ }, [wrapRef, trackProducing, trackIdentity, boundedStallMs, recovery]);
1285
1521
  const freezeReading = useCallback(() => {
1286
1522
  const sample = sampleRef.current;
1287
1523
  const lastFrameAtMs = sample.lastFrameAtMs;
@@ -1320,19 +1556,19 @@ function useDebouncedHide(wanted, delayMs) {
1320
1556
  }, [wanted, delayMs]);
1321
1557
  return shown;
1322
1558
  }
1323
- function useIdleWhileResting(videoRef, idleVideoUrl, showLive) {
1559
+ function useIdleWhileResting(videoRef, idleVideoUrl, showLive, resetToAnchor) {
1324
1560
  useEffect(() => {
1325
1561
  const video = videoRef.current;
1326
1562
  if (!video || !idleVideoUrl) return;
1327
- applyIdleRest(video, showLive);
1328
- }, [videoRef, idleVideoUrl, showLive]);
1563
+ applyIdleRest(video, showLive, resetToAnchor);
1564
+ }, [videoRef, idleVideoUrl, showLive, resetToAnchor]);
1329
1565
  }
1330
- function applyIdleRest(video, showLive) {
1566
+ function applyIdleRest(video, showLive, resetToAnchor = true) {
1331
1567
  if (showLive) {
1332
1568
  video.pause();
1333
1569
  return;
1334
1570
  }
1335
- if (video.currentTime !== 0) video.currentTime = 0;
1571
+ if (resetToAnchor && video.currentTime !== 0) video.currentTime = 0;
1336
1572
  if (video.paused) void Promise.resolve(video.play()).catch(() => {
1337
1573
  });
1338
1574
  }
@@ -2641,11 +2877,11 @@ function useRealtimeSession(input) {
2641
2877
  requestGracefulClose();
2642
2878
  lifecycle.reset();
2643
2879
  }, [lifecycle, requestGracefulClose]);
2644
- const performClip = useCallback(
2645
- (clipId, opts) => {
2880
+ const performAction = useCallback(
2881
+ (actionId, opts) => {
2646
2882
  const requestId = newTurnId();
2647
2883
  const publish = dataPublisherRef.current;
2648
- const trimmed = clipId.trim();
2884
+ const trimmed = actionId.trim();
2649
2885
  if (!publish || !trimmed) {
2650
2886
  const result = { requestId, accepted: false, reason: "not_connected" };
2651
2887
  cbRef.current.onClipResult?.(result);
@@ -2660,10 +2896,9 @@ function useRealtimeSession(input) {
2660
2896
  }, opts?.timeoutMs ?? 5e3);
2661
2897
  pendingClipsRef.current.set(requestId, { resolve, timer });
2662
2898
  publish({
2663
- kind: "clip_request",
2899
+ kind: "action_request",
2664
2900
  request_id: requestId,
2665
- clip_id: trimmed,
2666
- ...typeof opts?.holdSeconds === "number" ? { hold_seconds: opts.holdSeconds } : {}
2901
+ action_id: trimmed
2667
2902
  });
2668
2903
  });
2669
2904
  },
@@ -2708,7 +2943,7 @@ function useRealtimeSession(input) {
2708
2943
  retryTurn,
2709
2944
  end,
2710
2945
  behavior,
2711
- performClip,
2946
+ performAction,
2712
2947
  onLifecycleData,
2713
2948
  registerDataPublisher,
2714
2949
  registerTurnSender,
@@ -2730,7 +2965,7 @@ function useRealtimeSession(input) {
2730
2965
  retryTurn,
2731
2966
  end,
2732
2967
  behavior,
2733
- performClip,
2968
+ performAction,
2734
2969
  onLifecycleData,
2735
2970
  registerDataPublisher,
2736
2971
  registerTurnSender,
@@ -2894,4 +3129,4 @@ function createProxyClient(options) {
2894
3129
  };
2895
3130
  }
2896
3131
 
2897
- 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-IQUJLPYF.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-A2ATKWJT.js';
2
- import './chunk-IQUJLPYF.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 };