@fishaudio/agent-client 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,924 @@
1
+ import {
2
+ FishAgentError,
3
+ __privateAdd,
4
+ __privateGet,
5
+ __privateMethod,
6
+ __privateSet,
7
+ __privateWrapper
8
+ } from "./chunk-PEVOZB5Y.js";
9
+
10
+ // src/audio/output.ts
11
+ function assertOutputSelectionSupported() {
12
+ if (typeof HTMLMediaElement === "undefined" || !("setSinkId" in HTMLMediaElement.prototype)) {
13
+ throw new FishAgentError(
14
+ "device_change_failed",
15
+ "This browser does not support selecting an audio output device"
16
+ );
17
+ }
18
+ }
19
+ var AudioOutput = class {
20
+ constructor() {
21
+ this.volume = 1;
22
+ }
23
+ ensureElement() {
24
+ if (typeof document === "undefined") {
25
+ return void 0;
26
+ }
27
+ if (!this.element) {
28
+ this.element = document.createElement("audio");
29
+ this.element.autoplay = true;
30
+ this.element.setAttribute("playsinline", "");
31
+ this.element.volume = this.volume;
32
+ }
33
+ return this.element;
34
+ }
35
+ setStream(stream) {
36
+ const element = this.ensureElement();
37
+ if (!element) {
38
+ return;
39
+ }
40
+ element.srcObject = stream;
41
+ void element.play().catch(() => {
42
+ });
43
+ }
44
+ /** Call from a user gesture to satisfy browser autoplay policies. */
45
+ async startAudio() {
46
+ await this.element?.play();
47
+ }
48
+ /**
49
+ * Route playback to an output device. The element is created on demand so
50
+ * the browser validates the device id right here, even before the agent's
51
+ * audio arrives. Rejects with `device_change_failed` where output selection
52
+ * is unsupported or the device cannot be used.
53
+ */
54
+ async setSinkId(sinkId) {
55
+ assertOutputSelectionSupported();
56
+ const element = this.ensureElement();
57
+ try {
58
+ await element?.setSinkId(sinkId);
59
+ } catch (error) {
60
+ throw new FishAgentError(
61
+ "device_change_failed",
62
+ "Could not switch to the requested audio output device",
63
+ { cause: error }
64
+ );
65
+ }
66
+ }
67
+ setVolume(volume) {
68
+ this.volume = Math.min(1, Math.max(0, volume));
69
+ if (this.element) {
70
+ this.element.volume = this.volume;
71
+ }
72
+ }
73
+ getVolume() {
74
+ return this.volume;
75
+ }
76
+ dispose() {
77
+ if (this.element) {
78
+ this.element.srcObject = null;
79
+ this.element.remove();
80
+ this.element = void 0;
81
+ }
82
+ }
83
+ };
84
+
85
+ // src/audio/analysis.ts
86
+ var AudioStreamAnalyser = class {
87
+ setStream(stream) {
88
+ if (typeof AudioContext === "undefined") {
89
+ return;
90
+ }
91
+ this.context ?? (this.context = new AudioContext());
92
+ this.analyser ?? (this.analyser = this.context.createAnalyser());
93
+ this.source?.disconnect();
94
+ this.source = this.context.createMediaStreamSource(stream);
95
+ this.source.connect(this.analyser);
96
+ this.resume();
97
+ }
98
+ /** AudioContexts start suspended under autoplay policies; safe to call any time. */
99
+ resume() {
100
+ if (this.context?.state === "suspended") {
101
+ void this.context.resume().catch(() => {
102
+ });
103
+ }
104
+ }
105
+ /** RMS level in [0, 1]. */
106
+ getVolume() {
107
+ if (!this.analyser) {
108
+ return 0;
109
+ }
110
+ const samples = new Uint8Array(this.analyser.fftSize);
111
+ this.analyser.getByteTimeDomainData(samples);
112
+ let sumOfSquares = 0;
113
+ for (const sample of samples) {
114
+ const centered = (sample - 128) / 128;
115
+ sumOfSquares += centered * centered;
116
+ }
117
+ return Math.sqrt(sumOfSquares / samples.length);
118
+ }
119
+ getFrequencyData() {
120
+ if (!this.analyser) {
121
+ return new Uint8Array(0);
122
+ }
123
+ const data = new Uint8Array(this.analyser.frequencyBinCount);
124
+ this.analyser.getByteFrequencyData(data);
125
+ return data;
126
+ }
127
+ dispose() {
128
+ this.source?.disconnect();
129
+ this.analyser?.disconnect();
130
+ void this.context?.close().catch(() => {
131
+ });
132
+ this.source = void 0;
133
+ this.analyser = void 0;
134
+ this.context = void 0;
135
+ }
136
+ };
137
+
138
+ // src/sessionToken.ts
139
+ import {
140
+ SESSION_TRANSPORT_LIVEKIT
141
+ } from "@fishaudio/agent-protocol";
142
+ var SDK_VERSION = "0.0.1";
143
+ var DEFAULT_SERVER_URL = "https://api.fish.audio";
144
+ var SUPPORTED_TRANSPORTS = [SESSION_TRANSPORT_LIVEKIT];
145
+ function validateSessionToken(sessionToken) {
146
+ const transport = sessionToken.transport;
147
+ if (!SUPPORTED_TRANSPORTS.includes(transport)) {
148
+ throw new FishAgentError(
149
+ "unsupported_transport",
150
+ `Session token uses transport "${transport}" which this SDK version does not support; please upgrade @fishaudio/agent-client`
151
+ );
152
+ }
153
+ const expiresAt = Date.parse(sessionToken.expires_at);
154
+ if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {
155
+ throw new FishAgentError(
156
+ "session_expired",
157
+ "The session token's join deadline has passed; request a new session"
158
+ );
159
+ }
160
+ return sessionToken;
161
+ }
162
+ async function resolveSessionToken(options) {
163
+ const provided = [options.agentId, options.sessionToken].filter((value) => value !== void 0);
164
+ if (provided.length !== 1) {
165
+ throw new TypeError(
166
+ "Exactly one of `agentId` or `sessionToken` must be provided to start a session"
167
+ );
168
+ }
169
+ if (options.sessionToken) {
170
+ return validateSessionToken(options.sessionToken);
171
+ }
172
+ return validateSessionToken(await createPublicSession(options));
173
+ }
174
+ function detectClientTimezone() {
175
+ try {
176
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || void 0;
177
+ } catch {
178
+ return void 0;
179
+ }
180
+ }
181
+ async function createPublicSession(options) {
182
+ const serverUrl = (options.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/$/, "");
183
+ const clientTimezone = detectClientTimezone();
184
+ const request = {
185
+ agent_id: options.agentId,
186
+ ...options.language ? { language: options.language } : {},
187
+ ...options.timezone ? { timezone: options.timezone } : {},
188
+ ...clientTimezone ? { client_timezone: clientTimezone } : {},
189
+ ...options.worldContext !== void 0 ? { world_context: options.worldContext } : {},
190
+ ...options.overrides ? { overrides: options.overrides } : {},
191
+ ...options.dynamicVariables ? { dynamic_variables: options.dynamicVariables } : {},
192
+ ...options.endUserId ? { end_user_id: options.endUserId } : {},
193
+ ...options.metadata ? { metadata: options.metadata } : {},
194
+ ...options.toolEvents !== void 0 ? { tool_events: options.toolEvents } : {}
195
+ };
196
+ let response;
197
+ try {
198
+ response = await fetch(`${serverUrl}/v1/agent/sessions`, {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ "X-Fish-SDK": `agent-client/${SDK_VERSION}`
203
+ },
204
+ body: JSON.stringify(request)
205
+ });
206
+ } catch (cause) {
207
+ throw new FishAgentError("session_request_failed", "Could not reach the session endpoint", {
208
+ cause
209
+ });
210
+ }
211
+ if (!response.ok) {
212
+ throw await createSessionError(response);
213
+ }
214
+ return await response.json();
215
+ }
216
+ async function createSessionError(response) {
217
+ let message = `Session request failed with HTTP ${response.status}`;
218
+ let code;
219
+ try {
220
+ const body = await response.json();
221
+ message = body.message ?? body.error ?? message;
222
+ code = body.code;
223
+ } catch {
224
+ }
225
+ const options = { statusCode: response.status };
226
+ if (response.status === 409 && code === "unsupported_transport") {
227
+ return new FishAgentError(
228
+ "unsupported_transport",
229
+ `${message}; please upgrade @fishaudio/agent-client`,
230
+ options
231
+ );
232
+ }
233
+ if (response.status === 403) {
234
+ return /origin/i.test(message) ? new FishAgentError("origin_forbidden", message, options) : new FishAgentError("agent_not_public", message, options);
235
+ }
236
+ return new FishAgentError("session_request_failed", message, options);
237
+ }
238
+
239
+ // src/session/emitter.ts
240
+ var TypedEmitter = class {
241
+ constructor() {
242
+ this.listeners = /* @__PURE__ */ new Map();
243
+ }
244
+ on(event, listener) {
245
+ let set = this.listeners.get(event);
246
+ if (!set) {
247
+ set = /* @__PURE__ */ new Set();
248
+ this.listeners.set(event, set);
249
+ }
250
+ set.add(listener);
251
+ return this;
252
+ }
253
+ off(event, listener) {
254
+ this.listeners.get(event)?.delete(listener);
255
+ return this;
256
+ }
257
+ once(event, listener) {
258
+ const wrapper = ((...args) => {
259
+ this.off(event, wrapper);
260
+ listener(...args);
261
+ });
262
+ return this.on(event, wrapper);
263
+ }
264
+ emit(event, ...args) {
265
+ const set = this.listeners.get(event);
266
+ if (!set) {
267
+ return;
268
+ }
269
+ for (const listener of [...set]) {
270
+ try {
271
+ listener(...args);
272
+ } catch (error) {
273
+ console.error("[fish-agent] listener error", error);
274
+ }
275
+ }
276
+ }
277
+ };
278
+
279
+ // src/session/toolDispatcher.ts
280
+ var DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15e3;
281
+ var ClientToolDispatcher = class {
282
+ constructor(tools, timeoutMs, send, onError) {
283
+ this.timeoutMs = timeoutMs;
284
+ this.send = send;
285
+ this.onError = onError;
286
+ this.tools = /* @__PURE__ */ new Map();
287
+ for (const [name, handler] of Object.entries(tools ?? {})) {
288
+ this.tools.set(name, handler);
289
+ }
290
+ }
291
+ register(name, handler) {
292
+ this.tools.set(name, handler);
293
+ }
294
+ async dispatch(call) {
295
+ const handler = this.tools.get(call.toolName);
296
+ if (!handler) {
297
+ this.onError(
298
+ new FishAgentError("tool_failed", `Client tool "${call.toolName}" is not registered`)
299
+ );
300
+ if (call.expectsResponse) {
301
+ await this.reply(call.callId, { isError: true, result: "Tool is not registered" });
302
+ }
303
+ return;
304
+ }
305
+ try {
306
+ const result = await this.withTimeout(
307
+ Promise.resolve(handler(call.params, { callId: call.callId, toolName: call.toolName })),
308
+ call.toolName
309
+ );
310
+ if (call.expectsResponse) {
311
+ await this.reply(call.callId, result === void 0 ? {} : { result });
312
+ }
313
+ } catch (error) {
314
+ this.onError(
315
+ error instanceof FishAgentError ? error : new FishAgentError("tool_failed", `Client tool "${call.toolName}" threw`, {
316
+ cause: error
317
+ })
318
+ );
319
+ if (call.expectsResponse) {
320
+ await this.reply(call.callId, { isError: true, result: String(error) });
321
+ }
322
+ }
323
+ }
324
+ async reply(callId, body) {
325
+ try {
326
+ await this.send({ type: "client_tool.result", callId, ...body });
327
+ } catch (error) {
328
+ this.onError(
329
+ new FishAgentError("tool_failed", "Failed to deliver a client tool result", {
330
+ cause: error
331
+ })
332
+ );
333
+ }
334
+ }
335
+ withTimeout(promise, toolName) {
336
+ return new Promise((resolve, reject) => {
337
+ const timer = setTimeout(() => {
338
+ reject(
339
+ new FishAgentError(
340
+ "tool_timeout",
341
+ `Client tool "${toolName}" timed out after ${this.timeoutMs}ms`
342
+ )
343
+ );
344
+ }, this.timeoutMs);
345
+ promise.then(
346
+ (value) => {
347
+ clearTimeout(timer);
348
+ resolve(value);
349
+ },
350
+ (error) => {
351
+ clearTimeout(timer);
352
+ reject(error instanceof Error ? error : new Error(String(error)));
353
+ }
354
+ );
355
+ });
356
+ }
357
+ };
358
+
359
+ // src/session/wakeLock.ts
360
+ var _sentinel, _active, _onVisibilityChange, _WakeLockHolder_instances, request_fn;
361
+ var WakeLockHolder = class {
362
+ constructor() {
363
+ __privateAdd(this, _WakeLockHolder_instances);
364
+ __privateAdd(this, _sentinel);
365
+ __privateAdd(this, _active, false);
366
+ __privateAdd(this, _onVisibilityChange, () => {
367
+ if (document.visibilityState === "visible") {
368
+ void __privateMethod(this, _WakeLockHolder_instances, request_fn).call(this);
369
+ }
370
+ });
371
+ }
372
+ acquire() {
373
+ if (__privateGet(this, _active) || typeof document === "undefined" || typeof navigator === "undefined" || !navigator.wakeLock) {
374
+ return;
375
+ }
376
+ __privateSet(this, _active, true);
377
+ document.addEventListener("visibilitychange", __privateGet(this, _onVisibilityChange));
378
+ void __privateMethod(this, _WakeLockHolder_instances, request_fn).call(this);
379
+ }
380
+ release() {
381
+ if (!__privateGet(this, _active)) {
382
+ return;
383
+ }
384
+ __privateSet(this, _active, false);
385
+ document.removeEventListener("visibilitychange", __privateGet(this, _onVisibilityChange));
386
+ void __privateGet(this, _sentinel)?.release().catch(() => void 0);
387
+ __privateSet(this, _sentinel, void 0);
388
+ }
389
+ };
390
+ _sentinel = new WeakMap();
391
+ _active = new WeakMap();
392
+ _onVisibilityChange = new WeakMap();
393
+ _WakeLockHolder_instances = new WeakSet();
394
+ request_fn = async function() {
395
+ if (!__privateGet(this, _active)) {
396
+ return;
397
+ }
398
+ try {
399
+ const sentinel = await navigator.wakeLock.request("screen");
400
+ if (!__privateGet(this, _active)) {
401
+ void sentinel.release().catch(() => void 0);
402
+ return;
403
+ }
404
+ const previous = __privateGet(this, _sentinel);
405
+ __privateSet(this, _sentinel, sentinel);
406
+ if (previous && previous !== sentinel) {
407
+ void previous.release().catch(() => void 0);
408
+ }
409
+ } catch {
410
+ }
411
+ };
412
+
413
+ // src/session/agentSession.ts
414
+ var AGENT_JOIN_TIMEOUT_MS = 15e3;
415
+ var startWith;
416
+ var _status, _mode, _endReason, _sessionId, _micMuted, _endedByClient, _ending, _transport, _tools, _output, _outputAnalyser, _inputAnalyser, _wakeLock, _agentSegments, _transcript, _transcriptIndex, _typedMessageCount, _agentPresent, _pendingMessages, _joinTimer, _AgentSession_instances, endOnce_fn, _AgentSession_static, startWith_fn, asStartError_fn, asMicError_fn, asDeviceError_fn, sendClientEvent_fn, setStatus_fn, setMode_fn, handleAgentPresent_fn, abandonAgentWait_fn, handleConnectionState_fn, finalize_fn, handleAgentEvent_fn, recordSegment_fn, handleTranscription_fn, recordTypedUserMessage_fn;
417
+ var _AgentSession = class _AgentSession extends TypedEmitter {
418
+ constructor(sessionToken) {
419
+ super();
420
+ __privateAdd(this, _AgentSession_instances);
421
+ __privateAdd(this, _status, "connecting");
422
+ __privateAdd(this, _mode, "listening");
423
+ __privateAdd(this, _endReason);
424
+ __privateAdd(this, _sessionId);
425
+ __privateAdd(this, _micMuted, false);
426
+ __privateAdd(this, _endedByClient, false);
427
+ __privateAdd(this, _ending);
428
+ __privateAdd(this, _transport);
429
+ __privateAdd(this, _tools);
430
+ __privateAdd(this, _output, new AudioOutput());
431
+ __privateAdd(this, _outputAnalyser, new AudioStreamAnalyser());
432
+ __privateAdd(this, _inputAnalyser, new AudioStreamAnalyser());
433
+ __privateAdd(this, _wakeLock, new WakeLockHolder());
434
+ /** Text already delivered per agent segment, to derive deltas from full-text updates. */
435
+ __privateAdd(this, _agentSegments, /* @__PURE__ */ new Map());
436
+ /** Transcript so far, for late subscribers (getTranscript); upserted per segment. */
437
+ __privateAdd(this, _transcript, []);
438
+ __privateAdd(this, _transcriptIndex, /* @__PURE__ */ new Map());
439
+ __privateAdd(this, _typedMessageCount, 0);
440
+ // Data frames reach only participants already in the room — and the worker
441
+ // registers its handlers while starting up — so typed messages wait here
442
+ // until the agent publishes its first pipeline state.
443
+ __privateAdd(this, _agentPresent, false);
444
+ __privateAdd(this, _pendingMessages, []);
445
+ __privateAdd(this, _joinTimer);
446
+ __privateSet(this, _sessionId, sessionToken.session_id);
447
+ }
448
+ /**
449
+ * Obtain a session token (if needed), connect, publish the microphone and
450
+ * hand back a live session. Rejects with FishAgentError on session-request,
451
+ * permission or connect failures.
452
+ */
453
+ static async start(options) {
454
+ var _a;
455
+ const { LiveKitTransport } = await import("./livekit-B62YPROC.js");
456
+ return __privateMethod(_a = _AgentSession, _AgentSession_static, startWith_fn).call(_a, options, () => new LiveKitTransport());
457
+ }
458
+ // ---- getters ----
459
+ get sessionId() {
460
+ return __privateGet(this, _sessionId);
461
+ }
462
+ get status() {
463
+ return __privateGet(this, _status);
464
+ }
465
+ get mode() {
466
+ return __privateGet(this, _mode);
467
+ }
468
+ get isSpeaking() {
469
+ return __privateGet(this, _mode) === "speaking";
470
+ }
471
+ get micMuted() {
472
+ return __privateGet(this, _micMuted);
473
+ }
474
+ get endReason() {
475
+ return __privateGet(this, _endReason);
476
+ }
477
+ /**
478
+ * Snapshot of every transcript segment so far, in conversation order. Seed a
479
+ * consumer that subscribes after events already fired (e.g. a component
480
+ * mounted right after start), then apply live events by segmentId.
481
+ */
482
+ getTranscript() {
483
+ return __privateGet(this, _transcript).map((segment) => ({ ...segment }));
484
+ }
485
+ // ---- text & control channel ----
486
+ /** `audio: false` asks the agent to answer this turn in text only (no TTS). */
487
+ sendUserMessage(text, options) {
488
+ const trimmed = text.trim();
489
+ if (!trimmed) {
490
+ return;
491
+ }
492
+ const message = options?.audio === false ? { type: "user.message", text: trimmed, audio: false } : { type: "user.message", text: trimmed };
493
+ if (__privateGet(this, _agentPresent)) {
494
+ __privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, message);
495
+ } else {
496
+ __privateGet(this, _pendingMessages).push(message);
497
+ }
498
+ __privateMethod(this, _AgentSession_instances, recordTypedUserMessage_fn).call(this, trimmed);
499
+ }
500
+ sendUserActivity() {
501
+ __privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, { type: "user.activity" });
502
+ }
503
+ interrupt() {
504
+ __privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, { type: "user.interrupt" });
505
+ }
506
+ registerClientTool(name, handler) {
507
+ __privateGet(this, _tools).register(name, handler);
508
+ }
509
+ // ---- audio ----
510
+ /**
511
+ * After a `microphone: false` start, the first unmute captures and publishes
512
+ * the microphone — the permission prompt happens here. A denial rejects with
513
+ * `mic_permission_denied` and the session stays muted.
514
+ */
515
+ async setMicMuted(muted) {
516
+ var _a;
517
+ const previous = __privateGet(this, _micMuted);
518
+ __privateSet(this, _micMuted, muted);
519
+ try {
520
+ await __privateGet(this, _transport).setMicEnabled(!muted);
521
+ } catch (error) {
522
+ __privateSet(this, _micMuted, previous);
523
+ throw __privateMethod(_a = _AgentSession, _AgentSession_static, asMicError_fn).call(_a, error);
524
+ }
525
+ }
526
+ /** Call from a user gesture to satisfy browser autoplay policies. */
527
+ async startAudio() {
528
+ __privateGet(this, _outputAnalyser).resume();
529
+ __privateGet(this, _inputAnalyser).resume();
530
+ await __privateGet(this, _output).startAudio();
531
+ }
532
+ /**
533
+ * Switch the microphone mid-call. While the mic is not captured yet
534
+ * (`microphone: false` start, still muted) this records the preference for
535
+ * the first unmute. Rejects with `device_change_failed` when the device
536
+ * cannot be activated, switching back to the previous microphone (best
537
+ * effort — the transport stops the old capture before acquiring the new).
538
+ */
539
+ async setInputDevice(deviceId) {
540
+ var _a;
541
+ try {
542
+ await __privateGet(this, _transport).setInputDevice(deviceId);
543
+ } catch (error) {
544
+ throw __privateMethod(_a = _AgentSession, _AgentSession_static, asDeviceError_fn).call(_a, error, "Could not switch the microphone");
545
+ }
546
+ }
547
+ /**
548
+ * Route the agent's audio to an output device (`setSinkId`); pass `""` to
549
+ * return to the default device. Rejects with `device_change_failed` where
550
+ * the browser does not support output selection (common on mobile browsers)
551
+ * or the device cannot be used; playback stays on the previous device.
552
+ */
553
+ async setOutputDevice(deviceId) {
554
+ await __privateGet(this, _output).setSinkId(deviceId);
555
+ }
556
+ setOutputVolume(volume) {
557
+ __privateGet(this, _output).setVolume(volume);
558
+ }
559
+ getOutputVolume() {
560
+ return __privateGet(this, _outputAnalyser).getVolume();
561
+ }
562
+ getInputVolume() {
563
+ return __privateGet(this, _inputAnalyser).getVolume();
564
+ }
565
+ getOutputFrequencyData() {
566
+ return __privateGet(this, _outputAnalyser).getFrequencyData();
567
+ }
568
+ getInputFrequencyData() {
569
+ return __privateGet(this, _inputAnalyser).getFrequencyData();
570
+ }
571
+ /**
572
+ * Escape hatch: the underlying LiveKit `Room`, for needs the session API
573
+ * doesn't cover (connection-quality telemetry, publishing extra tracks).
574
+ * Code using it couples to this SDK's transport choice and livekit-client
575
+ * version — prefer the session API where one exists. `undefined` after the
576
+ * session ends, or when a custom transport doesn't expose a room.
577
+ */
578
+ getRoom() {
579
+ return __privateGet(this, _transport).getRoom?.();
580
+ }
581
+ // ---- lifecycle ----
582
+ end() {
583
+ if (__privateGet(this, _ending)) {
584
+ return __privateGet(this, _ending);
585
+ }
586
+ if (__privateGet(this, _status) === "ended") {
587
+ return Promise.resolve();
588
+ }
589
+ const ending = __privateMethod(this, _AgentSession_instances, endOnce_fn).call(this);
590
+ __privateSet(this, _ending, ending);
591
+ void ending.catch(() => {
592
+ if (__privateGet(this, _ending) === ending) {
593
+ __privateSet(this, _ending, void 0);
594
+ }
595
+ });
596
+ return ending;
597
+ }
598
+ };
599
+ _status = new WeakMap();
600
+ _mode = new WeakMap();
601
+ _endReason = new WeakMap();
602
+ _sessionId = new WeakMap();
603
+ _micMuted = new WeakMap();
604
+ _endedByClient = new WeakMap();
605
+ _ending = new WeakMap();
606
+ _transport = new WeakMap();
607
+ _tools = new WeakMap();
608
+ _output = new WeakMap();
609
+ _outputAnalyser = new WeakMap();
610
+ _inputAnalyser = new WeakMap();
611
+ _wakeLock = new WeakMap();
612
+ _agentSegments = new WeakMap();
613
+ _transcript = new WeakMap();
614
+ _transcriptIndex = new WeakMap();
615
+ _typedMessageCount = new WeakMap();
616
+ _agentPresent = new WeakMap();
617
+ _pendingMessages = new WeakMap();
618
+ _joinTimer = new WeakMap();
619
+ _AgentSession_instances = new WeakSet();
620
+ endOnce_fn = async function() {
621
+ __privateSet(this, _endedByClient, true);
622
+ try {
623
+ await __privateGet(this, _transport).sendClientEvent({ type: "user.hangup" });
624
+ } catch {
625
+ }
626
+ await __privateGet(this, _transport).disconnect();
627
+ __privateMethod(this, _AgentSession_instances, finalize_fn).call(this, "user_hangup");
628
+ };
629
+ _AgentSession_static = new WeakSet();
630
+ startWith_fn = async function(options, createTransport) {
631
+ var _a, _b, _c;
632
+ if (options.audio?.outputDeviceId !== void 0) {
633
+ assertOutputSelectionSupported();
634
+ }
635
+ const sessionToken = await resolveSessionToken(options);
636
+ const session = new _AgentSession(sessionToken);
637
+ if (options.audio?.outputDeviceId !== void 0) {
638
+ await __privateGet(session, _output).setSinkId(options.audio.outputDeviceId);
639
+ }
640
+ for (const [key, callback] of Object.entries(options.callbacks ?? {})) {
641
+ const event = key.charAt(2).toLowerCase() + key.slice(3);
642
+ session.on(event, callback);
643
+ }
644
+ const transport = createTransport(sessionToken);
645
+ __privateSet(session, _transport, transport);
646
+ if (options.microphone === false) {
647
+ __privateSet(session, _micMuted, true);
648
+ }
649
+ __privateSet(session, _tools, new ClientToolDispatcher(
650
+ options.clientTools,
651
+ options.clientToolTimeoutMs ?? DEFAULT_CLIENT_TOOL_TIMEOUT_MS,
652
+ (message) => transport.sendClientEvent(message),
653
+ (error) => session.emit("error", error)
654
+ ));
655
+ try {
656
+ await transport.connect(sessionToken, {
657
+ inputDeviceId: options.audio?.inputDeviceId,
658
+ microphone: options.microphone,
659
+ callbacks: {
660
+ onAgentEvent: (message) => {
661
+ var _a2;
662
+ return __privateMethod(_a2 = session, _AgentSession_instances, handleAgentEvent_fn).call(_a2, message);
663
+ },
664
+ // Mode model: "listening" is the default state; the framework's agent
665
+ // state supplies the thinking edge; speaking follows the playout-synced
666
+ // transcription segments (open = audible, closed = back to listening).
667
+ onAgentState: (state) => {
668
+ var _a2, _b2;
669
+ __privateMethod(_a2 = session, _AgentSession_instances, handleAgentPresent_fn).call(_a2);
670
+ if (state === "thinking") {
671
+ __privateMethod(_b2 = session, _AgentSession_instances, setMode_fn).call(_b2, "thinking");
672
+ }
673
+ },
674
+ onTranscription: (update) => {
675
+ var _a2;
676
+ return __privateMethod(_a2 = session, _AgentSession_instances, handleTranscription_fn).call(_a2, update);
677
+ },
678
+ onOutputStream: (stream) => {
679
+ __privateGet(session, _output).setStream(stream);
680
+ __privateGet(session, _outputAnalyser).setStream(stream);
681
+ },
682
+ onInputStream: (stream) => __privateGet(session, _inputAnalyser).setStream(stream),
683
+ onConnectionState: (state, reason) => {
684
+ var _a2;
685
+ return __privateMethod(_a2 = session, _AgentSession_instances, handleConnectionState_fn).call(_a2, state, reason);
686
+ }
687
+ }
688
+ });
689
+ } catch (error) {
690
+ await __privateGet(session, _transport).disconnect().catch(() => void 0);
691
+ __privateMethod(_a = session, _AgentSession_instances, finalize_fn).call(_a, "connection_lost");
692
+ throw __privateMethod(_b = _AgentSession, _AgentSession_static, asStartError_fn).call(_b, error);
693
+ }
694
+ if (__privateGet(session, _status) === "connecting") {
695
+ __privateMethod(_c = session, _AgentSession_instances, setStatus_fn).call(_c, "connected");
696
+ session.emit("connect", { sessionId: __privateGet(session, _sessionId) });
697
+ }
698
+ if (__privateGet(session, _status) !== "ended" && options.wakeLock !== false) {
699
+ __privateGet(session, _wakeLock).acquire();
700
+ }
701
+ if (!__privateGet(session, _agentPresent) && __privateGet(session, _status) === "connected") {
702
+ __privateSet(session, _joinTimer, setTimeout(() => {
703
+ var _a2;
704
+ return __privateMethod(_a2 = session, _AgentSession_instances, abandonAgentWait_fn).call(_a2);
705
+ }, AGENT_JOIN_TIMEOUT_MS));
706
+ }
707
+ return session;
708
+ };
709
+ asStartError_fn = function(error) {
710
+ if (error instanceof FishAgentError) {
711
+ return error;
712
+ }
713
+ if (error instanceof Error && error.name === "NotAllowedError") {
714
+ return new FishAgentError("mic_permission_denied", "Microphone permission was denied", {
715
+ cause: error
716
+ });
717
+ }
718
+ return new FishAgentError("connection_failed", "Could not establish the realtime session", {
719
+ cause: error
720
+ });
721
+ };
722
+ asMicError_fn = function(error) {
723
+ if (error instanceof FishAgentError) {
724
+ return error;
725
+ }
726
+ if (error instanceof Error && error.name === "NotAllowedError") {
727
+ return new FishAgentError("mic_permission_denied", "Microphone permission was denied", {
728
+ cause: error
729
+ });
730
+ }
731
+ if (error instanceof Error && (error.name === "OverconstrainedError" || error.name === "NotFoundError")) {
732
+ return new FishAgentError(
733
+ "device_change_failed",
734
+ "No usable microphone could be activated",
735
+ { cause: error }
736
+ );
737
+ }
738
+ return new FishAgentError("connection_failed", "Could not toggle the microphone", {
739
+ cause: error
740
+ });
741
+ };
742
+ asDeviceError_fn = function(error, message) {
743
+ if (error instanceof FishAgentError) {
744
+ return error;
745
+ }
746
+ return new FishAgentError("device_change_failed", message, { cause: error });
747
+ };
748
+ sendClientEvent_fn = function(message) {
749
+ void __privateGet(this, _transport).sendClientEvent(message).catch((error) => {
750
+ this.emit(
751
+ "error",
752
+ new FishAgentError("connection_failed", "Failed to send to the agent", { cause: error })
753
+ );
754
+ });
755
+ };
756
+ setStatus_fn = function(status) {
757
+ if (__privateGet(this, _status) !== status) {
758
+ __privateSet(this, _status, status);
759
+ this.emit("statusChange", status);
760
+ }
761
+ };
762
+ setMode_fn = function(mode) {
763
+ if (__privateGet(this, _mode) !== mode) {
764
+ __privateSet(this, _mode, mode);
765
+ this.emit("modeChange", mode);
766
+ }
767
+ };
768
+ handleAgentPresent_fn = function() {
769
+ if (__privateGet(this, _agentPresent)) {
770
+ return;
771
+ }
772
+ __privateSet(this, _agentPresent, true);
773
+ if (__privateGet(this, _joinTimer)) {
774
+ clearTimeout(__privateGet(this, _joinTimer));
775
+ __privateSet(this, _joinTimer, void 0);
776
+ }
777
+ const pending = __privateGet(this, _pendingMessages);
778
+ __privateSet(this, _pendingMessages, []);
779
+ for (const message of pending) {
780
+ __privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, message);
781
+ }
782
+ };
783
+ abandonAgentWait_fn = function() {
784
+ if (__privateGet(this, _status) === "ended" || __privateGet(this, _agentPresent)) {
785
+ return;
786
+ }
787
+ this.emit(
788
+ "error",
789
+ new FishAgentError("connection_failed", "The agent did not join the session")
790
+ );
791
+ void __privateGet(this, _transport).disconnect().catch(() => {
792
+ });
793
+ __privateMethod(this, _AgentSession_instances, finalize_fn).call(this, "connection_lost");
794
+ };
795
+ handleConnectionState_fn = function(state, reason) {
796
+ if (__privateGet(this, _status) === "ended") {
797
+ return;
798
+ }
799
+ if (state === "reconnecting") {
800
+ __privateMethod(this, _AgentSession_instances, setStatus_fn).call(this, "reconnecting");
801
+ return;
802
+ }
803
+ if (state === "connected") {
804
+ if (__privateGet(this, _status) === "reconnecting") {
805
+ __privateMethod(this, _AgentSession_instances, setStatus_fn).call(this, "connected");
806
+ }
807
+ return;
808
+ }
809
+ const ended = __privateGet(this, _endedByClient) ? "user_hangup" : reason === "AGENT_LEFT" || reason === "ROOM_DELETED" || reason === "ROOM_CLOSED" ? "agent_hangup" : "connection_lost";
810
+ __privateMethod(this, _AgentSession_instances, finalize_fn).call(this, ended);
811
+ };
812
+ finalize_fn = function(reason) {
813
+ if (__privateGet(this, _status) === "ended") {
814
+ return;
815
+ }
816
+ if (__privateGet(this, _joinTimer)) {
817
+ clearTimeout(__privateGet(this, _joinTimer));
818
+ __privateSet(this, _joinTimer, void 0);
819
+ }
820
+ __privateSet(this, _endReason, reason);
821
+ __privateMethod(this, _AgentSession_instances, setStatus_fn).call(this, "ended");
822
+ __privateGet(this, _wakeLock).release();
823
+ __privateGet(this, _output).dispose();
824
+ __privateGet(this, _outputAnalyser).dispose();
825
+ __privateGet(this, _inputAnalyser).dispose();
826
+ this.emit("disconnect", { reason });
827
+ };
828
+ handleAgentEvent_fn = function(message) {
829
+ switch (message.type) {
830
+ case "client_tool.call":
831
+ void __privateGet(this, _tools).dispatch(message);
832
+ return;
833
+ case "tool.started":
834
+ this.emit("toolCallStarted", {
835
+ callId: message.callId,
836
+ toolName: message.toolName,
837
+ source: message.toolSource,
838
+ input: message.input,
839
+ inputTruncated: message.inputTruncated === true
840
+ });
841
+ return;
842
+ case "tool.completed":
843
+ this.emit("toolCallCompleted", {
844
+ callId: message.callId,
845
+ toolName: message.toolName,
846
+ source: message.toolSource,
847
+ output: message.output,
848
+ outputTruncated: message.outputTruncated === true
849
+ });
850
+ return;
851
+ case "tool.failed":
852
+ this.emit("toolCallFailed", {
853
+ callId: message.callId,
854
+ toolName: message.toolName,
855
+ source: message.toolSource,
856
+ error: message.error
857
+ });
858
+ return;
859
+ case "error":
860
+ this.emit(
861
+ "error",
862
+ message.code === "provider_error" ? new FishAgentError("provider_error", "An upstream provider failed during the session") : new FishAgentError("internal_error", "The agent runtime hit an internal error")
863
+ );
864
+ return;
865
+ default:
866
+ return;
867
+ }
868
+ };
869
+ recordSegment_fn = function(role, segmentId, text, final) {
870
+ const key = `${role}:${segmentId}`;
871
+ const index = __privateGet(this, _transcriptIndex).get(key);
872
+ if (index === void 0) {
873
+ __privateGet(this, _transcriptIndex).set(key, __privateGet(this, _transcript).length);
874
+ __privateGet(this, _transcript).push({ segmentId, role, text, final });
875
+ } else {
876
+ __privateGet(this, _transcript)[index] = { segmentId, role, text, final };
877
+ }
878
+ };
879
+ handleTranscription_fn = function(update) {
880
+ if (update.role === "user") {
881
+ __privateMethod(this, _AgentSession_instances, recordSegment_fn).call(this, "user", update.segmentId, update.text, update.final);
882
+ this.emit("userTranscript", {
883
+ segmentId: update.segmentId,
884
+ text: update.text,
885
+ final: update.final
886
+ });
887
+ if (update.final) {
888
+ this.emit("message", { role: "user", text: update.text });
889
+ }
890
+ return;
891
+ }
892
+ __privateMethod(this, _AgentSession_instances, setMode_fn).call(this, update.final ? "listening" : "speaking");
893
+ __privateMethod(this, _AgentSession_instances, recordSegment_fn).call(this, "agent", update.segmentId, update.text, update.final);
894
+ const previous = __privateGet(this, _agentSegments).get(update.segmentId) ?? "";
895
+ const delta = update.text.startsWith(previous) ? update.text.slice(previous.length) : update.text;
896
+ __privateGet(this, _agentSegments).set(update.segmentId, update.text);
897
+ if (delta) {
898
+ this.emit("agentResponseDelta", { segmentId: update.segmentId, delta, text: update.text });
899
+ }
900
+ if (update.final) {
901
+ __privateGet(this, _agentSegments).delete(update.segmentId);
902
+ this.emit("agentResponse", { segmentId: update.segmentId, text: update.text });
903
+ this.emit("message", { role: "agent", text: update.text });
904
+ }
905
+ };
906
+ // Typed messages are not echoed by the server; the sender finalizes its own bubble.
907
+ recordTypedUserMessage_fn = function(text) {
908
+ const segmentId = `typed_${++__privateWrapper(this, _typedMessageCount)._}`;
909
+ __privateMethod(this, _AgentSession_instances, recordSegment_fn).call(this, "user", segmentId, text, true);
910
+ this.emit("userTranscript", { segmentId, text, final: true });
911
+ this.emit("message", { role: "user", text });
912
+ };
913
+ __privateAdd(_AgentSession, _AgentSession_static);
914
+ startWith = (options, createTransport) => {
915
+ var _a;
916
+ return __privateMethod(_a = _AgentSession, _AgentSession_static, startWith_fn).call(_a, options, createTransport);
917
+ };
918
+ var AgentSession = _AgentSession;
919
+ export {
920
+ AgentSession,
921
+ DEFAULT_SERVER_URL,
922
+ FishAgentError
923
+ };
924
+ //# sourceMappingURL=index.js.map