realtime-avatar 0.9.1 → 0.11.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.
@@ -858,38 +858,74 @@ var qualityToSignal = (q) => {
858
858
  }
859
859
  };
860
860
  function useAvatarQualityGovernor(input) {
861
- const { enabled, freezeReading, config = DEFAULT_GOVERNOR_CONFIG, tickMs = 1e3 } = input;
861
+ const { enabled, freezeReading, config: policy = DEFAULT_GOVERNOR_CONFIG, tickMs = 1e3 } = input;
862
862
  const room = useMaybeRoomContext();
863
863
  const { videoTrack } = useVoiceAssistant();
864
- const pausedSinceTick = useRef(false);
865
- const connQuality = useRef("unknown");
866
- const lastFreezeStat = useRef(null);
867
- const jitterTrend = useRef(null);
864
+ const {
865
+ openingCap,
866
+ downgradeFreezeMs,
867
+ probationFreezeMs,
868
+ openingDwellMs,
869
+ dwellBaseMs,
870
+ dwellMaxMs,
871
+ cleanMs,
872
+ probeMs,
873
+ healthyResetMs
874
+ } = policy;
875
+ const config = useMemo(() => ({
876
+ openingCap,
877
+ downgradeFreezeMs,
878
+ probationFreezeMs,
879
+ openingDwellMs,
880
+ dwellBaseMs,
881
+ dwellMaxMs,
882
+ cleanMs,
883
+ probeMs,
884
+ healthyResetMs
885
+ }), [
886
+ openingCap,
887
+ downgradeFreezeMs,
888
+ probationFreezeMs,
889
+ openingDwellMs,
890
+ dwellBaseMs,
891
+ dwellMaxMs,
892
+ cleanMs,
893
+ probeMs,
894
+ healthyResetMs
895
+ ]);
896
+ const freezeReadingRef = useRef(freezeReading);
897
+ freezeReadingRef.current = freezeReading;
898
+ const targetPublication = videoTrack?.publication;
899
+ const targetParticipant = videoTrack?.participant;
900
+ const targetTrack = targetPublication?.track;
868
901
  useEffect(() => {
869
- if (!enabled || !room) return;
870
- const targetPublication = videoTrack?.publication;
871
- const targetParticipant = videoTrack?.participant;
872
- pausedSinceTick.current = false;
873
- lastFreezeStat.current = null;
874
- jitterTrend.current = null;
875
- connQuality.current = qualityToSignal(
902
+ if (!enabled || !room || !targetPublication || !targetParticipant) return;
903
+ let pausedSinceTick = false;
904
+ let lastFreezeStat = null;
905
+ let jitterTrend = null;
906
+ let connQuality = qualityToSignal(
876
907
  targetParticipant?.connectionQuality ?? ConnectionQuality.Unknown
877
908
  );
878
909
  const onStreamState = (pub, streamState) => {
879
- if (targetPublication && pub !== targetPublication) return;
880
- if (streamState === Track.StreamState.Paused) pausedSinceTick.current = true;
910
+ if (pub !== targetPublication) return;
911
+ if (streamState === Track.StreamState.Paused) pausedSinceTick = true;
881
912
  };
882
913
  const onQuality = (q, participant) => {
883
- if (targetParticipant && participant.sid !== targetParticipant.sid) return;
884
- connQuality.current = qualityToSignal(q);
914
+ if (participant.sid !== targetParticipant.sid) return;
915
+ connQuality = qualityToSignal(q);
885
916
  };
886
917
  try {
887
918
  room.on(RoomEvent.TrackStreamStateChanged, onStreamState);
888
919
  room.on(RoomEvent.ConnectionQualityChanged, onQuality);
889
920
  } catch {
921
+ try {
922
+ room.off(RoomEvent.TrackStreamStateChanged, onStreamState);
923
+ room.off(RoomEvent.ConnectionQualityChanged, onQuality);
924
+ } catch {
925
+ }
890
926
  return;
891
927
  }
892
- let gov = initGovernor(Date.now());
928
+ let gov = initGovernor(Date.now(), config.openingCap);
893
929
  const readGetStatsSignals = async () => {
894
930
  try {
895
931
  const track = targetPublication?.track;
@@ -908,13 +944,13 @@ function useAvatarQualityGovernor(input) {
908
944
  }
909
945
  });
910
946
  const now = Date.now();
911
- const prev = lastFreezeStat.current;
912
- lastFreezeStat.current = { frozen: frozenTotalMs, ts: now };
913
- const trend = stepJitterBufferTrend(jitterTrend.current, {
947
+ const prev = lastFreezeStat;
948
+ lastFreezeStat = { frozen: frozenTotalMs, ts: now };
949
+ const trend = stepJitterBufferTrend(jitterTrend, {
914
950
  delaySeconds: jitterDelaySeconds,
915
951
  emittedCount: jitterEmittedCount
916
952
  });
917
- jitterTrend.current = trend.state;
953
+ jitterTrend = trend.state;
918
954
  return {
919
955
  freezeMs: prev ? Math.max(0, frozenTotalMs - prev.frozen) : 0,
920
956
  jitterRising: trend.rising
@@ -938,17 +974,17 @@ function useAvatarQualityGovernor(input) {
938
974
  if (disposed || tickRunning) return;
939
975
  tickRunning = true;
940
976
  try {
941
- const rvfc = freezeReading?.() ?? { freezeMsInWindow: 0, inhibited: false };
977
+ const rvfc = freezeReadingRef.current?.() ?? { freezeMsInWindow: 0, inhibited: false };
942
978
  const statsSignals = await readGetStatsSignals();
943
979
  if (disposed) return;
944
980
  const signal = {
945
- paused: pausedSinceTick.current,
981
+ paused: pausedSinceTick,
946
982
  freezeMsInWindow: Math.max(rvfc.freezeMsInWindow, statsSignals.freezeMs),
947
983
  jitterRising: statsSignals.jitterRising,
948
- connectionQuality: connQuality.current,
984
+ connectionQuality: connQuality,
949
985
  inhibited: rvfc.inhibited
950
986
  };
951
- pausedSinceTick.current = false;
987
+ pausedSinceTick = false;
952
988
  const { governor, action } = step(gov, signal, Date.now(), config);
953
989
  gov = governor;
954
990
  if (action) applyCap(action.setCap);
@@ -968,7 +1004,7 @@ function useAvatarQualityGovernor(input) {
968
1004
  } catch {
969
1005
  }
970
1006
  };
971
- }, [enabled, room, videoTrack, freezeReading, config, tickMs]);
1007
+ }, [enabled, room, targetPublication, targetParticipant, targetTrack, config, tickMs]);
972
1008
  }
973
1009
 
974
1010
  // ../client/src/react/avatar-video-surface.ts
@@ -1297,8 +1333,7 @@ var sessionChoreographySchema = z.object({
1297
1333
  { message: "idle_dwell_min_seconds must be <= idle_dwell_max_seconds" }
1298
1334
  );
1299
1335
  var sessionBehaviorSchema = z.object({
1300
- gestures_enabled: z.boolean().optional(),
1301
- gesture_freq: z.enum(["sparse", "balanced", "lively"]).optional()
1336
+ gestures_enabled: z.boolean().optional()
1302
1337
  }).strict();
1303
1338
  var transcriptWebhookSchema = z.object({
1304
1339
  url: z.string().url().max(500),
@@ -2398,11 +2433,11 @@ function useRealtimeSession(input) {
2398
2433
  requestGracefulClose();
2399
2434
  lifecycle.reset();
2400
2435
  }, [lifecycle, requestGracefulClose]);
2401
- const performClip = useCallback(
2402
- (clipId, opts) => {
2436
+ const performAction = useCallback(
2437
+ (actionId, opts) => {
2403
2438
  const requestId = newTurnId();
2404
2439
  const publish = dataPublisherRef.current;
2405
- const trimmed = clipId.trim();
2440
+ const trimmed = actionId.trim();
2406
2441
  if (!publish || !trimmed) {
2407
2442
  const result = { requestId, accepted: false, reason: "not_connected" };
2408
2443
  cbRef.current.onClipResult?.(result);
@@ -2417,10 +2452,9 @@ function useRealtimeSession(input) {
2417
2452
  }, opts?.timeoutMs ?? 5e3);
2418
2453
  pendingClipsRef.current.set(requestId, { resolve, timer });
2419
2454
  publish({
2420
- kind: "clip_request",
2455
+ kind: "action_request",
2421
2456
  request_id: requestId,
2422
- clip_id: trimmed,
2423
- ...typeof opts?.holdSeconds === "number" ? { hold_seconds: opts.holdSeconds } : {}
2457
+ action_id: trimmed
2424
2458
  });
2425
2459
  });
2426
2460
  },
@@ -2465,7 +2499,7 @@ function useRealtimeSession(input) {
2465
2499
  retryTurn,
2466
2500
  end,
2467
2501
  behavior,
2468
- performClip,
2502
+ performAction,
2469
2503
  onLifecycleData,
2470
2504
  registerDataPublisher,
2471
2505
  registerTurnSender,
@@ -2487,7 +2521,7 @@ function useRealtimeSession(input) {
2487
2521
  retryTurn,
2488
2522
  end,
2489
2523
  behavior,
2490
- performClip,
2524
+ performAction,
2491
2525
  onLifecycleData,
2492
2526
  registerDataPublisher,
2493
2527
  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-CWvT16wz.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-CWvT16wz.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;
@@ -865,38 +1059,74 @@ var qualityToSignal = (q) => {
865
1059
  }
866
1060
  };
867
1061
  function useAvatarQualityGovernor(input) {
868
- const { enabled, freezeReading, config = DEFAULT_GOVERNOR_CONFIG, tickMs = 1e3 } = input;
1062
+ const { enabled, freezeReading, config: policy = DEFAULT_GOVERNOR_CONFIG, tickMs = 1e3 } = input;
869
1063
  const room = useMaybeRoomContext();
870
1064
  const { videoTrack } = useVoiceAssistant();
871
- const pausedSinceTick = useRef(false);
872
- const connQuality = useRef("unknown");
873
- const lastFreezeStat = useRef(null);
874
- const jitterTrend = useRef(null);
1065
+ const {
1066
+ openingCap,
1067
+ downgradeFreezeMs,
1068
+ probationFreezeMs,
1069
+ openingDwellMs,
1070
+ dwellBaseMs,
1071
+ dwellMaxMs,
1072
+ cleanMs,
1073
+ probeMs,
1074
+ healthyResetMs
1075
+ } = policy;
1076
+ const config = useMemo(() => ({
1077
+ openingCap,
1078
+ downgradeFreezeMs,
1079
+ probationFreezeMs,
1080
+ openingDwellMs,
1081
+ dwellBaseMs,
1082
+ dwellMaxMs,
1083
+ cleanMs,
1084
+ probeMs,
1085
+ healthyResetMs
1086
+ }), [
1087
+ openingCap,
1088
+ downgradeFreezeMs,
1089
+ probationFreezeMs,
1090
+ openingDwellMs,
1091
+ dwellBaseMs,
1092
+ dwellMaxMs,
1093
+ cleanMs,
1094
+ probeMs,
1095
+ healthyResetMs
1096
+ ]);
1097
+ const freezeReadingRef = useRef(freezeReading);
1098
+ freezeReadingRef.current = freezeReading;
1099
+ const targetPublication = videoTrack?.publication;
1100
+ const targetParticipant = videoTrack?.participant;
1101
+ const targetTrack = targetPublication?.track;
875
1102
  useEffect(() => {
876
- if (!enabled || !room) return;
877
- const targetPublication = videoTrack?.publication;
878
- const targetParticipant = videoTrack?.participant;
879
- pausedSinceTick.current = false;
880
- lastFreezeStat.current = null;
881
- jitterTrend.current = null;
882
- connQuality.current = qualityToSignal(
1103
+ if (!enabled || !room || !targetPublication || !targetParticipant) return;
1104
+ let pausedSinceTick = false;
1105
+ let lastFreezeStat = null;
1106
+ let jitterTrend = null;
1107
+ let connQuality = qualityToSignal(
883
1108
  targetParticipant?.connectionQuality ?? ConnectionQuality.Unknown
884
1109
  );
885
1110
  const onStreamState = (pub, streamState) => {
886
- if (targetPublication && pub !== targetPublication) return;
887
- if (streamState === Track.StreamState.Paused) pausedSinceTick.current = true;
1111
+ if (pub !== targetPublication) return;
1112
+ if (streamState === Track.StreamState.Paused) pausedSinceTick = true;
888
1113
  };
889
1114
  const onQuality = (q, participant) => {
890
- if (targetParticipant && participant.sid !== targetParticipant.sid) return;
891
- connQuality.current = qualityToSignal(q);
1115
+ if (participant.sid !== targetParticipant.sid) return;
1116
+ connQuality = qualityToSignal(q);
892
1117
  };
893
1118
  try {
894
1119
  room.on(RoomEvent.TrackStreamStateChanged, onStreamState);
895
1120
  room.on(RoomEvent.ConnectionQualityChanged, onQuality);
896
1121
  } catch {
1122
+ try {
1123
+ room.off(RoomEvent.TrackStreamStateChanged, onStreamState);
1124
+ room.off(RoomEvent.ConnectionQualityChanged, onQuality);
1125
+ } catch {
1126
+ }
897
1127
  return;
898
1128
  }
899
- let gov = initGovernor(Date.now());
1129
+ let gov = initGovernor(Date.now(), config.openingCap);
900
1130
  const readGetStatsSignals = async () => {
901
1131
  try {
902
1132
  const track = targetPublication?.track;
@@ -915,13 +1145,13 @@ function useAvatarQualityGovernor(input) {
915
1145
  }
916
1146
  });
917
1147
  const now = Date.now();
918
- const prev = lastFreezeStat.current;
919
- lastFreezeStat.current = { frozen: frozenTotalMs, ts: now };
920
- const trend = stepJitterBufferTrend(jitterTrend.current, {
1148
+ const prev = lastFreezeStat;
1149
+ lastFreezeStat = { frozen: frozenTotalMs, ts: now };
1150
+ const trend = stepJitterBufferTrend(jitterTrend, {
921
1151
  delaySeconds: jitterDelaySeconds,
922
1152
  emittedCount: jitterEmittedCount
923
1153
  });
924
- jitterTrend.current = trend.state;
1154
+ jitterTrend = trend.state;
925
1155
  return {
926
1156
  freezeMs: prev ? Math.max(0, frozenTotalMs - prev.frozen) : 0,
927
1157
  jitterRising: trend.rising
@@ -945,17 +1175,17 @@ function useAvatarQualityGovernor(input) {
945
1175
  if (disposed || tickRunning) return;
946
1176
  tickRunning = true;
947
1177
  try {
948
- const rvfc = freezeReading?.() ?? { freezeMsInWindow: 0, inhibited: false };
1178
+ const rvfc = freezeReadingRef.current?.() ?? { freezeMsInWindow: 0, inhibited: false };
949
1179
  const statsSignals = await readGetStatsSignals();
950
1180
  if (disposed) return;
951
1181
  const signal = {
952
- paused: pausedSinceTick.current,
1182
+ paused: pausedSinceTick,
953
1183
  freezeMsInWindow: Math.max(rvfc.freezeMsInWindow, statsSignals.freezeMs),
954
1184
  jitterRising: statsSignals.jitterRising,
955
- connectionQuality: connQuality.current,
1185
+ connectionQuality: connQuality,
956
1186
  inhibited: rvfc.inhibited
957
1187
  };
958
- pausedSinceTick.current = false;
1188
+ pausedSinceTick = false;
959
1189
  const { governor, action } = step(gov, signal, Date.now(), config);
960
1190
  gov = governor;
961
1191
  if (action) applyCap(action.setCap);
@@ -975,7 +1205,7 @@ function useAvatarQualityGovernor(input) {
975
1205
  } catch {
976
1206
  }
977
1207
  };
978
- }, [enabled, room, videoTrack, freezeReading, config, tickMs]);
1208
+ }, [enabled, room, targetPublication, targetParticipant, targetTrack, config, tickMs]);
979
1209
  }
980
1210
 
981
1211
  // ../client/src/react/frame-recovery.ts
@@ -1027,6 +1257,8 @@ function AvatarVideoSurface(props) {
1027
1257
  onPlayoutDelayChange
1028
1258
  } = props;
1029
1259
  const testId = props["data-testid"];
1260
+ const visibilityCallback = useRef(props.onLiveVisibilityChange);
1261
+ visibilityCallback.current = props.onLiveVisibilityChange;
1030
1262
  const { videoTrack, audioTrack } = useVoiceAssistant();
1031
1263
  useAvatarPlayoutDelay(videoTrack, audioTrack);
1032
1264
  const playoutDelaySeconds = useAvatarAdaptivePlayoutDelay(
@@ -1069,6 +1301,9 @@ function AvatarVideoSurface(props) {
1069
1301
  liveWanted,
1070
1302
  connected && !networkStalled ? idleReturnDelayMs : 0
1071
1303
  );
1304
+ useEffect(() => {
1305
+ visibilityCallback.current?.(showLive);
1306
+ }, [showLive]);
1072
1307
  const liveDims = videoTrack?.publication?.dimensions ?? null;
1073
1308
  const idleVideoRef = useRef(null);
1074
1309
  const boxAspect = aspectRatio ?? null;
@@ -1572,8 +1807,7 @@ var sessionChoreographySchema = z.object({
1572
1807
  { message: "idle_dwell_min_seconds must be <= idle_dwell_max_seconds" }
1573
1808
  );
1574
1809
  var sessionBehaviorSchema = z.object({
1575
- gestures_enabled: z.boolean().optional(),
1576
- gesture_freq: z.enum(["sparse", "balanced", "lively"]).optional()
1810
+ gestures_enabled: z.boolean().optional()
1577
1811
  }).strict();
1578
1812
  var transcriptWebhookSchema = z.object({
1579
1813
  url: z.string().url().max(500),
@@ -2678,11 +2912,11 @@ function useRealtimeSession(input) {
2678
2912
  requestGracefulClose();
2679
2913
  lifecycle.reset();
2680
2914
  }, [lifecycle, requestGracefulClose]);
2681
- const performClip = useCallback(
2682
- (clipId, opts) => {
2915
+ const performAction = useCallback(
2916
+ (actionId, opts) => {
2683
2917
  const requestId = newTurnId();
2684
2918
  const publish = dataPublisherRef.current;
2685
- const trimmed = clipId.trim();
2919
+ const trimmed = actionId.trim();
2686
2920
  if (!publish || !trimmed) {
2687
2921
  const result = { requestId, accepted: false, reason: "not_connected" };
2688
2922
  cbRef.current.onClipResult?.(result);
@@ -2697,10 +2931,9 @@ function useRealtimeSession(input) {
2697
2931
  }, opts?.timeoutMs ?? 5e3);
2698
2932
  pendingClipsRef.current.set(requestId, { resolve, timer });
2699
2933
  publish({
2700
- kind: "clip_request",
2934
+ kind: "action_request",
2701
2935
  request_id: requestId,
2702
- clip_id: trimmed,
2703
- ...typeof opts?.holdSeconds === "number" ? { hold_seconds: opts.holdSeconds } : {}
2936
+ action_id: trimmed
2704
2937
  });
2705
2938
  });
2706
2939
  },
@@ -2745,7 +2978,7 @@ function useRealtimeSession(input) {
2745
2978
  retryTurn,
2746
2979
  end,
2747
2980
  behavior,
2748
- performClip,
2981
+ performAction,
2749
2982
  onLifecycleData,
2750
2983
  registerDataPublisher,
2751
2984
  registerTurnSender,
@@ -2767,7 +3000,7 @@ function useRealtimeSession(input) {
2767
3000
  retryTurn,
2768
3001
  end,
2769
3002
  behavior,
2770
- performClip,
3003
+ performAction,
2771
3004
  onLifecycleData,
2772
3005
  registerDataPublisher,
2773
3006
  registerTurnSender,
@@ -2931,4 +3164,4 @@ function createProxyClient(options) {
2931
3164
  };
2932
3165
  }
2933
3166
 
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 };
3167
+ 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-FoJMJimV.js';
3
+ import 'zod';