@animated-waffle/client 0.3.4 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ - Breaking: replace session overrides `persona` and `systemPrompt` with a complete
6
+ `characterCard`. `WaffleSessionClient` implementations must expose the new
7
+ microphone/input controls and return promises for push-to-talk controls.
8
+ - Add media clients for backend-authorized application sessions, with a versioned
9
+ capability handshake. Tools and context stay on the separate backend connection.
10
+ - Expose live input-mode switching, microphone control, and interruption.
11
+ - Return promises for queued push-to-talk controls and report control failures.
12
+
13
+ ## 0.3.5
14
+
15
+ - Add lazy, creator-BFF-authorized private Developer Feedback for persisted
16
+ Agent Sessions and Assistant messages.
17
+ - Expose the server-authored Agent Studio link and named Avatar presentation
18
+ profile from managed Sessions.
19
+
3
20
  ## 0.3.4
4
21
 
5
22
  - Classify public failures with `WaffleError.stage`; connection failures reject
package/README.md CHANGED
@@ -65,6 +65,41 @@ await waffle.connect()
65
65
  `endUserId` is one stable bare UUID per end user. An anonymous application can
66
66
  generate it once with `crypto.randomUUID()` and persist it on that device.
67
67
 
68
+ ## Creator Developer Feedback
69
+
70
+ Developer Feedback is opt-in and unavailable unless the application supplies
71
+ a callback to its own creator-authenticated backend. The SDK invokes it lazily
72
+ when feedback is opened or saved, after the API has returned the persisted
73
+ source Session id:
74
+
75
+ ```ts
76
+ const waffle = new WaffleClient({
77
+ agentId,
78
+ sessionToken: () => getSessionToken(agentId),
79
+ developerFeedback: ({ agentId, sourceSessionId, signal }) =>
80
+ fetch('/api/narya-developer-feedback-grant', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ agentId, sourceSessionId }),
84
+ signal,
85
+ }),
86
+ })
87
+ ```
88
+
89
+ That backend response uses the public grant shape
90
+ `{ endpoint, token, expires_at, source_session_id }`. It must be protected as a
91
+ creator development route and mint the grant with a server-side workspace key;
92
+ never expose that key to the browser. The feedback endpoint receives only the
93
+ five-minute grant with `credentials: 'omit'`. It cannot use the ordinary Agent
94
+ session token. Non-loopback SDK development origins must be included in the
95
+ API's configured origin allowlist.
96
+
97
+ `loadDeveloperFeedback()` reads the current private draft.
98
+ `submitDeveloperFeedback()` appends one Preserve/Change observation for the
99
+ whole Session or a persisted Assistant message. It never submits the round,
100
+ generates a revision, or publishes the Agent. Reuse an explicit feedback `id`
101
+ when retrying after an ambiguous network failure.
102
+
68
103
  To target a non-production environment:
69
104
 
