@fishaudio/agent-client 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -65,6 +65,14 @@ __export(livekit_exports, {
65
65
  function hasMessageType(value) {
66
66
  return typeof value === "object" && value !== null && typeof value.type === "string";
67
67
  }
68
+ function captureMicrophone(inputDeviceId) {
69
+ return (0, import_livekit_client.createLocalAudioTrack)({
70
+ deviceId: inputDeviceId,
71
+ echoCancellation: true,
72
+ noiseSuppression: true,
73
+ autoGainControl: true
74
+ });
75
+ }
68
76
  var import_livekit_client, import_agent_protocol2, LiveKitTransport;
69
77
  var init_livekit = __esm({
70
78
  "src/transport/livekit.ts"() {
@@ -77,6 +85,11 @@ var init_livekit = __esm({
77
85
  /** Local tracks already wired to re-emit their stream when livekit restarts them. */
78
86
  this.restartWiredTracks = /* @__PURE__ */ new WeakSet();
79
87
  }
88
+ prepareMicrophone(options) {
89
+ const pending = captureMicrophone(options.inputDeviceId);
90
+ pending.catch(() => void 0);
91
+ this.pendingMicTrack = pending;
92
+ }
80
93
  async connect(sessionToken, options) {
81
94
  this.callbacks = options.callbacks;
82
95
  const room = new import_livekit_client.Room({
@@ -166,6 +179,12 @@ var init_livekit = __esm({
166
179
  });
167
180
  }
168
181
  });
182
+ let micTrack;
183
+ if (options.microphone !== false) {
184
+ const pending = this.pendingMicTrack ?? captureMicrophone(options.inputDeviceId);
185
+ this.pendingMicTrack = pending;
186
+ micTrack = await pending;
187
+ }
169
188
  try {
170
189
  await room.connect(sessionToken.livekit_url, sessionToken.token);
171
190
  } catch (cause) {
@@ -179,8 +198,9 @@ var init_livekit = __esm({
179
198
  this.callbacks?.onAgentState(state);
180
199
  }
181
200
  }
182
- if (options.microphone !== false) {
183
- await room.localParticipant.setMicrophoneEnabled(true);
201
+ if (micTrack) {
202
+ await room.localParticipant.publishTrack(micTrack);
203
+ this.pendingMicTrack = void 0;
184
204
  this.publishInputStream();
185
205
  }
186
206
  }
@@ -189,6 +209,9 @@ var init_livekit = __esm({
189
209
  void room.disconnect();
190
210
  }
191
211
  async disconnect() {
212
+ const pendingMic = this.pendingMicTrack;
213
+ this.pendingMicTrack = void 0;
214
+ void pendingMic?.then((track) => track.stop()).catch(() => void 0);
192
215
  const room = this.room;
193
216
  this.room = void 0;
194
217
  this.callbacks = void 0;
@@ -747,13 +770,13 @@ var _AgentSession = class _AgentSession extends TypedEmitter {
747
770
  return __privateGet(this, _transcript).map((segment) => ({ ...segment }));
748
771
  }
749
772
  // ---- text & control channel ----
750
- /** `audio: false` asks the agent to answer this turn in text only (no TTS). */
773
+ /** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */
751
774
  sendUserMessage(text, options) {
752
775
  const trimmed = text.trim();
753
776
  if (!trimmed) {
754
777
  return;
755
778
  }
756
- const message = options?.audio === false ? { type: "user.message", text: trimmed, audio: false } : { type: "user.message", text: trimmed };
779
+ const message = options?.audio === true ? { type: "user.message", text: trimmed } : { type: "user.message", text: trimmed, audio: false };
757
780
  if (__privateGet(this, _agentPresent)) {
758
781
  __privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, message);
759
782
  } else {
@@ -896,16 +919,26 @@ startWith_fn = async function(options, createTransport) {
896
919
  if (options.audio?.outputDeviceId !== void 0) {
897
920
  assertOutputSelectionSupported();
898
921
  }
899
- const sessionToken = await resolveSessionToken(options);
900
- const session = new _AgentSession(sessionToken);
901
- if (options.audio?.outputDeviceId !== void 0) {
902
- await __privateGet(session, _output).setSinkId(options.audio.outputDeviceId);
922
+ const transport = createTransport();
923
+ if (options.microphone !== false) {
924
+ transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });
925
+ }
926
+ let sessionToken;
927
+ let session;
928
+ try {
929
+ sessionToken = await resolveSessionToken(options);
930
+ session = new _AgentSession(sessionToken);
931
+ if (options.audio?.outputDeviceId !== void 0) {
932
+ await __privateGet(session, _output).setSinkId(options.audio.outputDeviceId);
933
+ }
934
+ } catch (error) {
935
+ await transport.disconnect().catch(() => void 0);
936
+ throw error;
903
937
  }
904
938
  for (const [key, callback] of Object.entries(options.callbacks ?? {})) {
905
939
  const event = key.charAt(2).toLowerCase() + key.slice(3);
906
940
  session.on(event, callback);
907
941
  }
908
- const transport = createTransport(sessionToken);
909
942
  __privateSet(session, _transport, transport);
910
943
  if (options.microphone === false) {
911
944
  __privateSet(session, _micMuted, true);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/transport/livekit.ts","../src/index.ts","../src/audio/output.ts","../src/audio/analysis.ts","../src/session/agentSession.ts","../src/sessionToken.ts","../src/session/emitter.ts","../src/session/toolDispatcher.ts","../src/session/wakeLock.ts"],"sourcesContent":["export type FishAgentErrorCode =\n | \"session_request_failed\"\n | \"agent_not_public\"\n | \"origin_forbidden\"\n | \"unsupported_transport\"\n | \"mic_permission_denied\"\n /** A requested audio device could not be activated, or the browser does not support selecting it. */\n | \"device_change_failed\"\n | \"connection_failed\"\n | \"session_expired\"\n | \"tool_failed\"\n | \"tool_timeout\"\n /** An upstream model/speech provider failed during the session. */\n | \"provider_error\"\n /** The agent runtime hit an internal error. */\n | \"internal_error\";\n\nexport class FishAgentError extends Error {\n readonly code: FishAgentErrorCode;\n /** HTTP status of the failing session request, when the error originated there. */\n readonly statusCode?: number;\n\n constructor(\n code: FishAgentErrorCode,\n message: string,\n options?: { statusCode?: number; cause?: unknown },\n ) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"FishAgentError\";\n this.code = code;\n this.statusCode = options?.statusCode;\n }\n}\n","import {\n ConnectionState,\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n TrackEvent,\n type RemoteTrack,\n} from \"livekit-client\";\nimport {\n AGENT_EVENT_TOPIC,\n CLIENT_EVENT_TOPIC,\n type AgentSessionMessage,\n type ClientSessionMessage,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\nimport type { Transport, TransportCallbacks, TransportConnectOptions } from \"./types.js\";\n\nfunction hasMessageType(value: unknown): value is { type: string } {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { type?: unknown }).type === \"string\"\n );\n}\n\n/** Sole production Transport. LiveKit vocabulary must not escape this file,\n * except the `Room` handed out through the `getRoom()` escape hatch. */\nexport class LiveKitTransport implements Transport {\n private room?: Room;\n private callbacks?: TransportCallbacks;\n\n async connect(sessionToken: SessionToken, options: TransportConnectOptions): Promise<void> {\n this.callbacks = options.callbacks;\n const room = new Room({\n audioCaptureDefaults: {\n deviceId: options.inputDeviceId,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n this.room = room;\n\n room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {\n if (topic !== AGENT_EVENT_TOPIC) {\n return;\n }\n let message: unknown;\n try {\n message = JSON.parse(new TextDecoder().decode(payload));\n } catch {\n return;\n }\n if (hasMessageType(message)) {\n this.callbacks?.onAgentEvent(message as AgentSessionMessage);\n }\n });\n\n const AGENT_STATE_ATTRIBUTE = \"lk.agent.state\";\n room.on(RoomEvent.ParticipantAttributesChanged, (changed, participant) => {\n const state = changed[AGENT_STATE_ATTRIBUTE];\n if (!participant.isLocal && state) {\n this.callbacks?.onAgentState(state);\n }\n });\n\n room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack) => {\n if (track.kind === Track.Kind.Audio && track.mediaStreamTrack) {\n this.callbacks?.onOutputStream(new MediaStream([track.mediaStreamTrack]));\n }\n });\n\n // Both sides' transcripts ride LiveKit's built-in transcription streams:\n // agent segments are playout-synced delta streams (final on stream close —\n // an interrupted segment closes containing only the spoken text); user STT\n // segments are re-published per update with an explicit final attribute,\n // sent on behalf of the user, so sender identity distinguishes the roles.\n room.registerTextStreamHandler(\"lk.transcription\", async (reader, participantInfo) => {\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[\"lk.segment_id\"];\n if (!segmentId) {\n return;\n }\n const role =\n participantInfo.identity === room.localParticipant.identity ? \"user\" : \"agent\";\n const finalAttribute = attributes[\"lk.transcription_final\"] === \"true\";\n let text = \"\";\n let aborted = false;\n try {\n for await (const delta of reader) {\n text += delta;\n this.callbacks?.onTranscription({ segmentId, role, text, final: false });\n }\n } catch {\n aborted = true; // interrupted mid-stream; what arrived is what was spoken\n }\n if (text) {\n const final = role === \"agent\" ? true : finalAttribute && !aborted;\n this.callbacks?.onTranscription({ segmentId, role, text, final });\n }\n });\n\n room.on(RoomEvent.Reconnecting, () => {\n this.callbacks?.onConnectionState(\"reconnecting\");\n });\n room.on(RoomEvent.Reconnected, () => {\n // A full reconnect replays the roster from the join response before this\n // event; an agent still missing here hung up while we were away.\n if (room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n return;\n }\n this.callbacks?.onConnectionState(\"connected\");\n });\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n this.callbacks?.onConnectionState(\n \"disconnected\",\n reason === undefined ? undefined : DisconnectReason[reason],\n );\n // Remote-initiated ends never go through disconnect(); drop the refs here\n // so getRoom() honors its ended-means-undefined contract and the room\n // can be collected.\n if (this.room === room) {\n this.room = undefined;\n this.callbacks = undefined;\n }\n });\n // An agent-side hangup ends the worker without closing the room; the\n // agent participant leaving IS the end-of-call signal for the client.\n // A full reconnect also clears the roster this way, and can do so while\n // room.state still reads Connected (Reconnecting is only set right after),\n // so defer one microtask and require a room that is genuinely connected.\n room.on(RoomEvent.ParticipantDisconnected, () => {\n if (room.remoteParticipants.size === 0) {\n queueMicrotask(() => {\n if (room.state === ConnectionState.Connected && room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n }\n });\n }\n });\n\n try {\n await room.connect(sessionToken.livekit_url, sessionToken.token);\n } catch (cause) {\n throw new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause,\n });\n }\n // The attribute is sticky: if the agent joined and set its state before us,\n // no change event will fire — replay the current value once.\n for (const participant of room.remoteParticipants.values()) {\n const state = participant.attributes?.[AGENT_STATE_ATTRIBUTE];\n if (state) {\n this.callbacks?.onAgentState(state);\n }\n }\n if (options.microphone !== false) {\n await room.localParticipant.setMicrophoneEnabled(true);\n this.publishInputStream();\n }\n }\n\n private handleAgentLeft(room: Room): void {\n this.callbacks?.onConnectionState(\"disconnected\", \"AGENT_LEFT\");\n void room.disconnect();\n }\n\n async disconnect(): Promise<void> {\n const room = this.room;\n this.room = undefined;\n this.callbacks = undefined;\n await room?.disconnect();\n }\n\n getRoom(): Room | undefined {\n return this.room;\n }\n\n async setMicEnabled(enabled: boolean): Promise<void> {\n await this.room?.localParticipant.setMicrophoneEnabled(enabled);\n if (enabled) {\n // After a mic-less connect the first enable captures the track late;\n // re-wiring an already-published track is a harmless replace.\n this.publishInputStream();\n }\n }\n\n async setInputDevice(deviceId: string): Promise<void> {\n const room = this.room;\n if (!room) {\n return;\n }\n const previous = room.getActiveDevice(\"audioinput\");\n let switched: boolean;\n try {\n // exact: a wrong id must fail loudly, not silently fall back to another mic.\n switched = await room.switchActiveDevice(\"audioinput\", deviceId, true);\n } catch (error) {\n // livekit stops the current track before acquiring the new device, so a\n // failed pick would otherwise leave the call silently mic-dead — switch\n // back before surfacing the failure. livekit reports \"default\" until the\n // real captured id is known; that pseudo-id only resolves on Chrome, so\n // restore it non-exactly and let other browsers fall back to any mic.\n if (previous !== undefined) {\n await room\n .switchActiveDevice(\"audioinput\", previous, previous !== \"default\")\n .catch(() => undefined);\n this.publishInputStream();\n }\n throw error;\n }\n if (!switched) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not activate the requested microphone\",\n );\n }\n // A live switch replaces the publication's MediaStreamTrack — re-wire\n // analysis. While muted, livekit defers the swap to the next unmute and\n // setMicEnabled re-wires then.\n this.publishInputStream();\n }\n\n /** Local tracks already wired to re-emit their stream when livekit restarts them. */\n private readonly restartWiredTracks = new WeakSet<object>();\n\n private publishInputStream(): void {\n const room = this.room;\n if (!room) {\n return;\n }\n for (const publication of room.localParticipant.audioTrackPublications.values()) {\n const track = publication.track;\n if (track && !this.restartWiredTracks.has(track)) {\n this.restartWiredTracks.add(track);\n // Restarts replace the MediaStreamTrack without any Room-level event —\n // livekit's devicechange auto-recovery and getRoom() escape-hatch\n // switches would otherwise leave input analysis on a dead track.\n track.on(TrackEvent.Restarted, () => this.publishInputStream());\n }\n const mediaStreamTrack = track?.mediaStreamTrack;\n if (mediaStreamTrack) {\n this.callbacks?.onInputStream(new MediaStream([mediaStreamTrack]));\n }\n }\n }\n\n async sendClientEvent(message: ClientSessionMessage): Promise<void> {\n if (!this.room) {\n throw new FishAgentError(\"connection_failed\", \"Session is not connected\");\n }\n const payload = new TextEncoder().encode(JSON.stringify(message));\n await this.room.localParticipant.publishData(payload, {\n reliable: true,\n topic: CLIENT_EVENT_TOPIC,\n });\n }\n}\n","// Public surface. Transport-neutral except the deliberate escape hatch\n// `AgentSession.getRoom()`, whose return type is livekit-client's `Room`.\nexport { AgentSession, type AgentSessionOptions } from \"./session/agentSession.js\";\nexport { DEFAULT_SERVER_URL } from \"./sessionToken.js\";\nexport { FishAgentError, type FishAgentErrorCode } from \"./errors.js\";\nexport type {\n AgentMode,\n AgentResponseDeltaEvent,\n AgentResponseEvent,\n AgentSessionCallbacks,\n AgentSessionEvents,\n ConversationMessage,\n EndReason,\n SessionStatus,\n ToolCallCompletedEvent,\n ToolCallFailedEvent,\n ToolCallStartedEvent,\n TranscriptSegment,\n UserTranscriptEvent,\n} from \"./events.js\";\nexport type { ClientToolHandler } from \"./session/toolDispatcher.js\";\nexport type {\n AgentSessionCreateRequest,\n SessionOverrides,\n SessionToken,\n ToolCallSource,\n} from \"@fishaudio/agent-protocol\";\n","import { FishAgentError } from \"../errors.js\";\n\n/** Throws `device_change_failed` where the browser cannot route audio output (`setSinkId`). */\nexport function assertOutputSelectionSupported(): void {\n if (typeof HTMLMediaElement === \"undefined\" || !(\"setSinkId\" in HTMLMediaElement.prototype)) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"This browser does not support selecting an audio output device\",\n );\n }\n}\n\n/** Agent audio playback via a detached <audio> element (browser only; inert elsewhere). */\nexport class AudioOutput {\n private element?: HTMLAudioElement;\n private volume = 1;\n\n private ensureElement(): HTMLAudioElement | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n if (!this.element) {\n this.element = document.createElement(\"audio\");\n this.element.autoplay = true;\n // Same-effect attribute as playsInline for iOS Safari without DOM typings noise.\n this.element.setAttribute(\"playsinline\", \"\");\n this.element.volume = this.volume;\n }\n return this.element;\n }\n\n setStream(stream: MediaStream): void {\n const element = this.ensureElement();\n if (!element) {\n return;\n }\n element.srcObject = stream;\n void element.play().catch(() => {\n // Autoplay policy: playback stays pending until startAudio() runs in a user gesture.\n });\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n await this.element?.play();\n }\n\n /**\n * Route playback to an output device. The element is created on demand so\n * the browser validates the device id right here, even before the agent's\n * audio arrives. Rejects with `device_change_failed` where output selection\n * is unsupported or the device cannot be used.\n */\n async setSinkId(sinkId: string): Promise<void> {\n assertOutputSelectionSupported();\n const element = this.ensureElement();\n try {\n await element?.setSinkId(sinkId);\n } catch (error) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not switch to the requested audio output device\",\n { cause: error },\n );\n }\n }\n\n setVolume(volume: number): void {\n this.volume = Math.min(1, Math.max(0, volume));\n if (this.element) {\n this.element.volume = this.volume;\n }\n }\n\n getVolume(): number {\n return this.volume;\n }\n\n dispose(): void {\n if (this.element) {\n this.element.srcObject = null;\n this.element.remove();\n this.element = undefined;\n }\n }\n}\n","/**\n * WebAudio analyser over a MediaStream, feeding visualizers (levels + FFT)\n * without exposing any transport object. Browser only; every accessor\n * degrades to silence when WebAudio is unavailable.\n */\nexport class AudioStreamAnalyser {\n private context?: AudioContext;\n private analyser?: AnalyserNode;\n private source?: MediaStreamAudioSourceNode;\n\n setStream(stream: MediaStream): void {\n if (typeof AudioContext === \"undefined\") {\n return;\n }\n this.context ??= new AudioContext();\n this.analyser ??= this.context.createAnalyser();\n this.source?.disconnect();\n this.source = this.context.createMediaStreamSource(stream);\n this.source.connect(this.analyser);\n this.resume();\n }\n\n /** AudioContexts start suspended under autoplay policies; safe to call any time. */\n resume(): void {\n if (this.context?.state === \"suspended\") {\n void this.context.resume().catch(() => {});\n }\n }\n\n /** RMS level in [0, 1]. */\n getVolume(): number {\n if (!this.analyser) {\n return 0;\n }\n const samples = new Uint8Array(this.analyser.fftSize);\n this.analyser.getByteTimeDomainData(samples);\n let sumOfSquares = 0;\n for (const sample of samples) {\n const centered = (sample - 128) / 128;\n sumOfSquares += centered * centered;\n }\n return Math.sqrt(sumOfSquares / samples.length);\n }\n\n getFrequencyData(): Uint8Array {\n if (!this.analyser) {\n return new Uint8Array(0);\n }\n const data = new Uint8Array(this.analyser.frequencyBinCount);\n this.analyser.getByteFrequencyData(data);\n return data;\n }\n\n dispose(): void {\n this.source?.disconnect();\n this.analyser?.disconnect();\n void this.context?.close().catch(() => {});\n this.source = undefined;\n this.analyser = undefined;\n this.context = undefined;\n }\n}\n","// Type-only: the transport implementation still loads lazily (see start()).\nimport type { Room } from \"livekit-client\";\nimport type {\n AgentSessionMessage,\n ClientSessionMessage,\n SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { assertOutputSelectionSupported, AudioOutput } from \"../audio/output.js\";\nimport { AudioStreamAnalyser } from \"../audio/analysis.js\";\nimport { FishAgentError } from \"../errors.js\";\nimport type {\n AgentMode,\n AgentSessionCallbacks,\n AgentSessionEvents,\n EndReason,\n SessionStatus,\n TranscriptSegment,\n} from \"../events.js\";\nimport { resolveSessionToken, type SessionRequestOptions } from \"../sessionToken.js\";\nimport type {\n TranscriptionSegmentUpdate,\n Transport,\n TransportConnectionState,\n TransportFactory,\n} from \"../transport/types.js\";\nimport { TypedEmitter } from \"./emitter.js\";\nimport {\n ClientToolDispatcher,\n DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n type ClientToolHandler,\n} from \"./toolDispatcher.js\";\nimport { WakeLockHolder } from \"./wakeLock.js\";\n\nexport interface AgentSessionOptions extends SessionRequestOptions {\n clientTools?: Record<string, ClientToolHandler>;\n clientToolTimeoutMs?: number;\n /**\n * Capture the microphone on start. Default true. Set false to join without\n * requesting it (text-first UIs); the first `setMicMuted(false)` then\n * captures — the permission prompt happens there, so call it from a user\n * gesture.\n */\n microphone?: boolean;\n audio?: {\n inputDeviceId?: string;\n /** Playback device (`setSinkId`); start rejects with `device_change_failed`\n * where output selection is unsupported (before creating a server session)\n * or the device cannot be used (before connecting). */\n outputDeviceId?: string;\n };\n /**\n * Hold a screen wake lock while the session is live, so long calls survive\n * the phone trying to sleep. Default true; denial is silent. Set false to\n * leave screen policy to the page.\n */\n wakeLock?: boolean;\n callbacks?: Partial<AgentSessionCallbacks>;\n}\n\n/** How long after connect the agent participant gets to show up before the\n * session gives up — normal joins land within a couple of seconds. */\nconst AGENT_JOIN_TIMEOUT_MS = 15_000;\n\n/** Assigned in AgentSession's static block; bridges startAgentSession to the\n * class-private #startWith so no transport types sit on the public class. */\nlet startWith: (\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n) => Promise<AgentSession>;\n\nexport class AgentSession extends TypedEmitter<AgentSessionEvents> {\n #status: SessionStatus = \"connecting\";\n #mode: AgentMode = \"listening\";\n #endReason?: EndReason;\n #sessionId: string;\n #micMuted = false;\n #endedByClient = false;\n #ending?: Promise<void>;\n\n #transport!: Transport;\n #tools!: ClientToolDispatcher;\n readonly #output = new AudioOutput();\n readonly #outputAnalyser = new AudioStreamAnalyser();\n readonly #inputAnalyser = new AudioStreamAnalyser();\n readonly #wakeLock = new WakeLockHolder();\n\n /** Text already delivered per agent segment, to derive deltas from full-text updates. */\n readonly #agentSegments = new Map<string, string>();\n /** Transcript so far, for late subscribers (getTranscript); upserted per segment. */\n readonly #transcript: TranscriptSegment[] = [];\n readonly #transcriptIndex = new Map<string, number>();\n #typedMessageCount = 0;\n\n // Data frames reach only participants already in the room — and the worker\n // registers its handlers while starting up — so typed messages wait here\n // until the agent publishes its first pipeline state.\n #agentPresent = false;\n #pendingMessages: ClientSessionMessage[] = [];\n #joinTimer?: ReturnType<typeof setTimeout>;\n\n private constructor(sessionToken: SessionToken) {\n super();\n this.#sessionId = sessionToken.session_id;\n }\n\n /**\n * Obtain a session token (if needed), connect, publish the microphone and\n * hand back a live session. Rejects with FishAgentError on session-request,\n * permission or connect failures.\n */\n static async start(options: AgentSessionOptions): Promise<AgentSession> {\n // Lazy so the transport implementation (and livekit-client) never loads\n // in SSR bundles or in tests that inject their own transport.\n const { LiveKitTransport } = await import(\"../transport/livekit.js\");\n return AgentSession.#startWith(options, () => new LiveKitTransport());\n }\n\n // ---- getters ----\n\n get sessionId(): string {\n return this.#sessionId;\n }\n\n get status(): SessionStatus {\n return this.#status;\n }\n\n get mode(): AgentMode {\n return this.#mode;\n }\n\n get isSpeaking(): boolean {\n return this.#mode === \"speaking\";\n }\n\n get micMuted(): boolean {\n return this.#micMuted;\n }\n\n get endReason(): EndReason | undefined {\n return this.#endReason;\n }\n\n /**\n * Snapshot of every transcript segment so far, in conversation order. Seed a\n * consumer that subscribes after events already fired (e.g. a component\n * mounted right after start), then apply live events by segmentId.\n */\n getTranscript(): TranscriptSegment[] {\n return this.#transcript.map((segment) => ({ ...segment }));\n }\n\n // ---- text & control channel ----\n\n /** `audio: false` asks the agent to answer this turn in text only (no TTS). */\n sendUserMessage(text: string, options?: { audio?: boolean }): void {\n const trimmed = text.trim();\n if (!trimmed) {\n return;\n }\n const message: ClientSessionMessage =\n options?.audio === false\n ? { type: \"user.message\", text: trimmed, audio: false }\n : { type: \"user.message\", text: trimmed };\n if (this.#agentPresent) {\n this.#sendClientEvent(message);\n } else {\n this.#pendingMessages.push(message);\n }\n this.#recordTypedUserMessage(trimmed);\n }\n\n sendUserActivity(): void {\n this.#sendClientEvent({ type: \"user.activity\" });\n }\n\n interrupt(): void {\n this.#sendClientEvent({ type: \"user.interrupt\" });\n }\n\n registerClientTool(name: string, handler: ClientToolHandler): void {\n this.#tools.register(name, handler);\n }\n\n // ---- audio ----\n\n /**\n * After a `microphone: false` start, the first unmute captures and publishes\n * the microphone — the permission prompt happens here. A denial rejects with\n * `mic_permission_denied` and the session stays muted.\n */\n async setMicMuted(muted: boolean): Promise<void> {\n const previous = this.#micMuted;\n this.#micMuted = muted;\n try {\n await this.#transport.setMicEnabled(!muted);\n } catch (error) {\n this.#micMuted = previous;\n throw AgentSession.#asMicError(error);\n }\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n this.#outputAnalyser.resume();\n this.#inputAnalyser.resume();\n await this.#output.startAudio();\n }\n\n /**\n * Switch the microphone mid-call. While the mic is not captured yet\n * (`microphone: false` start, still muted) this records the preference for\n * the first unmute. Rejects with `device_change_failed` when the device\n * cannot be activated, switching back to the previous microphone (best\n * effort — the transport stops the old capture before acquiring the new).\n */\n async setInputDevice(deviceId: string): Promise<void> {\n try {\n await this.#transport.setInputDevice(deviceId);\n } catch (error) {\n throw AgentSession.#asDeviceError(error, \"Could not switch the microphone\");\n }\n }\n\n /**\n * Route the agent's audio to an output device (`setSinkId`); pass `\"\"` to\n * return to the default device. Rejects with `device_change_failed` where\n * the browser does not support output selection (common on mobile browsers)\n * or the device cannot be used; playback stays on the previous device.\n */\n async setOutputDevice(deviceId: string): Promise<void> {\n await this.#output.setSinkId(deviceId);\n }\n\n setOutputVolume(volume: number): void {\n this.#output.setVolume(volume);\n }\n\n getOutputVolume(): number {\n return this.#outputAnalyser.getVolume();\n }\n\n getInputVolume(): number {\n return this.#inputAnalyser.getVolume();\n }\n\n getOutputFrequencyData(): Uint8Array {\n return this.#outputAnalyser.getFrequencyData();\n }\n\n getInputFrequencyData(): Uint8Array {\n return this.#inputAnalyser.getFrequencyData();\n }\n\n /**\n * Escape hatch: the underlying LiveKit `Room`, for needs the session API\n * doesn't cover (connection-quality telemetry, publishing extra tracks).\n * Code using it couples to this SDK's transport choice and livekit-client\n * version — prefer the session API where one exists. `undefined` after the\n * session ends, or when a custom transport doesn't expose a room.\n */\n getRoom(): Room | undefined {\n return this.#transport.getRoom?.() as Room | undefined;\n }\n\n // ---- lifecycle ----\n\n end(): Promise<void> {\n if (this.#ending) {\n return this.#ending;\n }\n if (this.#status === \"ended\") {\n return Promise.resolve();\n }\n const ending = this.#endOnce();\n this.#ending = ending;\n void ending.catch(() => {\n if (this.#ending === ending) {\n this.#ending = undefined;\n }\n });\n return ending;\n }\n\n async #endOnce(): Promise<void> {\n this.#endedByClient = true;\n try {\n // Graceful hangup over the reverse channel; participant departure is the\n // worker-side fallback if this frame never arrives.\n await this.#transport.sendClientEvent({ type: \"user.hangup\" });\n } catch {\n // best effort\n }\n await this.#transport.disconnect();\n this.#finalize(\"user_hangup\");\n }\n\n // ---- internals ----\n\n static {\n startWith = (options, createTransport) => AgentSession.#startWith(options, createTransport);\n }\n\n static async #startWith(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n ): Promise<AgentSession> {\n if (options.audio?.outputDeviceId !== undefined) {\n // Cheap sync check first, so an unsupported browser fails before a\n // server session is even created.\n assertOutputSelectionSupported();\n }\n const sessionToken = await resolveSessionToken(options);\n const session = new AgentSession(sessionToken);\n if (options.audio?.outputDeviceId !== undefined) {\n // The browser validates the device id here, before anything connects;\n // rejects like a token failure — no events, no teardown needed yet.\n await session.#output.setSinkId(options.audio.outputDeviceId);\n }\n\n for (const [key, callback] of Object.entries(options.callbacks ?? {})) {\n const event = (key.charAt(2).toLowerCase() +\n key.slice(3)) as keyof AgentSessionEvents;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n session.on(event, callback as any);\n }\n\n const transport = createTransport(sessionToken);\n session.#transport = transport;\n if (options.microphone === false) {\n session.#micMuted = true;\n }\n session.#tools = new ClientToolDispatcher(\n options.clientTools,\n options.clientToolTimeoutMs ?? DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n (message) => transport.sendClientEvent(message),\n (error) => session.emit(\"error\", error),\n );\n\n try {\n await transport.connect(sessionToken, {\n inputDeviceId: options.audio?.inputDeviceId,\n microphone: options.microphone,\n callbacks: {\n onAgentEvent: (message) => session.#handleAgentEvent(message),\n // Mode model: \"listening\" is the default state; the framework's agent\n // state supplies the thinking edge; speaking follows the playout-synced\n // transcription segments (open = audible, closed = back to listening).\n onAgentState: (state) => {\n // Any published agent state proves the worker's pipeline is up and\n // its event handlers are registered — the gate for queued messages.\n session.#handleAgentPresent();\n if (state === \"thinking\") {\n session.#setMode(\"thinking\");\n }\n },\n onTranscription: (update) => session.#handleTranscription(update),\n onOutputStream: (stream) => {\n session.#output.setStream(stream);\n session.#outputAnalyser.setStream(stream);\n },\n onInputStream: (stream) => session.#inputAnalyser.setStream(stream),\n onConnectionState: (state, reason) => session.#handleConnectionState(state, reason),\n },\n });\n } catch (error) {\n // connect() can fail after the room is already joined (e.g. mic capture);\n // tear the transport down or the room stays connected with no owner.\n await session.#transport.disconnect().catch(() => undefined);\n session.#finalize(\"connection_lost\");\n throw AgentSession.#asStartError(error);\n }\n\n if (session.#status === \"connecting\") {\n session.#setStatus(\"connected\");\n session.emit(\"connect\", { sessionId: session.#sessionId });\n }\n if (session.#status !== \"ended\" && options.wakeLock !== false) {\n session.#wakeLock.acquire();\n }\n if (!session.#agentPresent && session.#status === \"connected\") {\n session.#joinTimer = setTimeout(() => session.#abandonAgentWait(), AGENT_JOIN_TIMEOUT_MS);\n }\n return session;\n }\n\n static #asStartError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n return new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause: error,\n });\n }\n\n static #asMicError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n if (\n error instanceof Error &&\n (error.name === \"OverconstrainedError\" || error.name === \"NotFoundError\")\n ) {\n // Device/constraint failures surface here both for a preference recorded\n // while muted and for a first capture with no usable microphone — keep\n // the device error code, with a message that fits either.\n return new FishAgentError(\n \"device_change_failed\",\n \"No usable microphone could be activated\",\n { cause: error },\n );\n }\n return new FishAgentError(\"connection_failed\", \"Could not toggle the microphone\", {\n cause: error,\n });\n }\n\n static #asDeviceError(error: unknown, message: string): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n return new FishAgentError(\"device_change_failed\", message, { cause: error });\n }\n\n #sendClientEvent(message: Parameters<Transport[\"sendClientEvent\"]>[0]): void {\n void this.#transport.sendClientEvent(message).catch((error: unknown) => {\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"Failed to send to the agent\", { cause: error }),\n );\n });\n }\n\n #setStatus(status: SessionStatus): void {\n if (this.#status !== status) {\n this.#status = status;\n this.emit(\"statusChange\", status);\n }\n }\n\n #setMode(mode: AgentMode): void {\n if (this.#mode !== mode) {\n this.#mode = mode;\n this.emit(\"modeChange\", mode);\n }\n }\n\n #handleAgentPresent(): void {\n if (this.#agentPresent) {\n return;\n }\n this.#agentPresent = true;\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n const pending = this.#pendingMessages;\n this.#pendingMessages = [];\n for (const message of pending) {\n this.#sendClientEvent(message);\n }\n }\n\n #abandonAgentWait(): void {\n if (this.#status === \"ended\" || this.#agentPresent) {\n return;\n }\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"The agent did not join the session\"),\n );\n void this.#transport.disconnect().catch(() => {});\n this.#finalize(\"connection_lost\");\n }\n\n #handleConnectionState(state: TransportConnectionState, reason?: string): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (state === \"reconnecting\") {\n this.#setStatus(\"reconnecting\");\n return;\n }\n if (state === \"connected\") {\n if (this.#status === \"reconnecting\") {\n this.#setStatus(\"connected\");\n }\n return;\n }\n // AGENT_LEFT = the agent participant hung up (worker ends without closing\n // the room). Room deletion covers server-forced ends and the duration\n // cap — the protocol has no session.ended message to tell these apart.\n const ended = this.#endedByClient\n ? \"user_hangup\"\n : reason === \"AGENT_LEFT\" || reason === \"ROOM_DELETED\" || reason === \"ROOM_CLOSED\"\n ? \"agent_hangup\"\n : \"connection_lost\";\n this.#finalize(ended);\n }\n\n #finalize(reason: EndReason): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n this.#endReason = reason;\n this.#setStatus(\"ended\");\n this.#wakeLock.release();\n this.#output.dispose();\n this.#outputAnalyser.dispose();\n this.#inputAnalyser.dispose();\n this.emit(\"disconnect\", { reason });\n }\n\n #handleAgentEvent(message: AgentSessionMessage): void {\n switch (message.type) {\n case \"client_tool.call\":\n void this.#tools.dispatch(message);\n return;\n case \"tool.started\":\n this.emit(\"toolCallStarted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n input: message.input,\n inputTruncated: message.inputTruncated === true,\n });\n return;\n case \"tool.completed\":\n this.emit(\"toolCallCompleted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n output: message.output,\n outputTruncated: message.outputTruncated === true,\n });\n return;\n case \"tool.failed\":\n this.emit(\"toolCallFailed\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n error: message.error,\n });\n return;\n case \"error\":\n this.emit(\n \"error\",\n message.code === \"provider_error\"\n ? new FishAgentError(\"provider_error\", \"An upstream provider failed during the session\")\n : new FishAgentError(\"internal_error\", \"The agent runtime hit an internal error\"),\n );\n return;\n default:\n // Forward compatibility: unknown message types are ignored by contract.\n return;\n }\n }\n\n #recordSegment(role: \"user\" | \"agent\", segmentId: string, text: string, final: boolean): void {\n const key = `${role}:${segmentId}`;\n const index = this.#transcriptIndex.get(key);\n if (index === undefined) {\n this.#transcriptIndex.set(key, this.#transcript.length);\n this.#transcript.push({ segmentId, role, text, final });\n } else {\n this.#transcript[index] = { segmentId, role, text, final };\n }\n }\n\n #handleTranscription(update: TranscriptionSegmentUpdate): void {\n if (update.role === \"user\") {\n this.#recordSegment(\"user\", update.segmentId, update.text, update.final);\n this.emit(\"userTranscript\", {\n segmentId: update.segmentId,\n text: update.text,\n final: update.final,\n });\n if (update.final) {\n this.emit(\"message\", { role: \"user\", text: update.text });\n }\n return;\n }\n // Agent speech: playout-synced, so streaming text means the agent is audibly\n // speaking; the segment closing means playout finished — back to listening.\n this.#setMode(update.final ? \"listening\" : \"speaking\");\n this.#recordSegment(\"agent\", update.segmentId, update.text, update.final);\n const previous = this.#agentSegments.get(update.segmentId) ?? \"\";\n const delta = update.text.startsWith(previous) ? update.text.slice(previous.length) : update.text;\n this.#agentSegments.set(update.segmentId, update.text);\n if (delta) {\n this.emit(\"agentResponseDelta\", { segmentId: update.segmentId, delta, text: update.text });\n }\n if (update.final) {\n this.#agentSegments.delete(update.segmentId);\n this.emit(\"agentResponse\", { segmentId: update.segmentId, text: update.text });\n this.emit(\"message\", { role: \"agent\", text: update.text });\n }\n }\n\n // Typed messages are not echoed by the server; the sender finalizes its own bubble.\n #recordTypedUserMessage(text: string): void {\n const segmentId = `typed_${++this.#typedMessageCount}`;\n this.#recordSegment(\"user\", segmentId, text, true);\n this.emit(\"userTranscript\", { segmentId, text, final: true });\n this.emit(\"message\", { role: \"user\", text });\n }\n}\n\n/** Internal seam: identical to AgentSession.start but with an injectable transport. */\nexport function startAgentSession(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n): Promise<AgentSession> {\n return startWith(options, createTransport);\n}\n","import {\n SESSION_TRANSPORT_LIVEKIT,\n type AgentSessionCreateRequest,\n type SessionLanguage,\n type SessionOverrides,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"./errors.js\";\n\nexport const SDK_VERSION = \"0.0.1\";\nexport const DEFAULT_SERVER_URL = \"https://api.fish.audio\";\n\nconst SUPPORTED_TRANSPORTS: string[] = [SESSION_TRANSPORT_LIVEKIT];\n\nexport interface SessionRequestOptions {\n /** Public agent: the SDK creates the session directly against the Fish API. */\n agentId?: string;\n /** Session token created by the host backend, passed through verbatim. */\n sessionToken?: SessionToken;\n serverUrl?: string;\n /** Sugar for `overrides.language`; an explicit override wins. Pair with a voice in that language. */\n language?: SessionLanguage;\n /**\n * IANA timezone for the agent's sense of local time in this session. Omit to\n * use the browser's timezone (sent automatically as a hint). With\n * `sessionToken` auth the host backend chooses instead, via `timezone` on its\n * session-creation call.\n */\n timezone?: string;\n /**\n * The agent knows the current date and time by default. Set false to withhold\n * both from this session's prompt. With `sessionToken` auth the host backend\n * chooses instead, via `world_context` on its session-creation call.\n */\n worldContext?: boolean;\n /** Per-session config overrides; each field must be allow-listed on the agent. */\n overrides?: SessionOverrides;\n /** `{{name}}` template values for the agent's prompt and first message. */\n dynamicVariables?: Record<string, string | number | boolean>;\n endUserId?: string;\n /** Caller-owned tag, stored and returned verbatim; the platform never interprets it. */\n metadata?: Record<string, unknown>;\n /**\n * Stream the agent's tool-call lifecycle (toolCallStarted/Completed/Failed\n * events, with payloads) to this session. Default true; set false when tool\n * data must stay hidden from this client. With `sessionToken` auth the host\n * backend chooses instead, via `tool_events` on its session-creation call.\n */\n toolEvents?: boolean;\n}\n\nexport function validateSessionToken(sessionToken: SessionToken): SessionToken {\n const transport: string = sessionToken.transport;\n if (!SUPPORTED_TRANSPORTS.includes(transport)) {\n throw new FishAgentError(\n \"unsupported_transport\",\n `Session token uses transport \"${transport}\" which this SDK version does not support; ` +\n \"please upgrade @fishaudio/agent-client\",\n );\n }\n const expiresAt = Date.parse(sessionToken.expires_at);\n if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {\n throw new FishAgentError(\n \"session_expired\",\n \"The session token's join deadline has passed; request a new session\",\n );\n }\n return sessionToken;\n}\n\nexport async function resolveSessionToken(options: SessionRequestOptions): Promise<SessionToken> {\n const provided = [options.agentId, options.sessionToken].filter((value) => value !== undefined);\n if (provided.length !== 1) {\n throw new TypeError(\n \"Exactly one of `agentId` or `sessionToken` must be provided to start a session\",\n );\n }\n\n if (options.sessionToken) {\n return validateSessionToken(options.sessionToken);\n }\n return validateSessionToken(await createPublicSession(options));\n}\n\n// The browser knows the end user's real timezone; without the hint the\n// platform would have to guess UTC.\nfunction detectClientTimezone(): string | undefined {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function createPublicSession(options: SessionRequestOptions): Promise<SessionToken> {\n const serverUrl = (options.serverUrl ?? DEFAULT_SERVER_URL).replace(/\\/$/, \"\");\n const clientTimezone = detectClientTimezone();\n const overrides: SessionOverrides | undefined =\n options.language || options.overrides\n ? { ...(options.language ? { language: options.language } : {}), ...options.overrides }\n : undefined;\n const request: AgentSessionCreateRequest = {\n agent_id: options.agentId as string,\n ...(options.timezone ? { timezone: options.timezone } : {}),\n ...(clientTimezone ? { client_timezone: clientTimezone } : {}),\n ...(options.worldContext !== undefined ? { world_context: options.worldContext } : {}),\n ...(overrides ? { overrides } : {}),\n ...(options.dynamicVariables ? { dynamic_variables: options.dynamicVariables } : {}),\n ...(options.endUserId ? { end_user_id: options.endUserId } : {}),\n ...(options.metadata ? { metadata: options.metadata } : {}),\n ...(options.toolEvents !== undefined ? { tool_events: options.toolEvents } : {}),\n };\n\n let response: Response;\n try {\n response = await fetch(`${serverUrl}/v1/agent/sessions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Fish-SDK\": `agent-client/${SDK_VERSION}`,\n },\n body: JSON.stringify(request),\n });\n } catch (cause) {\n throw new FishAgentError(\"session_request_failed\", \"Could not reach the session endpoint\", {\n cause,\n });\n }\n\n if (!response.ok) {\n throw await createSessionError(response);\n }\n return (await response.json()) as SessionToken;\n}\n\nasync function createSessionError(response: Response): Promise<FishAgentError> {\n let message = `Session request failed with HTTP ${response.status}`;\n let code: string | undefined;\n try {\n const body = (await response.json()) as { message?: string; error?: string; code?: string };\n message = body.message ?? body.error ?? message;\n code = body.code;\n } catch {\n // keep the status-based message\n }\n const options = { statusCode: response.status };\n // 409 alone is ambiguous (the server also uses it for e.g. an unpublished\n // agent); only the coded arm means \"this SDK is too old\".\n if (response.status === 409 && code === \"unsupported_transport\") {\n return new FishAgentError(\n \"unsupported_transport\",\n `${message}; please upgrade @fishaudio/agent-client`,\n options,\n );\n }\n if (response.status === 403) {\n return /origin/i.test(message)\n ? new FishAgentError(\"origin_forbidden\", message, options)\n : new FishAgentError(\"agent_not_public\", message, options);\n }\n return new FishAgentError(\"session_request_failed\", message, options);\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\ntype Listener = (...args: any[]) => void;\n\n/** Minimal typed emitter; listener errors are isolated so one bad consumer can't break the session. */\nexport class TypedEmitter<Events extends Record<keyof Events, Listener>> {\n private readonly listeners = new Map<keyof Events, Set<Listener>>();\n\n on<K extends keyof Events>(event: K, listener: Events[K]): this {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n return this;\n }\n\n off<K extends keyof Events>(event: K, listener: Events[K]): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n once<K extends keyof Events>(event: K, listener: Events[K]): this {\n const wrapper = ((...args: Parameters<Events[K]>) => {\n this.off(event, wrapper);\n listener(...args);\n }) as Events[K];\n return this.on(event, wrapper);\n }\n\n protected emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) {\n return;\n }\n for (const listener of [...set]) {\n try {\n listener(...args);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error(\"[fish-agent] listener error\", error);\n }\n }\n }\n}\n","import type { AgentSessionMessage, ClientSessionMessage } from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\n\nexport type ClientToolHandler = (\n params: Record<string, unknown>,\n context: { callId: string; toolName: string },\n) => unknown | Promise<unknown>;\n\nexport const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15_000;\n\ntype ClientToolCall = Extract<AgentSessionMessage, { type: \"client_tool.call\" }>;\n\nexport class ClientToolDispatcher {\n private readonly tools = new Map<string, ClientToolHandler>();\n\n constructor(\n tools: Record<string, ClientToolHandler> | undefined,\n private readonly timeoutMs: number,\n private readonly send: (message: ClientSessionMessage) => Promise<void>,\n private readonly onError: (error: FishAgentError) => void,\n ) {\n for (const [name, handler] of Object.entries(tools ?? {})) {\n this.tools.set(name, handler);\n }\n }\n\n register(name: string, handler: ClientToolHandler): void {\n this.tools.set(name, handler);\n }\n\n async dispatch(call: ClientToolCall): Promise<void> {\n const handler = this.tools.get(call.toolName);\n if (!handler) {\n this.onError(\n new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" is not registered`),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: \"Tool is not registered\" });\n }\n return;\n }\n\n try {\n const result = await this.withTimeout(\n Promise.resolve(handler(call.params, { callId: call.callId, toolName: call.toolName })),\n call.toolName,\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, result === undefined ? {} : { result });\n }\n } catch (error) {\n this.onError(\n error instanceof FishAgentError\n ? error\n : new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" threw`, {\n cause: error,\n }),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: String(error) });\n }\n }\n }\n\n private async reply(\n callId: string,\n body: { result?: unknown; isError?: boolean },\n ): Promise<void> {\n try {\n await this.send({ type: \"client_tool.result\", callId, ...body });\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool result\", {\n cause: error,\n }),\n );\n }\n }\n\n private withTimeout<T>(promise: Promise<T>, toolName: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new FishAgentError(\n \"tool_timeout\",\n `Client tool \"${toolName}\" timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n }\n}\n","/**\n * Holds a screen wake lock while a session is live (browser only; inert\n * elsewhere). The platform releases the lock whenever the page is hidden, so\n * visibility returns re-request it. Failures are silent by design: a session\n * works fine without the lock, the screen just may sleep mid-call.\n */\nexport class WakeLockHolder {\n #sentinel?: WakeLockSentinel;\n #active = false;\n\n readonly #onVisibilityChange = (): void => {\n if (document.visibilityState === \"visible\") {\n void this.#request();\n }\n };\n\n acquire(): void {\n if (\n this.#active ||\n typeof document === \"undefined\" ||\n typeof navigator === \"undefined\" ||\n !navigator.wakeLock\n ) {\n return;\n }\n this.#active = true;\n document.addEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#request();\n }\n\n async #request(): Promise<void> {\n if (!this.#active) {\n return;\n }\n try {\n const sentinel = await navigator.wakeLock.request(\"screen\");\n if (!this.#active) {\n void sentinel.release().catch(() => undefined);\n return;\n }\n // Concurrent requests (acquire + visibility return) may both resolve;\n // drop the earlier sentinel so exactly one is held.\n const previous = this.#sentinel;\n this.#sentinel = sentinel;\n if (previous && previous !== sentinel) {\n void previous.release().catch(() => undefined);\n }\n } catch {\n // Denied (battery saver, hidden tab, permissions policy) — run without it.\n }\n }\n\n release(): void {\n if (!this.#active) {\n return;\n }\n this.#active = false;\n document.removeEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#sentinel?.release().catch(() => undefined);\n this.#sentinel = undefined;\n }\n}\n"],"mappings":"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","names":["import_agent_protocol","LiveKitTransport","_a","_b"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/transport/livekit.ts","../src/index.ts","../src/audio/output.ts","../src/audio/analysis.ts","../src/session/agentSession.ts","../src/sessionToken.ts","../src/session/emitter.ts","../src/session/toolDispatcher.ts","../src/session/wakeLock.ts"],"sourcesContent":["export type FishAgentErrorCode =\n | \"session_request_failed\"\n | \"agent_not_public\"\n | \"origin_forbidden\"\n | \"unsupported_transport\"\n | \"mic_permission_denied\"\n /** A requested audio device could not be activated, or the browser does not support selecting it. */\n | \"device_change_failed\"\n | \"connection_failed\"\n | \"session_expired\"\n | \"tool_failed\"\n | \"tool_timeout\"\n /** An upstream model/speech provider failed during the session. */\n | \"provider_error\"\n /** The agent runtime hit an internal error. */\n | \"internal_error\";\n\nexport class FishAgentError extends Error {\n readonly code: FishAgentErrorCode;\n /** HTTP status of the failing session request, when the error originated there. */\n readonly statusCode?: number;\n\n constructor(\n code: FishAgentErrorCode,\n message: string,\n options?: { statusCode?: number; cause?: unknown },\n ) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"FishAgentError\";\n this.code = code;\n this.statusCode = options?.statusCode;\n }\n}\n","import {\n ConnectionState,\n createLocalAudioTrack,\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n TrackEvent,\n type LocalAudioTrack,\n type RemoteTrack,\n} from \"livekit-client\";\nimport {\n AGENT_EVENT_TOPIC,\n CLIENT_EVENT_TOPIC,\n type AgentSessionMessage,\n type ClientSessionMessage,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\nimport type { Transport, TransportCallbacks, TransportConnectOptions } from \"./types.js\";\n\nfunction hasMessageType(value: unknown): value is { type: string } {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { type?: unknown }).type === \"string\"\n );\n}\n\nfunction captureMicrophone(inputDeviceId?: string): Promise<LocalAudioTrack> {\n return createLocalAudioTrack({\n deviceId: inputDeviceId,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n });\n}\n\n/** Sole production Transport. LiveKit vocabulary must not escape this file,\n * except the `Room` handed out through the `getRoom()` escape hatch. */\nexport class LiveKitTransport implements Transport {\n private room?: Room;\n private callbacks?: TransportCallbacks;\n private pendingMicTrack?: Promise<LocalAudioTrack>;\n\n prepareMicrophone(options: { inputDeviceId?: string }): void {\n const pending = captureMicrophone(options.inputDeviceId);\n // A denial surfaces where connect() awaits the track, never as an\n // unhandled rejection here.\n pending.catch(() => undefined);\n this.pendingMicTrack = pending;\n }\n\n async connect(sessionToken: SessionToken, options: TransportConnectOptions): Promise<void> {\n this.callbacks = options.callbacks;\n const room = new Room({\n audioCaptureDefaults: {\n deviceId: options.inputDeviceId,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n this.room = room;\n\n room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {\n if (topic !== AGENT_EVENT_TOPIC) {\n return;\n }\n let message: unknown;\n try {\n message = JSON.parse(new TextDecoder().decode(payload));\n } catch {\n return;\n }\n if (hasMessageType(message)) {\n this.callbacks?.onAgentEvent(message as AgentSessionMessage);\n }\n });\n\n const AGENT_STATE_ATTRIBUTE = \"lk.agent.state\";\n room.on(RoomEvent.ParticipantAttributesChanged, (changed, participant) => {\n const state = changed[AGENT_STATE_ATTRIBUTE];\n if (!participant.isLocal && state) {\n this.callbacks?.onAgentState(state);\n }\n });\n\n room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack) => {\n if (track.kind === Track.Kind.Audio && track.mediaStreamTrack) {\n this.callbacks?.onOutputStream(new MediaStream([track.mediaStreamTrack]));\n }\n });\n\n // Both sides' transcripts ride LiveKit's built-in transcription streams:\n // agent segments are playout-synced delta streams (final on stream close —\n // an interrupted segment closes containing only the spoken text); user STT\n // segments are re-published per update with an explicit final attribute,\n // sent on behalf of the user, so sender identity distinguishes the roles.\n room.registerTextStreamHandler(\"lk.transcription\", async (reader, participantInfo) => {\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[\"lk.segment_id\"];\n if (!segmentId) {\n return;\n }\n const role =\n participantInfo.identity === room.localParticipant.identity ? \"user\" : \"agent\";\n const finalAttribute = attributes[\"lk.transcription_final\"] === \"true\";\n let text = \"\";\n let aborted = false;\n try {\n for await (const delta of reader) {\n text += delta;\n this.callbacks?.onTranscription({ segmentId, role, text, final: false });\n }\n } catch {\n aborted = true; // interrupted mid-stream; what arrived is what was spoken\n }\n if (text) {\n const final = role === \"agent\" ? true : finalAttribute && !aborted;\n this.callbacks?.onTranscription({ segmentId, role, text, final });\n }\n });\n\n room.on(RoomEvent.Reconnecting, () => {\n this.callbacks?.onConnectionState(\"reconnecting\");\n });\n room.on(RoomEvent.Reconnected, () => {\n // A full reconnect replays the roster from the join response before this\n // event; an agent still missing here hung up while we were away.\n if (room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n return;\n }\n this.callbacks?.onConnectionState(\"connected\");\n });\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n this.callbacks?.onConnectionState(\n \"disconnected\",\n reason === undefined ? undefined : DisconnectReason[reason],\n );\n // Remote-initiated ends never go through disconnect(); drop the refs here\n // so getRoom() honors its ended-means-undefined contract and the room\n // can be collected.\n if (this.room === room) {\n this.room = undefined;\n this.callbacks = undefined;\n }\n });\n // An agent-side hangup ends the worker without closing the room; the\n // agent participant leaving IS the end-of-call signal for the client.\n // A full reconnect also clears the roster this way, and can do so while\n // room.state still reads Connected (Reconnecting is only set right after),\n // so defer one microtask and require a room that is genuinely connected.\n room.on(RoomEvent.ParticipantDisconnected, () => {\n if (room.remoteParticipants.size === 0) {\n queueMicrotask(() => {\n if (room.state === ConnectionState.Connected && room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n }\n });\n }\n });\n\n let micTrack: LocalAudioTrack | undefined;\n if (options.microphone !== false) {\n // Usually settled long before this point: the capture starts inside the\n // user gesture (prepareMicrophone). Awaited before joining the room so a\n // denial fails the start without ever dispatching an agent. The promise\n // stays in pendingMicTrack until the room owns the track, so disconnect()\n // can release it after any failure in between.\n const pending = this.pendingMicTrack ?? captureMicrophone(options.inputDeviceId);\n this.pendingMicTrack = pending;\n micTrack = await pending;\n }\n\n try {\n await room.connect(sessionToken.livekit_url, sessionToken.token);\n } catch (cause) {\n throw new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause,\n });\n }\n // The attribute is sticky: if the agent joined and set its state before us,\n // no change event will fire — replay the current value once.\n for (const participant of room.remoteParticipants.values()) {\n const state = participant.attributes?.[AGENT_STATE_ATTRIBUTE];\n if (state) {\n this.callbacks?.onAgentState(state);\n }\n }\n if (micTrack) {\n await room.localParticipant.publishTrack(micTrack);\n this.pendingMicTrack = undefined;\n this.publishInputStream();\n }\n }\n\n private handleAgentLeft(room: Room): void {\n this.callbacks?.onConnectionState(\"disconnected\", \"AGENT_LEFT\");\n void room.disconnect();\n }\n\n async disconnect(): Promise<void> {\n // A capture the room never took ownership of (start failed between the\n // gesture-time capture and publish) is invisible to room.disconnect().\n const pendingMic = this.pendingMicTrack;\n this.pendingMicTrack = undefined;\n void pendingMic?.then((track) => track.stop()).catch(() => undefined);\n const room = this.room;\n this.room = undefined;\n this.callbacks = undefined;\n await room?.disconnect();\n }\n\n getRoom(): Room | undefined {\n return this.room;\n }\n\n async setMicEnabled(enabled: boolean): Promise<void> {\n await this.room?.localParticipant.setMicrophoneEnabled(enabled);\n if (enabled) {\n // After a mic-less connect the first enable captures the track late;\n // re-wiring an already-published track is a harmless replace.\n this.publishInputStream();\n }\n }\n\n async setInputDevice(deviceId: string): Promise<void> {\n const room = this.room;\n if (!room) {\n return;\n }\n const previous = room.getActiveDevice(\"audioinput\");\n let switched: boolean;\n try {\n // exact: a wrong id must fail loudly, not silently fall back to another mic.\n switched = await room.switchActiveDevice(\"audioinput\", deviceId, true);\n } catch (error) {\n // livekit stops the current track before acquiring the new device, so a\n // failed pick would otherwise leave the call silently mic-dead — switch\n // back before surfacing the failure. livekit reports \"default\" until the\n // real captured id is known; that pseudo-id only resolves on Chrome, so\n // restore it non-exactly and let other browsers fall back to any mic.\n if (previous !== undefined) {\n await room\n .switchActiveDevice(\"audioinput\", previous, previous !== \"default\")\n .catch(() => undefined);\n this.publishInputStream();\n }\n throw error;\n }\n if (!switched) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not activate the requested microphone\",\n );\n }\n // A live switch replaces the publication's MediaStreamTrack — re-wire\n // analysis. While muted, livekit defers the swap to the next unmute and\n // setMicEnabled re-wires then.\n this.publishInputStream();\n }\n\n /** Local tracks already wired to re-emit their stream when livekit restarts them. */\n private readonly restartWiredTracks = new WeakSet<object>();\n\n private publishInputStream(): void {\n const room = this.room;\n if (!room) {\n return;\n }\n for (const publication of room.localParticipant.audioTrackPublications.values()) {\n const track = publication.track;\n if (track && !this.restartWiredTracks.has(track)) {\n this.restartWiredTracks.add(track);\n // Restarts replace the MediaStreamTrack without any Room-level event —\n // livekit's devicechange auto-recovery and getRoom() escape-hatch\n // switches would otherwise leave input analysis on a dead track.\n track.on(TrackEvent.Restarted, () => this.publishInputStream());\n }\n const mediaStreamTrack = track?.mediaStreamTrack;\n if (mediaStreamTrack) {\n this.callbacks?.onInputStream(new MediaStream([mediaStreamTrack]));\n }\n }\n }\n\n async sendClientEvent(message: ClientSessionMessage): Promise<void> {\n if (!this.room) {\n throw new FishAgentError(\"connection_failed\", \"Session is not connected\");\n }\n const payload = new TextEncoder().encode(JSON.stringify(message));\n await this.room.localParticipant.publishData(payload, {\n reliable: true,\n topic: CLIENT_EVENT_TOPIC,\n });\n }\n}\n","// Public surface. Transport-neutral except the deliberate escape hatch\n// `AgentSession.getRoom()`, whose return type is livekit-client's `Room`.\nexport { AgentSession, type AgentSessionOptions } from \"./session/agentSession.js\";\nexport { DEFAULT_SERVER_URL } from \"./sessionToken.js\";\nexport { FishAgentError, type FishAgentErrorCode } from \"./errors.js\";\nexport type {\n AgentMode,\n AgentResponseDeltaEvent,\n AgentResponseEvent,\n AgentSessionCallbacks,\n AgentSessionEvents,\n ConversationMessage,\n EndReason,\n SessionStatus,\n ToolCallCompletedEvent,\n ToolCallFailedEvent,\n ToolCallStartedEvent,\n TranscriptSegment,\n UserTranscriptEvent,\n} from \"./events.js\";\nexport type { ClientToolHandler } from \"./session/toolDispatcher.js\";\nexport type {\n AgentSessionCreateRequest,\n SessionOverrides,\n SessionToken,\n ToolCallSource,\n} from \"@fishaudio/agent-protocol\";\n","import { FishAgentError } from \"../errors.js\";\n\n/** Throws `device_change_failed` where the browser cannot route audio output (`setSinkId`). */\nexport function assertOutputSelectionSupported(): void {\n if (typeof HTMLMediaElement === \"undefined\" || !(\"setSinkId\" in HTMLMediaElement.prototype)) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"This browser does not support selecting an audio output device\",\n );\n }\n}\n\n/** Agent audio playback via a detached <audio> element (browser only; inert elsewhere). */\nexport class AudioOutput {\n private element?: HTMLAudioElement;\n private volume = 1;\n\n private ensureElement(): HTMLAudioElement | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n if (!this.element) {\n this.element = document.createElement(\"audio\");\n this.element.autoplay = true;\n // Same-effect attribute as playsInline for iOS Safari without DOM typings noise.\n this.element.setAttribute(\"playsinline\", \"\");\n this.element.volume = this.volume;\n }\n return this.element;\n }\n\n setStream(stream: MediaStream): void {\n const element = this.ensureElement();\n if (!element) {\n return;\n }\n element.srcObject = stream;\n void element.play().catch(() => {\n // Autoplay policy: playback stays pending until startAudio() runs in a user gesture.\n });\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n await this.element?.play();\n }\n\n /**\n * Route playback to an output device. The element is created on demand so\n * the browser validates the device id right here, even before the agent's\n * audio arrives. Rejects with `device_change_failed` where output selection\n * is unsupported or the device cannot be used.\n */\n async setSinkId(sinkId: string): Promise<void> {\n assertOutputSelectionSupported();\n const element = this.ensureElement();\n try {\n await element?.setSinkId(sinkId);\n } catch (error) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not switch to the requested audio output device\",\n { cause: error },\n );\n }\n }\n\n setVolume(volume: number): void {\n this.volume = Math.min(1, Math.max(0, volume));\n if (this.element) {\n this.element.volume = this.volume;\n }\n }\n\n getVolume(): number {\n return this.volume;\n }\n\n dispose(): void {\n if (this.element) {\n this.element.srcObject = null;\n this.element.remove();\n this.element = undefined;\n }\n }\n}\n","/**\n * WebAudio analyser over a MediaStream, feeding visualizers (levels + FFT)\n * without exposing any transport object. Browser only; every accessor\n * degrades to silence when WebAudio is unavailable.\n */\nexport class AudioStreamAnalyser {\n private context?: AudioContext;\n private analyser?: AnalyserNode;\n private source?: MediaStreamAudioSourceNode;\n\n setStream(stream: MediaStream): void {\n if (typeof AudioContext === \"undefined\") {\n return;\n }\n this.context ??= new AudioContext();\n this.analyser ??= this.context.createAnalyser();\n this.source?.disconnect();\n this.source = this.context.createMediaStreamSource(stream);\n this.source.connect(this.analyser);\n this.resume();\n }\n\n /** AudioContexts start suspended under autoplay policies; safe to call any time. */\n resume(): void {\n if (this.context?.state === \"suspended\") {\n void this.context.resume().catch(() => {});\n }\n }\n\n /** RMS level in [0, 1]. */\n getVolume(): number {\n if (!this.analyser) {\n return 0;\n }\n const samples = new Uint8Array(this.analyser.fftSize);\n this.analyser.getByteTimeDomainData(samples);\n let sumOfSquares = 0;\n for (const sample of samples) {\n const centered = (sample - 128) / 128;\n sumOfSquares += centered * centered;\n }\n return Math.sqrt(sumOfSquares / samples.length);\n }\n\n getFrequencyData(): Uint8Array {\n if (!this.analyser) {\n return new Uint8Array(0);\n }\n const data = new Uint8Array(this.analyser.frequencyBinCount);\n this.analyser.getByteFrequencyData(data);\n return data;\n }\n\n dispose(): void {\n this.source?.disconnect();\n this.analyser?.disconnect();\n void this.context?.close().catch(() => {});\n this.source = undefined;\n this.analyser = undefined;\n this.context = undefined;\n }\n}\n","// Type-only: the transport implementation still loads lazily (see start()).\nimport type { Room } from \"livekit-client\";\nimport type {\n AgentSessionMessage,\n ClientSessionMessage,\n SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { assertOutputSelectionSupported, AudioOutput } from \"../audio/output.js\";\nimport { AudioStreamAnalyser } from \"../audio/analysis.js\";\nimport { FishAgentError } from \"../errors.js\";\nimport type {\n AgentMode,\n AgentSessionCallbacks,\n AgentSessionEvents,\n EndReason,\n SessionStatus,\n TranscriptSegment,\n} from \"../events.js\";\nimport { resolveSessionToken, type SessionRequestOptions } from \"../sessionToken.js\";\nimport type {\n TranscriptionSegmentUpdate,\n Transport,\n TransportConnectionState,\n TransportFactory,\n} from \"../transport/types.js\";\nimport { TypedEmitter } from \"./emitter.js\";\nimport {\n ClientToolDispatcher,\n DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n type ClientToolHandler,\n} from \"./toolDispatcher.js\";\nimport { WakeLockHolder } from \"./wakeLock.js\";\n\nexport interface AgentSessionOptions extends SessionRequestOptions {\n clientTools?: Record<string, ClientToolHandler>;\n clientToolTimeoutMs?: number;\n /**\n * Capture the microphone on start. Default true. Set false to join without\n * requesting it (text-first UIs); the first `setMicMuted(false)` then\n * captures — the permission prompt happens there, so call it from a user\n * gesture.\n */\n microphone?: boolean;\n audio?: {\n inputDeviceId?: string;\n /** Playback device (`setSinkId`); start rejects with `device_change_failed`\n * where output selection is unsupported (before creating a server session)\n * or the device cannot be used (before connecting). */\n outputDeviceId?: string;\n };\n /**\n * Hold a screen wake lock while the session is live, so long calls survive\n * the phone trying to sleep. Default true; denial is silent. Set false to\n * leave screen policy to the page.\n */\n wakeLock?: boolean;\n callbacks?: Partial<AgentSessionCallbacks>;\n}\n\n/** How long after connect the agent participant gets to show up before the\n * session gives up — normal joins land within a couple of seconds. */\nconst AGENT_JOIN_TIMEOUT_MS = 15_000;\n\n/** Assigned in AgentSession's static block; bridges startAgentSession to the\n * class-private #startWith so no transport types sit on the public class. */\nlet startWith: (\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n) => Promise<AgentSession>;\n\nexport class AgentSession extends TypedEmitter<AgentSessionEvents> {\n #status: SessionStatus = \"connecting\";\n #mode: AgentMode = \"listening\";\n #endReason?: EndReason;\n #sessionId: string;\n #micMuted = false;\n #endedByClient = false;\n #ending?: Promise<void>;\n\n #transport!: Transport;\n #tools!: ClientToolDispatcher;\n readonly #output = new AudioOutput();\n readonly #outputAnalyser = new AudioStreamAnalyser();\n readonly #inputAnalyser = new AudioStreamAnalyser();\n readonly #wakeLock = new WakeLockHolder();\n\n /** Text already delivered per agent segment, to derive deltas from full-text updates. */\n readonly #agentSegments = new Map<string, string>();\n /** Transcript so far, for late subscribers (getTranscript); upserted per segment. */\n readonly #transcript: TranscriptSegment[] = [];\n readonly #transcriptIndex = new Map<string, number>();\n #typedMessageCount = 0;\n\n // Data frames reach only participants already in the room — and the worker\n // registers its handlers while starting up — so typed messages wait here\n // until the agent publishes its first pipeline state.\n #agentPresent = false;\n #pendingMessages: ClientSessionMessage[] = [];\n #joinTimer?: ReturnType<typeof setTimeout>;\n\n private constructor(sessionToken: SessionToken) {\n super();\n this.#sessionId = sessionToken.session_id;\n }\n\n /**\n * Obtain a session token (if needed), connect, publish the microphone and\n * hand back a live session. Rejects with FishAgentError on session-request,\n * permission or connect failures.\n */\n static async start(options: AgentSessionOptions): Promise<AgentSession> {\n // Lazy so the transport implementation (and livekit-client) never loads\n // in SSR bundles or in tests that inject their own transport.\n const { LiveKitTransport } = await import(\"../transport/livekit.js\");\n return AgentSession.#startWith(options, () => new LiveKitTransport());\n }\n\n // ---- getters ----\n\n get sessionId(): string {\n return this.#sessionId;\n }\n\n get status(): SessionStatus {\n return this.#status;\n }\n\n get mode(): AgentMode {\n return this.#mode;\n }\n\n get isSpeaking(): boolean {\n return this.#mode === \"speaking\";\n }\n\n get micMuted(): boolean {\n return this.#micMuted;\n }\n\n get endReason(): EndReason | undefined {\n return this.#endReason;\n }\n\n /**\n * Snapshot of every transcript segment so far, in conversation order. Seed a\n * consumer that subscribes after events already fired (e.g. a component\n * mounted right after start), then apply live events by segmentId.\n */\n getTranscript(): TranscriptSegment[] {\n return this.#transcript.map((segment) => ({ ...segment }));\n }\n\n // ---- text & control channel ----\n\n /** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */\n sendUserMessage(text: string, options?: { audio?: boolean }): void {\n const trimmed = text.trim();\n if (!trimmed) {\n return;\n }\n const message: ClientSessionMessage =\n options?.audio === true\n ? { type: \"user.message\", text: trimmed }\n : { type: \"user.message\", text: trimmed, audio: false };\n if (this.#agentPresent) {\n this.#sendClientEvent(message);\n } else {\n this.#pendingMessages.push(message);\n }\n this.#recordTypedUserMessage(trimmed);\n }\n\n sendUserActivity(): void {\n this.#sendClientEvent({ type: \"user.activity\" });\n }\n\n interrupt(): void {\n this.#sendClientEvent({ type: \"user.interrupt\" });\n }\n\n registerClientTool(name: string, handler: ClientToolHandler): void {\n this.#tools.register(name, handler);\n }\n\n // ---- audio ----\n\n /**\n * After a `microphone: false` start, the first unmute captures and publishes\n * the microphone — the permission prompt happens here. A denial rejects with\n * `mic_permission_denied` and the session stays muted.\n */\n async setMicMuted(muted: boolean): Promise<void> {\n const previous = this.#micMuted;\n this.#micMuted = muted;\n try {\n await this.#transport.setMicEnabled(!muted);\n } catch (error) {\n this.#micMuted = previous;\n throw AgentSession.#asMicError(error);\n }\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n this.#outputAnalyser.resume();\n this.#inputAnalyser.resume();\n await this.#output.startAudio();\n }\n\n /**\n * Switch the microphone mid-call. While the mic is not captured yet\n * (`microphone: false` start, still muted) this records the preference for\n * the first unmute. Rejects with `device_change_failed` when the device\n * cannot be activated, switching back to the previous microphone (best\n * effort — the transport stops the old capture before acquiring the new).\n */\n async setInputDevice(deviceId: string): Promise<void> {\n try {\n await this.#transport.setInputDevice(deviceId);\n } catch (error) {\n throw AgentSession.#asDeviceError(error, \"Could not switch the microphone\");\n }\n }\n\n /**\n * Route the agent's audio to an output device (`setSinkId`); pass `\"\"` to\n * return to the default device. Rejects with `device_change_failed` where\n * the browser does not support output selection (common on mobile browsers)\n * or the device cannot be used; playback stays on the previous device.\n */\n async setOutputDevice(deviceId: string): Promise<void> {\n await this.#output.setSinkId(deviceId);\n }\n\n setOutputVolume(volume: number): void {\n this.#output.setVolume(volume);\n }\n\n getOutputVolume(): number {\n return this.#outputAnalyser.getVolume();\n }\n\n getInputVolume(): number {\n return this.#inputAnalyser.getVolume();\n }\n\n getOutputFrequencyData(): Uint8Array {\n return this.#outputAnalyser.getFrequencyData();\n }\n\n getInputFrequencyData(): Uint8Array {\n return this.#inputAnalyser.getFrequencyData();\n }\n\n /**\n * Escape hatch: the underlying LiveKit `Room`, for needs the session API\n * doesn't cover (connection-quality telemetry, publishing extra tracks).\n * Code using it couples to this SDK's transport choice and livekit-client\n * version — prefer the session API where one exists. `undefined` after the\n * session ends, or when a custom transport doesn't expose a room.\n */\n getRoom(): Room | undefined {\n return this.#transport.getRoom?.() as Room | undefined;\n }\n\n // ---- lifecycle ----\n\n end(): Promise<void> {\n if (this.#ending) {\n return this.#ending;\n }\n if (this.#status === \"ended\") {\n return Promise.resolve();\n }\n const ending = this.#endOnce();\n this.#ending = ending;\n void ending.catch(() => {\n if (this.#ending === ending) {\n this.#ending = undefined;\n }\n });\n return ending;\n }\n\n async #endOnce(): Promise<void> {\n this.#endedByClient = true;\n try {\n // Graceful hangup over the reverse channel; participant departure is the\n // worker-side fallback if this frame never arrives.\n await this.#transport.sendClientEvent({ type: \"user.hangup\" });\n } catch {\n // best effort\n }\n await this.#transport.disconnect();\n this.#finalize(\"user_hangup\");\n }\n\n // ---- internals ----\n\n static {\n startWith = (options, createTransport) => AgentSession.#startWith(options, createTransport);\n }\n\n static async #startWith(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n ): Promise<AgentSession> {\n if (options.audio?.outputDeviceId !== undefined) {\n // Cheap sync check first, so an unsupported browser fails before a\n // server session is even created.\n assertOutputSelectionSupported();\n }\n const transport = createTransport();\n if (options.microphone !== false) {\n // Inside the user gesture that called start(), before the token\n // round-trip pushes getUserMedia out of the gesture's transient\n // activation (see Transport.prepareMicrophone). A denial surfaces in\n // transport.connect().\n transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });\n }\n let sessionToken: SessionToken;\n let session: AgentSession;\n try {\n sessionToken = await resolveSessionToken(options);\n session = new AgentSession(sessionToken);\n if (options.audio?.outputDeviceId !== undefined) {\n // The browser validates the device id here, before anything connects;\n // rejects like a token failure — no events yet.\n await session.#output.setSinkId(options.audio.outputDeviceId);\n }\n } catch (error) {\n // The gesture-time capture must not outlive a failed start.\n await transport.disconnect().catch(() => undefined);\n throw error;\n }\n\n for (const [key, callback] of Object.entries(options.callbacks ?? {})) {\n const event = (key.charAt(2).toLowerCase() +\n key.slice(3)) as keyof AgentSessionEvents;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n session.on(event, callback as any);\n }\n\n session.#transport = transport;\n if (options.microphone === false) {\n session.#micMuted = true;\n }\n session.#tools = new ClientToolDispatcher(\n options.clientTools,\n options.clientToolTimeoutMs ?? DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n (message) => transport.sendClientEvent(message),\n (error) => session.emit(\"error\", error),\n );\n\n try {\n await transport.connect(sessionToken, {\n inputDeviceId: options.audio?.inputDeviceId,\n microphone: options.microphone,\n callbacks: {\n onAgentEvent: (message) => session.#handleAgentEvent(message),\n // Mode model: \"listening\" is the default state; the framework's agent\n // state supplies the thinking edge; speaking follows the playout-synced\n // transcription segments (open = audible, closed = back to listening).\n onAgentState: (state) => {\n // Any published agent state proves the worker's pipeline is up and\n // its event handlers are registered — the gate for queued messages.\n session.#handleAgentPresent();\n if (state === \"thinking\") {\n session.#setMode(\"thinking\");\n }\n },\n onTranscription: (update) => session.#handleTranscription(update),\n onOutputStream: (stream) => {\n session.#output.setStream(stream);\n session.#outputAnalyser.setStream(stream);\n },\n onInputStream: (stream) => session.#inputAnalyser.setStream(stream),\n onConnectionState: (state, reason) => session.#handleConnectionState(state, reason),\n },\n });\n } catch (error) {\n // connect() can fail after the room is already joined (e.g. mic capture);\n // tear the transport down or the room stays connected with no owner.\n await session.#transport.disconnect().catch(() => undefined);\n session.#finalize(\"connection_lost\");\n throw AgentSession.#asStartError(error);\n }\n\n if (session.#status === \"connecting\") {\n session.#setStatus(\"connected\");\n session.emit(\"connect\", { sessionId: session.#sessionId });\n }\n if (session.#status !== \"ended\" && options.wakeLock !== false) {\n session.#wakeLock.acquire();\n }\n if (!session.#agentPresent && session.#status === \"connected\") {\n session.#joinTimer = setTimeout(() => session.#abandonAgentWait(), AGENT_JOIN_TIMEOUT_MS);\n }\n return session;\n }\n\n static #asStartError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n return new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause: error,\n });\n }\n\n static #asMicError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n if (\n error instanceof Error &&\n (error.name === \"OverconstrainedError\" || error.name === \"NotFoundError\")\n ) {\n // Device/constraint failures surface here both for a preference recorded\n // while muted and for a first capture with no usable microphone — keep\n // the device error code, with a message that fits either.\n return new FishAgentError(\n \"device_change_failed\",\n \"No usable microphone could be activated\",\n { cause: error },\n );\n }\n return new FishAgentError(\"connection_failed\", \"Could not toggle the microphone\", {\n cause: error,\n });\n }\n\n static #asDeviceError(error: unknown, message: string): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n return new FishAgentError(\"device_change_failed\", message, { cause: error });\n }\n\n #sendClientEvent(message: Parameters<Transport[\"sendClientEvent\"]>[0]): void {\n void this.#transport.sendClientEvent(message).catch((error: unknown) => {\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"Failed to send to the agent\", { cause: error }),\n );\n });\n }\n\n #setStatus(status: SessionStatus): void {\n if (this.#status !== status) {\n this.#status = status;\n this.emit(\"statusChange\", status);\n }\n }\n\n #setMode(mode: AgentMode): void {\n if (this.#mode !== mode) {\n this.#mode = mode;\n this.emit(\"modeChange\", mode);\n }\n }\n\n #handleAgentPresent(): void {\n if (this.#agentPresent) {\n return;\n }\n this.#agentPresent = true;\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n const pending = this.#pendingMessages;\n this.#pendingMessages = [];\n for (const message of pending) {\n this.#sendClientEvent(message);\n }\n }\n\n #abandonAgentWait(): void {\n if (this.#status === \"ended\" || this.#agentPresent) {\n return;\n }\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"The agent did not join the session\"),\n );\n void this.#transport.disconnect().catch(() => {});\n this.#finalize(\"connection_lost\");\n }\n\n #handleConnectionState(state: TransportConnectionState, reason?: string): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (state === \"reconnecting\") {\n this.#setStatus(\"reconnecting\");\n return;\n }\n if (state === \"connected\") {\n if (this.#status === \"reconnecting\") {\n this.#setStatus(\"connected\");\n }\n return;\n }\n // AGENT_LEFT = the agent participant hung up (worker ends without closing\n // the room). Room deletion covers server-forced ends and the duration\n // cap — the protocol has no session.ended message to tell these apart.\n const ended = this.#endedByClient\n ? \"user_hangup\"\n : reason === \"AGENT_LEFT\" || reason === \"ROOM_DELETED\" || reason === \"ROOM_CLOSED\"\n ? \"agent_hangup\"\n : \"connection_lost\";\n this.#finalize(ended);\n }\n\n #finalize(reason: EndReason): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n this.#endReason = reason;\n this.#setStatus(\"ended\");\n this.#wakeLock.release();\n this.#output.dispose();\n this.#outputAnalyser.dispose();\n this.#inputAnalyser.dispose();\n this.emit(\"disconnect\", { reason });\n }\n\n #handleAgentEvent(message: AgentSessionMessage): void {\n switch (message.type) {\n case \"client_tool.call\":\n void this.#tools.dispatch(message);\n return;\n case \"tool.started\":\n this.emit(\"toolCallStarted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n input: message.input,\n inputTruncated: message.inputTruncated === true,\n });\n return;\n case \"tool.completed\":\n this.emit(\"toolCallCompleted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n output: message.output,\n outputTruncated: message.outputTruncated === true,\n });\n return;\n case \"tool.failed\":\n this.emit(\"toolCallFailed\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n error: message.error,\n });\n return;\n case \"error\":\n this.emit(\n \"error\",\n message.code === \"provider_error\"\n ? new FishAgentError(\"provider_error\", \"An upstream provider failed during the session\")\n : new FishAgentError(\"internal_error\", \"The agent runtime hit an internal error\"),\n );\n return;\n default:\n // Forward compatibility: unknown message types are ignored by contract.\n return;\n }\n }\n\n #recordSegment(role: \"user\" | \"agent\", segmentId: string, text: string, final: boolean): void {\n const key = `${role}:${segmentId}`;\n const index = this.#transcriptIndex.get(key);\n if (index === undefined) {\n this.#transcriptIndex.set(key, this.#transcript.length);\n this.#transcript.push({ segmentId, role, text, final });\n } else {\n this.#transcript[index] = { segmentId, role, text, final };\n }\n }\n\n #handleTranscription(update: TranscriptionSegmentUpdate): void {\n if (update.role === \"user\") {\n this.#recordSegment(\"user\", update.segmentId, update.text, update.final);\n this.emit(\"userTranscript\", {\n segmentId: update.segmentId,\n text: update.text,\n final: update.final,\n });\n if (update.final) {\n this.emit(\"message\", { role: \"user\", text: update.text });\n }\n return;\n }\n // Agent speech: playout-synced, so streaming text means the agent is audibly\n // speaking; the segment closing means playout finished — back to listening.\n this.#setMode(update.final ? \"listening\" : \"speaking\");\n this.#recordSegment(\"agent\", update.segmentId, update.text, update.final);\n const previous = this.#agentSegments.get(update.segmentId) ?? \"\";\n const delta = update.text.startsWith(previous) ? update.text.slice(previous.length) : update.text;\n this.#agentSegments.set(update.segmentId, update.text);\n if (delta) {\n this.emit(\"agentResponseDelta\", { segmentId: update.segmentId, delta, text: update.text });\n }\n if (update.final) {\n this.#agentSegments.delete(update.segmentId);\n this.emit(\"agentResponse\", { segmentId: update.segmentId, text: update.text });\n this.emit(\"message\", { role: \"agent\", text: update.text });\n }\n }\n\n // Typed messages are not echoed by the server; the sender finalizes its own bubble.\n #recordTypedUserMessage(text: string): void {\n const segmentId = `typed_${++this.#typedMessageCount}`;\n this.#recordSegment(\"user\", segmentId, text, true);\n this.emit(\"userTranscript\", { segmentId, text, final: true });\n this.emit(\"message\", { role: \"user\", text });\n }\n}\n\n/** Internal seam: identical to AgentSession.start but with an injectable transport. */\nexport function startAgentSession(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n): Promise<AgentSession> {\n return startWith(options, createTransport);\n}\n","import {\n SESSION_TRANSPORT_LIVEKIT,\n type AgentSessionCreateRequest,\n type SessionLanguage,\n type SessionOverrides,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"./errors.js\";\n\nexport const SDK_VERSION = \"0.0.1\";\nexport const DEFAULT_SERVER_URL = \"https://api.fish.audio\";\n\nconst SUPPORTED_TRANSPORTS: string[] = [SESSION_TRANSPORT_LIVEKIT];\n\nexport interface SessionRequestOptions {\n /** Public agent: the SDK creates the session directly against the Fish API. */\n agentId?: string;\n /** Session token created by the host backend, passed through verbatim. */\n sessionToken?: SessionToken;\n serverUrl?: string;\n /** Sugar for `overrides.language`; an explicit override wins. Pair with a voice in that language. */\n language?: SessionLanguage;\n /**\n * IANA timezone for the agent's sense of local time in this session. Omit to\n * use the browser's timezone (sent automatically as a hint). With\n * `sessionToken` auth the host backend chooses instead, via `timezone` on its\n * session-creation call.\n */\n timezone?: string;\n /**\n * The agent knows the current date and time by default. Set false to withhold\n * both from this session's prompt. With `sessionToken` auth the host backend\n * chooses instead, via `world_context` on its session-creation call.\n */\n worldContext?: boolean;\n /** Per-session config overrides; each field must be allow-listed on the agent. */\n overrides?: SessionOverrides;\n /** `{{name}}` template values for the agent's prompt and first message. */\n dynamicVariables?: Record<string, string | number | boolean>;\n endUserId?: string;\n /** Caller-owned tag, stored and returned verbatim; the platform never interprets it. */\n metadata?: Record<string, unknown>;\n /**\n * Stream the agent's tool-call lifecycle (toolCallStarted/Completed/Failed\n * events, with payloads) to this session. Default true; set false when tool\n * data must stay hidden from this client. With `sessionToken` auth the host\n * backend chooses instead, via `tool_events` on its session-creation call.\n */\n toolEvents?: boolean;\n}\n\nexport function validateSessionToken(sessionToken: SessionToken): SessionToken {\n const transport: string = sessionToken.transport;\n if (!SUPPORTED_TRANSPORTS.includes(transport)) {\n throw new FishAgentError(\n \"unsupported_transport\",\n `Session token uses transport \"${transport}\" which this SDK version does not support; ` +\n \"please upgrade @fishaudio/agent-client\",\n );\n }\n const expiresAt = Date.parse(sessionToken.expires_at);\n if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {\n throw new FishAgentError(\n \"session_expired\",\n \"The session token's join deadline has passed; request a new session\",\n );\n }\n return sessionToken;\n}\n\nexport async function resolveSessionToken(options: SessionRequestOptions): Promise<SessionToken> {\n const provided = [options.agentId, options.sessionToken].filter((value) => value !== undefined);\n if (provided.length !== 1) {\n throw new TypeError(\n \"Exactly one of `agentId` or `sessionToken` must be provided to start a session\",\n );\n }\n\n if (options.sessionToken) {\n return validateSessionToken(options.sessionToken);\n }\n return validateSessionToken(await createPublicSession(options));\n}\n\n// The browser knows the end user's real timezone; without the hint the\n// platform would have to guess UTC.\nfunction detectClientTimezone(): string | undefined {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function createPublicSession(options: SessionRequestOptions): Promise<SessionToken> {\n const serverUrl = (options.serverUrl ?? DEFAULT_SERVER_URL).replace(/\\/$/, \"\");\n const clientTimezone = detectClientTimezone();\n const overrides: SessionOverrides | undefined =\n options.language || options.overrides\n ? { ...(options.language ? { language: options.language } : {}), ...options.overrides }\n : undefined;\n const request: AgentSessionCreateRequest = {\n agent_id: options.agentId as string,\n ...(options.timezone ? { timezone: options.timezone } : {}),\n ...(clientTimezone ? { client_timezone: clientTimezone } : {}),\n ...(options.worldContext !== undefined ? { world_context: options.worldContext } : {}),\n ...(overrides ? { overrides } : {}),\n ...(options.dynamicVariables ? { dynamic_variables: options.dynamicVariables } : {}),\n ...(options.endUserId ? { end_user_id: options.endUserId } : {}),\n ...(options.metadata ? { metadata: options.metadata } : {}),\n ...(options.toolEvents !== undefined ? { tool_events: options.toolEvents } : {}),\n };\n\n let response: Response;\n try {\n response = await fetch(`${serverUrl}/v1/agent/sessions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Fish-SDK\": `agent-client/${SDK_VERSION}`,\n },\n body: JSON.stringify(request),\n });\n } catch (cause) {\n throw new FishAgentError(\"session_request_failed\", \"Could not reach the session endpoint\", {\n cause,\n });\n }\n\n if (!response.ok) {\n throw await createSessionError(response);\n }\n return (await response.json()) as SessionToken;\n}\n\nasync function createSessionError(response: Response): Promise<FishAgentError> {\n let message = `Session request failed with HTTP ${response.status}`;\n let code: string | undefined;\n try {\n const body = (await response.json()) as { message?: string; error?: string; code?: string };\n message = body.message ?? body.error ?? message;\n code = body.code;\n } catch {\n // keep the status-based message\n }\n const options = { statusCode: response.status };\n // 409 alone is ambiguous (the server also uses it for e.g. an unpublished\n // agent); only the coded arm means \"this SDK is too old\".\n if (response.status === 409 && code === \"unsupported_transport\") {\n return new FishAgentError(\n \"unsupported_transport\",\n `${message}; please upgrade @fishaudio/agent-client`,\n options,\n );\n }\n if (response.status === 403) {\n return /origin/i.test(message)\n ? new FishAgentError(\"origin_forbidden\", message, options)\n : new FishAgentError(\"agent_not_public\", message, options);\n }\n return new FishAgentError(\"session_request_failed\", message, options);\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\ntype Listener = (...args: any[]) => void;\n\n/** Minimal typed emitter; listener errors are isolated so one bad consumer can't break the session. */\nexport class TypedEmitter<Events extends Record<keyof Events, Listener>> {\n private readonly listeners = new Map<keyof Events, Set<Listener>>();\n\n on<K extends keyof Events>(event: K, listener: Events[K]): this {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n return this;\n }\n\n off<K extends keyof Events>(event: K, listener: Events[K]): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n once<K extends keyof Events>(event: K, listener: Events[K]): this {\n const wrapper = ((...args: Parameters<Events[K]>) => {\n this.off(event, wrapper);\n listener(...args);\n }) as Events[K];\n return this.on(event, wrapper);\n }\n\n protected emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) {\n return;\n }\n for (const listener of [...set]) {\n try {\n listener(...args);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error(\"[fish-agent] listener error\", error);\n }\n }\n }\n}\n","import type { AgentSessionMessage, ClientSessionMessage } from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\n\nexport type ClientToolHandler = (\n params: Record<string, unknown>,\n context: { callId: string; toolName: string },\n) => unknown | Promise<unknown>;\n\nexport const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15_000;\n\ntype ClientToolCall = Extract<AgentSessionMessage, { type: \"client_tool.call\" }>;\n\nexport class ClientToolDispatcher {\n private readonly tools = new Map<string, ClientToolHandler>();\n\n constructor(\n tools: Record<string, ClientToolHandler> | undefined,\n private readonly timeoutMs: number,\n private readonly send: (message: ClientSessionMessage) => Promise<void>,\n private readonly onError: (error: FishAgentError) => void,\n ) {\n for (const [name, handler] of Object.entries(tools ?? {})) {\n this.tools.set(name, handler);\n }\n }\n\n register(name: string, handler: ClientToolHandler): void {\n this.tools.set(name, handler);\n }\n\n async dispatch(call: ClientToolCall): Promise<void> {\n const handler = this.tools.get(call.toolName);\n if (!handler) {\n this.onError(\n new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" is not registered`),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: \"Tool is not registered\" });\n }\n return;\n }\n\n try {\n const result = await this.withTimeout(\n Promise.resolve(handler(call.params, { callId: call.callId, toolName: call.toolName })),\n call.toolName,\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, result === undefined ? {} : { result });\n }\n } catch (error) {\n this.onError(\n error instanceof FishAgentError\n ? error\n : new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" threw`, {\n cause: error,\n }),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: String(error) });\n }\n }\n }\n\n private async reply(\n callId: string,\n body: { result?: unknown; isError?: boolean },\n ): Promise<void> {\n try {\n await this.send({ type: \"client_tool.result\", callId, ...body });\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool result\", {\n cause: error,\n }),\n );\n }\n }\n\n private withTimeout<T>(promise: Promise<T>, toolName: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new FishAgentError(\n \"tool_timeout\",\n `Client tool \"${toolName}\" timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n }\n}\n","/**\n * Holds a screen wake lock while a session is live (browser only; inert\n * elsewhere). The platform releases the lock whenever the page is hidden, so\n * visibility returns re-request it. Failures are silent by design: a session\n * works fine without the lock, the screen just may sleep mid-call.\n */\nexport class WakeLockHolder {\n #sentinel?: WakeLockSentinel;\n #active = false;\n\n readonly #onVisibilityChange = (): void => {\n if (document.visibilityState === \"visible\") {\n void this.#request();\n }\n };\n\n acquire(): void {\n if (\n this.#active ||\n typeof document === \"undefined\" ||\n typeof navigator === \"undefined\" ||\n !navigator.wakeLock\n ) {\n return;\n }\n this.#active = true;\n document.addEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#request();\n }\n\n async #request(): Promise<void> {\n if (!this.#active) {\n return;\n }\n try {\n const sentinel = await navigator.wakeLock.request(\"screen\");\n if (!this.#active) {\n void sentinel.release().catch(() => undefined);\n return;\n }\n // Concurrent requests (acquire + visibility return) may both resolve;\n // drop the earlier sentinel so exactly one is held.\n const previous = this.#sentinel;\n this.#sentinel = sentinel;\n if (previous && previous !== sentinel) {\n void previous.release().catch(() => undefined);\n }\n } catch {\n // Denied (battery saver, hidden tab, permissions policy) — run without it.\n }\n }\n\n release(): void {\n if (!this.#active) {\n return;\n }\n this.#active = false;\n document.removeEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#sentinel?.release().catch(() => undefined);\n this.#sentinel = undefined;\n }\n}\n"],"mappings":"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","names":["import_agent_protocol","LiveKitTransport","_a","_b"]}
package/dist/index.d.cts CHANGED
@@ -202,7 +202,7 @@ declare class AgentSession extends TypedEmitter<AgentSessionEvents> {
202
202
  * mounted right after start), then apply live events by segmentId.
203
203
  */
204
204
  getTranscript(): TranscriptSegment[];
205
- /** `audio: false` asks the agent to answer this turn in text only (no TTS). */
205
+ /** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */
206
206
  sendUserMessage(text: string, options?: {
207
207
  audio?: boolean;
208
208
  }): void;
package/dist/index.d.ts CHANGED
@@ -202,7 +202,7 @@ declare class AgentSession extends TypedEmitter<AgentSessionEvents> {
202
202
  * mounted right after start), then apply live events by segmentId.
203
203
  */
204
204
  getTranscript(): TranscriptSegment[];
205
- /** `audio: false` asks the agent to answer this turn in text only (no TTS). */
205
+ /** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */
206
206
  sendUserMessage(text: string, options?: {
207
207
  audio?: boolean;
208
208
  }): void;
package/dist/index.js CHANGED
@@ -452,7 +452,7 @@ var _AgentSession = class _AgentSession extends TypedEmitter {
452
452
  */
453
453
  static async start(options) {
454
454
  var _a;
455
- const { LiveKitTransport } = await import("./livekit-B62YPROC.js");
455
+ const { LiveKitTransport } = await import("./livekit-DBSPD4GR.js");
456
456
  return __privateMethod(_a = _AgentSession, _AgentSession_static, startWith_fn).call(_a, options, () => new LiveKitTransport());
457
457
  }
458
458
  // ---- getters ----
@@ -483,13 +483,13 @@ var _AgentSession = class _AgentSession extends TypedEmitter {
483
483
  return __privateGet(this, _transcript).map((segment) => ({ ...segment }));
484
484
  }
485
485
  // ---- text & control channel ----
486
- /** `audio: false` asks the agent to answer this turn in text only (no TTS). */
486
+ /** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */
487
487
  sendUserMessage(text, options) {
488
488
  const trimmed = text.trim();
489
489
  if (!trimmed) {
490
490
  return;
491
491
  }
492
- const message = options?.audio === false ? { type: "user.message", text: trimmed, audio: false } : { type: "user.message", text: trimmed };
492
+ const message = options?.audio === true ? { type: "user.message", text: trimmed } : { type: "user.message", text: trimmed, audio: false };
493
493
  if (__privateGet(this, _agentPresent)) {
494
494
  __privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, message);
495
495
  } else {
@@ -632,16 +632,26 @@ startWith_fn = async function(options, createTransport) {
632
632
  if (options.audio?.outputDeviceId !== void 0) {
633
633
  assertOutputSelectionSupported();
634
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);
635
+ const transport = createTransport();
636
+ if (options.microphone !== false) {
637
+ transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });
638
+ }
639
+ let sessionToken;
640
+ let session;
641
+ try {
642
+ sessionToken = await resolveSessionToken(options);
643
+ session = new _AgentSession(sessionToken);
644
+ if (options.audio?.outputDeviceId !== void 0) {
645
+ await __privateGet(session, _output).setSinkId(options.audio.outputDeviceId);
646
+ }
647
+ } catch (error) {
648
+ await transport.disconnect().catch(() => void 0);
649
+ throw error;
639
650
  }
640
651
  for (const [key, callback] of Object.entries(options.callbacks ?? {})) {
641
652
  const event = key.charAt(2).toLowerCase() + key.slice(3);
642
653
  session.on(event, callback);
643
654
  }
644
- const transport = createTransport(sessionToken);
645
655
  __privateSet(session, _transport, transport);
646
656
  if (options.microphone === false) {
647
657
  __privateSet(session, _micMuted, true);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audio/output.ts","../src/audio/analysis.ts","../src/sessionToken.ts","../src/session/emitter.ts","../src/session/toolDispatcher.ts","../src/session/wakeLock.ts","../src/session/agentSession.ts"],"sourcesContent":["import { FishAgentError } from \"../errors.js\";\n\n/** Throws `device_change_failed` where the browser cannot route audio output (`setSinkId`). */\nexport function assertOutputSelectionSupported(): void {\n if (typeof HTMLMediaElement === \"undefined\" || !(\"setSinkId\" in HTMLMediaElement.prototype)) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"This browser does not support selecting an audio output device\",\n );\n }\n}\n\n/** Agent audio playback via a detached <audio> element (browser only; inert elsewhere). */\nexport class AudioOutput {\n private element?: HTMLAudioElement;\n private volume = 1;\n\n private ensureElement(): HTMLAudioElement | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n if (!this.element) {\n this.element = document.createElement(\"audio\");\n this.element.autoplay = true;\n // Same-effect attribute as playsInline for iOS Safari without DOM typings noise.\n this.element.setAttribute(\"playsinline\", \"\");\n this.element.volume = this.volume;\n }\n return this.element;\n }\n\n setStream(stream: MediaStream): void {\n const element = this.ensureElement();\n if (!element) {\n return;\n }\n element.srcObject = stream;\n void element.play().catch(() => {\n // Autoplay policy: playback stays pending until startAudio() runs in a user gesture.\n });\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n await this.element?.play();\n }\n\n /**\n * Route playback to an output device. The element is created on demand so\n * the browser validates the device id right here, even before the agent's\n * audio arrives. Rejects with `device_change_failed` where output selection\n * is unsupported or the device cannot be used.\n */\n async setSinkId(sinkId: string): Promise<void> {\n assertOutputSelectionSupported();\n const element = this.ensureElement();\n try {\n await element?.setSinkId(sinkId);\n } catch (error) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not switch to the requested audio output device\",\n { cause: error },\n );\n }\n }\n\n setVolume(volume: number): void {\n this.volume = Math.min(1, Math.max(0, volume));\n if (this.element) {\n this.element.volume = this.volume;\n }\n }\n\n getVolume(): number {\n return this.volume;\n }\n\n dispose(): void {\n if (this.element) {\n this.element.srcObject = null;\n this.element.remove();\n this.element = undefined;\n }\n }\n}\n","/**\n * WebAudio analyser over a MediaStream, feeding visualizers (levels + FFT)\n * without exposing any transport object. Browser only; every accessor\n * degrades to silence when WebAudio is unavailable.\n */\nexport class AudioStreamAnalyser {\n private context?: AudioContext;\n private analyser?: AnalyserNode;\n private source?: MediaStreamAudioSourceNode;\n\n setStream(stream: MediaStream): void {\n if (typeof AudioContext === \"undefined\") {\n return;\n }\n this.context ??= new AudioContext();\n this.analyser ??= this.context.createAnalyser();\n this.source?.disconnect();\n this.source = this.context.createMediaStreamSource(stream);\n this.source.connect(this.analyser);\n this.resume();\n }\n\n /** AudioContexts start suspended under autoplay policies; safe to call any time. */\n resume(): void {\n if (this.context?.state === \"suspended\") {\n void this.context.resume().catch(() => {});\n }\n }\n\n /** RMS level in [0, 1]. */\n getVolume(): number {\n if (!this.analyser) {\n return 0;\n }\n const samples = new Uint8Array(this.analyser.fftSize);\n this.analyser.getByteTimeDomainData(samples);\n let sumOfSquares = 0;\n for (const sample of samples) {\n const centered = (sample - 128) / 128;\n sumOfSquares += centered * centered;\n }\n return Math.sqrt(sumOfSquares / samples.length);\n }\n\n getFrequencyData(): Uint8Array {\n if (!this.analyser) {\n return new Uint8Array(0);\n }\n const data = new Uint8Array(this.analyser.frequencyBinCount);\n this.analyser.getByteFrequencyData(data);\n return data;\n }\n\n dispose(): void {\n this.source?.disconnect();\n this.analyser?.disconnect();\n void this.context?.close().catch(() => {});\n this.source = undefined;\n this.analyser = undefined;\n this.context = undefined;\n }\n}\n","import {\n SESSION_TRANSPORT_LIVEKIT,\n type AgentSessionCreateRequest,\n type SessionLanguage,\n type SessionOverrides,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"./errors.js\";\n\nexport const SDK_VERSION = \"0.0.1\";\nexport const DEFAULT_SERVER_URL = \"https://api.fish.audio\";\n\nconst SUPPORTED_TRANSPORTS: string[] = [SESSION_TRANSPORT_LIVEKIT];\n\nexport interface SessionRequestOptions {\n /** Public agent: the SDK creates the session directly against the Fish API. */\n agentId?: string;\n /** Session token created by the host backend, passed through verbatim. */\n sessionToken?: SessionToken;\n serverUrl?: string;\n /** Sugar for `overrides.language`; an explicit override wins. Pair with a voice in that language. */\n language?: SessionLanguage;\n /**\n * IANA timezone for the agent's sense of local time in this session. Omit to\n * use the browser's timezone (sent automatically as a hint). With\n * `sessionToken` auth the host backend chooses instead, via `timezone` on its\n * session-creation call.\n */\n timezone?: string;\n /**\n * The agent knows the current date and time by default. Set false to withhold\n * both from this session's prompt. With `sessionToken` auth the host backend\n * chooses instead, via `world_context` on its session-creation call.\n */\n worldContext?: boolean;\n /** Per-session config overrides; each field must be allow-listed on the agent. */\n overrides?: SessionOverrides;\n /** `{{name}}` template values for the agent's prompt and first message. */\n dynamicVariables?: Record<string, string | number | boolean>;\n endUserId?: string;\n /** Caller-owned tag, stored and returned verbatim; the platform never interprets it. */\n metadata?: Record<string, unknown>;\n /**\n * Stream the agent's tool-call lifecycle (toolCallStarted/Completed/Failed\n * events, with payloads) to this session. Default true; set false when tool\n * data must stay hidden from this client. With `sessionToken` auth the host\n * backend chooses instead, via `tool_events` on its session-creation call.\n */\n toolEvents?: boolean;\n}\n\nexport function validateSessionToken(sessionToken: SessionToken): SessionToken {\n const transport: string = sessionToken.transport;\n if (!SUPPORTED_TRANSPORTS.includes(transport)) {\n throw new FishAgentError(\n \"unsupported_transport\",\n `Session token uses transport \"${transport}\" which this SDK version does not support; ` +\n \"please upgrade @fishaudio/agent-client\",\n );\n }\n const expiresAt = Date.parse(sessionToken.expires_at);\n if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {\n throw new FishAgentError(\n \"session_expired\",\n \"The session token's join deadline has passed; request a new session\",\n );\n }\n return sessionToken;\n}\n\nexport async function resolveSessionToken(options: SessionRequestOptions): Promise<SessionToken> {\n const provided = [options.agentId, options.sessionToken].filter((value) => value !== undefined);\n if (provided.length !== 1) {\n throw new TypeError(\n \"Exactly one of `agentId` or `sessionToken` must be provided to start a session\",\n );\n }\n\n if (options.sessionToken) {\n return validateSessionToken(options.sessionToken);\n }\n return validateSessionToken(await createPublicSession(options));\n}\n\n// The browser knows the end user's real timezone; without the hint the\n// platform would have to guess UTC.\nfunction detectClientTimezone(): string | undefined {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function createPublicSession(options: SessionRequestOptions): Promise<SessionToken> {\n const serverUrl = (options.serverUrl ?? DEFAULT_SERVER_URL).replace(/\\/$/, \"\");\n const clientTimezone = detectClientTimezone();\n const overrides: SessionOverrides | undefined =\n options.language || options.overrides\n ? { ...(options.language ? { language: options.language } : {}), ...options.overrides }\n : undefined;\n const request: AgentSessionCreateRequest = {\n agent_id: options.agentId as string,\n ...(options.timezone ? { timezone: options.timezone } : {}),\n ...(clientTimezone ? { client_timezone: clientTimezone } : {}),\n ...(options.worldContext !== undefined ? { world_context: options.worldContext } : {}),\n ...(overrides ? { overrides } : {}),\n ...(options.dynamicVariables ? { dynamic_variables: options.dynamicVariables } : {}),\n ...(options.endUserId ? { end_user_id: options.endUserId } : {}),\n ...(options.metadata ? { metadata: options.metadata } : {}),\n ...(options.toolEvents !== undefined ? { tool_events: options.toolEvents } : {}),\n };\n\n let response: Response;\n try {\n response = await fetch(`${serverUrl}/v1/agent/sessions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Fish-SDK\": `agent-client/${SDK_VERSION}`,\n },\n body: JSON.stringify(request),\n });\n } catch (cause) {\n throw new FishAgentError(\"session_request_failed\", \"Could not reach the session endpoint\", {\n cause,\n });\n }\n\n if (!response.ok) {\n throw await createSessionError(response);\n }\n return (await response.json()) as SessionToken;\n}\n\nasync function createSessionError(response: Response): Promise<FishAgentError> {\n let message = `Session request failed with HTTP ${response.status}`;\n let code: string | undefined;\n try {\n const body = (await response.json()) as { message?: string; error?: string; code?: string };\n message = body.message ?? body.error ?? message;\n code = body.code;\n } catch {\n // keep the status-based message\n }\n const options = { statusCode: response.status };\n // 409 alone is ambiguous (the server also uses it for e.g. an unpublished\n // agent); only the coded arm means \"this SDK is too old\".\n if (response.status === 409 && code === \"unsupported_transport\") {\n return new FishAgentError(\n \"unsupported_transport\",\n `${message}; please upgrade @fishaudio/agent-client`,\n options,\n );\n }\n if (response.status === 403) {\n return /origin/i.test(message)\n ? new FishAgentError(\"origin_forbidden\", message, options)\n : new FishAgentError(\"agent_not_public\", message, options);\n }\n return new FishAgentError(\"session_request_failed\", message, options);\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\ntype Listener = (...args: any[]) => void;\n\n/** Minimal typed emitter; listener errors are isolated so one bad consumer can't break the session. */\nexport class TypedEmitter<Events extends Record<keyof Events, Listener>> {\n private readonly listeners = new Map<keyof Events, Set<Listener>>();\n\n on<K extends keyof Events>(event: K, listener: Events[K]): this {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n return this;\n }\n\n off<K extends keyof Events>(event: K, listener: Events[K]): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n once<K extends keyof Events>(event: K, listener: Events[K]): this {\n const wrapper = ((...args: Parameters<Events[K]>) => {\n this.off(event, wrapper);\n listener(...args);\n }) as Events[K];\n return this.on(event, wrapper);\n }\n\n protected emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) {\n return;\n }\n for (const listener of [...set]) {\n try {\n listener(...args);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error(\"[fish-agent] listener error\", error);\n }\n }\n }\n}\n","import type { AgentSessionMessage, ClientSessionMessage } from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\n\nexport type ClientToolHandler = (\n params: Record<string, unknown>,\n context: { callId: string; toolName: string },\n) => unknown | Promise<unknown>;\n\nexport const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15_000;\n\ntype ClientToolCall = Extract<AgentSessionMessage, { type: \"client_tool.call\" }>;\n\nexport class ClientToolDispatcher {\n private readonly tools = new Map<string, ClientToolHandler>();\n\n constructor(\n tools: Record<string, ClientToolHandler> | undefined,\n private readonly timeoutMs: number,\n private readonly send: (message: ClientSessionMessage) => Promise<void>,\n private readonly onError: (error: FishAgentError) => void,\n ) {\n for (const [name, handler] of Object.entries(tools ?? {})) {\n this.tools.set(name, handler);\n }\n }\n\n register(name: string, handler: ClientToolHandler): void {\n this.tools.set(name, handler);\n }\n\n async dispatch(call: ClientToolCall): Promise<void> {\n const handler = this.tools.get(call.toolName);\n if (!handler) {\n this.onError(\n new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" is not registered`),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: \"Tool is not registered\" });\n }\n return;\n }\n\n try {\n const result = await this.withTimeout(\n Promise.resolve(handler(call.params, { callId: call.callId, toolName: call.toolName })),\n call.toolName,\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, result === undefined ? {} : { result });\n }\n } catch (error) {\n this.onError(\n error instanceof FishAgentError\n ? error\n : new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" threw`, {\n cause: error,\n }),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: String(error) });\n }\n }\n }\n\n private async reply(\n callId: string,\n body: { result?: unknown; isError?: boolean },\n ): Promise<void> {\n try {\n await this.send({ type: \"client_tool.result\", callId, ...body });\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool result\", {\n cause: error,\n }),\n );\n }\n }\n\n private withTimeout<T>(promise: Promise<T>, toolName: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new FishAgentError(\n \"tool_timeout\",\n `Client tool \"${toolName}\" timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n }\n}\n","/**\n * Holds a screen wake lock while a session is live (browser only; inert\n * elsewhere). The platform releases the lock whenever the page is hidden, so\n * visibility returns re-request it. Failures are silent by design: a session\n * works fine without the lock, the screen just may sleep mid-call.\n */\nexport class WakeLockHolder {\n #sentinel?: WakeLockSentinel;\n #active = false;\n\n readonly #onVisibilityChange = (): void => {\n if (document.visibilityState === \"visible\") {\n void this.#request();\n }\n };\n\n acquire(): void {\n if (\n this.#active ||\n typeof document === \"undefined\" ||\n typeof navigator === \"undefined\" ||\n !navigator.wakeLock\n ) {\n return;\n }\n this.#active = true;\n document.addEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#request();\n }\n\n async #request(): Promise<void> {\n if (!this.#active) {\n return;\n }\n try {\n const sentinel = await navigator.wakeLock.request(\"screen\");\n if (!this.#active) {\n void sentinel.release().catch(() => undefined);\n return;\n }\n // Concurrent requests (acquire + visibility return) may both resolve;\n // drop the earlier sentinel so exactly one is held.\n const previous = this.#sentinel;\n this.#sentinel = sentinel;\n if (previous && previous !== sentinel) {\n void previous.release().catch(() => undefined);\n }\n } catch {\n // Denied (battery saver, hidden tab, permissions policy) — run without it.\n }\n }\n\n release(): void {\n if (!this.#active) {\n return;\n }\n this.#active = false;\n document.removeEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#sentinel?.release().catch(() => undefined);\n this.#sentinel = undefined;\n }\n}\n","// Type-only: the transport implementation still loads lazily (see start()).\nimport type { Room } from \"livekit-client\";\nimport type {\n AgentSessionMessage,\n ClientSessionMessage,\n SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { assertOutputSelectionSupported, AudioOutput } from \"../audio/output.js\";\nimport { AudioStreamAnalyser } from \"../audio/analysis.js\";\nimport { FishAgentError } from \"../errors.js\";\nimport type {\n AgentMode,\n AgentSessionCallbacks,\n AgentSessionEvents,\n EndReason,\n SessionStatus,\n TranscriptSegment,\n} from \"../events.js\";\nimport { resolveSessionToken, type SessionRequestOptions } from \"../sessionToken.js\";\nimport type {\n TranscriptionSegmentUpdate,\n Transport,\n TransportConnectionState,\n TransportFactory,\n} from \"../transport/types.js\";\nimport { TypedEmitter } from \"./emitter.js\";\nimport {\n ClientToolDispatcher,\n DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n type ClientToolHandler,\n} from \"./toolDispatcher.js\";\nimport { WakeLockHolder } from \"./wakeLock.js\";\n\nexport interface AgentSessionOptions extends SessionRequestOptions {\n clientTools?: Record<string, ClientToolHandler>;\n clientToolTimeoutMs?: number;\n /**\n * Capture the microphone on start. Default true. Set false to join without\n * requesting it (text-first UIs); the first `setMicMuted(false)` then\n * captures — the permission prompt happens there, so call it from a user\n * gesture.\n */\n microphone?: boolean;\n audio?: {\n inputDeviceId?: string;\n /** Playback device (`setSinkId`); start rejects with `device_change_failed`\n * where output selection is unsupported (before creating a server session)\n * or the device cannot be used (before connecting). */\n outputDeviceId?: string;\n };\n /**\n * Hold a screen wake lock while the session is live, so long calls survive\n * the phone trying to sleep. Default true; denial is silent. Set false to\n * leave screen policy to the page.\n */\n wakeLock?: boolean;\n callbacks?: Partial<AgentSessionCallbacks>;\n}\n\n/** How long after connect the agent participant gets to show up before the\n * session gives up — normal joins land within a couple of seconds. */\nconst AGENT_JOIN_TIMEOUT_MS = 15_000;\n\n/** Assigned in AgentSession's static block; bridges startAgentSession to the\n * class-private #startWith so no transport types sit on the public class. */\nlet startWith: (\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n) => Promise<AgentSession>;\n\nexport class AgentSession extends TypedEmitter<AgentSessionEvents> {\n #status: SessionStatus = \"connecting\";\n #mode: AgentMode = \"listening\";\n #endReason?: EndReason;\n #sessionId: string;\n #micMuted = false;\n #endedByClient = false;\n #ending?: Promise<void>;\n\n #transport!: Transport;\n #tools!: ClientToolDispatcher;\n readonly #output = new AudioOutput();\n readonly #outputAnalyser = new AudioStreamAnalyser();\n readonly #inputAnalyser = new AudioStreamAnalyser();\n readonly #wakeLock = new WakeLockHolder();\n\n /** Text already delivered per agent segment, to derive deltas from full-text updates. */\n readonly #agentSegments = new Map<string, string>();\n /** Transcript so far, for late subscribers (getTranscript); upserted per segment. */\n readonly #transcript: TranscriptSegment[] = [];\n readonly #transcriptIndex = new Map<string, number>();\n #typedMessageCount = 0;\n\n // Data frames reach only participants already in the room — and the worker\n // registers its handlers while starting up — so typed messages wait here\n // until the agent publishes its first pipeline state.\n #agentPresent = false;\n #pendingMessages: ClientSessionMessage[] = [];\n #joinTimer?: ReturnType<typeof setTimeout>;\n\n private constructor(sessionToken: SessionToken) {\n super();\n this.#sessionId = sessionToken.session_id;\n }\n\n /**\n * Obtain a session token (if needed), connect, publish the microphone and\n * hand back a live session. Rejects with FishAgentError on session-request,\n * permission or connect failures.\n */\n static async start(options: AgentSessionOptions): Promise<AgentSession> {\n // Lazy so the transport implementation (and livekit-client) never loads\n // in SSR bundles or in tests that inject their own transport.\n const { LiveKitTransport } = await import(\"../transport/livekit.js\");\n return AgentSession.#startWith(options, () => new LiveKitTransport());\n }\n\n // ---- getters ----\n\n get sessionId(): string {\n return this.#sessionId;\n }\n\n get status(): SessionStatus {\n return this.#status;\n }\n\n get mode(): AgentMode {\n return this.#mode;\n }\n\n get isSpeaking(): boolean {\n return this.#mode === \"speaking\";\n }\n\n get micMuted(): boolean {\n return this.#micMuted;\n }\n\n get endReason(): EndReason | undefined {\n return this.#endReason;\n }\n\n /**\n * Snapshot of every transcript segment so far, in conversation order. Seed a\n * consumer that subscribes after events already fired (e.g. a component\n * mounted right after start), then apply live events by segmentId.\n */\n getTranscript(): TranscriptSegment[] {\n return this.#transcript.map((segment) => ({ ...segment }));\n }\n\n // ---- text & control channel ----\n\n /** `audio: false` asks the agent to answer this turn in text only (no TTS). */\n sendUserMessage(text: string, options?: { audio?: boolean }): void {\n const trimmed = text.trim();\n if (!trimmed) {\n return;\n }\n const message: ClientSessionMessage =\n options?.audio === false\n ? { type: \"user.message\", text: trimmed, audio: false }\n : { type: \"user.message\", text: trimmed };\n if (this.#agentPresent) {\n this.#sendClientEvent(message);\n } else {\n this.#pendingMessages.push(message);\n }\n this.#recordTypedUserMessage(trimmed);\n }\n\n sendUserActivity(): void {\n this.#sendClientEvent({ type: \"user.activity\" });\n }\n\n interrupt(): void {\n this.#sendClientEvent({ type: \"user.interrupt\" });\n }\n\n registerClientTool(name: string, handler: ClientToolHandler): void {\n this.#tools.register(name, handler);\n }\n\n // ---- audio ----\n\n /**\n * After a `microphone: false` start, the first unmute captures and publishes\n * the microphone — the permission prompt happens here. A denial rejects with\n * `mic_permission_denied` and the session stays muted.\n */\n async setMicMuted(muted: boolean): Promise<void> {\n const previous = this.#micMuted;\n this.#micMuted = muted;\n try {\n await this.#transport.setMicEnabled(!muted);\n } catch (error) {\n this.#micMuted = previous;\n throw AgentSession.#asMicError(error);\n }\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n this.#outputAnalyser.resume();\n this.#inputAnalyser.resume();\n await this.#output.startAudio();\n }\n\n /**\n * Switch the microphone mid-call. While the mic is not captured yet\n * (`microphone: false` start, still muted) this records the preference for\n * the first unmute. Rejects with `device_change_failed` when the device\n * cannot be activated, switching back to the previous microphone (best\n * effort — the transport stops the old capture before acquiring the new).\n */\n async setInputDevice(deviceId: string): Promise<void> {\n try {\n await this.#transport.setInputDevice(deviceId);\n } catch (error) {\n throw AgentSession.#asDeviceError(error, \"Could not switch the microphone\");\n }\n }\n\n /**\n * Route the agent's audio to an output device (`setSinkId`); pass `\"\"` to\n * return to the default device. Rejects with `device_change_failed` where\n * the browser does not support output selection (common on mobile browsers)\n * or the device cannot be used; playback stays on the previous device.\n */\n async setOutputDevice(deviceId: string): Promise<void> {\n await this.#output.setSinkId(deviceId);\n }\n\n setOutputVolume(volume: number): void {\n this.#output.setVolume(volume);\n }\n\n getOutputVolume(): number {\n return this.#outputAnalyser.getVolume();\n }\n\n getInputVolume(): number {\n return this.#inputAnalyser.getVolume();\n }\n\n getOutputFrequencyData(): Uint8Array {\n return this.#outputAnalyser.getFrequencyData();\n }\n\n getInputFrequencyData(): Uint8Array {\n return this.#inputAnalyser.getFrequencyData();\n }\n\n /**\n * Escape hatch: the underlying LiveKit `Room`, for needs the session API\n * doesn't cover (connection-quality telemetry, publishing extra tracks).\n * Code using it couples to this SDK's transport choice and livekit-client\n * version — prefer the session API where one exists. `undefined` after the\n * session ends, or when a custom transport doesn't expose a room.\n */\n getRoom(): Room | undefined {\n return this.#transport.getRoom?.() as Room | undefined;\n }\n\n // ---- lifecycle ----\n\n end(): Promise<void> {\n if (this.#ending) {\n return this.#ending;\n }\n if (this.#status === \"ended\") {\n return Promise.resolve();\n }\n const ending = this.#endOnce();\n this.#ending = ending;\n void ending.catch(() => {\n if (this.#ending === ending) {\n this.#ending = undefined;\n }\n });\n return ending;\n }\n\n async #endOnce(): Promise<void> {\n this.#endedByClient = true;\n try {\n // Graceful hangup over the reverse channel; participant departure is the\n // worker-side fallback if this frame never arrives.\n await this.#transport.sendClientEvent({ type: \"user.hangup\" });\n } catch {\n // best effort\n }\n await this.#transport.disconnect();\n this.#finalize(\"user_hangup\");\n }\n\n // ---- internals ----\n\n static {\n startWith = (options, createTransport) => AgentSession.#startWith(options, createTransport);\n }\n\n static async #startWith(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n ): Promise<AgentSession> {\n if (options.audio?.outputDeviceId !== undefined) {\n // Cheap sync check first, so an unsupported browser fails before a\n // server session is even created.\n assertOutputSelectionSupported();\n }\n const sessionToken = await resolveSessionToken(options);\n const session = new AgentSession(sessionToken);\n if (options.audio?.outputDeviceId !== undefined) {\n // The browser validates the device id here, before anything connects;\n // rejects like a token failure — no events, no teardown needed yet.\n await session.#output.setSinkId(options.audio.outputDeviceId);\n }\n\n for (const [key, callback] of Object.entries(options.callbacks ?? {})) {\n const event = (key.charAt(2).toLowerCase() +\n key.slice(3)) as keyof AgentSessionEvents;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n session.on(event, callback as any);\n }\n\n const transport = createTransport(sessionToken);\n session.#transport = transport;\n if (options.microphone === false) {\n session.#micMuted = true;\n }\n session.#tools = new ClientToolDispatcher(\n options.clientTools,\n options.clientToolTimeoutMs ?? DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n (message) => transport.sendClientEvent(message),\n (error) => session.emit(\"error\", error),\n );\n\n try {\n await transport.connect(sessionToken, {\n inputDeviceId: options.audio?.inputDeviceId,\n microphone: options.microphone,\n callbacks: {\n onAgentEvent: (message) => session.#handleAgentEvent(message),\n // Mode model: \"listening\" is the default state; the framework's agent\n // state supplies the thinking edge; speaking follows the playout-synced\n // transcription segments (open = audible, closed = back to listening).\n onAgentState: (state) => {\n // Any published agent state proves the worker's pipeline is up and\n // its event handlers are registered — the gate for queued messages.\n session.#handleAgentPresent();\n if (state === \"thinking\") {\n session.#setMode(\"thinking\");\n }\n },\n onTranscription: (update) => session.#handleTranscription(update),\n onOutputStream: (stream) => {\n session.#output.setStream(stream);\n session.#outputAnalyser.setStream(stream);\n },\n onInputStream: (stream) => session.#inputAnalyser.setStream(stream),\n onConnectionState: (state, reason) => session.#handleConnectionState(state, reason),\n },\n });\n } catch (error) {\n // connect() can fail after the room is already joined (e.g. mic capture);\n // tear the transport down or the room stays connected with no owner.\n await session.#transport.disconnect().catch(() => undefined);\n session.#finalize(\"connection_lost\");\n throw AgentSession.#asStartError(error);\n }\n\n if (session.#status === \"connecting\") {\n session.#setStatus(\"connected\");\n session.emit(\"connect\", { sessionId: session.#sessionId });\n }\n if (session.#status !== \"ended\" && options.wakeLock !== false) {\n session.#wakeLock.acquire();\n }\n if (!session.#agentPresent && session.#status === \"connected\") {\n session.#joinTimer = setTimeout(() => session.#abandonAgentWait(), AGENT_JOIN_TIMEOUT_MS);\n }\n return session;\n }\n\n static #asStartError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n return new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause: error,\n });\n }\n\n static #asMicError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n if (\n error instanceof Error &&\n (error.name === \"OverconstrainedError\" || error.name === \"NotFoundError\")\n ) {\n // Device/constraint failures surface here both for a preference recorded\n // while muted and for a first capture with no usable microphone — keep\n // the device error code, with a message that fits either.\n return new FishAgentError(\n \"device_change_failed\",\n \"No usable microphone could be activated\",\n { cause: error },\n );\n }\n return new FishAgentError(\"connection_failed\", \"Could not toggle the microphone\", {\n cause: error,\n });\n }\n\n static #asDeviceError(error: unknown, message: string): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n return new FishAgentError(\"device_change_failed\", message, { cause: error });\n }\n\n #sendClientEvent(message: Parameters<Transport[\"sendClientEvent\"]>[0]): void {\n void this.#transport.sendClientEvent(message).catch((error: unknown) => {\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"Failed to send to the agent\", { cause: error }),\n );\n });\n }\n\n #setStatus(status: SessionStatus): void {\n if (this.#status !== status) {\n this.#status = status;\n this.emit(\"statusChange\", status);\n }\n }\n\n #setMode(mode: AgentMode): void {\n if (this.#mode !== mode) {\n this.#mode = mode;\n this.emit(\"modeChange\", mode);\n }\n }\n\n #handleAgentPresent(): void {\n if (this.#agentPresent) {\n return;\n }\n this.#agentPresent = true;\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n const pending = this.#pendingMessages;\n this.#pendingMessages = [];\n for (const message of pending) {\n this.#sendClientEvent(message);\n }\n }\n\n #abandonAgentWait(): void {\n if (this.#status === \"ended\" || this.#agentPresent) {\n return;\n }\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"The agent did not join the session\"),\n );\n void this.#transport.disconnect().catch(() => {});\n this.#finalize(\"connection_lost\");\n }\n\n #handleConnectionState(state: TransportConnectionState, reason?: string): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (state === \"reconnecting\") {\n this.#setStatus(\"reconnecting\");\n return;\n }\n if (state === \"connected\") {\n if (this.#status === \"reconnecting\") {\n this.#setStatus(\"connected\");\n }\n return;\n }\n // AGENT_LEFT = the agent participant hung up (worker ends without closing\n // the room). Room deletion covers server-forced ends and the duration\n // cap — the protocol has no session.ended message to tell these apart.\n const ended = this.#endedByClient\n ? \"user_hangup\"\n : reason === \"AGENT_LEFT\" || reason === \"ROOM_DELETED\" || reason === \"ROOM_CLOSED\"\n ? \"agent_hangup\"\n : \"connection_lost\";\n this.#finalize(ended);\n }\n\n #finalize(reason: EndReason): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n this.#endReason = reason;\n this.#setStatus(\"ended\");\n this.#wakeLock.release();\n this.#output.dispose();\n this.#outputAnalyser.dispose();\n this.#inputAnalyser.dispose();\n this.emit(\"disconnect\", { reason });\n }\n\n #handleAgentEvent(message: AgentSessionMessage): void {\n switch (message.type) {\n case \"client_tool.call\":\n void this.#tools.dispatch(message);\n return;\n case \"tool.started\":\n this.emit(\"toolCallStarted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n input: message.input,\n inputTruncated: message.inputTruncated === true,\n });\n return;\n case \"tool.completed\":\n this.emit(\"toolCallCompleted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n output: message.output,\n outputTruncated: message.outputTruncated === true,\n });\n return;\n case \"tool.failed\":\n this.emit(\"toolCallFailed\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n error: message.error,\n });\n return;\n case \"error\":\n this.emit(\n \"error\",\n message.code === \"provider_error\"\n ? new FishAgentError(\"provider_error\", \"An upstream provider failed during the session\")\n : new FishAgentError(\"internal_error\", \"The agent runtime hit an internal error\"),\n );\n return;\n default:\n // Forward compatibility: unknown message types are ignored by contract.\n return;\n }\n }\n\n #recordSegment(role: \"user\" | \"agent\", segmentId: string, text: string, final: boolean): void {\n const key = `${role}:${segmentId}`;\n const index = this.#transcriptIndex.get(key);\n if (index === undefined) {\n this.#transcriptIndex.set(key, this.#transcript.length);\n this.#transcript.push({ segmentId, role, text, final });\n } else {\n this.#transcript[index] = { segmentId, role, text, final };\n }\n }\n\n #handleTranscription(update: TranscriptionSegmentUpdate): void {\n if (update.role === \"user\") {\n this.#recordSegment(\"user\", update.segmentId, update.text, update.final);\n this.emit(\"userTranscript\", {\n segmentId: update.segmentId,\n text: update.text,\n final: update.final,\n });\n if (update.final) {\n this.emit(\"message\", { role: \"user\", text: update.text });\n }\n return;\n }\n // Agent speech: playout-synced, so streaming text means the agent is audibly\n // speaking; the segment closing means playout finished — back to listening.\n this.#setMode(update.final ? \"listening\" : \"speaking\");\n this.#recordSegment(\"agent\", update.segmentId, update.text, update.final);\n const previous = this.#agentSegments.get(update.segmentId) ?? \"\";\n const delta = update.text.startsWith(previous) ? update.text.slice(previous.length) : update.text;\n this.#agentSegments.set(update.segmentId, update.text);\n if (delta) {\n this.emit(\"agentResponseDelta\", { segmentId: update.segmentId, delta, text: update.text });\n }\n if (update.final) {\n this.#agentSegments.delete(update.segmentId);\n this.emit(\"agentResponse\", { segmentId: update.segmentId, text: update.text });\n this.emit(\"message\", { role: \"agent\", text: update.text });\n }\n }\n\n // Typed messages are not echoed by the server; the sender finalizes its own bubble.\n #recordTypedUserMessage(text: string): void {\n const segmentId = `typed_${++this.#typedMessageCount}`;\n this.#recordSegment(\"user\", segmentId, text, true);\n this.emit(\"userTranscript\", { segmentId, text, final: true });\n this.emit(\"message\", { role: \"user\", text });\n }\n}\n\n/** Internal seam: identical to AgentSession.start but with an injectable transport. */\nexport function startAgentSession(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n): Promise<AgentSession> {\n return startWith(options, createTransport);\n}\n"],"mappings":"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","names":["_a","_b"]}
1
+ {"version":3,"sources":["../src/audio/output.ts","../src/audio/analysis.ts","../src/sessionToken.ts","../src/session/emitter.ts","../src/session/toolDispatcher.ts","../src/session/wakeLock.ts","../src/session/agentSession.ts"],"sourcesContent":["import { FishAgentError } from \"../errors.js\";\n\n/** Throws `device_change_failed` where the browser cannot route audio output (`setSinkId`). */\nexport function assertOutputSelectionSupported(): void {\n if (typeof HTMLMediaElement === \"undefined\" || !(\"setSinkId\" in HTMLMediaElement.prototype)) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"This browser does not support selecting an audio output device\",\n );\n }\n}\n\n/** Agent audio playback via a detached <audio> element (browser only; inert elsewhere). */\nexport class AudioOutput {\n private element?: HTMLAudioElement;\n private volume = 1;\n\n private ensureElement(): HTMLAudioElement | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n if (!this.element) {\n this.element = document.createElement(\"audio\");\n this.element.autoplay = true;\n // Same-effect attribute as playsInline for iOS Safari without DOM typings noise.\n this.element.setAttribute(\"playsinline\", \"\");\n this.element.volume = this.volume;\n }\n return this.element;\n }\n\n setStream(stream: MediaStream): void {\n const element = this.ensureElement();\n if (!element) {\n return;\n }\n element.srcObject = stream;\n void element.play().catch(() => {\n // Autoplay policy: playback stays pending until startAudio() runs in a user gesture.\n });\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n await this.element?.play();\n }\n\n /**\n * Route playback to an output device. The element is created on demand so\n * the browser validates the device id right here, even before the agent's\n * audio arrives. Rejects with `device_change_failed` where output selection\n * is unsupported or the device cannot be used.\n */\n async setSinkId(sinkId: string): Promise<void> {\n assertOutputSelectionSupported();\n const element = this.ensureElement();\n try {\n await element?.setSinkId(sinkId);\n } catch (error) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not switch to the requested audio output device\",\n { cause: error },\n );\n }\n }\n\n setVolume(volume: number): void {\n this.volume = Math.min(1, Math.max(0, volume));\n if (this.element) {\n this.element.volume = this.volume;\n }\n }\n\n getVolume(): number {\n return this.volume;\n }\n\n dispose(): void {\n if (this.element) {\n this.element.srcObject = null;\n this.element.remove();\n this.element = undefined;\n }\n }\n}\n","/**\n * WebAudio analyser over a MediaStream, feeding visualizers (levels + FFT)\n * without exposing any transport object. Browser only; every accessor\n * degrades to silence when WebAudio is unavailable.\n */\nexport class AudioStreamAnalyser {\n private context?: AudioContext;\n private analyser?: AnalyserNode;\n private source?: MediaStreamAudioSourceNode;\n\n setStream(stream: MediaStream): void {\n if (typeof AudioContext === \"undefined\") {\n return;\n }\n this.context ??= new AudioContext();\n this.analyser ??= this.context.createAnalyser();\n this.source?.disconnect();\n this.source = this.context.createMediaStreamSource(stream);\n this.source.connect(this.analyser);\n this.resume();\n }\n\n /** AudioContexts start suspended under autoplay policies; safe to call any time. */\n resume(): void {\n if (this.context?.state === \"suspended\") {\n void this.context.resume().catch(() => {});\n }\n }\n\n /** RMS level in [0, 1]. */\n getVolume(): number {\n if (!this.analyser) {\n return 0;\n }\n const samples = new Uint8Array(this.analyser.fftSize);\n this.analyser.getByteTimeDomainData(samples);\n let sumOfSquares = 0;\n for (const sample of samples) {\n const centered = (sample - 128) / 128;\n sumOfSquares += centered * centered;\n }\n return Math.sqrt(sumOfSquares / samples.length);\n }\n\n getFrequencyData(): Uint8Array {\n if (!this.analyser) {\n return new Uint8Array(0);\n }\n const data = new Uint8Array(this.analyser.frequencyBinCount);\n this.analyser.getByteFrequencyData(data);\n return data;\n }\n\n dispose(): void {\n this.source?.disconnect();\n this.analyser?.disconnect();\n void this.context?.close().catch(() => {});\n this.source = undefined;\n this.analyser = undefined;\n this.context = undefined;\n }\n}\n","import {\n SESSION_TRANSPORT_LIVEKIT,\n type AgentSessionCreateRequest,\n type SessionLanguage,\n type SessionOverrides,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"./errors.js\";\n\nexport const SDK_VERSION = \"0.0.1\";\nexport const DEFAULT_SERVER_URL = \"https://api.fish.audio\";\n\nconst SUPPORTED_TRANSPORTS: string[] = [SESSION_TRANSPORT_LIVEKIT];\n\nexport interface SessionRequestOptions {\n /** Public agent: the SDK creates the session directly against the Fish API. */\n agentId?: string;\n /** Session token created by the host backend, passed through verbatim. */\n sessionToken?: SessionToken;\n serverUrl?: string;\n /** Sugar for `overrides.language`; an explicit override wins. Pair with a voice in that language. */\n language?: SessionLanguage;\n /**\n * IANA timezone for the agent's sense of local time in this session. Omit to\n * use the browser's timezone (sent automatically as a hint). With\n * `sessionToken` auth the host backend chooses instead, via `timezone` on its\n * session-creation call.\n */\n timezone?: string;\n /**\n * The agent knows the current date and time by default. Set false to withhold\n * both from this session's prompt. With `sessionToken` auth the host backend\n * chooses instead, via `world_context` on its session-creation call.\n */\n worldContext?: boolean;\n /** Per-session config overrides; each field must be allow-listed on the agent. */\n overrides?: SessionOverrides;\n /** `{{name}}` template values for the agent's prompt and first message. */\n dynamicVariables?: Record<string, string | number | boolean>;\n endUserId?: string;\n /** Caller-owned tag, stored and returned verbatim; the platform never interprets it. */\n metadata?: Record<string, unknown>;\n /**\n * Stream the agent's tool-call lifecycle (toolCallStarted/Completed/Failed\n * events, with payloads) to this session. Default true; set false when tool\n * data must stay hidden from this client. With `sessionToken` auth the host\n * backend chooses instead, via `tool_events` on its session-creation call.\n */\n toolEvents?: boolean;\n}\n\nexport function validateSessionToken(sessionToken: SessionToken): SessionToken {\n const transport: string = sessionToken.transport;\n if (!SUPPORTED_TRANSPORTS.includes(transport)) {\n throw new FishAgentError(\n \"unsupported_transport\",\n `Session token uses transport \"${transport}\" which this SDK version does not support; ` +\n \"please upgrade @fishaudio/agent-client\",\n );\n }\n const expiresAt = Date.parse(sessionToken.expires_at);\n if (!Number.isNaN(expiresAt) && expiresAt <= Date.now()) {\n throw new FishAgentError(\n \"session_expired\",\n \"The session token's join deadline has passed; request a new session\",\n );\n }\n return sessionToken;\n}\n\nexport async function resolveSessionToken(options: SessionRequestOptions): Promise<SessionToken> {\n const provided = [options.agentId, options.sessionToken].filter((value) => value !== undefined);\n if (provided.length !== 1) {\n throw new TypeError(\n \"Exactly one of `agentId` or `sessionToken` must be provided to start a session\",\n );\n }\n\n if (options.sessionToken) {\n return validateSessionToken(options.sessionToken);\n }\n return validateSessionToken(await createPublicSession(options));\n}\n\n// The browser knows the end user's real timezone; without the hint the\n// platform would have to guess UTC.\nfunction detectClientTimezone(): string | undefined {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;\n } catch {\n return undefined;\n }\n}\n\nasync function createPublicSession(options: SessionRequestOptions): Promise<SessionToken> {\n const serverUrl = (options.serverUrl ?? DEFAULT_SERVER_URL).replace(/\\/$/, \"\");\n const clientTimezone = detectClientTimezone();\n const overrides: SessionOverrides | undefined =\n options.language || options.overrides\n ? { ...(options.language ? { language: options.language } : {}), ...options.overrides }\n : undefined;\n const request: AgentSessionCreateRequest = {\n agent_id: options.agentId as string,\n ...(options.timezone ? { timezone: options.timezone } : {}),\n ...(clientTimezone ? { client_timezone: clientTimezone } : {}),\n ...(options.worldContext !== undefined ? { world_context: options.worldContext } : {}),\n ...(overrides ? { overrides } : {}),\n ...(options.dynamicVariables ? { dynamic_variables: options.dynamicVariables } : {}),\n ...(options.endUserId ? { end_user_id: options.endUserId } : {}),\n ...(options.metadata ? { metadata: options.metadata } : {}),\n ...(options.toolEvents !== undefined ? { tool_events: options.toolEvents } : {}),\n };\n\n let response: Response;\n try {\n response = await fetch(`${serverUrl}/v1/agent/sessions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Fish-SDK\": `agent-client/${SDK_VERSION}`,\n },\n body: JSON.stringify(request),\n });\n } catch (cause) {\n throw new FishAgentError(\"session_request_failed\", \"Could not reach the session endpoint\", {\n cause,\n });\n }\n\n if (!response.ok) {\n throw await createSessionError(response);\n }\n return (await response.json()) as SessionToken;\n}\n\nasync function createSessionError(response: Response): Promise<FishAgentError> {\n let message = `Session request failed with HTTP ${response.status}`;\n let code: string | undefined;\n try {\n const body = (await response.json()) as { message?: string; error?: string; code?: string };\n message = body.message ?? body.error ?? message;\n code = body.code;\n } catch {\n // keep the status-based message\n }\n const options = { statusCode: response.status };\n // 409 alone is ambiguous (the server also uses it for e.g. an unpublished\n // agent); only the coded arm means \"this SDK is too old\".\n if (response.status === 409 && code === \"unsupported_transport\") {\n return new FishAgentError(\n \"unsupported_transport\",\n `${message}; please upgrade @fishaudio/agent-client`,\n options,\n );\n }\n if (response.status === 403) {\n return /origin/i.test(message)\n ? new FishAgentError(\"origin_forbidden\", message, options)\n : new FishAgentError(\"agent_not_public\", message, options);\n }\n return new FishAgentError(\"session_request_failed\", message, options);\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\ntype Listener = (...args: any[]) => void;\n\n/** Minimal typed emitter; listener errors are isolated so one bad consumer can't break the session. */\nexport class TypedEmitter<Events extends Record<keyof Events, Listener>> {\n private readonly listeners = new Map<keyof Events, Set<Listener>>();\n\n on<K extends keyof Events>(event: K, listener: Events[K]): this {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n return this;\n }\n\n off<K extends keyof Events>(event: K, listener: Events[K]): this {\n this.listeners.get(event)?.delete(listener);\n return this;\n }\n\n once<K extends keyof Events>(event: K, listener: Events[K]): this {\n const wrapper = ((...args: Parameters<Events[K]>) => {\n this.off(event, wrapper);\n listener(...args);\n }) as Events[K];\n return this.on(event, wrapper);\n }\n\n protected emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {\n const set = this.listeners.get(event);\n if (!set) {\n return;\n }\n for (const listener of [...set]) {\n try {\n listener(...args);\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error(\"[fish-agent] listener error\", error);\n }\n }\n }\n}\n","import type { AgentSessionMessage, ClientSessionMessage } from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\n\nexport type ClientToolHandler = (\n params: Record<string, unknown>,\n context: { callId: string; toolName: string },\n) => unknown | Promise<unknown>;\n\nexport const DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15_000;\n\ntype ClientToolCall = Extract<AgentSessionMessage, { type: \"client_tool.call\" }>;\n\nexport class ClientToolDispatcher {\n private readonly tools = new Map<string, ClientToolHandler>();\n\n constructor(\n tools: Record<string, ClientToolHandler> | undefined,\n private readonly timeoutMs: number,\n private readonly send: (message: ClientSessionMessage) => Promise<void>,\n private readonly onError: (error: FishAgentError) => void,\n ) {\n for (const [name, handler] of Object.entries(tools ?? {})) {\n this.tools.set(name, handler);\n }\n }\n\n register(name: string, handler: ClientToolHandler): void {\n this.tools.set(name, handler);\n }\n\n async dispatch(call: ClientToolCall): Promise<void> {\n const handler = this.tools.get(call.toolName);\n if (!handler) {\n this.onError(\n new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" is not registered`),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: \"Tool is not registered\" });\n }\n return;\n }\n\n try {\n const result = await this.withTimeout(\n Promise.resolve(handler(call.params, { callId: call.callId, toolName: call.toolName })),\n call.toolName,\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, result === undefined ? {} : { result });\n }\n } catch (error) {\n this.onError(\n error instanceof FishAgentError\n ? error\n : new FishAgentError(\"tool_failed\", `Client tool \"${call.toolName}\" threw`, {\n cause: error,\n }),\n );\n if (call.expectsResponse) {\n await this.reply(call.callId, { isError: true, result: String(error) });\n }\n }\n }\n\n private async reply(\n callId: string,\n body: { result?: unknown; isError?: boolean },\n ): Promise<void> {\n try {\n await this.send({ type: \"client_tool.result\", callId, ...body });\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool result\", {\n cause: error,\n }),\n );\n }\n }\n\n private withTimeout<T>(promise: Promise<T>, toolName: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new FishAgentError(\n \"tool_timeout\",\n `Client tool \"${toolName}\" timed out after ${this.timeoutMs}ms`,\n ),\n );\n }, this.timeoutMs);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n }\n}\n","/**\n * Holds a screen wake lock while a session is live (browser only; inert\n * elsewhere). The platform releases the lock whenever the page is hidden, so\n * visibility returns re-request it. Failures are silent by design: a session\n * works fine without the lock, the screen just may sleep mid-call.\n */\nexport class WakeLockHolder {\n #sentinel?: WakeLockSentinel;\n #active = false;\n\n readonly #onVisibilityChange = (): void => {\n if (document.visibilityState === \"visible\") {\n void this.#request();\n }\n };\n\n acquire(): void {\n if (\n this.#active ||\n typeof document === \"undefined\" ||\n typeof navigator === \"undefined\" ||\n !navigator.wakeLock\n ) {\n return;\n }\n this.#active = true;\n document.addEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#request();\n }\n\n async #request(): Promise<void> {\n if (!this.#active) {\n return;\n }\n try {\n const sentinel = await navigator.wakeLock.request(\"screen\");\n if (!this.#active) {\n void sentinel.release().catch(() => undefined);\n return;\n }\n // Concurrent requests (acquire + visibility return) may both resolve;\n // drop the earlier sentinel so exactly one is held.\n const previous = this.#sentinel;\n this.#sentinel = sentinel;\n if (previous && previous !== sentinel) {\n void previous.release().catch(() => undefined);\n }\n } catch {\n // Denied (battery saver, hidden tab, permissions policy) — run without it.\n }\n }\n\n release(): void {\n if (!this.#active) {\n return;\n }\n this.#active = false;\n document.removeEventListener(\"visibilitychange\", this.#onVisibilityChange);\n void this.#sentinel?.release().catch(() => undefined);\n this.#sentinel = undefined;\n }\n}\n","// Type-only: the transport implementation still loads lazily (see start()).\nimport type { Room } from \"livekit-client\";\nimport type {\n AgentSessionMessage,\n ClientSessionMessage,\n SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { assertOutputSelectionSupported, AudioOutput } from \"../audio/output.js\";\nimport { AudioStreamAnalyser } from \"../audio/analysis.js\";\nimport { FishAgentError } from \"../errors.js\";\nimport type {\n AgentMode,\n AgentSessionCallbacks,\n AgentSessionEvents,\n EndReason,\n SessionStatus,\n TranscriptSegment,\n} from \"../events.js\";\nimport { resolveSessionToken, type SessionRequestOptions } from \"../sessionToken.js\";\nimport type {\n TranscriptionSegmentUpdate,\n Transport,\n TransportConnectionState,\n TransportFactory,\n} from \"../transport/types.js\";\nimport { TypedEmitter } from \"./emitter.js\";\nimport {\n ClientToolDispatcher,\n DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n type ClientToolHandler,\n} from \"./toolDispatcher.js\";\nimport { WakeLockHolder } from \"./wakeLock.js\";\n\nexport interface AgentSessionOptions extends SessionRequestOptions {\n clientTools?: Record<string, ClientToolHandler>;\n clientToolTimeoutMs?: number;\n /**\n * Capture the microphone on start. Default true. Set false to join without\n * requesting it (text-first UIs); the first `setMicMuted(false)` then\n * captures — the permission prompt happens there, so call it from a user\n * gesture.\n */\n microphone?: boolean;\n audio?: {\n inputDeviceId?: string;\n /** Playback device (`setSinkId`); start rejects with `device_change_failed`\n * where output selection is unsupported (before creating a server session)\n * or the device cannot be used (before connecting). */\n outputDeviceId?: string;\n };\n /**\n * Hold a screen wake lock while the session is live, so long calls survive\n * the phone trying to sleep. Default true; denial is silent. Set false to\n * leave screen policy to the page.\n */\n wakeLock?: boolean;\n callbacks?: Partial<AgentSessionCallbacks>;\n}\n\n/** How long after connect the agent participant gets to show up before the\n * session gives up — normal joins land within a couple of seconds. */\nconst AGENT_JOIN_TIMEOUT_MS = 15_000;\n\n/** Assigned in AgentSession's static block; bridges startAgentSession to the\n * class-private #startWith so no transport types sit on the public class. */\nlet startWith: (\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n) => Promise<AgentSession>;\n\nexport class AgentSession extends TypedEmitter<AgentSessionEvents> {\n #status: SessionStatus = \"connecting\";\n #mode: AgentMode = \"listening\";\n #endReason?: EndReason;\n #sessionId: string;\n #micMuted = false;\n #endedByClient = false;\n #ending?: Promise<void>;\n\n #transport!: Transport;\n #tools!: ClientToolDispatcher;\n readonly #output = new AudioOutput();\n readonly #outputAnalyser = new AudioStreamAnalyser();\n readonly #inputAnalyser = new AudioStreamAnalyser();\n readonly #wakeLock = new WakeLockHolder();\n\n /** Text already delivered per agent segment, to derive deltas from full-text updates. */\n readonly #agentSegments = new Map<string, string>();\n /** Transcript so far, for late subscribers (getTranscript); upserted per segment. */\n readonly #transcript: TranscriptSegment[] = [];\n readonly #transcriptIndex = new Map<string, number>();\n #typedMessageCount = 0;\n\n // Data frames reach only participants already in the room — and the worker\n // registers its handlers while starting up — so typed messages wait here\n // until the agent publishes its first pipeline state.\n #agentPresent = false;\n #pendingMessages: ClientSessionMessage[] = [];\n #joinTimer?: ReturnType<typeof setTimeout>;\n\n private constructor(sessionToken: SessionToken) {\n super();\n this.#sessionId = sessionToken.session_id;\n }\n\n /**\n * Obtain a session token (if needed), connect, publish the microphone and\n * hand back a live session. Rejects with FishAgentError on session-request,\n * permission or connect failures.\n */\n static async start(options: AgentSessionOptions): Promise<AgentSession> {\n // Lazy so the transport implementation (and livekit-client) never loads\n // in SSR bundles or in tests that inject their own transport.\n const { LiveKitTransport } = await import(\"../transport/livekit.js\");\n return AgentSession.#startWith(options, () => new LiveKitTransport());\n }\n\n // ---- getters ----\n\n get sessionId(): string {\n return this.#sessionId;\n }\n\n get status(): SessionStatus {\n return this.#status;\n }\n\n get mode(): AgentMode {\n return this.#mode;\n }\n\n get isSpeaking(): boolean {\n return this.#mode === \"speaking\";\n }\n\n get micMuted(): boolean {\n return this.#micMuted;\n }\n\n get endReason(): EndReason | undefined {\n return this.#endReason;\n }\n\n /**\n * Snapshot of every transcript segment so far, in conversation order. Seed a\n * consumer that subscribes after events already fired (e.g. a component\n * mounted right after start), then apply live events by segmentId.\n */\n getTranscript(): TranscriptSegment[] {\n return this.#transcript.map((segment) => ({ ...segment }));\n }\n\n // ---- text & control channel ----\n\n /** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */\n sendUserMessage(text: string, options?: { audio?: boolean }): void {\n const trimmed = text.trim();\n if (!trimmed) {\n return;\n }\n const message: ClientSessionMessage =\n options?.audio === true\n ? { type: \"user.message\", text: trimmed }\n : { type: \"user.message\", text: trimmed, audio: false };\n if (this.#agentPresent) {\n this.#sendClientEvent(message);\n } else {\n this.#pendingMessages.push(message);\n }\n this.#recordTypedUserMessage(trimmed);\n }\n\n sendUserActivity(): void {\n this.#sendClientEvent({ type: \"user.activity\" });\n }\n\n interrupt(): void {\n this.#sendClientEvent({ type: \"user.interrupt\" });\n }\n\n registerClientTool(name: string, handler: ClientToolHandler): void {\n this.#tools.register(name, handler);\n }\n\n // ---- audio ----\n\n /**\n * After a `microphone: false` start, the first unmute captures and publishes\n * the microphone — the permission prompt happens here. A denial rejects with\n * `mic_permission_denied` and the session stays muted.\n */\n async setMicMuted(muted: boolean): Promise<void> {\n const previous = this.#micMuted;\n this.#micMuted = muted;\n try {\n await this.#transport.setMicEnabled(!muted);\n } catch (error) {\n this.#micMuted = previous;\n throw AgentSession.#asMicError(error);\n }\n }\n\n /** Call from a user gesture to satisfy browser autoplay policies. */\n async startAudio(): Promise<void> {\n this.#outputAnalyser.resume();\n this.#inputAnalyser.resume();\n await this.#output.startAudio();\n }\n\n /**\n * Switch the microphone mid-call. While the mic is not captured yet\n * (`microphone: false` start, still muted) this records the preference for\n * the first unmute. Rejects with `device_change_failed` when the device\n * cannot be activated, switching back to the previous microphone (best\n * effort — the transport stops the old capture before acquiring the new).\n */\n async setInputDevice(deviceId: string): Promise<void> {\n try {\n await this.#transport.setInputDevice(deviceId);\n } catch (error) {\n throw AgentSession.#asDeviceError(error, \"Could not switch the microphone\");\n }\n }\n\n /**\n * Route the agent's audio to an output device (`setSinkId`); pass `\"\"` to\n * return to the default device. Rejects with `device_change_failed` where\n * the browser does not support output selection (common on mobile browsers)\n * or the device cannot be used; playback stays on the previous device.\n */\n async setOutputDevice(deviceId: string): Promise<void> {\n await this.#output.setSinkId(deviceId);\n }\n\n setOutputVolume(volume: number): void {\n this.#output.setVolume(volume);\n }\n\n getOutputVolume(): number {\n return this.#outputAnalyser.getVolume();\n }\n\n getInputVolume(): number {\n return this.#inputAnalyser.getVolume();\n }\n\n getOutputFrequencyData(): Uint8Array {\n return this.#outputAnalyser.getFrequencyData();\n }\n\n getInputFrequencyData(): Uint8Array {\n return this.#inputAnalyser.getFrequencyData();\n }\n\n /**\n * Escape hatch: the underlying LiveKit `Room`, for needs the session API\n * doesn't cover (connection-quality telemetry, publishing extra tracks).\n * Code using it couples to this SDK's transport choice and livekit-client\n * version — prefer the session API where one exists. `undefined` after the\n * session ends, or when a custom transport doesn't expose a room.\n */\n getRoom(): Room | undefined {\n return this.#transport.getRoom?.() as Room | undefined;\n }\n\n // ---- lifecycle ----\n\n end(): Promise<void> {\n if (this.#ending) {\n return this.#ending;\n }\n if (this.#status === \"ended\") {\n return Promise.resolve();\n }\n const ending = this.#endOnce();\n this.#ending = ending;\n void ending.catch(() => {\n if (this.#ending === ending) {\n this.#ending = undefined;\n }\n });\n return ending;\n }\n\n async #endOnce(): Promise<void> {\n this.#endedByClient = true;\n try {\n // Graceful hangup over the reverse channel; participant departure is the\n // worker-side fallback if this frame never arrives.\n await this.#transport.sendClientEvent({ type: \"user.hangup\" });\n } catch {\n // best effort\n }\n await this.#transport.disconnect();\n this.#finalize(\"user_hangup\");\n }\n\n // ---- internals ----\n\n static {\n startWith = (options, createTransport) => AgentSession.#startWith(options, createTransport);\n }\n\n static async #startWith(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n ): Promise<AgentSession> {\n if (options.audio?.outputDeviceId !== undefined) {\n // Cheap sync check first, so an unsupported browser fails before a\n // server session is even created.\n assertOutputSelectionSupported();\n }\n const transport = createTransport();\n if (options.microphone !== false) {\n // Inside the user gesture that called start(), before the token\n // round-trip pushes getUserMedia out of the gesture's transient\n // activation (see Transport.prepareMicrophone). A denial surfaces in\n // transport.connect().\n transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });\n }\n let sessionToken: SessionToken;\n let session: AgentSession;\n try {\n sessionToken = await resolveSessionToken(options);\n session = new AgentSession(sessionToken);\n if (options.audio?.outputDeviceId !== undefined) {\n // The browser validates the device id here, before anything connects;\n // rejects like a token failure — no events yet.\n await session.#output.setSinkId(options.audio.outputDeviceId);\n }\n } catch (error) {\n // The gesture-time capture must not outlive a failed start.\n await transport.disconnect().catch(() => undefined);\n throw error;\n }\n\n for (const [key, callback] of Object.entries(options.callbacks ?? {})) {\n const event = (key.charAt(2).toLowerCase() +\n key.slice(3)) as keyof AgentSessionEvents;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n session.on(event, callback as any);\n }\n\n session.#transport = transport;\n if (options.microphone === false) {\n session.#micMuted = true;\n }\n session.#tools = new ClientToolDispatcher(\n options.clientTools,\n options.clientToolTimeoutMs ?? DEFAULT_CLIENT_TOOL_TIMEOUT_MS,\n (message) => transport.sendClientEvent(message),\n (error) => session.emit(\"error\", error),\n );\n\n try {\n await transport.connect(sessionToken, {\n inputDeviceId: options.audio?.inputDeviceId,\n microphone: options.microphone,\n callbacks: {\n onAgentEvent: (message) => session.#handleAgentEvent(message),\n // Mode model: \"listening\" is the default state; the framework's agent\n // state supplies the thinking edge; speaking follows the playout-synced\n // transcription segments (open = audible, closed = back to listening).\n onAgentState: (state) => {\n // Any published agent state proves the worker's pipeline is up and\n // its event handlers are registered — the gate for queued messages.\n session.#handleAgentPresent();\n if (state === \"thinking\") {\n session.#setMode(\"thinking\");\n }\n },\n onTranscription: (update) => session.#handleTranscription(update),\n onOutputStream: (stream) => {\n session.#output.setStream(stream);\n session.#outputAnalyser.setStream(stream);\n },\n onInputStream: (stream) => session.#inputAnalyser.setStream(stream),\n onConnectionState: (state, reason) => session.#handleConnectionState(state, reason),\n },\n });\n } catch (error) {\n // connect() can fail after the room is already joined (e.g. mic capture);\n // tear the transport down or the room stays connected with no owner.\n await session.#transport.disconnect().catch(() => undefined);\n session.#finalize(\"connection_lost\");\n throw AgentSession.#asStartError(error);\n }\n\n if (session.#status === \"connecting\") {\n session.#setStatus(\"connected\");\n session.emit(\"connect\", { sessionId: session.#sessionId });\n }\n if (session.#status !== \"ended\" && options.wakeLock !== false) {\n session.#wakeLock.acquire();\n }\n if (!session.#agentPresent && session.#status === \"connected\") {\n session.#joinTimer = setTimeout(() => session.#abandonAgentWait(), AGENT_JOIN_TIMEOUT_MS);\n }\n return session;\n }\n\n static #asStartError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n return new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause: error,\n });\n }\n\n static #asMicError(error: unknown): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n if (error instanceof Error && error.name === \"NotAllowedError\") {\n return new FishAgentError(\"mic_permission_denied\", \"Microphone permission was denied\", {\n cause: error,\n });\n }\n if (\n error instanceof Error &&\n (error.name === \"OverconstrainedError\" || error.name === \"NotFoundError\")\n ) {\n // Device/constraint failures surface here both for a preference recorded\n // while muted and for a first capture with no usable microphone — keep\n // the device error code, with a message that fits either.\n return new FishAgentError(\n \"device_change_failed\",\n \"No usable microphone could be activated\",\n { cause: error },\n );\n }\n return new FishAgentError(\"connection_failed\", \"Could not toggle the microphone\", {\n cause: error,\n });\n }\n\n static #asDeviceError(error: unknown, message: string): FishAgentError {\n if (error instanceof FishAgentError) {\n return error;\n }\n return new FishAgentError(\"device_change_failed\", message, { cause: error });\n }\n\n #sendClientEvent(message: Parameters<Transport[\"sendClientEvent\"]>[0]): void {\n void this.#transport.sendClientEvent(message).catch((error: unknown) => {\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"Failed to send to the agent\", { cause: error }),\n );\n });\n }\n\n #setStatus(status: SessionStatus): void {\n if (this.#status !== status) {\n this.#status = status;\n this.emit(\"statusChange\", status);\n }\n }\n\n #setMode(mode: AgentMode): void {\n if (this.#mode !== mode) {\n this.#mode = mode;\n this.emit(\"modeChange\", mode);\n }\n }\n\n #handleAgentPresent(): void {\n if (this.#agentPresent) {\n return;\n }\n this.#agentPresent = true;\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n const pending = this.#pendingMessages;\n this.#pendingMessages = [];\n for (const message of pending) {\n this.#sendClientEvent(message);\n }\n }\n\n #abandonAgentWait(): void {\n if (this.#status === \"ended\" || this.#agentPresent) {\n return;\n }\n this.emit(\n \"error\",\n new FishAgentError(\"connection_failed\", \"The agent did not join the session\"),\n );\n void this.#transport.disconnect().catch(() => {});\n this.#finalize(\"connection_lost\");\n }\n\n #handleConnectionState(state: TransportConnectionState, reason?: string): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (state === \"reconnecting\") {\n this.#setStatus(\"reconnecting\");\n return;\n }\n if (state === \"connected\") {\n if (this.#status === \"reconnecting\") {\n this.#setStatus(\"connected\");\n }\n return;\n }\n // AGENT_LEFT = the agent participant hung up (worker ends without closing\n // the room). Room deletion covers server-forced ends and the duration\n // cap — the protocol has no session.ended message to tell these apart.\n const ended = this.#endedByClient\n ? \"user_hangup\"\n : reason === \"AGENT_LEFT\" || reason === \"ROOM_DELETED\" || reason === \"ROOM_CLOSED\"\n ? \"agent_hangup\"\n : \"connection_lost\";\n this.#finalize(ended);\n }\n\n #finalize(reason: EndReason): void {\n if (this.#status === \"ended\") {\n return;\n }\n if (this.#joinTimer) {\n clearTimeout(this.#joinTimer);\n this.#joinTimer = undefined;\n }\n this.#endReason = reason;\n this.#setStatus(\"ended\");\n this.#wakeLock.release();\n this.#output.dispose();\n this.#outputAnalyser.dispose();\n this.#inputAnalyser.dispose();\n this.emit(\"disconnect\", { reason });\n }\n\n #handleAgentEvent(message: AgentSessionMessage): void {\n switch (message.type) {\n case \"client_tool.call\":\n void this.#tools.dispatch(message);\n return;\n case \"tool.started\":\n this.emit(\"toolCallStarted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n input: message.input,\n inputTruncated: message.inputTruncated === true,\n });\n return;\n case \"tool.completed\":\n this.emit(\"toolCallCompleted\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n output: message.output,\n outputTruncated: message.outputTruncated === true,\n });\n return;\n case \"tool.failed\":\n this.emit(\"toolCallFailed\", {\n callId: message.callId,\n toolName: message.toolName,\n source: message.toolSource,\n error: message.error,\n });\n return;\n case \"error\":\n this.emit(\n \"error\",\n message.code === \"provider_error\"\n ? new FishAgentError(\"provider_error\", \"An upstream provider failed during the session\")\n : new FishAgentError(\"internal_error\", \"The agent runtime hit an internal error\"),\n );\n return;\n default:\n // Forward compatibility: unknown message types are ignored by contract.\n return;\n }\n }\n\n #recordSegment(role: \"user\" | \"agent\", segmentId: string, text: string, final: boolean): void {\n const key = `${role}:${segmentId}`;\n const index = this.#transcriptIndex.get(key);\n if (index === undefined) {\n this.#transcriptIndex.set(key, this.#transcript.length);\n this.#transcript.push({ segmentId, role, text, final });\n } else {\n this.#transcript[index] = { segmentId, role, text, final };\n }\n }\n\n #handleTranscription(update: TranscriptionSegmentUpdate): void {\n if (update.role === \"user\") {\n this.#recordSegment(\"user\", update.segmentId, update.text, update.final);\n this.emit(\"userTranscript\", {\n segmentId: update.segmentId,\n text: update.text,\n final: update.final,\n });\n if (update.final) {\n this.emit(\"message\", { role: \"user\", text: update.text });\n }\n return;\n }\n // Agent speech: playout-synced, so streaming text means the agent is audibly\n // speaking; the segment closing means playout finished — back to listening.\n this.#setMode(update.final ? \"listening\" : \"speaking\");\n this.#recordSegment(\"agent\", update.segmentId, update.text, update.final);\n const previous = this.#agentSegments.get(update.segmentId) ?? \"\";\n const delta = update.text.startsWith(previous) ? update.text.slice(previous.length) : update.text;\n this.#agentSegments.set(update.segmentId, update.text);\n if (delta) {\n this.emit(\"agentResponseDelta\", { segmentId: update.segmentId, delta, text: update.text });\n }\n if (update.final) {\n this.#agentSegments.delete(update.segmentId);\n this.emit(\"agentResponse\", { segmentId: update.segmentId, text: update.text });\n this.emit(\"message\", { role: \"agent\", text: update.text });\n }\n }\n\n // Typed messages are not echoed by the server; the sender finalizes its own bubble.\n #recordTypedUserMessage(text: string): void {\n const segmentId = `typed_${++this.#typedMessageCount}`;\n this.#recordSegment(\"user\", segmentId, text, true);\n this.emit(\"userTranscript\", { segmentId, text, final: true });\n this.emit(\"message\", { role: \"user\", text });\n }\n}\n\n/** Internal seam: identical to AgentSession.start but with an injectable transport. */\nexport function startAgentSession(\n options: AgentSessionOptions,\n createTransport: TransportFactory,\n): Promise<AgentSession> {\n return startWith(options, createTransport);\n}\n"],"mappings":"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","names":["_a","_b"]}
@@ -5,6 +5,7 @@ import {
5
5
  // src/transport/livekit.ts
6
6
  import {
7
7
  ConnectionState,
8
+ createLocalAudioTrack,
8
9
  DisconnectReason,
9
10
  Room,
10
11
  RoomEvent,
@@ -18,11 +19,24 @@ import {
18
19
  function hasMessageType(value) {
19
20
  return typeof value === "object" && value !== null && typeof value.type === "string";
20
21
  }
22
+ function captureMicrophone(inputDeviceId) {
23
+ return createLocalAudioTrack({
24
+ deviceId: inputDeviceId,
25
+ echoCancellation: true,
26
+ noiseSuppression: true,
27
+ autoGainControl: true
28
+ });
29
+ }
21
30
  var LiveKitTransport = class {
22
31
  constructor() {
23
32
  /** Local tracks already wired to re-emit their stream when livekit restarts them. */
24
33
  this.restartWiredTracks = /* @__PURE__ */ new WeakSet();
25
34
  }
35
+ prepareMicrophone(options) {
36
+ const pending = captureMicrophone(options.inputDeviceId);
37
+ pending.catch(() => void 0);
38
+ this.pendingMicTrack = pending;
39
+ }
26
40
  async connect(sessionToken, options) {
27
41
  this.callbacks = options.callbacks;
28
42
  const room = new Room({
@@ -112,6 +126,12 @@ var LiveKitTransport = class {
112
126
  });
113
127
  }
114
128
  });
129
+ let micTrack;
130
+ if (options.microphone !== false) {
131
+ const pending = this.pendingMicTrack ?? captureMicrophone(options.inputDeviceId);
132
+ this.pendingMicTrack = pending;
133
+ micTrack = await pending;
134
+ }
115
135
  try {
116
136
  await room.connect(sessionToken.livekit_url, sessionToken.token);
117
137
  } catch (cause) {
@@ -125,8 +145,9 @@ var LiveKitTransport = class {
125
145
  this.callbacks?.onAgentState(state);
126
146
  }
127
147
  }
128
- if (options.microphone !== false) {
129
- await room.localParticipant.setMicrophoneEnabled(true);
148
+ if (micTrack) {
149
+ await room.localParticipant.publishTrack(micTrack);
150
+ this.pendingMicTrack = void 0;
130
151
  this.publishInputStream();
131
152
  }
132
153
  }
@@ -135,6 +156,9 @@ var LiveKitTransport = class {
135
156
  void room.disconnect();
136
157
  }
137
158
  async disconnect() {
159
+ const pendingMic = this.pendingMicTrack;
160
+ this.pendingMicTrack = void 0;
161
+ void pendingMic?.then((track) => track.stop()).catch(() => void 0);
138
162
  const room = this.room;
139
163
  this.room = void 0;
140
164
  this.callbacks = void 0;
@@ -204,4 +228,4 @@ var LiveKitTransport = class {
204
228
  export {
205
229
  LiveKitTransport
206
230
  };
207
- //# sourceMappingURL=livekit-B62YPROC.js.map
231
+ //# sourceMappingURL=livekit-DBSPD4GR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transport/livekit.ts"],"sourcesContent":["import {\n ConnectionState,\n createLocalAudioTrack,\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n TrackEvent,\n type LocalAudioTrack,\n type RemoteTrack,\n} from \"livekit-client\";\nimport {\n AGENT_EVENT_TOPIC,\n CLIENT_EVENT_TOPIC,\n type AgentSessionMessage,\n type ClientSessionMessage,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\nimport type { Transport, TransportCallbacks, TransportConnectOptions } from \"./types.js\";\n\nfunction hasMessageType(value: unknown): value is { type: string } {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { type?: unknown }).type === \"string\"\n );\n}\n\nfunction captureMicrophone(inputDeviceId?: string): Promise<LocalAudioTrack> {\n return createLocalAudioTrack({\n deviceId: inputDeviceId,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n });\n}\n\n/** Sole production Transport. LiveKit vocabulary must not escape this file,\n * except the `Room` handed out through the `getRoom()` escape hatch. */\nexport class LiveKitTransport implements Transport {\n private room?: Room;\n private callbacks?: TransportCallbacks;\n private pendingMicTrack?: Promise<LocalAudioTrack>;\n\n prepareMicrophone(options: { inputDeviceId?: string }): void {\n const pending = captureMicrophone(options.inputDeviceId);\n // A denial surfaces where connect() awaits the track, never as an\n // unhandled rejection here.\n pending.catch(() => undefined);\n this.pendingMicTrack = pending;\n }\n\n async connect(sessionToken: SessionToken, options: TransportConnectOptions): Promise<void> {\n this.callbacks = options.callbacks;\n const room = new Room({\n audioCaptureDefaults: {\n deviceId: options.inputDeviceId,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n this.room = room;\n\n room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {\n if (topic !== AGENT_EVENT_TOPIC) {\n return;\n }\n let message: unknown;\n try {\n message = JSON.parse(new TextDecoder().decode(payload));\n } catch {\n return;\n }\n if (hasMessageType(message)) {\n this.callbacks?.onAgentEvent(message as AgentSessionMessage);\n }\n });\n\n const AGENT_STATE_ATTRIBUTE = \"lk.agent.state\";\n room.on(RoomEvent.ParticipantAttributesChanged, (changed, participant) => {\n const state = changed[AGENT_STATE_ATTRIBUTE];\n if (!participant.isLocal && state) {\n this.callbacks?.onAgentState(state);\n }\n });\n\n room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack) => {\n if (track.kind === Track.Kind.Audio && track.mediaStreamTrack) {\n this.callbacks?.onOutputStream(new MediaStream([track.mediaStreamTrack]));\n }\n });\n\n // Both sides' transcripts ride LiveKit's built-in transcription streams:\n // agent segments are playout-synced delta streams (final on stream close —\n // an interrupted segment closes containing only the spoken text); user STT\n // segments are re-published per update with an explicit final attribute,\n // sent on behalf of the user, so sender identity distinguishes the roles.\n room.registerTextStreamHandler(\"lk.transcription\", async (reader, participantInfo) => {\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[\"lk.segment_id\"];\n if (!segmentId) {\n return;\n }\n const role =\n participantInfo.identity === room.localParticipant.identity ? \"user\" : \"agent\";\n const finalAttribute = attributes[\"lk.transcription_final\"] === \"true\";\n let text = \"\";\n let aborted = false;\n try {\n for await (const delta of reader) {\n text += delta;\n this.callbacks?.onTranscription({ segmentId, role, text, final: false });\n }\n } catch {\n aborted = true; // interrupted mid-stream; what arrived is what was spoken\n }\n if (text) {\n const final = role === \"agent\" ? true : finalAttribute && !aborted;\n this.callbacks?.onTranscription({ segmentId, role, text, final });\n }\n });\n\n room.on(RoomEvent.Reconnecting, () => {\n this.callbacks?.onConnectionState(\"reconnecting\");\n });\n room.on(RoomEvent.Reconnected, () => {\n // A full reconnect replays the roster from the join response before this\n // event; an agent still missing here hung up while we were away.\n if (room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n return;\n }\n this.callbacks?.onConnectionState(\"connected\");\n });\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n this.callbacks?.onConnectionState(\n \"disconnected\",\n reason === undefined ? undefined : DisconnectReason[reason],\n );\n // Remote-initiated ends never go through disconnect(); drop the refs here\n // so getRoom() honors its ended-means-undefined contract and the room\n // can be collected.\n if (this.room === room) {\n this.room = undefined;\n this.callbacks = undefined;\n }\n });\n // An agent-side hangup ends the worker without closing the room; the\n // agent participant leaving IS the end-of-call signal for the client.\n // A full reconnect also clears the roster this way, and can do so while\n // room.state still reads Connected (Reconnecting is only set right after),\n // so defer one microtask and require a room that is genuinely connected.\n room.on(RoomEvent.ParticipantDisconnected, () => {\n if (room.remoteParticipants.size === 0) {\n queueMicrotask(() => {\n if (room.state === ConnectionState.Connected && room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n }\n });\n }\n });\n\n let micTrack: LocalAudioTrack | undefined;\n if (options.microphone !== false) {\n // Usually settled long before this point: the capture starts inside the\n // user gesture (prepareMicrophone). Awaited before joining the room so a\n // denial fails the start without ever dispatching an agent. The promise\n // stays in pendingMicTrack until the room owns the track, so disconnect()\n // can release it after any failure in between.\n const pending = this.pendingMicTrack ?? captureMicrophone(options.inputDeviceId);\n this.pendingMicTrack = pending;\n micTrack = await pending;\n }\n\n try {\n await room.connect(sessionToken.livekit_url, sessionToken.token);\n } catch (cause) {\n throw new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause,\n });\n }\n // The attribute is sticky: if the agent joined and set its state before us,\n // no change event will fire — replay the current value once.\n for (const participant of room.remoteParticipants.values()) {\n const state = participant.attributes?.[AGENT_STATE_ATTRIBUTE];\n if (state) {\n this.callbacks?.onAgentState(state);\n }\n }\n if (micTrack) {\n await room.localParticipant.publishTrack(micTrack);\n this.pendingMicTrack = undefined;\n this.publishInputStream();\n }\n }\n\n private handleAgentLeft(room: Room): void {\n this.callbacks?.onConnectionState(\"disconnected\", \"AGENT_LEFT\");\n void room.disconnect();\n }\n\n async disconnect(): Promise<void> {\n // A capture the room never took ownership of (start failed between the\n // gesture-time capture and publish) is invisible to room.disconnect().\n const pendingMic = this.pendingMicTrack;\n this.pendingMicTrack = undefined;\n void pendingMic?.then((track) => track.stop()).catch(() => undefined);\n const room = this.room;\n this.room = undefined;\n this.callbacks = undefined;\n await room?.disconnect();\n }\n\n getRoom(): Room | undefined {\n return this.room;\n }\n\n async setMicEnabled(enabled: boolean): Promise<void> {\n await this.room?.localParticipant.setMicrophoneEnabled(enabled);\n if (enabled) {\n // After a mic-less connect the first enable captures the track late;\n // re-wiring an already-published track is a harmless replace.\n this.publishInputStream();\n }\n }\n\n async setInputDevice(deviceId: string): Promise<void> {\n const room = this.room;\n if (!room) {\n return;\n }\n const previous = room.getActiveDevice(\"audioinput\");\n let switched: boolean;\n try {\n // exact: a wrong id must fail loudly, not silently fall back to another mic.\n switched = await room.switchActiveDevice(\"audioinput\", deviceId, true);\n } catch (error) {\n // livekit stops the current track before acquiring the new device, so a\n // failed pick would otherwise leave the call silently mic-dead — switch\n // back before surfacing the failure. livekit reports \"default\" until the\n // real captured id is known; that pseudo-id only resolves on Chrome, so\n // restore it non-exactly and let other browsers fall back to any mic.\n if (previous !== undefined) {\n await room\n .switchActiveDevice(\"audioinput\", previous, previous !== \"default\")\n .catch(() => undefined);\n this.publishInputStream();\n }\n throw error;\n }\n if (!switched) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not activate the requested microphone\",\n );\n }\n // A live switch replaces the publication's MediaStreamTrack — re-wire\n // analysis. While muted, livekit defers the swap to the next unmute and\n // setMicEnabled re-wires then.\n this.publishInputStream();\n }\n\n /** Local tracks already wired to re-emit their stream when livekit restarts them. */\n private readonly restartWiredTracks = new WeakSet<object>();\n\n private publishInputStream(): void {\n const room = this.room;\n if (!room) {\n return;\n }\n for (const publication of room.localParticipant.audioTrackPublications.values()) {\n const track = publication.track;\n if (track && !this.restartWiredTracks.has(track)) {\n this.restartWiredTracks.add(track);\n // Restarts replace the MediaStreamTrack without any Room-level event —\n // livekit's devicechange auto-recovery and getRoom() escape-hatch\n // switches would otherwise leave input analysis on a dead track.\n track.on(TrackEvent.Restarted, () => this.publishInputStream());\n }\n const mediaStreamTrack = track?.mediaStreamTrack;\n if (mediaStreamTrack) {\n this.callbacks?.onInputStream(new MediaStream([mediaStreamTrack]));\n }\n }\n }\n\n async sendClientEvent(message: ClientSessionMessage): Promise<void> {\n if (!this.room) {\n throw new FishAgentError(\"connection_failed\", \"Session is not connected\");\n }\n const payload = new TextEncoder().encode(JSON.stringify(message));\n await this.room.localParticipant.publishData(payload, {\n reliable: true,\n topic: CLIENT_EVENT_TOPIC,\n });\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishaudio/agent-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Fish Audio voice-agent client SDK for browsers: sessions, realtime audio, transcripts, client tools.",
5
5
  "keywords": [
6
6
  "fish-audio",
@@ -47,18 +47,19 @@
47
47
  "publishConfig": {
48
48
  "access": "public"
49
49
  },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "prepublishOnly": "pnpm build",
53
+ "test": "vitest run",
54
+ "typecheck": "tsc --noEmit"
55
+ },
50
56
  "dependencies": {
51
- "livekit-client": "^2.20.0",
52
- "@fishaudio/agent-protocol": "^0.1.0"
57
+ "@fishaudio/agent-protocol": "workspace:^",
58
+ "livekit-client": "^2.20.0"
53
59
  },
54
60
  "devDependencies": {
55
61
  "tsup": "^8.5.0",
56
62
  "typescript": "^5.9.3",
57
63
  "vitest": "^4.0.5"
58
- },
59
- "scripts": {
60
- "build": "tsup",
61
- "test": "vitest run",
62
- "typecheck": "tsc --noEmit"
63
64
  }
64
- }
65
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/transport/livekit.ts"],"sourcesContent":["import {\n ConnectionState,\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n TrackEvent,\n type RemoteTrack,\n} from \"livekit-client\";\nimport {\n AGENT_EVENT_TOPIC,\n CLIENT_EVENT_TOPIC,\n type AgentSessionMessage,\n type ClientSessionMessage,\n type SessionToken,\n} from \"@fishaudio/agent-protocol\";\nimport { FishAgentError } from \"../errors.js\";\nimport type { Transport, TransportCallbacks, TransportConnectOptions } from \"./types.js\";\n\nfunction hasMessageType(value: unknown): value is { type: string } {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { type?: unknown }).type === \"string\"\n );\n}\n\n/** Sole production Transport. LiveKit vocabulary must not escape this file,\n * except the `Room` handed out through the `getRoom()` escape hatch. */\nexport class LiveKitTransport implements Transport {\n private room?: Room;\n private callbacks?: TransportCallbacks;\n\n async connect(sessionToken: SessionToken, options: TransportConnectOptions): Promise<void> {\n this.callbacks = options.callbacks;\n const room = new Room({\n audioCaptureDefaults: {\n deviceId: options.inputDeviceId,\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n },\n });\n this.room = room;\n\n room.on(RoomEvent.DataReceived, (payload, _participant, _kind, topic) => {\n if (topic !== AGENT_EVENT_TOPIC) {\n return;\n }\n let message: unknown;\n try {\n message = JSON.parse(new TextDecoder().decode(payload));\n } catch {\n return;\n }\n if (hasMessageType(message)) {\n this.callbacks?.onAgentEvent(message as AgentSessionMessage);\n }\n });\n\n const AGENT_STATE_ATTRIBUTE = \"lk.agent.state\";\n room.on(RoomEvent.ParticipantAttributesChanged, (changed, participant) => {\n const state = changed[AGENT_STATE_ATTRIBUTE];\n if (!participant.isLocal && state) {\n this.callbacks?.onAgentState(state);\n }\n });\n\n room.on(RoomEvent.TrackSubscribed, (track: RemoteTrack) => {\n if (track.kind === Track.Kind.Audio && track.mediaStreamTrack) {\n this.callbacks?.onOutputStream(new MediaStream([track.mediaStreamTrack]));\n }\n });\n\n // Both sides' transcripts ride LiveKit's built-in transcription streams:\n // agent segments are playout-synced delta streams (final on stream close —\n // an interrupted segment closes containing only the spoken text); user STT\n // segments are re-published per update with an explicit final attribute,\n // sent on behalf of the user, so sender identity distinguishes the roles.\n room.registerTextStreamHandler(\"lk.transcription\", async (reader, participantInfo) => {\n const attributes = reader.info.attributes ?? {};\n const segmentId = attributes[\"lk.segment_id\"];\n if (!segmentId) {\n return;\n }\n const role =\n participantInfo.identity === room.localParticipant.identity ? \"user\" : \"agent\";\n const finalAttribute = attributes[\"lk.transcription_final\"] === \"true\";\n let text = \"\";\n let aborted = false;\n try {\n for await (const delta of reader) {\n text += delta;\n this.callbacks?.onTranscription({ segmentId, role, text, final: false });\n }\n } catch {\n aborted = true; // interrupted mid-stream; what arrived is what was spoken\n }\n if (text) {\n const final = role === \"agent\" ? true : finalAttribute && !aborted;\n this.callbacks?.onTranscription({ segmentId, role, text, final });\n }\n });\n\n room.on(RoomEvent.Reconnecting, () => {\n this.callbacks?.onConnectionState(\"reconnecting\");\n });\n room.on(RoomEvent.Reconnected, () => {\n // A full reconnect replays the roster from the join response before this\n // event; an agent still missing here hung up while we were away.\n if (room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n return;\n }\n this.callbacks?.onConnectionState(\"connected\");\n });\n room.on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n this.callbacks?.onConnectionState(\n \"disconnected\",\n reason === undefined ? undefined : DisconnectReason[reason],\n );\n // Remote-initiated ends never go through disconnect(); drop the refs here\n // so getRoom() honors its ended-means-undefined contract and the room\n // can be collected.\n if (this.room === room) {\n this.room = undefined;\n this.callbacks = undefined;\n }\n });\n // An agent-side hangup ends the worker without closing the room; the\n // agent participant leaving IS the end-of-call signal for the client.\n // A full reconnect also clears the roster this way, and can do so while\n // room.state still reads Connected (Reconnecting is only set right after),\n // so defer one microtask and require a room that is genuinely connected.\n room.on(RoomEvent.ParticipantDisconnected, () => {\n if (room.remoteParticipants.size === 0) {\n queueMicrotask(() => {\n if (room.state === ConnectionState.Connected && room.remoteParticipants.size === 0) {\n this.handleAgentLeft(room);\n }\n });\n }\n });\n\n try {\n await room.connect(sessionToken.livekit_url, sessionToken.token);\n } catch (cause) {\n throw new FishAgentError(\"connection_failed\", \"Could not establish the realtime session\", {\n cause,\n });\n }\n // The attribute is sticky: if the agent joined and set its state before us,\n // no change event will fire — replay the current value once.\n for (const participant of room.remoteParticipants.values()) {\n const state = participant.attributes?.[AGENT_STATE_ATTRIBUTE];\n if (state) {\n this.callbacks?.onAgentState(state);\n }\n }\n if (options.microphone !== false) {\n await room.localParticipant.setMicrophoneEnabled(true);\n this.publishInputStream();\n }\n }\n\n private handleAgentLeft(room: Room): void {\n this.callbacks?.onConnectionState(\"disconnected\", \"AGENT_LEFT\");\n void room.disconnect();\n }\n\n async disconnect(): Promise<void> {\n const room = this.room;\n this.room = undefined;\n this.callbacks = undefined;\n await room?.disconnect();\n }\n\n getRoom(): Room | undefined {\n return this.room;\n }\n\n async setMicEnabled(enabled: boolean): Promise<void> {\n await this.room?.localParticipant.setMicrophoneEnabled(enabled);\n if (enabled) {\n // After a mic-less connect the first enable captures the track late;\n // re-wiring an already-published track is a harmless replace.\n this.publishInputStream();\n }\n }\n\n async setInputDevice(deviceId: string): Promise<void> {\n const room = this.room;\n if (!room) {\n return;\n }\n const previous = room.getActiveDevice(\"audioinput\");\n let switched: boolean;\n try {\n // exact: a wrong id must fail loudly, not silently fall back to another mic.\n switched = await room.switchActiveDevice(\"audioinput\", deviceId, true);\n } catch (error) {\n // livekit stops the current track before acquiring the new device, so a\n // failed pick would otherwise leave the call silently mic-dead — switch\n // back before surfacing the failure. livekit reports \"default\" until the\n // real captured id is known; that pseudo-id only resolves on Chrome, so\n // restore it non-exactly and let other browsers fall back to any mic.\n if (previous !== undefined) {\n await room\n .switchActiveDevice(\"audioinput\", previous, previous !== \"default\")\n .catch(() => undefined);\n this.publishInputStream();\n }\n throw error;\n }\n if (!switched) {\n throw new FishAgentError(\n \"device_change_failed\",\n \"Could not activate the requested microphone\",\n );\n }\n // A live switch replaces the publication's MediaStreamTrack — re-wire\n // analysis. While muted, livekit defers the swap to the next unmute and\n // setMicEnabled re-wires then.\n this.publishInputStream();\n }\n\n /** Local tracks already wired to re-emit their stream when livekit restarts them. */\n private readonly restartWiredTracks = new WeakSet<object>();\n\n private publishInputStream(): void {\n const room = this.room;\n if (!room) {\n return;\n }\n for (const publication of room.localParticipant.audioTrackPublications.values()) {\n const track = publication.track;\n if (track && !this.restartWiredTracks.has(track)) {\n this.restartWiredTracks.add(track);\n // Restarts replace the MediaStreamTrack without any Room-level event —\n // livekit's devicechange auto-recovery and getRoom() escape-hatch\n // switches would otherwise leave input analysis on a dead track.\n track.on(TrackEvent.Restarted, () => this.publishInputStream());\n }\n const mediaStreamTrack = track?.mediaStreamTrack;\n if (mediaStreamTrack) {\n this.callbacks?.onInputStream(new MediaStream([mediaStreamTrack]));\n }\n }\n }\n\n async sendClientEvent(message: ClientSessionMessage): Promise<void> {\n if (!this.room) {\n throw new FishAgentError(\"connection_failed\", \"Session is not connected\");\n }\n const payload = new TextEncoder().encode(JSON.stringify(message));\n await this.room.localParticipant.publishData(payload, {\n reliable: true,\n topic: CLIENT_EVENT_TOPIC,\n });\n }\n}\n"],"mappings":"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","names":[]}