@animated-waffle/client 0.3.3 → 0.3.5
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 +14 -0
- package/README.md +47 -1
- package/dist/index.d.ts +59 -3
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5
|
|
4
|
+
|
|
5
|
+
- Add lazy, creator-BFF-authorized private Developer Feedback for persisted
|
|
6
|
+
Agent Sessions and Assistant messages.
|
|
7
|
+
- Expose the server-authored Agent Studio link and named Avatar presentation
|
|
8
|
+
profile from managed Sessions.
|
|
9
|
+
|
|
10
|
+
## 0.3.4
|
|
11
|
+
|
|
12
|
+
- Classify public failures with `WaffleError.stage`; connection failures reject
|
|
13
|
+
and emit the same error instance.
|
|
14
|
+
- Report the integration family for observability with `reportedSource`,
|
|
15
|
+
defaulting to `SDK`.
|
|
16
|
+
|
|
3
17
|
## 0.3.3
|
|
4
18
|
|
|
5
19
|
- Start a fresh connection after an in-flight attempt is disconnected, including setup-cleanup-setup consumers such as React StrictMode.
|
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
|
|
@@ -82,8 +117,14 @@ const waffle = new WaffleClient({
|
|
|
82
117
|
provided; blank values are omitted. It is mutually exclusive with the
|
|
83
118
|
advanced `startSession` callback, so token-path consumers do not recreate
|
|
84
119
|
the raw managed-session request.
|
|
120
|
+
- `reportedSource?: 'Web' | 'SDK' | 'Client' | 'Unknown'` — self-reported
|
|
121
|
+
integration family for observability only. The SDK defaults to `SDK`.
|
|
85
122
|
- `connect({ inputMode?, sessionId?, overrides? })` — `'push-to-talk'`
|
|
86
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.
|
|
87
128
|
- `inputAudioEnabled: false` — never requests or enables the browser microphone;
|
|
88
129
|
text input, Agent audio, transcripts, and reconnects continue to work. Push-
|
|
89
130
|
to-talk controls fail closed with `input_audio_disabled`. The default is
|
|
@@ -97,6 +138,8 @@ const waffle = new WaffleClient({
|
|
|
97
138
|
- `disconnect()`, `sendText(text, { audioResponse? })`. `audioResponse` defaults
|
|
98
139
|
to `true`; `false` keeps the typed turn and transcript but suppresses reply
|
|
99
140
|
audio for that turn.
|
|
141
|
+
- `loadDeveloperFeedback()`, `submitDeveloperFeedback(input)` — available only
|
|
142
|
+
with the explicit creator BFF callback above.
|
|
100
143
|
- `startPushToTalk()` / `commitPushToTalk()` / `cancelPushToTalk()`.
|
|
101
144
|
- `on('connectionState' | 'session' | 'remoteAudioTrack' | 'transcriptProgress' | 'transcript' |
|
|
102
145
|
'agentSpeaking' | 'declined' | 'error', fn)` — `declined` carries the
|
|
@@ -108,7 +151,10 @@ const waffle = new WaffleClient({
|
|
|
108
151
|
- Transcript progress is ephemeral and keyed by a runtime `segmentId`; user ASR
|
|
109
152
|
and assistant playout update it incrementally until `complete` closes it.
|
|
110
153
|
- `connectionState`, `session`, `remoteAudioTrack` getters; `status` is the
|
|
111
|
-
deprecated 0.1 alias. Failures throw `WaffleError` with `code
|
|
154
|
+
deprecated 0.1 alias. Failures throw `WaffleError` with `code` and a typed
|
|
155
|
+
`stage`: `session`, `transport`, `audio`, `transcript`, `avatar-load`, or
|
|
156
|
+
`avatar-renderer`. A failed `connect()` emits and rejects with the same error
|
|
157
|
+
instance.
|
|
112
158
|
|
|
113
159
|
`./internal` is a private contract consumed by `@animated-waffle/react`; it is
|
|
114
160
|
not part of the public API and may change without notice.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,47 @@ type WaffleConnectionState = 'disconnected' | 'connecting' | 'connected' | 'disc
|
|
|
2
2
|
/** @deprecated Use WaffleConnectionState. */
|
|
3
3
|
type WaffleStatus = WaffleConnectionState;
|
|
4
4
|
type WaffleInputMode = 'automatic' | 'push-to-talk';
|
|
5
|
+
type WaffleDeveloperFeedbackAction = 'preserve' | 'change';
|
|
6
|
+
type WaffleDeveloperFeedbackDimension = 'persona' | 'instructions' | 'memory' | 'avatar' | 'voice' | 'tool_runtime' | 'unknown';
|
|
7
|
+
interface WaffleDeveloperFeedbackInput {
|
|
8
|
+
/** Reuse this UUID to retry the same append safely after an ambiguous failure. */
|
|
9
|
+
id?: string;
|
|
10
|
+
scope: 'session' | 'message';
|
|
11
|
+
targetMessageId?: string | null;
|
|
12
|
+
action: WaffleDeveloperFeedbackAction;
|
|
13
|
+
dimension: WaffleDeveloperFeedbackDimension;
|
|
14
|
+
note?: string | null;
|
|
15
|
+
preferredReply?: string | null;
|
|
16
|
+
}
|
|
17
|
+
interface WaffleDeveloperFeedbackState {
|
|
18
|
+
roundId: string | null;
|
|
19
|
+
pendingCount: number;
|
|
20
|
+
/** Server-authored continuation link. The SDK never constructs this URL. */
|
|
21
|
+
dashboardUrl: string | null;
|
|
22
|
+
}
|
|
23
|
+
interface WaffleDeveloperFeedbackAvailability {
|
|
24
|
+
sourceSessionId: string;
|
|
25
|
+
}
|
|
26
|
+
interface WaffleDeveloperFeedbackGrant {
|
|
27
|
+
endpoint: string;
|
|
28
|
+
token: string;
|
|
29
|
+
expiresAt: string;
|
|
30
|
+
sourceSessionId: string;
|
|
31
|
+
}
|
|
32
|
+
interface WaffleDeveloperFeedbackGrantRequest {
|
|
33
|
+
agentId: string;
|
|
34
|
+
sourceSessionId: string;
|
|
35
|
+
signal: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
type WaffleDeveloperFeedbackGrantProvider = (input: WaffleDeveloperFeedbackGrantRequest) => Response | Promise<Response>;
|
|
5
38
|
interface WaffleSession {
|
|
6
39
|
id: string;
|
|
7
40
|
agentId: string;
|
|
41
|
+
/** Server-authored management target for this exact Agent and source session. */
|
|
42
|
+
agentStudioUrl?: string;
|
|
8
43
|
avatarId?: string;
|
|
9
44
|
inputMode: WaffleInputMode;
|
|
45
|
+
developerFeedback?: WaffleDeveloperFeedbackAvailability;
|
|
10
46
|
}
|
|
11
47
|
interface WaffleTranscript {
|
|
12
48
|
/** The persisted conversation message id. */
|
|
@@ -31,7 +67,7 @@ interface WaffleClientEvents {
|
|
|
31
67
|
/** @deprecated Subscribe to connectionState. */
|
|
32
68
|
status: WaffleStatus;
|
|
33
69
|
session: WaffleSession | null;
|
|
34
|
-
error:
|
|
70
|
+
error: WaffleError;
|
|
35
71
|
agentSpeaking: boolean;
|
|
36
72
|
remoteAudioTrack: MediaStreamTrack | null;
|
|
37
73
|
transcriptProgress: WaffleTranscriptProgress;
|
|
@@ -80,6 +116,13 @@ interface WaffleClientBaseOptions {
|
|
|
80
116
|
* turn this off, or the voice is heard twice.
|
|
81
117
|
*/
|
|
82
118
|
audioPlayback?: boolean;
|
|
119
|
+
/** Self-reported integration family for observability only. Defaults to SDK. */
|
|
120
|
+
reportedSource?: 'Web' | 'SDK' | 'Client' | 'Unknown';
|
|
121
|
+
/**
|
|
122
|
+
* Ask a creator-authenticated backend for one short-lived, exact-Session
|
|
123
|
+
* feedback grant. The SDK calls this lazily and never handles an API key.
|
|
124
|
+
*/
|
|
125
|
+
developerFeedback?: WaffleDeveloperFeedbackGrantProvider;
|
|
83
126
|
}
|
|
84
127
|
interface WaffleTokenClientOptions extends WaffleClientBaseOptions {
|
|
85
128
|
/** A short-lived token minted for this Agent by the Narya API. */
|
|
@@ -122,6 +165,7 @@ interface WaffleSessionStartInput {
|
|
|
122
165
|
inputMode: WaffleInputMode;
|
|
123
166
|
requestId: string;
|
|
124
167
|
sessionId: string;
|
|
168
|
+
reportedSource: 'Web' | 'SDK' | 'Client' | 'Unknown';
|
|
125
169
|
overrides?: Readonly<WaffleSessionOverrides>;
|
|
126
170
|
signal: AbortSignal;
|
|
127
171
|
}
|
|
@@ -142,16 +186,20 @@ interface WaffleSessionClient {
|
|
|
142
186
|
on<K extends keyof WaffleClientEvents>(event: K, listener: (payload: WaffleClientEvents[K]) => void): () => void;
|
|
143
187
|
connect(options?: WaffleSessionStartOptions): Promise<WaffleSession>;
|
|
144
188
|
disconnect(): Promise<void>;
|
|
189
|
+
loadDeveloperFeedback(): Promise<WaffleDeveloperFeedbackState>;
|
|
190
|
+
submitDeveloperFeedback(input: WaffleDeveloperFeedbackInput): Promise<WaffleDeveloperFeedbackState>;
|
|
145
191
|
sendText(text: string, options?: WaffleSendTextOptions): Promise<void>;
|
|
146
192
|
startPushToTalk(): void;
|
|
147
193
|
commitPushToTalk(): void;
|
|
148
194
|
cancelPushToTalk(): void;
|
|
149
195
|
}
|
|
150
196
|
type Listener<K extends keyof WaffleClientEvents> = (payload: WaffleClientEvents[K]) => void;
|
|
197
|
+
type WaffleErrorStage = 'session' | 'transport' | 'audio' | 'transcript' | 'avatar-load' | 'avatar-renderer';
|
|
151
198
|
declare class WaffleError extends Error {
|
|
152
199
|
readonly status?: number | undefined;
|
|
153
200
|
readonly code?: string | undefined;
|
|
154
|
-
|
|
201
|
+
readonly stage: WaffleErrorStage;
|
|
202
|
+
constructor(message: string, status?: number | undefined, code?: string | undefined, stage?: WaffleErrorStage, options?: ErrorOptions);
|
|
155
203
|
}
|
|
156
204
|
declare class WaffleClient implements WaffleSessionClient {
|
|
157
205
|
private room;
|
|
@@ -178,6 +226,9 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
178
226
|
private turnControl;
|
|
179
227
|
private lifecycleVersion;
|
|
180
228
|
private recreateRoomOnConnect;
|
|
229
|
+
private developerFeedbackGrant;
|
|
230
|
+
private developerFeedbackGrantPromise;
|
|
231
|
+
private developerFeedbackGrantAbort;
|
|
181
232
|
constructor(options: WaffleClientOptions);
|
|
182
233
|
private createRoom;
|
|
183
234
|
get status(): WaffleStatus;
|
|
@@ -190,6 +241,8 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
190
241
|
connect(options?: WaffleConnectOptions): Promise<WaffleSession>;
|
|
191
242
|
disconnect(): Promise<void>;
|
|
192
243
|
sendText(text: string, options?: WaffleSendTextOptions): Promise<void>;
|
|
244
|
+
loadDeveloperFeedback(): Promise<WaffleDeveloperFeedbackState>;
|
|
245
|
+
submitDeveloperFeedback(input: WaffleDeveloperFeedbackInput): Promise<WaffleDeveloperFeedbackState>;
|
|
193
246
|
startPushToTalk(): void;
|
|
194
247
|
commitPushToTalk(): void;
|
|
195
248
|
/** @deprecated Use commitPushToTalk(). */
|
|
@@ -200,6 +253,8 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
200
253
|
private sendInputControl;
|
|
201
254
|
private assertPushToTalkAvailable;
|
|
202
255
|
private assertConnected;
|
|
256
|
+
private requestDeveloperFeedback;
|
|
257
|
+
private resolveDeveloperFeedbackGrant;
|
|
203
258
|
private handleParticipant;
|
|
204
259
|
private handleDisconnected;
|
|
205
260
|
private setAudioTrack;
|
|
@@ -207,10 +262,11 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
207
262
|
private completeTranscriptExecution;
|
|
208
263
|
private clearTranscriptProgress;
|
|
209
264
|
private setStatus;
|
|
265
|
+
private clearDeveloperFeedbackGrant;
|
|
210
266
|
private fail;
|
|
211
267
|
private resolveSessionToken;
|
|
212
268
|
private withTimeout;
|
|
213
269
|
private emit;
|
|
214
270
|
}
|
|
215
271
|
|
|
216
|
-
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 };
|
|
272
|
+
export { 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 C,ParticipantKind as k,Room as R,RoomEvent as w,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,s){super(t);this.status=n;this.code=s;this.name="WaffleError"}status;code};function P(o){return o!==null&&typeof o=="object"&&!Array.isArray(o)?o:null}function K(o=N){let e;try{e=new URL(o)}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(o,e){if(typeof o!="string"||o.trim().length===0)throw new i(`Session response is missing ${e}.`);return o}function L(o){let e=T(o,"avatar.asset_hash");if(!D.test(e))throw new i("Session response contains an invalid Avatar asset hash.");return e}function E(o){return typeof o=="number"&&Number.isFinite(o)?o:void 0}function g(o){return typeof o=="string"&&o.length>0?o:void 0}function x(o,e){let t=g(o[`${e}.speech_id`]),n=Number(o[`${e}.speech_step_index`]);return t&&Number.isSafeInteger(n)&&n>0?JSON.stringify([t,n]):null}function V(o,e,t){let n=P(o);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 s=P(n.connection);if(!s||s.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let a=T(s.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(s.token,"connection.token")}}function j(o){let e=P(o);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 q(o){if(!o||typeof o!="object")return null;let e=o;if(e.type!=="agent-declined")return null;let t=g(e.reason);return t?{reason:t}:null}function W(o){let{avatar:e,room:t,token:n,...s}=o;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=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(w.ParticipantConnected,t=>{e===this.room&&this.handleParticipant(t)}),e.on(w.ParticipantDisconnected,t=>{e!==this.room||t.kind!==k.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(w.ParticipantAttributesChanged,(t,n)=>{e===this.room&&this.handleParticipant(n)}),e.on(w.TrackSubscribed,(t,n,s)=>{e===this.room&&s.kind===k.AGENT&&t.kind===A.Kind.Audio&&this.setAudioTrack(t.mediaStreamTrack)}),e.on(w.TrackUnsubscribed,(t,n,s)=>{e===this.room&&s.kind===k.AGENT&&t.kind===A.Kind.Audio&&this.setAudioTrack(null)}),e.on(w.DataReceived,(t,n,s,a)=>{if(!(e!==this.room||n?.kind!==k.AGENT||s!==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=q(d);l&&this.emit("declined",l);return}let r=j(d);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!==k.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=s.audioTrackPublications.has(r);if(p===h)return;let m=p?"user":"assistant";if(n.identity!==(p?e.localParticipant.identity:s.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(s=>{this.emit("error",s instanceof Error?s:new i("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 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:s,final:!0,...Number.isFinite(p)&&p>0?{timestamp:new Date(p).toISOString()}:{}})}).catch(s=>{this.emit("error",s instanceof Error?s: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&&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 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}:{}})}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(),a=crypto.randomUUID(),d=this.options.startSession,r=d?void 0:this.options.sessionContext,l=d?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}:{}}),p=()=>this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,requestId:a,sessionId:s,...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=V(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)),y(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 S=this.room;try{if(await S.connect(u.room,u.token),await S.startAudio(),this.options.inputAudioEnabled!==!1){let c=await S.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 S.remoteParticipants.values())this.handleParticipant(c);await this.withTimeout(I,M,"Timed out waiting for the Waffle Agent.",()=>{S.disconnect().catch(()=>{})})}catch(c){this.readyWaiter?.version===t&&(this.readyWaiter=null),this.currentStatus!=="disconnected"&&await S.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.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,s=this.turnControl.then(()=>n.localParticipant.performRpc({destinationIdentity:t,method:e,payload:""}));this.turnControl=s.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===k.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),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",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),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 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,s){return new Promise((a,d)=>{let r=setTimeout(()=>{s?.(),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 I}from"./chunk-BXRNROI7.js";import{DataPacket_Kind as M,ParticipantKind as P,Room as G,RoomEvent as W,Track as x}from"livekit-client";var D=15e3,U=3e4,N="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,K=/^sha256:[a-f0-9]{64}$/,i=class extends Error{constructor(t,n,o,s="session",l){super(t,l);this.status=n;this.code=o;this.stage=s;this.name="WaffleError"}status;code;stage};function b(r,e,t){return r instanceof i?r:new i(r instanceof Error?r.message:t,void 0,void 0,e,{cause:r})}function y(r){return r!==null&&typeof r=="object"&&!Array.isArray(r)?r:null}function q(r=N){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 L(r){let e=v(r,"avatar.asset_hash");if(!K.test(e))throw new i("Session response contains an invalid Avatar asset hash.");return e}function F(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 C(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 j(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 E(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 V(r,e,t,n){let o=y(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 s=y(o.connection);if(!s||s.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let l=v(s.url,"connection.url");try{let f=new URL(l),k=f.hostname==="localhost"||f.hostname==="127.0.0.1"||f.hostname==="[::1]";if(f.protocol!=="wss:"&&!(f.protocol==="ws:"&&k))throw new Error}catch{throw new i("Session response contains an invalid connection URL.")}let a=v(o.agent_id,"agent_id");if(a!==t)throw new i("Session response contains a mismatched Agent id.");let c=g(o.agent_studio_url);if(c)try{let f=new URL(c),k=f.hostname==="localhost"||f.hostname==="127.0.0.1"||f.hostname==="[::1]";if(f.protocol!=="https:"&&!(f.protocol==="http:"&&k))throw new Error}catch{throw new i("Session response contains an invalid Agent Studio URL.")}let d=g(o.avatar_id),p=y(o.avatar),w=null;if(d||p){if(!d||!p||v(p.id,"avatar.id")!==d)throw new i("Session response contains mismatched Avatar metadata.");let f=v(p.engine,"avatar.engine");if(f!=="vrm")throw new i("Session response contains an unsupported Avatar engine.");let k=v(p.model_url,"avatar.model_url");try{let S=new URL(k),T=S.hostname==="localhost"||S.hostname==="127.0.0.1"||S.hostname==="[::1]";if(S.protocol!=="https:"&&!(S.protocol==="http:"&&T))throw new Error}catch{throw new i("Session response contains an invalid Avatar model URL.")}w={id:d,name:v(p.name,"avatar.name"),engine:f,modelUrl:k,assetHash:L(p.asset_hash),runtimeProfile:v(p.runtime_profile,"avatar.runtime_profile"),...H(p.presentation_profile)}}let h=v(o.session_id,"session_id");return{id:h,agentId:a,...c?{agentStudioUrl:c}:{},...d?{avatarId:d}:{},...n?{developerFeedback:{sourceSessionId:h}}:{},avatar:w,inputMode:e,room:l,token:v(s.token,"connection.token")}}function H(r){if(r===void 0)return{};let e=y(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 B(r){let e=y(r);if(!e||typeof e.type!="string")return null;let t={contextId:g(e.contextId)??null,segmentId:g(e.segmentId),occurrence:F(e.occurrence),audioTime:F(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 $(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 _(r){let{avatar:e,room:t,token:n,...o}=r;return o}var R=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;developerFeedbackGrant=null;developerFeedbackGrantPromise=null;developerFeedbackGrantAbort=null;constructor(e){if(this.options=e,this.apiOrigin=q(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(W.ParticipantConnected,t=>{e===this.room&&this.handleParticipant(t)}),e.on(W.ParticipantDisconnected,t=>{e!==this.room||t.kind!==P.AGENT||(this.agentIdentity=null,this.currentStatus!=="disconnecting"&&(this.handleDisconnected("The Waffle Agent disconnected."),e.disconnect().catch(n=>{this.emit("error",b(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,o)=>{e===this.room&&o.kind===P.AGENT&&t.kind===x.Kind.Audio&&this.setAudioTrack(t.mediaStreamTrack)}),e.on(W.TrackUnsubscribed,(t,n,o)=>{e===this.room&&o.kind===P.AGENT&&t.kind===x.Kind.Audio&&this.setAudioTrack(null)}),e.on(W.DataReceived,(t,n,o,s)=>{if(!(e!==this.room||n?.kind!==P.AGENT||o!==M.RELIABLE)&&!(s!=="avatar.performance"&&s!=="animated-waffle.turn"))try{let l=JSON.parse(new TextDecoder().decode(t));if(s==="animated-waffle.turn"){let c=$(l);c&&this.emit("declined",c);return}let a=B(l);a&&I(this,"avatar",a)}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 o=this.agentIdentity?e.remoteParticipants.get(this.agentIdentity):void 0;if(e!==this.room||this.currentStatus!=="connected"||o?.kind!==P.AGENT)return;let s=t.info.attributes??{},l=g(s["lk.segment_id"]),a=g(s["lk.transcribed_track_id"]),c=s["lk.transcription_final"];if(!l||!a||c!=="true"&&c!=="false")return;let d=e.localParticipant.audioTrackPublications.has(a),p=o.audioTrackPublications.has(a);if(d===p)return;let w=d?"user":"assistant";if(n.identity!==(d?e.localParticipant.identity:o.identity))return;let h=w==="assistant"?E(s,"lk"):null;if(w==="assistant"&&!h||h&&(this.progressExecutions.set(l,h),this.completedProgressExecutions.has(h)))return;let f="";for await(let k of t){if(f+=k,e!==this.room||this.currentStatus!=="connected"||h&&this.completedProgressExecutions.has(h))return;if(f.length>0){let S={segmentId:l,role:w,text:f,complete:!1};this.progressText.set(l,S),this.emit("transcriptProgress",S)}}c==="true"&&f.length>0&&(this.emit("transcriptProgress",{segmentId:l,role:w,text:f,complete:!0}),this.progressExecutions.delete(l),this.progressText.delete(l))})().catch(o=>{this.emit("error",b(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 s=t.info.attributes??{},l=g(s["animated-waffle.message_id"]),a=s["animated-waffle.role"];if(!l||a!=="user"&&a!=="assistant"||s["animated-waffle.final"]!=="true")return;let c=a==="assistant"?E(s,"animated-waffle"):null;c&&this.completeTranscriptExecution(c);let d=Number(t.info.timestamp);this.emit("transcript",{id:l,role:a,text:o,final:!0,...Number.isFinite(d)&&d>0?{timestamp:new Date(d).toISOString()}:{}})}).catch(o=>{this.emit("error",b(o,"transcript","Transcript stream failed."))})}),e}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?_(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(_(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 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){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(),s=crypto.randomUUID(),l=this.options.startSession,a=l?void 0:this.options.sessionContext,c=l?void 0:JSON.stringify({chat_session_id:o,input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic",...e.overrides?{overrides:e.overrides}:{},...a?.userName?.trim()?{user_name:a.userName}:{},...a?.timeZone?.trim()?{time_zone:a.timeZone}:{}}),d=()=>this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,requestId:s,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":s,"X-Animated-Waffle-Reported-Source":this.options.reportedSource??"SDK"},signal:n.signal,body:c})),p;try{p=await this.withTimeout(l?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=b(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 w=await p.json().catch(()=>null);if(!p.ok){let u=y(w),m=new i(g(u?.detail)??"Could not create session.",p.status,g(u?.code));throw this.fail(m),m}let h;try{h=V(w,this.activeInputMode,this.options.agentId,typeof this.options.developerFeedback=="function")}catch(u){let m=b(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",_(h)),I(this,"presentation",h.avatar),this.setStatus("connecting");let f,k,S=new Promise((u,m)=>{f=u,k=m});this.readyWaiter={reject:k,resolve:f,version:t};let T=this.room;try{if(await T.connect(h.room,h.token).catch(u=>{throw b(u,"transport","Could not connect to the Agent room.")}),await T.startAudio().catch(u=>{throw b(u,"audio","Could not start Agent audio.")}),this.options.inputAudioEnabled!==!1){let u=await T.localParticipant.setMicrophoneEnabled(!0).catch(O=>{throw b(O,"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 T.remoteParticipants.values())this.handleParticipant(u);await this.withTimeout(S,U,"Timed out waiting for the Waffle Agent.",()=>{T.disconnect().catch(()=>{})},"transport")}catch(u){this.readyWaiter?.version===t&&(this.readyWaiter=null),this.currentStatus!=="disconnected"&&await T.disconnect().catch(()=>{});let m=b(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"),_(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.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(()=>{},s=>{this.emit("error",b(s,"transport","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.")}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 s=await o.json().catch(()=>null);if(!o.ok){let p=y(s);throw new i(g(p?.detail)??"Could not save Developer Feedback.",o.status,g(p?.code))}let l=y(s),a=l?.round_id,c=l?.pending_count,d=l?.dashboard_url;if(a!==null&&(typeof a!="string"||!A.test(a))||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"&&C(d,"dashboard_url"),{roundId:a,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 s=>{let l=await s.json().catch(()=>null);if(!s.ok){let d=y(l);throw new i(g(d?.detail)??"Could not authorize Developer Feedback.",s.status,g(d?.code))}let a=y(l),c={endpoint:C(a?.endpoint,"developerFeedback.endpoint"),token:v(a?.token,"developerFeedback.token"),expiresAt:j(a?.expires_at,"developer_feedback.expires_at"),sourceSessionId:v(a?.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===P.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===x.Kind.Audio){this.setAudioTrack(n.mediaStreamTrack);break}}}}handleDisconnected(e){this.readyWaiter?.reject(new i(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),I(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",b(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),I(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),I(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,s="session"){return new Promise((l,a)=>{let c=setTimeout(()=>{o?.(),a(new i(n,void 0,void 0,s))},t);e.then(d=>{clearTimeout(c),l(d)},d=>{clearTimeout(c),a(d)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{R as WaffleClient,i as WaffleError};
|
package/dist/internal.d.ts
CHANGED
|
@@ -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;
|