70
105
  ```ts
@@ -86,6 +121,10 @@ const waffle = new WaffleClient({
86
121
  integration family for observability only. The SDK defaults to `SDK`.
87
122
  - `connect({ inputMode?, sessionId?, overrides? })` — `'push-to-talk'`
88
123
  (default) or `'automatic'`; resolves only when the Agent runtime is ready.
124
+ - `session.agentStudioUrl?` — server-authored dashboard target for the exact
125
+ Agent and source Session. Development UI may show “Open Agent in Studio”
126
+ when present; do not rebuild it from `avatarId`. Older protocol-v2 servers
127
+ may omit it during rolling upgrades.
89
128
  - `inputAudioEnabled: false` — never requests or enables the browser microphone;
90
129
  text input, Agent audio, transcripts, and reconnects continue to work. Push-
91
130
  to-talk controls fail closed with `input_audio_disabled`. The default is
@@ -99,6 +138,8 @@ const waffle = new WaffleClient({
99
138
  - `disconnect()`, `sendText(text, { audioResponse? })`. `audioResponse` defaults
100
139
  to `true`; `false` keeps the typed turn and transcript but suppresses reply
101
140
  audio for that turn.
141
+ - `loadDeveloperFeedback()`, `submitDeveloperFeedback(input)` — available only
142
+ with the explicit creator BFF callback above.
102
143
  - `startPushToTalk()` / `commitPushToTalk()` / `cancelPushToTalk()`.
103
144
  - `on('connectionState' | 'session' | 'remoteAudioTrack' | 'transcriptProgress' | 'transcript' |
104
145
  'agentSpeaking' | 'declined' | 'error', fn)` — `declined` carries the
@@ -120,3 +161,11 @@ not part of the public API and may change without notice.
120
161
 
121
162
  See the [SDK documentation](https://animated-waffle.narya.ai/docs/sdk) for the
122
163
  complete integration guide.
164
+
165
+ ## Application sessions
166
+
167
+ For an application-owned conversation, use `application: true` with a
168
+ `startSession` callback returning the media session created by
169
+ `WaffleServer.startApplicationSession`. The backend retains the separate control
170
+ grant; the browser never executes tools or forwards privileged callbacks.
171
+ See the [application-session contract](../../docs/public/sdk.md#application-owned-tools-and-conversation-state).
package/dist/index.d.ts CHANGED
@@ -1,12 +1,112 @@
1
+ import { z } from 'zod';
2
+
3
+ declare const characterCardSchema: z.ZodObject<{
4
+ spec: z.ZodLiteral<"chara_card_v3">;
5
+ spec_version: z.ZodLiteral<"3.0">;
6
+ data: z.ZodObject<{
7
+ name: z.ZodString;
8
+ description: z.ZodString;
9
+ personality: z.ZodString;
10
+ scenario: z.ZodString;
11
+ first_mes: z.ZodString;
12
+ mes_example: z.ZodString;
13
+ creator_notes: z.ZodString;
14
+ system_prompt: z.ZodString;
15
+ post_history_instructions: z.ZodString;
16
+ alternate_greetings: z.ZodArray<z.ZodString>;
17
+ tags: z.ZodArray<z.ZodString>;
18
+ creator: z.ZodString;
19
+ character_version: z.ZodString;
20
+ extensions: z.ZodRecord<z.ZodString, z.ZodJSONSchema>;
21
+ group_only_greetings: z.ZodArray<z.ZodString>;
22
+ nickname: z.ZodOptional<z.ZodString>;
23
+ creator_notes_multilingual: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
24
+ source: z.ZodOptional<z.ZodArray<z.ZodString>>;
25
+ creation_date: z.ZodOptional<z.ZodNumber>;
26
+ modification_date: z.ZodOptional<z.ZodNumber>;
27
+ assets: z.ZodOptional<z.ZodArray<z.ZodObject<{
28
+ type: z.ZodString;
29
+ uri: z.ZodString;
30
+ name: z.ZodString;
31
+ ext: z.ZodString;
32
+ }, z.core.$catchall<z.ZodJSONSchema>>>>;
33
+ character_book: z.ZodOptional<z.ZodObject<{
34
+ name: z.ZodOptional<z.ZodString>;
35
+ description: z.ZodOptional<z.ZodString>;
36
+ scan_depth: z.ZodOptional<z.ZodNumber>;
37
+ token_budget: z.ZodOptional<z.ZodNumber>;
38
+ recursive_scanning: z.ZodOptional<z.ZodBoolean>;
39
+ extensions: z.ZodRecord<z.ZodString, z.ZodJSONSchema>;
40
+ entries: z.ZodArray<z.ZodObject<{
41
+ keys: z.ZodArray<z.ZodString>;
42
+ content: z.ZodString;
43
+ extensions: z.ZodRecord<z.ZodString, z.ZodJSONSchema>;
44
+ enabled: z.ZodBoolean;
45
+ insertion_order: z.ZodNumber;
46
+ use_regex: z.ZodBoolean;
47
+ case_sensitive: z.ZodOptional<z.ZodBoolean>;
48
+ name: z.ZodOptional<z.ZodString>;
49
+ priority: z.ZodOptional<z.ZodNumber>;
50
+ id: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
51
+ comment: z.ZodOptional<z.ZodString>;
52
+ selective: z.ZodOptional<z.ZodBoolean>;
53
+ secondary_keys: z.ZodOptional<z.ZodArray<z.ZodString>>;
54
+ constant: z.ZodOptional<z.ZodBoolean>;
55
+ position: z.ZodOptional<z.ZodEnum<{
56
+ before_char: "before_char";
57
+ after_char: "after_char";
58
+ }>>;
59
+ }, z.core.$catchall<z.ZodJSONSchema>>>;
60
+ }, z.core.$catchall<z.ZodJSONSchema>>>;
61
+ }, z.core.$catchall<z.ZodJSONSchema>>;
62
+ }, z.core.$catchall<z.ZodJSONSchema>>;
63
+ type CharacterCard = z.infer<typeof characterCardSchema>;
64
+
1
65
  type WaffleConnectionState = 'disconnected' | 'connecting' | 'connected' | 'disconnecting';
2
66
  /** @deprecated Use WaffleConnectionState. */
3
67
  type WaffleStatus = WaffleConnectionState;
4
68
  type WaffleInputMode = 'automatic' | 'push-to-talk';
69
+ type WaffleDeveloperFeedbackAction = 'preserve' | 'change';
70
+ type WaffleDeveloperFeedbackDimension = 'persona' | 'instructions' | 'memory' | 'avatar' | 'voice' | 'tool_runtime' | 'unknown';
71
+ interface WaffleDeveloperFeedbackInput {
72
+ /** Reuse this UUID to retry the same append safely after an ambiguous failure. */
73
+ id?: string;
74
+ scope: 'session' | 'message';
75
+ targetMessageId?: string | null;
76
+ action: WaffleDeveloperFeedbackAction;
77
+ dimension: WaffleDeveloperFeedbackDimension;
78
+ note?: string | null;
79
+ preferredReply?: string | null;
80
+ }
81
+ interface WaffleDeveloperFeedbackState {
82
+ roundId: string | null;
83
+ pendingCount: number;
84
+ /** Server-authored continuation link. The SDK never constructs this URL. */
85
+ dashboardUrl: string | null;
86
+ }
87
+ interface WaffleDeveloperFeedbackAvailability {
88
+ sourceSessionId: string;
89
+ }
90
+ interface WaffleDeveloperFeedbackGrant {
91
+ endpoint: string;
92
+ token: string;
93
+ expiresAt: string;
94
+ sourceSessionId: string;
95
+ }
96
+ interface WaffleDeveloperFeedbackGrantRequest {
97
+ agentId: string;
98
+ sourceSessionId: string;
99
+ signal: AbortSignal;
100
+ }
101
+ type WaffleDeveloperFeedbackGrantProvider = (input: WaffleDeveloperFeedbackGrantRequest) => Response | Promise<Response>;
5
102
  interface WaffleSession {
6
103
  id: string;
7
104
  agentId: string;
105
+ /** Server-authored management target for this exact Agent and source session. */
106
+ agentStudioUrl?: string;
8
107
  avatarId?: string;
9
108
  inputMode: WaffleInputMode;
109
+ developerFeedback?: WaffleDeveloperFeedbackAvailability;
10
110
  }
11
111
  interface WaffleTranscript {
12
112
  /** The persisted conversation message id. */
@@ -51,13 +151,12 @@ interface WaffleDecline {
51
151
  }
52
152
  type WaffleSessionToken = () => string | Promise<string>;
53
153
  interface WaffleSessionOverrides {
54
- persona?: string;
154
+ characterCard?: CharacterCard;
55
155
  llmProvider?: string | null;
56
156
  llmModel?: string | null;
57
157
  thinkingLevel?: string;
58
158
  webSearchEnabled?: boolean;
59
159
  memoryEnabled?: boolean;
60
- systemPrompt?: string;
61
160
  asrProvider?: string | null;
62
161
  ttsProvider?: string;
63
162
  ttsModelId?: string;
@@ -69,6 +168,8 @@ interface WaffleSessionOverrides {
69
168
  avatarEnabled?: boolean;
70
169
  }
71
170
  interface WaffleClientBaseOptions {
171
+ /** Require an application session whose tools and context are controlled by the backend. */
172
+ application?: boolean;
72
173
  /** The published Agent to connect to. */
73
174
  agentId: string;
74
175
  /** Disable browser microphone access for text-triggered, output-only sessions. */
@@ -82,6 +183,11 @@ interface WaffleClientBaseOptions {
82
183
  audioPlayback?: boolean;
83
184
  /** Self-reported integration family for observability only. Defaults to SDK. */
84
185
  reportedSource?: 'Web' | 'SDK' | 'Client' | 'Unknown';
186
+ /**
187
+ * Ask a creator-authenticated backend for one short-lived, exact-Session
188
+ * feedback grant. The SDK calls this lazily and never handles an API key.
189
+ */
190
+ developerFeedback?: WaffleDeveloperFeedbackGrantProvider;
85
191
  }
86
192
  interface WaffleTokenClientOptions extends WaffleClientBaseOptions {
87
193
  /** A short-lived token minted for this Agent by the Narya API. */
@@ -135,6 +241,7 @@ interface WaffleSessionClient {
135
241
  readonly status: WaffleStatus;
136
242
  readonly session: WaffleSession | null;
137
243
  readonly remoteAudioTrack: MediaStreamTrack | null;
244
+ readonly microphoneTrack: MediaStreamTrack | null;
138
245
  /**
139
246
  * Silence the Agent's voice while it keeps performing: the session, the
140
247
  * transcript and the Avatar's cues carry on, only playback goes quiet. Has
@@ -145,17 +252,30 @@ interface WaffleSessionClient {
145
252
  on<K extends keyof WaffleClientEvents>(event: K, listener: (payload: WaffleClientEvents[K]) => void): () => void;
146
253
  connect(options?: WaffleSessionStartOptions): Promise<WaffleSession>;
147
254
  disconnect(): Promise<void>;
255
+ loadDeveloperFeedback(): Promise<WaffleDeveloperFeedbackState>;
256
+ submitDeveloperFeedback(input: WaffleDeveloperFeedbackInput): Promise<WaffleDeveloperFeedbackState>;
148
257
  sendText(text: string, options?: WaffleSendTextOptions): Promise<void>;
149
- startPushToTalk(): void;
150
- commitPushToTalk(): void;
151
- cancelPushToTalk(): void;
258
+ startPushToTalk(): Promise<void>;
259
+ commitPushToTalk(): Promise<void>;
260
+ cancelPushToTalk(): Promise<void>;
261
+ setInputMode(mode: WaffleInputMode): Promise<void>;
262
+ interrupt(): Promise<void>;
263
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
152
264
  }
153
265
  type Listener<K extends keyof WaffleClientEvents> = (payload: WaffleClientEvents[K]) => void;
154
266
  type WaffleErrorStage = 'session' | 'transport' | 'audio' | 'transcript' | 'avatar-load' | 'avatar-renderer';
267
+ type PushToTalkControlMethod = 'start_turn' | 'end_turn' | 'cancel_turn';
155
268
  declare class WaffleError extends Error {
156
269
  readonly status?: number | undefined;
157
270
  readonly code?: string | undefined;
158
271
  readonly stage: WaffleErrorStage;
272
+ /**
273
+ * Set only on push-to-talk control-RPC failures (NAR-2321). Lets the UI
274
+ * telemetry attribute the failure to the exact control method and LiveKit
275
+ * error code without leaking those raw values into user-facing copy.
276
+ */
277
+ rpcMethod?: PushToTalkControlMethod;
278
+ livekitErrorCode?: number;
159
279
  constructor(message: string, status?: number | undefined, code?: string | undefined, stage?: WaffleErrorStage, options?: ErrorOptions);
160
280
  }
161
281
  declare class WaffleClient implements WaffleSessionClient {
@@ -181,8 +301,12 @@ declare class WaffleClient implements WaffleSessionClient {
181
301
  private turnId;
182
302
  private nextTurnId;
183
303
  private turnControl;
304
+ private failedStartTurnKey;
184
305
  private lifecycleVersion;
185
306
  private recreateRoomOnConnect;
307
+ private developerFeedbackGrant;
308
+ private developerFeedbackGrantPromise;
309
+ private developerFeedbackGrantAbort;
186
310
  constructor(options: WaffleClientOptions);
187
311
  private createRoom;
188
312
  get status(): WaffleStatus;
@@ -195,16 +319,27 @@ declare class WaffleClient implements WaffleSessionClient {
195
319
  connect(options?: WaffleConnectOptions): Promise<WaffleSession>;
196
320
  disconnect(): Promise<void>;
197
321
  sendText(text: string, options?: WaffleSendTextOptions): Promise<void>;
198
- startPushToTalk(): void;
199
- commitPushToTalk(): void;
322
+ /** Switch turn detection without replacing the conversation. */
323
+ setInputMode(mode: WaffleInputMode): Promise<void>;
324
+ interrupt(): Promise<void>;
325
+ get microphoneTrack(): MediaStreamTrack | null;
326
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
327
+ private applicationControl;
328
+ loadDeveloperFeedback(): Promise<WaffleDeveloperFeedbackState>;
329
+ submitDeveloperFeedback(input: WaffleDeveloperFeedbackInput): Promise<WaffleDeveloperFeedbackState>;
330
+ startPushToTalk(): Promise<void>;
331
+ commitPushToTalk(): Promise<void>;
200
332
  /** @deprecated Use commitPushToTalk(). */
201
333
  stopPushToTalk(): void;
202
- cancelPushToTalk(): void;
334
+ cancelPushToTalk(): Promise<void>;
203
335
  private startInternal;
204
336
  private stopInternal;
205
337
  private sendInputControl;
338
+ private performControlRpc;
206
339
  private assertPushToTalkAvailable;
207
340
  private assertConnected;
341
+ private requestDeveloperFeedback;
342
+ private resolveDeveloperFeedbackGrant;
208
343
  private handleParticipant;
209
344
  private handleDisconnected;
210
345
  private setAudioTrack;
@@ -212,10 +347,11 @@ declare class WaffleClient implements WaffleSessionClient {
212
347
  private completeTranscriptExecution;
213
348
  private clearTranscriptProgress;
214
349
  private setStatus;
350
+ private clearDeveloperFeedbackGrant;
215
351
  private fail;
216
352
  private resolveSessionToken;
217
353
  private withTimeout;
218
354
  private emit;
219
355
  }
220
356
 
221
- export { WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, type WaffleDecline, WaffleError, type WaffleErrorStage, type WaffleInputMode, type WaffleSendTextOptions, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleStatus, type WaffleTranscript, type WaffleTranscriptProgress };
357
+ export { type CharacterCard, type PushToTalkControlMethod, WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, type WaffleDecline, type WaffleDeveloperFeedbackAction, type WaffleDeveloperFeedbackAvailability, type WaffleDeveloperFeedbackDimension, type WaffleDeveloperFeedbackGrant, type WaffleDeveloperFeedbackGrantProvider, type WaffleDeveloperFeedbackGrantRequest, type WaffleDeveloperFeedbackInput, type WaffleDeveloperFeedbackState, WaffleError, type WaffleErrorStage, type WaffleInputMode, type WaffleSendTextOptions, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleStatus, type WaffleTranscript, type WaffleTranscriptProgress };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{b as y}from"./chunk-BXRNROI7.js";import{DataPacket_Kind as O,ParticipantKind as W,Room as M,RoomEvent as w,Track as C}from"livekit-client";var N=15e3,D=3e4,U="https://animated-waffle.narya.ai",K=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,L=/^sha256:[a-f0-9]{64}$/,o=class extends Error{constructor(t,n,s,l="session",c){super(t,c);this.status=n;this.code=s;this.stage=l;this.name="WaffleError"}status;code;stage};function v(i,e,t){return i instanceof o?i:new o(i instanceof Error?i.message:t,void 0,void 0,e,{cause:i})}function b(i){return i!==null&&typeof i=="object"&&!Array.isArray(i)?i:null}function V(i=U){let e;try{e=new URL(i)}catch{throw new o("apiOrigin must be a valid URL.",void 0,"invalid_api_origin")}let t=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";if(e.protocol!=="https:"&&!(e.protocol==="http:"&&t)||e.username||e.password||e.pathname!=="/"&&e.pathname!==""||e.search||e.hash)throw new o("apiOrigin must be an HTTPS origin or an HTTP loopback origin.",void 0,"invalid_api_origin");return e.origin}function T(i,e){if(typeof i!="string"||i.trim().length===0)throw new o(`Session response is missing ${e}.`);return i}function j(i){let e=T(i,"avatar.asset_hash");if(!L.test(e))throw new o("Session response contains an invalid Avatar asset hash.");return e}function E(i){return typeof i=="number"&&Number.isFinite(i)?i:void 0}function g(i){return typeof i=="string"&&i.length>0?i:void 0}function x(i,e){let t=g(i[`${e}.speech_id`]),n=Number(i[`${e}.speech_step_index`]);return t&&Number.isSafeInteger(n)&&n>0?JSON.stringify([t,n]):null}function q(i,e,t){let n=b(i);if(!n)throw new o("Session response must be a JSON object.");if(n.protocol_version!==2)throw new o("Session response has an unsupported protocol version.");let s=b(n.connection);if(!s||s.transport!=="webrtc")throw new o("Session response is missing a WebRTC connection.");let l=T(s.url,"connection.url");try{let p=new URL(l),m=p.hostname==="localhost"||p.hostname==="127.0.0.1"||p.hostname==="[::1]";if(p.protocol!=="wss:"&&!(p.protocol==="ws:"&&m))throw new Error}catch{throw new o("Session response contains an invalid connection URL.")}let c=T(n.agent_id,"agent_id");if(c!==t)throw new o("Session response contains a mismatched Agent id.");let r=g(n.avatar_id),d=b(n.avatar),f=null;if(r||d){if(!r||!d||T(d.id,"avatar.id")!==r)throw new o("Session response contains mismatched Avatar metadata.");let p=T(d.engine,"avatar.engine");if(p!=="vrm")throw new o("Session response contains an unsupported Avatar engine.");let m=T(d.model_url,"avatar.model_url");try{let u=new URL(m),S=u.hostname==="localhost"||u.hostname==="127.0.0.1"||u.hostname==="[::1]";if(u.protocol!=="https:"&&!(u.protocol==="http:"&&S))throw new Error}catch{throw new o("Session response contains an invalid Avatar model URL.")}f={id:r,name:T(d.name,"avatar.name"),engine:p,modelUrl:m,assetHash:j(d.asset_hash),runtimeProfile:T(d.runtime_profile,"avatar.runtime_profile")}}return{id:T(n.session_id,"session_id"),agentId:c,...r?{avatarId:r}:{},avatar:f,inputMode:e,room:l,token:T(s.token,"connection.token")}}function H(i){let e=b(i);if(!e||typeof e.type!="string")return null;let t={contextId:g(e.contextId)??null,segmentId:g(e.segmentId),occurrence:E(e.occurrence),audioTime:E(e.audioTime)};if(e.type==="avatar-emotion"){let n=g(e.emotion);return n?{phase:"emotion",emotion:n,...t}:null}if(e.type==="avatar-vocal-action"){let n=g(e.action);return n?{phase:"vocal-action",action:n,...t}:null}return null}function G(i){if(!i||typeof i!="object")return null;let e=i;if(e.type!=="agent-declined")return null;let t=g(e.reason);return t?{reason:t}:null}function A(i){let{avatar:e,room:t,token:n,...s}=i;return s}var _=class{room;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=null;startVersion=null;stopPromise=null;requestAbort=null;readyWaiter=null;audio=null;audioMuted=!1;currentRemoteAudioTrack=null;activeInputMode="push-to-talk";agentIdentity=null;progressExecutions=new Map;progressText=new Map;completedProgressExecutions=new Set;turnId=null;nextTurnId=0;turnControl=Promise.resolve();lifecycleVersion=0;recreateRoomOnConnect=!1;constructor(e){if(this.options=e,this.apiOrigin=V(e.apiOrigin),!K.test(e.agentId))throw new o("agentId must be a UUID.",void 0,"invalid_agent_id");if(!e.startSession&&typeof e.sessionToken!="function")throw new o("sessionToken must be a function.",void 0,"invalid_session_token");this.room=this.createRoom()}createRoom(){let e=new M({adaptiveStream:!0,dynacast:!0,audioCaptureDefaults:{voiceIsolation:!1}});return e.on(w.ParticipantConnected,t=>{e===this.room&&this.handleParticipant(t)}),e.on(w.ParticipantDisconnected,t=>{e!==this.room||t.kind!==W.AGENT||(this.agentIdentity=null,this.currentStatus!=="disconnecting"&&(this.handleDisconnected("The Waffle Agent disconnected."),e.disconnect().catch(n=>{this.emit("error",v(n,"transport","Could not close the Agent room."))})))}),e.on(w.ParticipantAttributesChanged,(t,n)=>{e===this.room&&this.handleParticipant(n)}),e.on(w.TrackSubscribed,(t,n,s)=>{e===this.room&&s.kind===W.AGENT&&t.kind===C.Kind.Audio&&this.setAudioTrack(t.mediaStreamTrack)}),e.on(w.TrackUnsubscribed,(t,n,s)=>{e===this.room&&s.kind===W.AGENT&&t.kind===C.Kind.Audio&&this.setAudioTrack(null)}),e.on(w.DataReceived,(t,n,s,l)=>{if(!(e!==this.room||n?.kind!==W.AGENT||s!==O.RELIABLE)&&!(l!=="avatar.performance"&&l!=="animated-waffle.turn"))try{let c=JSON.parse(new TextDecoder().decode(t));if(l==="animated-waffle.turn"){let d=G(c);d&&this.emit("declined",d);return}let r=H(c);r&&y(this,"avatar",r)}catch{}}),e.on(w.Disconnected,()=>{e===this.room&&this.handleDisconnected("The Agent session disconnected before it was ready.")}),e.registerTextStreamHandler("lk.transcription",(t,n)=>{(async()=>{let s=this.agentIdentity?e.remoteParticipants.get(this.agentIdentity):void 0;if(e!==this.room||this.currentStatus!=="connected"||s?.kind!==W.AGENT)return;let l=t.info.attributes??{},c=g(l["lk.segment_id"]),r=g(l["lk.transcribed_track_id"]),d=l["lk.transcription_final"];if(!c||!r||d!=="true"&&d!=="false")return;let f=e.localParticipant.audioTrackPublications.has(r),p=s.audioTrackPublications.has(r);if(f===p)return;let m=f?"user":"assistant";if(n.identity!==(f?e.localParticipant.identity:s.identity))return;let u=m==="assistant"?x(l,"lk"):null;if(m==="assistant"&&!u||u&&(this.progressExecutions.set(c,u),this.completedProgressExecutions.has(u)))return;let S="";for await(let P of t){if(S+=P,e!==this.room||this.currentStatus!=="connected"||u&&this.completedProgressExecutions.has(u))return;if(S.length>0){let I={segmentId:c,role:m,text:S,complete:!1};this.progressText.set(c,I),this.emit("transcriptProgress",I)}}d==="true"&&S.length>0&&(this.emit("transcriptProgress",{segmentId:c,role:m,text:S,complete:!0}),this.progressExecutions.delete(c),this.progressText.delete(c))})().catch(s=>{this.emit("error",v(s,"transcript","Transcript stream failed."))})}),e.registerTextStreamHandler("animated-waffle.transcript",(t,n)=>{t.readAll().then(s=>{if(e!==this.room||n.identity!==this.agentIdentity||!s)return;let l=t.info.attributes??{},c=g(l["animated-waffle.message_id"]),r=l["animated-waffle.role"];if(!c||r!=="user"&&r!=="assistant"||l["animated-waffle.final"]!=="true")return;let d=r==="assistant"?x(l,"animated-waffle"):null;d&&this.completeTranscriptExecution(d);let f=Number(t.info.timestamp);this.emit("transcript",{id:c,role:r,text:s,final:!0,...Number.isFinite(f)&&f>0?{timestamp:new Date(f).toISOString()}:{}})}).catch(s=>{this.emit("error",v(s,"transcript","Transcript stream failed."))})}),e}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?A(this.currentSession):null}get remoteAudioTrack(){return this.currentRemoteAudioTrack}get muted(){return this.audioMuted}set muted(e){this.audioMuted=e,this.audio&&(this.audio.muted=e)}on(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>n.delete(t)}connect(e={}){if(this.currentSession&&this.status==="connected")return Promise.resolve(A(this.currentSession));if(this.startPromise&&this.startVersion===this.lifecycleVersion)return this.startPromise;let t=++this.lifecycleVersion,n=(async()=>{let s=this.stopPromise;if(s&&await s,t!==this.lifecycleVersion)throw new o("The connection attempt was cancelled.");return this.recreateRoomOnConnect&&(this.room=this.createRoom(),this.recreateRoomOnConnect=!1),this.startInternal(e,t)})().finally(()=>{this.startPromise===n&&(this.startPromise=null,this.startVersion=null)});return this.startVersion=t,this.startPromise=n,n}disconnect(){if(this.lifecycleVersion+=1,this.stopPromise)return this.stopPromise;let e=this.stopInternal().finally(()=>{this.stopPromise===e&&(this.stopPromise=null)});return this.stopPromise=e,e}async sendText(e,t){if(!e.trim())return;this.assertConnected();let n=t?.audioResponse===!1?{"animated-waffle.audio_response":"false"}:void 0;await this.room.localParticipant.sendText(e,{topic:"lk.chat",...n?{attributes:n}:{}})}startPushToTalk(){if(this.assertPushToTalkAvailable(),this.turnId===null){this.nextTurnId+=1,this.turnId=this.nextTurnId;try{this.sendInputControl("start_turn")}catch(e){throw this.turnId=null,e}}}commitPushToTalk(){this.assertPushToTalkAvailable(),this.turnId!==null&&(this.sendInputControl("end_turn"),this.turnId=null)}stopPushToTalk(){this.commitPushToTalk()}cancelPushToTalk(){this.assertPushToTalkAvailable(),this.turnId!==null&&(this.sendInputControl("cancel_turn"),this.turnId=null)}async startInternal(e,t){this.activeInputMode=e.inputMode??"push-to-talk",this.setStatus("connecting");let n=new AbortController;this.requestAbort=n;let s=e.sessionId??crypto.randomUUID(),l=crypto.randomUUID(),c=this.options.startSession,r=c?void 0:this.options.sessionContext,d=c?void 0:JSON.stringify({chat_session_id:s,input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic",...e.overrides?{overrides:e.overrides}:{},...r?.userName?.trim()?{user_name:r.userName}:{},...r?.timeZone?.trim()?{time_zone:r.timeZone}:{}}),f=()=>this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,requestId:l,sessionId:s,reportedSource:this.options.reportedSource??"SDK",...e.overrides?{overrides:e.overrides}:{},signal:n.signal})):this.resolveSessionToken().then(a=>fetch(this.apiOrigin+"/v1/agents/"+encodeURIComponent(this.options.agentId)+"/sessions",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json",Authorization:"Bearer "+a,"Idempotency-Key":l,"X-Animated-Waffle-Reported-Source":this.options.reportedSource??"SDK"},signal:n.signal,body:d})),p;try{p=await this.withTimeout(c?f():f().catch(a=>{if(n.signal.aborted)throw a;return f()}),N,"Timed out creating the Waffle session.",()=>n.abort())}catch(a){let h=v(a,"session","Could not create session.");throw t!==this.lifecycleVersion?new o("The connection attempt was cancelled."):(this.fail(h),h)}finally{this.requestAbort===n&&(this.requestAbort=null)}if(t!==this.lifecycleVersion)throw new o("The connection attempt was cancelled.");let m=await p.json().catch(()=>null);if(!p.ok){let a=b(m),h=new o(g(a?.detail)??"Could not create session.",p.status,g(a?.code));throw this.fail(h),h}let u;try{u=q(m,this.activeInputMode,this.options.agentId)}catch(a){let h=v(a,"session","Could not parse the Waffle session.");throw this.fail(h),h}this.currentSession=u,this.emit("session",A(u)),y(this,"presentation",u.avatar),this.setStatus("connecting");let S,P,I=new Promise((a,h)=>{S=a,P=h});this.readyWaiter={reject:P,resolve:S,version:t};let k=this.room;try{if(await k.connect(u.room,u.token).catch(a=>{throw v(a,"transport","Could not connect to the Agent room.")}),await k.startAudio().catch(a=>{throw v(a,"audio","Could not start Agent audio.")}),this.options.inputAudioEnabled!==!1){let a=await k.localParticipant.setMicrophoneEnabled(!0).catch(R=>{throw v(R,"audio","Could not start the microphone.")}),h=a?.audioTrack?.mediaStreamTrack;if(!a||a.isMuted||a.isUpstreamPaused||!h||!h.enabled||h.muted||h.readyState!=="live")throw new o("The microphone track did not publish.",void 0,void 0,"audio")}for(let a of k.remoteParticipants.values())this.handleParticipant(a);await this.withTimeout(I,D,"Timed out waiting for the Waffle Agent.",()=>{k.disconnect().catch(()=>{})},"transport")}catch(a){this.readyWaiter?.version===t&&(this.readyWaiter=null),this.currentStatus!=="disconnected"&&await k.disconnect().catch(()=>{});let h=v(a,"transport","Could not connect.");throw t!==this.lifecycleVersion?new o("The connection attempt was cancelled."):(this.fail(h),h)}finally{this.readyWaiter?.version===t&&(this.readyWaiter=null)}if(t!==this.lifecycleVersion)throw new o("The connection attempt was cancelled.");return this.setStatus("connected"),A(u)}async stopInternal(){if(this.readyWaiter?.reject(new o("The connection attempt was cancelled.")),this.readyWaiter=null,this.requestAbort?.abort(),this.turnId=null,this.turnControl=Promise.resolve(),this.agentIdentity=null,this.clearTranscriptProgress(),this.stopAudio(),this.currentStatus!=="disconnected"){this.setStatus("disconnecting");try{await this.room.disconnect()}finally{this.emit("agentSpeaking",!1),this.setStatus("disconnected")}}}sendInputControl(e){if(this.activeInputMode!=="push-to-talk")throw new o('Push-to-talk controls require inputMode: "push-to-talk".');this.assertConnected();let t=this.agentIdentity;if(!t)throw new o("The Waffle Agent is not ready.");let n=this.room,s=this.turnControl.then(()=>n.localParticipant.performRpc({destinationIdentity:t,method:e,payload:""}));this.turnControl=s.then(()=>{},l=>{this.emit("error",v(l,"transport","Push-to-talk control failed."))})}assertPushToTalkAvailable(){if(this.options.inputAudioEnabled===!1)throw new o("Push-to-talk requires input audio to be enabled.",void 0,"input_audio_disabled")}assertConnected(){if(this.status!=="connected")throw new o("Start the Waffle session before sending input.")}handleParticipant(e){if(e.kind===W.AGENT){this.agentIdentity=e.identity,this.emit("agentSpeaking",e.attributes["lk.agent.state"]==="speaking"),e.attributes["animated-waffle.runtime.ready"]==="true"&&this.readyWaiter?.resolve();for(let t of e.audioTrackPublications.values()){let n=t.track;if(n?.kind===C.Kind.Audio){this.setAudioTrack(n.mediaStreamTrack);break}}}}handleDisconnected(e){this.readyWaiter?.reject(new o(e,void 0,void 0,"transport")),this.readyWaiter=null,this.agentIdentity=null,this.clearTranscriptProgress(),this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected")}setAudioTrack(e){if(e===this.currentRemoteAudioTrack||(this.stopAudio(!1),this.currentRemoteAudioTrack=e,this.emit("remoteAudioTrack",e),y(this,"audioTrack",e),!e||typeof document>"u")||this.options.audioPlayback===!1)return;let t=document.createElement("audio");t.autoplay=!0,t.muted=this.audioMuted,t.setAttribute("playsinline",""),t.srcObject=new MediaStream([e]),this.audio=t,t.play().catch(n=>{this.emit("error",v(n,"audio","Browser blocked agent audio."))})}stopAudio(e=!0){this.audio&&(this.audio.pause(),this.audio.srcObject=null,this.audio=null),e&&this.currentRemoteAudioTrack&&(this.currentRemoteAudioTrack=null,this.emit("remoteAudioTrack",null),y(this,"audioTrack",null))}completeTranscriptExecution(e){this.completedProgressExecutions.add(e);for(let[t,n]of this.progressText)this.progressExecutions.get(t)===e&&(this.emit("transcriptProgress",{...n,complete:!0}),this.progressExecutions.delete(t),this.progressText.delete(t))}clearTranscriptProgress(){this.progressExecutions.clear(),this.progressText.clear(),this.completedProgressExecutions.clear()}setStatus(e){e==="disconnected"&&(this.recreateRoomOnConnect=!0),e==="disconnected"&&this.currentSession&&(this.currentSession=null,this.emit("session",null),y(this,"presentation",null)),this.currentStatus!==e&&(this.currentStatus=e,this.emit("connectionState",e),this.emit("status",e))}fail(e){this.readyWaiter?.reject(e),this.readyWaiter=null,this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected"),this.emit("error",e)}async resolveSessionToken(){if(!this.options.sessionToken)throw new o("No session token provider is configured.");let e=await this.options.sessionToken();if(!e.trim())throw new o("sessionToken must not be empty.",void 0,"invalid_session_token");return e}withTimeout(e,t,n,s,l="session"){return new Promise((c,r)=>{let d=setTimeout(()=>{s?.(),r(new o(n,void 0,void 0,l))},t);e.then(f=>{clearTimeout(d),c(f)},f=>{clearTimeout(d),r(f)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{_ as WaffleClient,o as WaffleError};
1
+ import{b as _}from"./chunk-BXRNROI7.js";import{DataPacket_Kind as U,ParticipantKind as W,Room as G,RoomEvent as P,RpcError as M,Track as C}from"livekit-client";var D=15e3,K=3e4,q="https://animated-waffle.narya.ai",A=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,L=/^sha256:[a-f0-9]{64}$/,i=class extends Error{constructor(t,n,o,a="session",s){super(t,s);this.status=n;this.code=o;this.stage=a;this.name="WaffleError"}status;code;stage;rpcMethod;livekitErrorCode},I={APPLICATION_ERROR:1500,CONNECTION_TIMEOUT:1501,RESPONSE_TIMEOUT:1502,RECIPIENT_DISCONNECTED:1503},j=new Set([I.CONNECTION_TIMEOUT,I.RESPONSE_TIMEOUT]);function V(r){return r instanceof M&&j.has(r.code)}function H(r){return r===I.RECIPIENT_DISCONNECTED?"push_to_talk_agent_unavailable":r===I.CONNECTION_TIMEOUT||r===I.RESPONSE_TIMEOUT?"push_to_talk_timeout":"push_to_talk_failed"}function B(r){return r===I.RECIPIENT_DISCONNECTED?"The Waffle agent dropped out for a moment. Release and press again to talk.":r===I.CONNECTION_TIMEOUT||r===I.RESPONSE_TIMEOUT?"The voice turn took too long to reach the agent. Release and try again.":"That voice turn didn\u2019t go through. Press and hold to talk again."}function $(r,e){let t=r instanceof M?r.code:void 0,n=new i(B(t),void 0,H(t),"transport",{cause:r});return n.rpcMethod=e,typeof t=="number"&&(n.livekitErrorCode=t),n}function T(r,e,t){return r instanceof i?r:new i(r instanceof Error?r.message:t,void 0,void 0,e,{cause:r})}function S(r){return r!==null&&typeof r=="object"&&!Array.isArray(r)?r:null}function J(r=q){let e;try{e=new URL(r)}catch{throw new i("apiOrigin must be a valid URL.",void 0,"invalid_api_origin")}let t=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";if(e.protocol!=="https:"&&!(e.protocol==="http:"&&t)||e.username||e.password||e.pathname!=="/"&&e.pathname!==""||e.search||e.hash)throw new i("apiOrigin must be an HTTPS origin or an HTTP loopback origin.",void 0,"invalid_api_origin");return e.origin}function v(r,e){if(typeof r!="string"||r.trim().length===0)throw new i(`Session response is missing ${e}.`);return r}function z(r){let e=v(r,"avatar.asset_hash");if(!L.test(e))throw new i("Session response contains an invalid Avatar asset hash.");return e}function x(r){return typeof r=="number"&&Number.isFinite(r)?r:void 0}function g(r){return typeof r=="string"&&r.length>0?r:void 0}function R(r,e){let t=v(r,e);try{let n=new URL(t),o=n.hostname==="localhost"||n.hostname==="127.0.0.1"||n.hostname==="[::1]";if(n.protocol!=="https:"&&!(n.protocol==="http:"&&o))throw new Error;if(n.username||n.password||n.hash)throw new Error;return n.toString()}catch{throw new i(`Received an invalid ${e}.`)}}function Z(r,e){let t=v(r,e);if(!/^\d{4}-\d{2}-\d{2}T/.test(t)||!Number.isFinite(Date.parse(t)))throw new i(`Received an invalid ${e}.`);return t}function O(r,e){let t=g(r[`${e}.speech_id`]),n=Number(r[`${e}.speech_step_index`]);return t&&Number.isSafeInteger(n)&&n>0?JSON.stringify([t,n]):null}function Y(r,e,t,n){let o=S(r);if(!o)throw new i("Session response must be a JSON object.");if(o.protocol_version!==2)throw new i("Session response has an unsupported protocol version.");let a=S(o.connection);if(!a||a.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let s=v(a.url,"connection.url");try{let p=new URL(s),k=p.hostname==="localhost"||p.hostname==="127.0.0.1"||p.hostname==="[::1]";if(p.protocol!=="wss:"&&!(p.protocol==="ws:"&&k))throw new Error}catch{throw new i("Session response contains an invalid connection URL.")}let l=v(o.agent_id,"agent_id");if(l!==t)throw new i("Session response contains a mismatched Agent id.");let c=g(o.agent_studio_url);if(c)try{let p=new URL(c),k=p.hostname==="localhost"||p.hostname==="127.0.0.1"||p.hostname==="[::1]";if(p.protocol!=="https:"&&!(p.protocol==="http:"&&k))throw new Error}catch{throw new i("Session response contains an invalid Agent Studio URL.")}let d=g(o.avatar_id),f=S(o.avatar),b=null;if(d||f){if(!d||!f||v(f.id,"avatar.id")!==d)throw new i("Session response contains mismatched Avatar metadata.");let p=v(f.engine,"avatar.engine");if(p!=="vrm")throw new i("Session response contains an unsupported Avatar engine.");let k=v(f.model_url,"avatar.model_url");try{let w=new URL(k),y=w.hostname==="localhost"||w.hostname==="127.0.0.1"||w.hostname==="[::1]";if(w.protocol!=="https:"&&!(w.protocol==="http:"&&y))throw new Error}catch{throw new i("Session response contains an invalid Avatar model URL.")}b={id:d,name:v(f.name,"avatar.name"),engine:p,modelUrl:k,assetHash:z(f.asset_hash),runtimeProfile:v(f.runtime_profile,"avatar.runtime_profile"),...X(f.presentation_profile)}}let h=v(o.session_id,"session_id");return{id:h,agentId:l,...c?{agentStudioUrl:c}:{},...d?{avatarId:d}:{},...n?{developerFeedback:{sourceSessionId:h}}:{},avatar:b,inputMode:e,room:s,token:v(a.token,"connection.token")}}function X(r){if(r===void 0)return{};let e=S(r);if(!e)throw new i("Session response contains an invalid Avatar presentation profile.");let t=v(e.framing,"avatar.presentation_profile.framing"),n=v(e.attention,"avatar.presentation_profile.attention"),o=v(e.emotion,"avatar.presentation_profile.emotion");if(!["default","full","upper","face"].includes(t)||!["natural","focused","lively"].includes(n)||!["natural","subtle","expressive"].includes(o))throw new i("Session response contains an unsupported Avatar presentation profile.");return{presentationProfile:{framing:t,attention:n,emotion:o}}}function Q(r){let e=S(r);if(!e||typeof e.type!="string")return null;let t={contextId:g(e.contextId)??null,segmentId:g(e.segmentId),occurrence:x(e.occurrence),audioTime:x(e.audioTime)};if(e.type==="avatar-emotion"){let n=g(e.emotion);return n?{phase:"emotion",emotion:n,...t}:null}if(e.type==="avatar-vocal-action"){let n=g(e.action);return n?{phase:"vocal-action",action:n,...t}:null}return null}function ee(r){if(!r||typeof r!="object")return null;let e=r;if(e.type!=="agent-declined")return null;let t=g(e.reason);return t?{reason:t}:null}function E(r){let{avatar:e,room:t,token:n,...o}=r;return o}var F=class{room;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=null;startVersion=null;stopPromise=null;requestAbort=null;readyWaiter=null;audio=null;audioMuted=!1;currentRemoteAudioTrack=null;activeInputMode="push-to-talk";agentIdentity=null;progressExecutions=new Map;progressText=new Map;completedProgressExecutions=new Set;turnId=null;nextTurnId=0;turnControl=Promise.resolve();failedStartTurnKey=null;lifecycleVersion=0;recreateRoomOnConnect=!1;developerFeedbackGrant=null;developerFeedbackGrantPromise=null;developerFeedbackGrantAbort=null;constructor(e){if(this.options=e,this.apiOrigin=J(e.apiOrigin),!A.test(e.agentId))throw new i("agentId must be a UUID.",void 0,"invalid_agent_id");if(!e.startSession&&typeof e.sessionToken!="function")throw new i("sessionToken must be a function.",void 0,"invalid_session_token");this.room=this.createRoom()}createRoom(){let e=new G({adaptiveStream:!0,dynacast:!0,audioCaptureDefaults:{voiceIsolation:!1}});return e.on(P.ParticipantConnected,t=>{e===this.room&&this.handleParticipant(t)}),e.on(P.ParticipantDisconnected,t=>{e!==this.room||t.kind!==W.AGENT||(this.agentIdentity=null,this.currentStatus!=="disconnecting"&&(this.handleDisconnected("The Waffle Agent disconnected."),e.disconnect().catch(n=>{this.emit("error",T(n,"transport","Could not close the Agent room."))})))}),e.on(P.ParticipantAttributesChanged,(t,n)=>{e===this.room&&this.handleParticipant(n)}),e.on(P.TrackSubscribed,(t,n,o)=>{e===this.room&&o.kind===W.AGENT&&t.kind===C.Kind.Audio&&this.setAudioTrack(t.mediaStreamTrack)}),e.on(P.TrackUnsubscribed,(t,n,o)=>{e===this.room&&o.kind===W.AGENT&&t.kind===C.Kind.Audio&&this.setAudioTrack(null)}),e.on(P.DataReceived,(t,n,o,a)=>{if(!(e!==this.room||n?.kind!==W.AGENT||o!==U.RELIABLE)&&!(a!=="avatar.performance"&&a!=="animated-waffle.turn"))try{let s=JSON.parse(new TextDecoder().decode(t));if(a==="animated-waffle.turn"){let c=ee(s);c&&this.emit("declined",c);return}let l=Q(s);l&&_(this,"avatar",l)}catch{}}),e.on(P.Disconnected,()=>{e===this.room&&this.handleDisconnected("The Agent session disconnected before it was ready.")}),e.registerTextStreamHandler("lk.transcription",(t,n)=>{(async()=>{let o=this.agentIdentity?e.remoteParticipants.get(this.agentIdentity):void 0;if(e!==this.room||this.currentStatus!=="connected"||o?.kind!==W.AGENT)return;let a=t.info.attributes??{},s=g(a["lk.segment_id"]),l=g(a["lk.transcribed_track_id"]),c=a["lk.transcription_final"];if(!s||!l||c!=="true"&&c!=="false")return;let d=e.localParticipant.audioTrackPublications.has(l),f=o.audioTrackPublications.has(l);if(d===f)return;let b=d?"user":"assistant";if(n.identity!==(d?e.localParticipant.identity:o.identity))return;let h=b==="assistant"?O(a,"lk"):null;if(b==="assistant"&&!h||h&&(this.progressExecutions.set(s,h),this.completedProgressExecutions.has(h)))return;let p="";for await(let k of t){if(p+=k,e!==this.room||this.currentStatus!=="connected"||h&&this.completedProgressExecutions.has(h))return;if(p.length>0){let w={segmentId:s,role:b,text:p,complete:!1};this.progressText.set(s,w),this.emit("transcriptProgress",w)}}c==="true"&&p.length>0&&(this.emit("transcriptProgress",{segmentId:s,role:b,text:p,complete:!0}),this.progressExecutions.delete(s),this.progressText.delete(s))})().catch(o=>{this.emit("error",T(o,"transcript","Transcript stream failed."))})}),e.registerTextStreamHandler("animated-waffle.transcript",(t,n)=>{t.readAll().then(o=>{if(e!==this.room||n.identity!==this.agentIdentity||!o)return;let a=t.info.attributes??{},s=g(a["animated-waffle.message_id"]),l=a["animated-waffle.role"];if(!s||l!=="user"&&l!=="assistant"||a["animated-waffle.final"]!=="true")return;let c=l==="assistant"?O(a,"animated-waffle"):null;c&&this.completeTranscriptExecution(c);let d=Number(t.info.timestamp);this.emit("transcript",{id:s,role:l,text:o,final:!0,...Number.isFinite(d)&&d>0?{timestamp:new Date(d).toISOString()}:{}})}).catch(o=>{this.emit("error",T(o,"transcript","Transcript stream failed."))})}),e}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?E(this.currentSession):null}get remoteAudioTrack(){return this.currentRemoteAudioTrack}get muted(){return this.audioMuted}set muted(e){this.audioMuted=e,this.audio&&(this.audio.muted=e)}on(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>n.delete(t)}connect(e={}){if(this.currentSession&&this.status==="connected")return Promise.resolve(E(this.currentSession));if(this.startPromise&&this.startVersion===this.lifecycleVersion)return this.startPromise;let t=++this.lifecycleVersion,n=(async()=>{let o=this.stopPromise;if(o&&await o,t!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");return this.recreateRoomOnConnect&&(this.room=this.createRoom(),this.recreateRoomOnConnect=!1),this.startInternal(e,t)})().finally(()=>{this.startPromise===n&&(this.startPromise=null,this.startVersion=null)});return this.startVersion=t,this.startPromise=n,n}disconnect(){if(this.lifecycleVersion+=1,this.stopPromise)return this.stopPromise;let e=this.stopInternal().finally(()=>{this.stopPromise===e&&(this.stopPromise=null)});return this.stopPromise=e,e}async sendText(e,t){if(!e.trim())return;this.assertConnected();let n=t?.audioResponse===!1?{"animated-waffle.audio_response":"false"}:void 0;await this.room.localParticipant.sendText(e,{topic:"lk.chat",...n?{attributes:n}:{}})}async setInputMode(e){await this.applicationControl("input_mode",{mode:e}),this.activeInputMode=e,this.turnId=null}async interrupt(){await this.applicationControl("interrupt",{})}get microphoneTrack(){return this.room.localParticipant.getTrackPublication(C.Source.Microphone)?.audioTrack?.mediaStreamTrack??null}async setMicrophoneEnabled(e){if(this.assertConnected(),e&&this.options.inputAudioEnabled===!1)throw new i("Input audio is disabled.",void 0,"input_audio_disabled");await this.room.localParticipant.setMicrophoneEnabled(e)}async applicationControl(e,t){if(this.assertConnected(),!this.options.application||!this.agentIdentity)throw new i("Application sessions are required.",void 0,"application_session_required");return this.room.localParticipant.performRpc({destinationIdentity:this.agentIdentity,method:"application.control",payload:JSON.stringify({action:e,...t})})}async loadDeveloperFeedback(){return this.requestDeveloperFeedback("GET")}async submitDeveloperFeedback(e){let t=e.id??crypto.randomUUID();if(!A.test(t))throw new i("Developer Feedback id must be a UUID.",void 0,"invalid_feedback");if(e.scope==="message"&&!e.targetMessageId)throw new i("Message feedback requires a persisted Assistant message id.",void 0,"invalid_feedback");if(e.scope==="session"&&e.targetMessageId)throw new i("Session feedback cannot target one message.",void 0,"invalid_feedback");if(e.targetMessageId&&!A.test(e.targetMessageId))throw new i("Developer Feedback targetMessageId must be a UUID.",void 0,"invalid_feedback");if(e.action==="change"&&!e.note?.trim()&&!e.preferredReply?.trim()&&e.dimension!=="avatar"&&e.dimension!=="voice")throw new i("Describe the requested change or provide a preferred reply.",void 0,"invalid_feedback");return this.requestDeveloperFeedback("POST",{id:t,scope:e.scope,targetMessageId:e.targetMessageId??null,action:e.action,dimension:e.dimension,note:e.note??null,preferredReply:e.preferredReply??null})}startPushToTalk(){if(this.assertPushToTalkAvailable(),this.turnId!==null)return this.turnControl;this.nextTurnId+=1,this.turnId=this.nextTurnId;try{return this.sendInputControl("start_turn",String(this.turnId))}catch(e){throw this.turnId=null,e}}commitPushToTalk(){if(this.assertPushToTalkAvailable(),this.turnId===null)return this.turnControl;let e=String(this.turnId);return this.turnId=null,this.sendInputControl("end_turn",e)}stopPushToTalk(){this.commitPushToTalk()}cancelPushToTalk(){if(this.assertPushToTalkAvailable(),this.turnId===null)return this.turnControl;let e=String(this.turnId);return this.turnId=null,this.sendInputControl("cancel_turn",e)}async startInternal(e,t){this.activeInputMode=e.inputMode??"push-to-talk",this.setStatus("connecting");let n=new AbortController;this.requestAbort=n;let o=e.sessionId??crypto.randomUUID(),a=crypto.randomUUID(),s=this.options.startSession,l=s?void 0:this.options.sessionContext,c=s?void 0:JSON.stringify({chat_session_id:o,input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic",...e.overrides?{overrides:e.overrides}:{},...l?.userName?.trim()?{user_name:l.userName}:{},...l?.timeZone?.trim()?{time_zone:l.timeZone}:{}}),d=()=>this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,requestId:a,sessionId:o,reportedSource:this.options.reportedSource??"SDK",...e.overrides?{overrides:e.overrides}:{},signal:n.signal})):this.resolveSessionToken().then(u=>fetch(this.apiOrigin+"/v1/agents/"+encodeURIComponent(this.options.agentId)+"/sessions",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json",Authorization:"Bearer "+u,"Idempotency-Key":a,"X-Animated-Waffle-Reported-Source":this.options.reportedSource??"SDK"},signal:n.signal,body:c})),f;try{f=await this.withTimeout(s?d():d().catch(u=>{if(n.signal.aborted)throw u;return d()}),D,"Timed out creating the Waffle session.",()=>n.abort())}catch(u){let m=T(u,"session","Could not create session.");throw t!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(m),m)}finally{this.requestAbort===n&&(this.requestAbort=null)}if(t!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");let b=await f.json().catch(()=>null);if(!f.ok){let u=S(b),m=new i(g(u?.detail)??"Could not create session.",f.status,g(u?.code));throw this.fail(m),m}let h;try{h=Y(b,this.activeInputMode,this.options.agentId,typeof this.options.developerFeedback=="function")}catch(u){let m=T(u,"session","Could not parse the Waffle session.");throw this.fail(m),m}this.currentSession?.id!==h.id&&this.clearDeveloperFeedbackGrant(),this.currentSession=h,this.emit("session",E(h)),_(this,"presentation",h.avatar),this.setStatus("connecting");let p,k,w=new Promise((u,m)=>{p=u,k=m});this.readyWaiter={reject:k,resolve:p,version:t};let y=this.room;try{if(await y.connect(h.room,h.token).catch(u=>{throw T(u,"transport","Could not connect to the Agent room.")}),await y.startAudio().catch(u=>{throw T(u,"audio","Could not start Agent audio.")}),this.options.inputAudioEnabled!==!1){let u=await y.localParticipant.setMicrophoneEnabled(!0).catch(N=>{throw T(N,"audio","Could not start the microphone.")}),m=u?.audioTrack?.mediaStreamTrack;if(!u||u.isMuted||u.isUpstreamPaused||!m||!m.enabled||m.muted||m.readyState!=="live")throw new i("The microphone track did not publish.",void 0,void 0,"audio")}for(let u of y.remoteParticipants.values())this.handleParticipant(u);await this.withTimeout(w,K,"Timed out waiting for the Waffle Agent.",()=>{y.disconnect().catch(()=>{})},"transport")}catch(u){this.readyWaiter?.version===t&&(this.readyWaiter=null),this.currentStatus!=="disconnected"&&await y.disconnect().catch(()=>{});let m=T(u,"transport","Could not connect.");throw t!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(m),m)}finally{this.readyWaiter?.version===t&&(this.readyWaiter=null)}if(t!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");return this.setStatus("connected"),E(h)}async stopInternal(){if(this.readyWaiter?.reject(new i("The connection attempt was cancelled.")),this.readyWaiter=null,this.requestAbort?.abort(),this.turnId=null,this.turnControl=Promise.resolve(),this.failedStartTurnKey=null,this.agentIdentity=null,this.clearTranscriptProgress(),this.stopAudio(),this.currentStatus!=="disconnected"){this.setStatus("disconnecting");try{await this.room.disconnect()}finally{this.emit("agentSpeaking",!1),this.setStatus("disconnected")}}}sendInputControl(e,t){if(this.activeInputMode!=="push-to-talk")throw new i('Push-to-talk controls require inputMode: "push-to-talk".');this.assertConnected();let n=this.agentIdentity;if(!n)throw new i("The Waffle Agent is not ready.",void 0,"push_to_talk_agent_unavailable","transport");let o=this.room,a=this.turnControl.then(()=>{if(e!=="start_turn"&&this.failedStartTurnKey===t){this.failedStartTurnKey=null;return}return this.performControlRpc(o,n,e,t)});return this.turnControl=a.then(()=>{},s=>{e==="start_turn"&&(this.failedStartTurnKey=t),this.turnId!==null&&String(this.turnId)===t&&(this.turnId=null),this.emit("error",$(s,e))}),this.turnControl}async performControlRpc(e,t,n,o){let a=JSON.stringify({turnId:o}),s=n==="start_turn"?2:1,l;for(let c=1;c<=s;c+=1)try{return await e.localParticipant.performRpc({destinationIdentity:t,method:n,payload:a})}catch(d){if(l=d,c<s&&V(d))continue;throw d}throw l}assertPushToTalkAvailable(){if(this.options.inputAudioEnabled===!1)throw new i("Push-to-talk requires input audio to be enabled.",void 0,"input_audio_disabled")}assertConnected(){if(this.status!=="connected")throw new i("Start the Waffle session before sending input.")}async requestDeveloperFeedback(e,t){if(typeof this.options.developerFeedback!="function")throw new i("Developer Feedback is not enabled for this client.",void 0,"developer_feedback_disabled");let n=await this.resolveDeveloperFeedbackGrant(),o;try{o=await fetch(n.endpoint,{method:e,credentials:"omit",cache:"no-store",headers:{Accept:"application/json",Authorization:`Bearer ${n.token}`,...t?{"Content-Type":"application/json"}:{}},...t?{body:JSON.stringify(t)}:{}})}catch{throw new i("Could not save Developer Feedback.",void 0,"developer_feedback_network_error")}let a=await o.json().catch(()=>null);if(!o.ok){let f=S(a);throw new i(g(f?.detail)??"Could not save Developer Feedback.",o.status,g(f?.code))}let s=S(a),l=s?.round_id,c=s?.pending_count,d=s?.dashboard_url;if(l!==null&&(typeof l!="string"||!A.test(l))||typeof c!="number"||!Number.isSafeInteger(c)||c<0||d!==null&&typeof d!="string")throw new i("Developer Feedback response is invalid.");return typeof d=="string"&&R(d,"dashboard_url"),{roundId:l,pendingCount:c,dashboardUrl:d}}resolveDeveloperFeedbackGrant(){let e=this.currentSession,t=this.options.developerFeedback;if(!e||typeof t!="function")throw new i("Start a Developer Feedback-enabled Waffle session first.",void 0,"developer_feedback_unavailable");if(this.developerFeedbackGrant?.sourceSessionId===e.id&&Date.parse(this.developerFeedbackGrant.expiresAt)>Date.now())return Promise.resolve(this.developerFeedbackGrant);if(this.developerFeedbackGrantPromise)return this.developerFeedbackGrantPromise;let n=new AbortController;this.developerFeedbackGrantAbort=n;let o=this.withTimeout(Promise.resolve(t({agentId:e.agentId,sourceSessionId:e.id,signal:n.signal})),D,"Timed out requesting a Developer Feedback grant.",()=>n.abort()).then(async a=>{let s=await a.json().catch(()=>null);if(!a.ok){let d=S(s);throw new i(g(d?.detail)??"Could not authorize Developer Feedback.",a.status,g(d?.code))}let l=S(s),c={endpoint:R(l?.endpoint,"developerFeedback.endpoint"),token:v(l?.token,"developerFeedback.token"),expiresAt:Z(l?.expires_at,"developer_feedback.expires_at"),sourceSessionId:v(l?.source_session_id,"developer_feedback.source_session_id")};if(c.sourceSessionId!==e.id)throw new i("Developer Feedback grant targets a different Session.");if(Date.parse(c.expiresAt)<=Date.now())throw new i("Developer Feedback grant is already expired.");if(this.currentSession?.id!==e.id)throw new i("The Waffle session changed while authorizing feedback.");return this.developerFeedbackGrant=c,c}).finally(()=>{this.developerFeedbackGrantPromise===o&&(this.developerFeedbackGrantPromise=null),this.developerFeedbackGrantAbort===n&&(this.developerFeedbackGrantAbort=null)});return this.developerFeedbackGrantPromise=o,o}handleParticipant(e){if(e.kind===W.AGENT){if(this.agentIdentity=e.identity,this.emit("agentSpeaking",e.attributes["lk.agent.state"]==="speaking"),e.attributes["animated-waffle.runtime.ready"]==="true"){if(this.options.application&&e.attributes["animated-waffle.application.version"]!=="2"){this.readyWaiter?.reject(new i("The voice service does not support application sessions.",void 0,"application_session_unsupported"));return}this.readyWaiter?.resolve()}for(let t of e.audioTrackPublications.values()){let n=t.track;if(n?.kind===C.Kind.Audio){this.setAudioTrack(n.mediaStreamTrack);break}}}}handleDisconnected(e){this.readyWaiter?.reject(new i(e,void 0,void 0,"transport")),this.readyWaiter=null,this.turnId=null,this.turnControl=Promise.resolve(),this.failedStartTurnKey=null,this.agentIdentity=null,this.clearTranscriptProgress(),this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected")}setAudioTrack(e){if(e===this.currentRemoteAudioTrack||(this.stopAudio(!1),this.currentRemoteAudioTrack=e,this.emit("remoteAudioTrack",e),_(this,"audioTrack",e),!e||typeof document>"u")||this.options.audioPlayback===!1)return;let t=document.createElement("audio");t.autoplay=!0,t.muted=this.audioMuted,t.setAttribute("playsinline",""),t.srcObject=new MediaStream([e]),this.audio=t,t.play().catch(n=>{this.emit("error",T(n,"audio","Browser blocked agent audio."))})}stopAudio(e=!0){this.audio&&(this.audio.pause(),this.audio.srcObject=null,this.audio=null),e&&this.currentRemoteAudioTrack&&(this.currentRemoteAudioTrack=null,this.emit("remoteAudioTrack",null),_(this,"audioTrack",null))}completeTranscriptExecution(e){this.completedProgressExecutions.add(e);for(let[t,n]of this.progressText)this.progressExecutions.get(t)===e&&(this.emit("transcriptProgress",{...n,complete:!0}),this.progressExecutions.delete(t),this.progressText.delete(t))}clearTranscriptProgress(){this.progressExecutions.clear(),this.progressText.clear(),this.completedProgressExecutions.clear()}setStatus(e){e==="disconnected"&&(this.recreateRoomOnConnect=!0),e==="disconnected"&&this.currentSession&&(this.clearDeveloperFeedbackGrant(),this.currentSession=null,this.emit("session",null),_(this,"presentation",null)),this.currentStatus!==e&&(this.currentStatus=e,this.emit("connectionState",e),this.emit("status",e))}clearDeveloperFeedbackGrant(){this.developerFeedbackGrantAbort?.abort(),this.developerFeedbackGrantAbort=null,this.developerFeedbackGrantPromise=null,this.developerFeedbackGrant=null}fail(e){this.readyWaiter?.reject(e),this.readyWaiter=null,this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected"),this.emit("error",e)}async resolveSessionToken(){if(!this.options.sessionToken)throw new i("No session token provider is configured.");let e=await this.options.sessionToken();if(!e.trim())throw new i("sessionToken must not be empty.",void 0,"invalid_session_token");return e}withTimeout(e,t,n,o,a="session"){return new Promise((s,l)=>{let c=setTimeout(()=>{o?.(),l(new i(n,void 0,void 0,a))},t);e.then(d=>{clearTimeout(c),s(d)},d=>{clearTimeout(c),l(d)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{F as WaffleClient,i as WaffleError};
@@ -15,6 +15,11 @@ interface InternalAvatarPresentation {
15
15
  modelUrl: string;
16
16
  assetHash: string;
17
17
  runtimeProfile: string;
18
+ presentationProfile?: {
19
+ framing: 'default' | 'full' | 'upper' | 'face';
20
+ attention: 'natural' | 'focused' | 'lively';
21
+ emotion: 'natural' | 'subtle' | 'expressive';
22
+ };
18
23
  }
19
24
  interface InternalWaffleEvents {
20
25
  avatar: InternalAvatarEvent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animated-waffle/client",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Framework-independent browser SDK for Narya Animated Waffle talking Agents.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -34,12 +34,14 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "livekit-client": "2.21.0"
37
+ "livekit-client": "2.21.0",
38
+ "zod": "^4.3.6"
38
39
  },
39
40
  "devDependencies": {
40
41
  "tsup": "^8.5.0",
41
42
  "typescript": "^5.7.2",
42
- "vitest": "^3.0.5"
43
+ "vitest": "^3.0.5",
44
+ "@companion-demo/schema": "0.0.0"
43
45
  },
44
46
  "scripts": {
45
47
  "build": "tsup",