@animated-waffle/client 0.3.2 → 0.3.4

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.4
4
+
5
+ - Classify public failures with `WaffleError.stage`; connection failures reject
6
+ and emit the same error instance.
7
+ - Report the integration family for observability with `reportedSource`,
8
+ defaulting to `SDK`.
9
+
10
+ ## 0.3.3
11
+
12
+ - Start a fresh connection after an in-flight attempt is disconnected, including setup-cleanup-setup consumers such as React StrictMode.
13
+
3
14
  ## 0.3.2
4
15
 
5
16
  - Emit `declined` when an opted-in Agent deliberately answers a live turn with silence.
package/README.md CHANGED
@@ -82,6 +82,8 @@ const waffle = new WaffleClient({
82
82
  provided; blank values are omitted. It is mutually exclusive with the
83
83
  advanced `startSession` callback, so token-path consumers do not recreate
84
84
  the raw managed-session request.
85
+ - `reportedSource?: 'Web' | 'SDK' | 'Client' | 'Unknown'` — self-reported
86
+ integration family for observability only. The SDK defaults to `SDK`.
85
87
  - `connect({ inputMode?, sessionId?, overrides? })` — `'push-to-talk'`
86
88
  (default) or `'automatic'`; resolves only when the Agent runtime is ready.
87
89
  - `inputAudioEnabled: false` — never requests or enables the browser microphone;
@@ -108,7 +110,10 @@ const waffle = new WaffleClient({
108
110
  - Transcript progress is ephemeral and keyed by a runtime `segmentId`; user ASR
109
111
  and assistant playout update it incrementally until `complete` closes it.
110
112
  - `connectionState`, `session`, `remoteAudioTrack` getters; `status` is the
111
- deprecated 0.1 alias. Failures throw `WaffleError` with `code`.
113
+ deprecated 0.1 alias. Failures throw `WaffleError` with `code` and a typed
114
+ `stage`: `session`, `transport`, `audio`, `transcript`, `avatar-load`, or
115
+ `avatar-renderer`. A failed `connect()` emits and rejects with the same error
116
+ instance.
112
117
 
113
118
  `./internal` is a private contract consumed by `@animated-waffle/react`; it is
114
119
  not part of the public API and may change without notice.
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ interface WaffleClientEvents {
31
31
  /** @deprecated Subscribe to connectionState. */
32
32
  status: WaffleStatus;
33
33
  session: WaffleSession | null;
34
- error: Error;
34
+ error: WaffleError;
35
35
  agentSpeaking: boolean;
36
36
  remoteAudioTrack: MediaStreamTrack | null;
37
37
  transcriptProgress: WaffleTranscriptProgress;
@@ -80,6 +80,8 @@ interface WaffleClientBaseOptions {
80
80
  * turn this off, or the voice is heard twice.
81
81
  */
82
82
  audioPlayback?: boolean;
83
+ /** Self-reported integration family for observability only. Defaults to SDK. */
84
+ reportedSource?: 'Web' | 'SDK' | 'Client' | 'Unknown';
83
85
  }
84
86
  interface WaffleTokenClientOptions extends WaffleClientBaseOptions {
85
87
  /** A short-lived token minted for this Agent by the Narya API. */
@@ -122,6 +124,7 @@ interface WaffleSessionStartInput {
122
124
  inputMode: WaffleInputMode;
123
125
  requestId: string;
124
126
  sessionId: string;
127
+ reportedSource: 'Web' | 'SDK' | 'Client' | 'Unknown';
125
128
  overrides?: Readonly<WaffleSessionOverrides>;
126
129
  signal: AbortSignal;
127
130
  }
@@ -148,10 +151,12 @@ interface WaffleSessionClient {
148
151
  cancelPushToTalk(): void;
149
152
  }
150
153
  type Listener<K extends keyof WaffleClientEvents> = (payload: WaffleClientEvents[K]) => void;
154
+ type WaffleErrorStage = 'session' | 'transport' | 'audio' | 'transcript' | 'avatar-load' | 'avatar-renderer';
151
155
  declare class WaffleError extends Error {
152
156
  readonly status?: number | undefined;
153
157
  readonly code?: string | undefined;
154
- constructor(message: string, status?: number | undefined, code?: string | undefined);
158
+ readonly stage: WaffleErrorStage;
159
+ constructor(message: string, status?: number | undefined, code?: string | undefined, stage?: WaffleErrorStage, options?: ErrorOptions);
155
160
  }
156
161
  declare class WaffleClient implements WaffleSessionClient {
157
162
  private room;
@@ -161,6 +166,7 @@ declare class WaffleClient implements WaffleSessionClient {
161
166
  private currentStatus;
162
167
  private currentSession;
163
168
  private startPromise;
169
+ private startVersion;
164
170
  private stopPromise;
165
171
  private requestAbort;
166
172
  private readyWaiter;
@@ -212,4 +218,4 @@ declare class WaffleClient implements WaffleSessionClient {
212
218
  private emit;
213
219
  }
214
220
 
215
- export { WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, type WaffleDecline, WaffleError, type WaffleInputMode, type WaffleSendTextOptions, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleStatus, type WaffleTranscript, type WaffleTranscriptProgress };
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 };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{b as k}from"./chunk-BXRNROI7.js";import{DataPacket_Kind as C,ParticipantKind as y,Room as R,RoomEvent as S,Track as A}from"livekit-client";var O=15e3,M=3e4,N="https://animated-waffle.narya.ai",U=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,D=/^sha256:[a-f0-9]{64}$/,i=class extends Error{constructor(t,n,o){super(t);this.status=n;this.code=o;this.name="WaffleError"}status;code};function P(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)?s:null}function K(s=N){let e;try{e=new URL(s)}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 T(s,e){if(typeof s!="string"||s.trim().length===0)throw new i(`Session response is missing ${e}.`);return s}function L(s){let e=T(s,"avatar.asset_hash");if(!D.test(e))throw new i("Session response contains an invalid Avatar asset hash.");return e}function E(s){return typeof s=="number"&&Number.isFinite(s)?s:void 0}function g(s){return typeof s=="string"&&s.length>0?s:void 0}function x(s,e){let t=g(s[`${e}.speech_id`]),n=Number(s[`${e}.speech_step_index`]);return t&&Number.isSafeInteger(n)&&n>0?JSON.stringify([t,n]):null}function j(s,e,t){let n=P(s);if(!n)throw new i("Session response must be a JSON object.");if(n.protocol_version!==2)throw new i("Session response has an unsupported protocol version.");let o=P(n.connection);if(!o||o.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let a=T(o.url,"connection.url");try{let h=new URL(a),m=h.hostname==="localhost"||h.hostname==="127.0.0.1"||h.hostname==="[::1]";if(h.protocol!=="wss:"&&!(h.protocol==="ws:"&&m))throw new Error}catch{throw new i("Session response contains an invalid connection URL.")}let d=T(n.agent_id,"agent_id");if(d!==t)throw new i("Session response contains a mismatched Agent id.");let r=g(n.avatar_id),l=P(n.avatar),p=null;if(r||l){if(!r||!l||T(l.id,"avatar.id")!==r)throw new i("Session response contains mismatched Avatar metadata.");let h=T(l.engine,"avatar.engine");if(h!=="vrm")throw new i("Session response contains an unsupported Avatar engine.");let m=T(l.model_url,"avatar.model_url");try{let u=new URL(m),v=u.hostname==="localhost"||u.hostname==="127.0.0.1"||u.hostname==="[::1]";if(u.protocol!=="https:"&&!(u.protocol==="http:"&&v))throw new Error}catch{throw new i("Session response contains an invalid Avatar model URL.")}p={id:r,name:T(l.name,"avatar.name"),engine:h,modelUrl:m,assetHash:L(l.asset_hash),runtimeProfile:T(l.runtime_profile,"avatar.runtime_profile")}}return{id:T(n.session_id,"session_id"),agentId:d,...r?{avatarId:r}:{},avatar:p,inputMode:e,room:a,token:T(o.token,"connection.token")}}function q(s){let e=P(s);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 H(s){if(!s||typeof s!="object")return null;let e=s;if(e.type!=="agent-declined")return null;let t=g(e.reason);return t?{reason:t}:null}function W(s){let{avatar:e,room:t,token:n,...o}=s;return o}var _=class{room;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=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=K(e.apiOrigin),!U.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 R({adaptiveStream:!0,dynacast:!0,audioCaptureDefaults:{voiceIsolation:!1}});return e.on(S.ParticipantConnected,t=>{e===this.room&&this.handleParticipant(t)}),e.on(S.ParticipantDisconnected,t=>{e!==this.room||t.kind!==y.AGENT||(this.agentIdentity=null,this.currentStatus!=="disconnecting"&&(this.handleDisconnected("The Waffle Agent disconnected."),e.disconnect().catch(n=>{this.emit("error",n instanceof Error?n:new i("Could not close the Agent room."))})))}),e.on(S.ParticipantAttributesChanged,(t,n)=>{e===this.room&&this.handleParticipant(n)}),e.on(S.TrackSubscribed,(t,n,o)=>{e===this.room&&o.kind===y.AGENT&&t.kind===A.Kind.Audio&&this.setAudioTrack(t.mediaStreamTrack)}),e.on(S.TrackUnsubscribed,(t,n,o)=>{e===this.room&&o.kind===y.AGENT&&t.kind===A.Kind.Audio&&this.setAudioTrack(null)}),e.on(S.DataReceived,(t,n,o,a)=>{if(!(e!==this.room||n?.kind!==y.AGENT||o!==C.RELIABLE)&&!(a!=="avatar.performance"&&a!=="animated-waffle.turn"))try{let d=JSON.parse(new TextDecoder().decode(t));if(a==="animated-waffle.turn"){let l=H(d);l&&this.emit("declined",l);return}let r=q(d);r&&k(this,"avatar",r)}catch{}}),e.on(S.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!==y.AGENT)return;let a=t.info.attributes??{},d=g(a["lk.segment_id"]),r=g(a["lk.transcribed_track_id"]),l=a["lk.transcription_final"];if(!d||!r||l!=="true"&&l!=="false")return;let p=e.localParticipant.audioTrackPublications.has(r),h=o.audioTrackPublications.has(r);if(p===h)return;let m=p?"user":"assistant";if(n.identity!==(p?e.localParticipant.identity:o.identity))return;let u=m==="assistant"?x(a,"lk"):null;if(m==="assistant"&&!u||u&&(this.progressExecutions.set(d,u),this.completedProgressExecutions.has(u)))return;let v="";for await(let b of t){if(v+=b,e!==this.room||this.currentStatus!=="connected"||u&&this.completedProgressExecutions.has(u))return;if(v.length>0){let I={segmentId:d,role:m,text:v,complete:!1};this.progressText.set(d,I),this.emit("transcriptProgress",I)}}l==="true"&&v.length>0&&(this.emit("transcriptProgress",{segmentId:d,role:m,text:v,complete:!0}),this.progressExecutions.delete(d),this.progressText.delete(d))})().catch(o=>{this.emit("error",o instanceof Error?o:new i("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??{},d=g(a["animated-waffle.message_id"]),r=a["animated-waffle.role"];if(!d||r!=="user"&&r!=="assistant"||a["animated-waffle.final"]!=="true")return;let l=r==="assistant"?x(a,"animated-waffle"):null;l&&this.completeTranscriptExecution(l);let p=Number(t.info.timestamp);this.emit("transcript",{id:d,role:r,text:o,final:!0,...Number.isFinite(p)&&p>0?{timestamp:new Date(p).toISOString()}:{}})}).catch(o=>{this.emit("error",o instanceof Error?o:new i("Transcript stream failed."))})}),e}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?W(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(W(this.currentSession));if(this.startPromise)return this.startPromise;this.recreateRoomOnConnect&&(this.room=this.createRoom(),this.recreateRoomOnConnect=!1);let t=++this.lifecycleVersion,n=this.startInternal(e,t).finally(()=>{this.startPromise===n&&(this.startPromise=null)});return this.startPromise=n,n}disconnect(){if(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 o=e.sessionId??crypto.randomUUID(),a=crypto.randomUUID(),d=this.options.startSession,r=d?void 0:this.options.sessionContext,l=d?void 0:JSON.stringify({chat_session_id:o,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}:{}}),p=()=>this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,requestId:a,sessionId:o,...e.overrides?{overrides:e.overrides}:{},signal:n.signal})):this.resolveSessionToken().then(c=>fetch(this.apiOrigin+"/v1/agents/"+encodeURIComponent(this.options.agentId)+"/sessions",{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json",Authorization:"Bearer "+c,"Idempotency-Key":a},signal:n.signal,body:l})),h;try{h=await this.withTimeout(d?p():p().catch(c=>{if(n.signal.aborted)throw c;return p()}),O,"Timed out creating the Waffle session.",()=>n.abort())}catch(c){let f=c instanceof Error?c:new i("Could not create session.");throw t!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(f),f)}finally{this.requestAbort===n&&(this.requestAbort=null)}if(t!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");let m=await h.json().catch(()=>null);if(!h.ok){let c=P(m),f=new i(g(c?.detail)??"Could not create session.",h.status,g(c?.code));throw this.fail(f),f}let u;try{u=j(m,this.activeInputMode,this.options.agentId)}catch(c){let f=c instanceof Error?c:new i("Could not parse the Waffle session.");throw this.fail(f),f}this.currentSession=u,this.emit("session",W(u)),k(this,"presentation",u.avatar),this.setStatus("connecting");let v,b,I=new Promise((c,f)=>{v=c,b=f});this.readyWaiter={reject:b,resolve:v,version:t};let w=this.room;try{if(await w.connect(u.room,u.token),await w.startAudio(),this.options.inputAudioEnabled!==!1){let c=await w.localParticipant.setMicrophoneEnabled(!0),f=c?.audioTrack?.mediaStreamTrack;if(!c||c.isMuted||c.isUpstreamPaused||!f||!f.enabled||f.muted||f.readyState!=="live")throw new i("The microphone track did not publish.")}for(let c of w.remoteParticipants.values())this.handleParticipant(c);await this.withTimeout(I,M,"Timed out waiting for the Waffle Agent.",()=>{w.disconnect().catch(()=>{})})}catch(c){this.readyWaiter?.version===t&&(this.readyWaiter=null),this.currentStatus!=="disconnected"&&await w.disconnect().catch(()=>{});let f=c instanceof Error?c:new i("Could not connect.");throw t!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(f),f)}finally{this.readyWaiter?.version===t&&(this.readyWaiter=null)}if(t!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");return this.setStatus("connected"),W(u)}async stopInternal(){if(this.lifecycleVersion+=1,this.readyWaiter?.reject(new i("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 i('Push-to-talk controls require inputMode: "push-to-talk".');this.assertConnected();let t=this.agentIdentity;if(!t)throw new i("The Waffle Agent is not ready.");let n=this.room,o=this.turnControl.then(()=>n.localParticipant.performRpc({destinationIdentity:t,method:e,payload:""}));this.turnControl=o.then(()=>{},a=>{this.emit("error",a instanceof Error?a:new i("Push-to-talk control failed."))})}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.")}handleParticipant(e){if(e.kind===y.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===A.Kind.Audio){this.setAudioTrack(n.mediaStreamTrack);break}}}}handleDisconnected(e){this.readyWaiter?.reject(new i(e)),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),k(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",n instanceof Error?n:new i("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),k(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),k(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 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){return new Promise((a,d)=>{let r=setTimeout(()=>{o?.(),d(new i(n))},t);e.then(l=>{clearTimeout(r),a(l)},l=>{clearTimeout(r),d(l)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{_ as WaffleClient,i as WaffleError};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animated-waffle/client",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Framework-independent browser SDK for Narya Animated Waffle talking Agents.",
5
5
  "type": "module",
6
6
  "sideEffects": false,