@fishaudio/agent-client 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/index.cjs +90 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +88 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -11
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Framework-agnostic JavaScript/TypeScript SDK for embedding [Fish Audio](https://fish.audio) voice agents into any website or web app: realtime voice over WebRTC, live transcripts, text input, and client tools — behind one small, event-driven API.
|
|
4
4
|
|
|
5
|
-
Using React? [`@fishaudio/agent-react`](https://
|
|
5
|
+
Using React? [`@fishaudio/agent-react`](https://www.npmjs.com/package/@fishaudio/agent-react) wraps this SDK in hooks and components.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -36,10 +36,10 @@ Starting a session requests microphone access; call `start()` from a user gestur
|
|
|
36
36
|
|
|
37
37
|
## Documentation
|
|
38
38
|
|
|
39
|
-
- [Quickstart](https://
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [Events](https://
|
|
43
|
-
- [Client tools](https://
|
|
44
|
-
- [
|
|
45
|
-
- [Errors](https://
|
|
39
|
+
- [Quickstart](https://docs.fish.audio/agents/quickstart) — first call, with or without a backend.
|
|
40
|
+
- [Authenticated sessions](https://docs.fish.audio/agents/deploy/authenticated-sessions) and [public agents](https://docs.fish.audio/agents/deploy/public-agents) — server-created session tokens vs. a public agent id.
|
|
41
|
+
- [Web SDK reference](https://docs.fish.audio/agents/deploy/web-sdk) — `start()` options, the session API, lifecycle and reconnection.
|
|
42
|
+
- [Events](https://docs.fish.audio/agents/deploy/web-sdk#events) — event reference and live transcripts.
|
|
43
|
+
- [Client tools](https://docs.fish.audio/agents/build/client-tools) — let the agent call functions in the browser.
|
|
44
|
+
- [Overrides](https://docs.fish.audio/agents/deploy/authenticated-sessions#overrides) and [dynamic variables](https://docs.fish.audio/agents/build/dynamic-variables) — per-session customization.
|
|
45
|
+
- [Errors](https://docs.fish.audio/agents/deploy/web-sdk#errors) — `FishAgentError` codes.
|
package/dist/index.cjs
CHANGED
|
@@ -286,7 +286,8 @@ var src_exports = {};
|
|
|
286
286
|
__export(src_exports, {
|
|
287
287
|
AgentSession: () => AgentSession,
|
|
288
288
|
DEFAULT_SERVER_URL: () => DEFAULT_SERVER_URL,
|
|
289
|
-
FishAgentError: () => FishAgentError
|
|
289
|
+
FishAgentError: () => FishAgentError,
|
|
290
|
+
MAX_CLIENT_TOOL_RESULT_BYTES: () => MAX_CLIENT_TOOL_RESULT_BYTES
|
|
290
291
|
});
|
|
291
292
|
module.exports = __toCommonJS(src_exports);
|
|
292
293
|
|
|
@@ -422,6 +423,22 @@ var AudioStreamAnalyser = class {
|
|
|
422
423
|
// src/session/agentSession.ts
|
|
423
424
|
init_errors();
|
|
424
425
|
|
|
426
|
+
// src/events.ts
|
|
427
|
+
var AGENT_SESSION_EVENT_NAMES = [
|
|
428
|
+
"connect",
|
|
429
|
+
"disconnect",
|
|
430
|
+
"statusChange",
|
|
431
|
+
"modeChange",
|
|
432
|
+
"userTranscript",
|
|
433
|
+
"agentResponseDelta",
|
|
434
|
+
"agentResponse",
|
|
435
|
+
"message",
|
|
436
|
+
"toolCallStarted",
|
|
437
|
+
"toolCallCompleted",
|
|
438
|
+
"toolCallFailed",
|
|
439
|
+
"error"
|
|
440
|
+
];
|
|
441
|
+
|
|
425
442
|
// src/sessionToken.ts
|
|
426
443
|
var import_agent_protocol = require("@fishaudio/agent-protocol");
|
|
427
444
|
init_errors();
|
|
@@ -565,6 +582,8 @@ var TypedEmitter = class {
|
|
|
565
582
|
// src/session/toolDispatcher.ts
|
|
566
583
|
init_errors();
|
|
567
584
|
var DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15e3;
|
|
585
|
+
var MAX_CLIENT_TOOL_RESULT_BYTES = 6e4;
|
|
586
|
+
var textEncoder = new TextEncoder();
|
|
568
587
|
var ClientToolDispatcher = class {
|
|
569
588
|
constructor(tools, timeoutMs, send, onError) {
|
|
570
589
|
this.timeoutMs = timeoutMs;
|
|
@@ -609,14 +628,56 @@ var ClientToolDispatcher = class {
|
|
|
609
628
|
}
|
|
610
629
|
}
|
|
611
630
|
async reply(callId, body) {
|
|
631
|
+
const message = { type: "client_tool.result", callId, ...body };
|
|
632
|
+
const rejected = this.undeliverable(message);
|
|
633
|
+
if (rejected !== void 0) {
|
|
634
|
+
this.onError(new FishAgentError("tool_failed", rejected));
|
|
635
|
+
await this.deliver({ type: "client_tool.result", callId, isError: true, result: rejected });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
612
638
|
try {
|
|
613
|
-
await this.send(
|
|
639
|
+
await this.send(message);
|
|
614
640
|
} catch (error) {
|
|
615
641
|
this.onError(
|
|
616
642
|
new FishAgentError("tool_failed", "Failed to deliver a client tool result", {
|
|
617
643
|
cause: error
|
|
618
644
|
})
|
|
619
645
|
);
|
|
646
|
+
if (body.isError) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
await this.deliver({
|
|
650
|
+
type: "client_tool.result",
|
|
651
|
+
callId,
|
|
652
|
+
isError: true,
|
|
653
|
+
result: `Client tool result could not be delivered: ${String(error)}`
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/** Why the message cannot go over the wire as-is, or undefined if it can. */
|
|
658
|
+
undeliverable(message) {
|
|
659
|
+
let serialized;
|
|
660
|
+
try {
|
|
661
|
+
serialized = JSON.stringify(message);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
return `Client tool result is not JSON-serializable: ${String(error)}`;
|
|
664
|
+
}
|
|
665
|
+
const bytes = textEncoder.encode(serialized).byteLength;
|
|
666
|
+
if (bytes > MAX_CLIENT_TOOL_RESULT_BYTES) {
|
|
667
|
+
return `Client tool result is too large to send (${bytes} bytes serialized; limit ${MAX_CLIENT_TOOL_RESULT_BYTES}). Return a summary or a reference instead.`;
|
|
668
|
+
}
|
|
669
|
+
return void 0;
|
|
670
|
+
}
|
|
671
|
+
/** Last-resort send of an error result; a failure here is reported, not retried. */
|
|
672
|
+
async deliver(message) {
|
|
673
|
+
try {
|
|
674
|
+
await this.send(message);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
this.onError(
|
|
677
|
+
new FishAgentError("tool_failed", "Failed to deliver a client tool error result", {
|
|
678
|
+
cause: error
|
|
679
|
+
})
|
|
680
|
+
);
|
|
620
681
|
}
|
|
621
682
|
}
|
|
622
683
|
withTimeout(promise, toolName) {
|
|
@@ -699,6 +760,29 @@ request_fn = async function() {
|
|
|
699
760
|
|
|
700
761
|
// src/session/agentSession.ts
|
|
701
762
|
var AGENT_JOIN_TIMEOUT_MS = 15e3;
|
|
763
|
+
var EVENT_NAMES = new Set(AGENT_SESSION_EVENT_NAMES);
|
|
764
|
+
function resolveCallbackSubscriptions(callbacks) {
|
|
765
|
+
const subscriptions = [];
|
|
766
|
+
for (const [key, callback] of Object.entries(callbacks ?? {})) {
|
|
767
|
+
if (callback === void 0) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const event = /^on[A-Z]/.test(key) ? key.charAt(2).toLowerCase() + key.slice(3) : void 0;
|
|
771
|
+
if (event === void 0 || !EVENT_NAMES.has(event)) {
|
|
772
|
+
const expected = AGENT_SESSION_EVENT_NAMES.map(
|
|
773
|
+
(name) => `on${name.charAt(0).toUpperCase()}${name.slice(1)}`
|
|
774
|
+
).join(", ");
|
|
775
|
+
throw new TypeError(
|
|
776
|
+
`Unknown callback "${key}". Callback keys are event names prefixed with "on": ${expected}.`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
if (typeof callback !== "function") {
|
|
780
|
+
throw new TypeError(`Callback "${key}" must be a function, got ${typeof callback}.`);
|
|
781
|
+
}
|
|
782
|
+
subscriptions.push([event, callback]);
|
|
783
|
+
}
|
|
784
|
+
return subscriptions;
|
|
785
|
+
}
|
|
702
786
|
var startWith;
|
|
703
787
|
var _status, _mode, _endReason, _sessionId, _micMuted, _endedByClient, _ending, _transport, _tools, _output, _outputAnalyser, _inputAnalyser, _wakeLock, _agentSegments, _transcript, _transcriptIndex, _typedMessageCount, _agentPresent, _pendingMessages, _joinTimer, _AgentSession_instances, endOnce_fn, _AgentSession_static, startWith_fn, asStartError_fn, asMicError_fn, asDeviceError_fn, sendClientEvent_fn, setStatus_fn, setMode_fn, handleAgentPresent_fn, abandonAgentWait_fn, handleConnectionState_fn, finalize_fn, handleAgentEvent_fn, recordSegment_fn, handleTranscription_fn, recordTypedUserMessage_fn;
|
|
704
788
|
var _AgentSession = class _AgentSession extends TypedEmitter {
|
|
@@ -919,6 +1003,7 @@ startWith_fn = async function(options, createTransport) {
|
|
|
919
1003
|
if (options.audio?.outputDeviceId !== void 0) {
|
|
920
1004
|
assertOutputSelectionSupported();
|
|
921
1005
|
}
|
|
1006
|
+
const callbackSubscriptions = resolveCallbackSubscriptions(options.callbacks);
|
|
922
1007
|
const transport = createTransport();
|
|
923
1008
|
if (options.microphone !== false) {
|
|
924
1009
|
transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });
|
|
@@ -935,8 +1020,7 @@ startWith_fn = async function(options, createTransport) {
|
|
|
935
1020
|
await transport.disconnect().catch(() => void 0);
|
|
936
1021
|
throw error;
|
|
937
1022
|
}
|
|
938
|
-
for (const [
|
|
939
|
-
const event = key.charAt(2).toLowerCase() + key.slice(3);
|
|
1023
|
+
for (const [event, callback] of callbackSubscriptions) {
|
|
940
1024
|
session.on(event, callback);
|
|
941
1025
|
}
|
|
942
1026
|
__privateSet(session, _transport, transport);
|
|
@@ -1220,6 +1304,7 @@ init_errors();
|
|
|
1220
1304
|
0 && (module.exports = {
|
|
1221
1305
|
AgentSession,
|
|
1222
1306
|
DEFAULT_SERVER_URL,
|
|
1223
|
-
FishAgentError
|
|
1307
|
+
FishAgentError,
|
|
1308
|
+
MAX_CLIENT_TOOL_RESULT_BYTES
|
|
1224
1309
|
});
|
|
1225
1310
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 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"]}
|
|
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/events.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 { MAX_CLIENT_TOOL_RESULT_BYTES } 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 {\n AGENT_SESSION_EVENT_NAMES,\n type AgentMode,\n type AgentSessionCallbacks,\n type AgentSessionEvents,\n type EndReason,\n type SessionStatus,\n type 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\nconst EVENT_NAMES = new Set<string>(AGENT_SESSION_EVENT_NAMES);\n\n/**\n * Maps the `callbacks` shorthand (`onUserTranscript`) to event names\n * (`userTranscript`). Unknown keys throw: silently subscribing to a\n * non-existent event would hide typos and the bare event names people\n * reach for first (`userTranscript` instead of `onUserTranscript`).\n */\nfunction resolveCallbackSubscriptions(\n callbacks: Partial<AgentSessionCallbacks> | undefined,\n): Array<[keyof AgentSessionEvents, unknown]> {\n const subscriptions: Array<[keyof AgentSessionEvents, unknown]> = [];\n for (const [key, callback] of Object.entries(callbacks ?? {})) {\n if (callback === undefined) {\n continue;\n }\n const event = /^on[A-Z]/.test(key) ? key.charAt(2).toLowerCase() + key.slice(3) : undefined;\n if (event === undefined || !EVENT_NAMES.has(event)) {\n const expected = AGENT_SESSION_EVENT_NAMES.map(\n (name) => `on${name.charAt(0).toUpperCase()}${name.slice(1)}`,\n ).join(\", \");\n throw new TypeError(\n `Unknown callback \"${key}\". Callback keys are event names prefixed with \"on\": ${expected}.`,\n );\n }\n if (typeof callback !== \"function\") {\n throw new TypeError(`Callback \"${key}\" must be a function, got ${typeof callback}.`);\n }\n subscriptions.push([event as keyof AgentSessionEvents, callback]);\n }\n return subscriptions;\n}\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 // Validated before anything with side effects: a typo in a callback name\n // is a programming error and must not cost a microphone prompt or a\n // server session to discover.\n const callbackSubscriptions = resolveCallbackSubscriptions(options.callbacks);\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 [event, callback] of callbackSubscriptions) {\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 type { ToolCallSource } from \"@fishaudio/agent-protocol\";\nimport type { FishAgentError } from \"./errors.js\";\n\nexport type SessionStatus = \"connecting\" | \"connected\" | \"reconnecting\" | \"ended\";\n\nexport type AgentMode = \"listening\" | \"thinking\" | \"speaking\";\n\nexport type EndReason = \"user_hangup\" | \"agent_hangup\" | \"connection_lost\";\n\nexport interface UserTranscriptEvent {\n /** Groups updates of one utterance; interim updates replace, they never append. */\n segmentId: string;\n /** Full text of the utterance so far. */\n text: string;\n final: boolean;\n}\n\nexport interface AgentResponseDeltaEvent {\n /** Groups streaming updates of one agent speech segment. */\n segmentId: string;\n delta: string;\n /** Accumulated segment text including this delta. */\n text: string;\n}\n\nexport interface AgentResponseEvent {\n segmentId: string;\n text: string;\n}\n\n/** One transcript segment; `text` is the whole segment so far, refined in place. */\nexport interface TranscriptSegment {\n segmentId: string;\n role: \"user\" | \"agent\";\n text: string;\n final: boolean;\n}\n\nexport interface ConversationMessage {\n role: \"user\" | \"agent\";\n text: string;\n}\n\n/**\n * Tool-call lifecycle, forwarded for every tool the agent runs (including client\n * tools, which complete once this client returns their result). `callId` pairs one\n * started event with exactly one completed/failed event; terminal events repeat\n * name/source so a listener attached mid-call can still render a full item.\n * Emission is a session-creation option (`toolEvents`/`tool_events`, default true).\n */\nexport interface ToolCallStartedEvent {\n callId: string;\n toolName: string;\n source: ToolCallSource;\n /** Tool arguments as a JSON string; parse only when `inputTruncated` is false. */\n input: string;\n inputTruncated: boolean;\n}\n\nexport interface ToolCallCompletedEvent {\n callId: string;\n toolName: string;\n source: ToolCallSource;\n /** Tool result as a JSON string; parse only when `outputTruncated` is false. */\n output: string;\n outputTruncated: boolean;\n}\n\nexport interface ToolCallFailedEvent {\n callId: string;\n toolName: string;\n source: ToolCallSource;\n error: string;\n}\n\nexport interface AgentSessionEvents {\n connect: (event: { sessionId: string }) => void;\n disconnect: (event: { reason: EndReason }) => void;\n statusChange: (status: SessionStatus) => void;\n modeChange: (mode: AgentMode) => void;\n userTranscript: (event: UserTranscriptEvent) => void;\n agentResponseDelta: (event: AgentResponseDeltaEvent) => void;\n agentResponse: (event: AgentResponseEvent) => void;\n /** Finalized messages only, in conversation order — the \"just give me a chat log\" event. */\n message: (message: ConversationMessage) => void;\n toolCallStarted: (event: ToolCallStartedEvent) => void;\n toolCallCompleted: (event: ToolCallCompletedEvent) => void;\n toolCallFailed: (event: ToolCallFailedEvent) => void;\n error: (error: FishAgentError) => void;\n}\n\nexport type AgentSessionCallbacks = {\n [K in keyof AgentSessionEvents as `on${Capitalize<K>}`]: AgentSessionEvents[K];\n};\n\n/** Every event name, for runtime validation of the `callbacks` shorthand. */\nexport const AGENT_SESSION_EVENT_NAMES = [\n \"connect\",\n \"disconnect\",\n \"statusChange\",\n \"modeChange\",\n \"userTranscript\",\n \"agentResponseDelta\",\n \"agentResponse\",\n \"message\",\n \"toolCallStarted\",\n \"toolCallCompleted\",\n \"toolCallFailed\",\n \"error\",\n] as const satisfies readonly (keyof AgentSessionEvents)[];\n\n// Compile-time check that the list above stays exhaustive.\ntype MissingEventName = Exclude<keyof AgentSessionEvents, (typeof AGENT_SESSION_EVENT_NAMES)[number]>;\nconst _assertEventNamesExhaustive: MissingEventName extends never ? true : never = true;\nvoid _assertEventNamesExhaustive;\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\n/**\n * Upper bound on the serialized `client_tool.result` message. The realtime\n * data channel rejects packets above 64 000 bytes; this leaves headroom for\n * the packet envelope. Larger results are replaced by an error result so the\n * agent hears back instead of waiting out its server-side timeout.\n */\nexport const MAX_CLIENT_TOOL_RESULT_BYTES = 60_000;\n\nconst textEncoder = new TextEncoder();\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 const message: ClientSessionMessage = { type: \"client_tool.result\", callId, ...body };\n const rejected = this.undeliverable(message);\n if (rejected !== undefined) {\n this.onError(new FishAgentError(\"tool_failed\", rejected));\n await this.deliver({ type: \"client_tool.result\", callId, isError: true, result: rejected });\n return;\n }\n try {\n await this.send(message);\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool result\", {\n cause: error,\n }),\n );\n if (body.isError) {\n return;\n }\n // The agent is still waiting on this call. Trade the lost result for a\n // short error result so it can recover in conversation now, rather than\n // after its server-side tool timeout.\n await this.deliver({\n type: \"client_tool.result\",\n callId,\n isError: true,\n result: `Client tool result could not be delivered: ${String(error)}`,\n });\n }\n }\n\n /** Why the message cannot go over the wire as-is, or undefined if it can. */\n private undeliverable(message: ClientSessionMessage): string | undefined {\n let serialized: string;\n try {\n serialized = JSON.stringify(message);\n } catch (error) {\n return `Client tool result is not JSON-serializable: ${String(error)}`;\n }\n const bytes = textEncoder.encode(serialized).byteLength;\n if (bytes > MAX_CLIENT_TOOL_RESULT_BYTES) {\n return (\n `Client tool result is too large to send (${bytes} bytes serialized; ` +\n `limit ${MAX_CLIENT_TOOL_RESULT_BYTES}). Return a summary or a reference instead.`\n );\n }\n return undefined;\n }\n\n /** Last-resort send of an error result; a failure here is reported, not retried. */\n private async deliver(message: ClientSessionMessage): Promise<void> {\n try {\n await this.send(message);\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool error 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
|
@@ -155,6 +155,13 @@ type ClientToolHandler = (params: Record<string, unknown>, context: {
|
|
|
155
155
|
callId: string;
|
|
156
156
|
toolName: string;
|
|
157
157
|
}) => unknown | Promise<unknown>;
|
|
158
|
+
/**
|
|
159
|
+
* Upper bound on the serialized `client_tool.result` message. The realtime
|
|
160
|
+
* data channel rejects packets above 64 000 bytes; this leaves headroom for
|
|
161
|
+
* the packet envelope. Larger results are replaced by an error result so the
|
|
162
|
+
* agent hears back instead of waiting out its server-side timeout.
|
|
163
|
+
*/
|
|
164
|
+
declare const MAX_CLIENT_TOOL_RESULT_BYTES = 60000;
|
|
158
165
|
|
|
159
166
|
interface AgentSessionOptions extends SessionRequestOptions {
|
|
160
167
|
clientTools?: Record<string, ClientToolHandler>;
|
|
@@ -248,4 +255,4 @@ declare class AgentSession extends TypedEmitter<AgentSessionEvents> {
|
|
|
248
255
|
end(): Promise<void>;
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
export { type AgentMode, type AgentResponseDeltaEvent, type AgentResponseEvent, AgentSession, type AgentSessionCallbacks, type AgentSessionEvents, type AgentSessionOptions, type ClientToolHandler, type ConversationMessage, DEFAULT_SERVER_URL, type EndReason, FishAgentError, type FishAgentErrorCode, type SessionStatus, type ToolCallCompletedEvent, type ToolCallFailedEvent, type ToolCallStartedEvent, type TranscriptSegment, type UserTranscriptEvent };
|
|
258
|
+
export { type AgentMode, type AgentResponseDeltaEvent, type AgentResponseEvent, AgentSession, type AgentSessionCallbacks, type AgentSessionEvents, type AgentSessionOptions, type ClientToolHandler, type ConversationMessage, DEFAULT_SERVER_URL, type EndReason, FishAgentError, type FishAgentErrorCode, MAX_CLIENT_TOOL_RESULT_BYTES, type SessionStatus, type ToolCallCompletedEvent, type ToolCallFailedEvent, type ToolCallStartedEvent, type TranscriptSegment, type UserTranscriptEvent };
|
package/dist/index.d.ts
CHANGED
|
@@ -155,6 +155,13 @@ type ClientToolHandler = (params: Record<string, unknown>, context: {
|
|
|
155
155
|
callId: string;
|
|
156
156
|
toolName: string;
|
|
157
157
|
}) => unknown | Promise<unknown>;
|
|
158
|
+
/**
|
|
159
|
+
* Upper bound on the serialized `client_tool.result` message. The realtime
|
|
160
|
+
* data channel rejects packets above 64 000 bytes; this leaves headroom for
|
|
161
|
+
* the packet envelope. Larger results are replaced by an error result so the
|
|
162
|
+
* agent hears back instead of waiting out its server-side timeout.
|
|
163
|
+
*/
|
|
164
|
+
declare const MAX_CLIENT_TOOL_RESULT_BYTES = 60000;
|
|
158
165
|
|
|
159
166
|
interface AgentSessionOptions extends SessionRequestOptions {
|
|
160
167
|
clientTools?: Record<string, ClientToolHandler>;
|
|
@@ -248,4 +255,4 @@ declare class AgentSession extends TypedEmitter<AgentSessionEvents> {
|
|
|
248
255
|
end(): Promise<void>;
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
export { type AgentMode, type AgentResponseDeltaEvent, type AgentResponseEvent, AgentSession, type AgentSessionCallbacks, type AgentSessionEvents, type AgentSessionOptions, type ClientToolHandler, type ConversationMessage, DEFAULT_SERVER_URL, type EndReason, FishAgentError, type FishAgentErrorCode, type SessionStatus, type ToolCallCompletedEvent, type ToolCallFailedEvent, type ToolCallStartedEvent, type TranscriptSegment, type UserTranscriptEvent };
|
|
258
|
+
export { type AgentMode, type AgentResponseDeltaEvent, type AgentResponseEvent, AgentSession, type AgentSessionCallbacks, type AgentSessionEvents, type AgentSessionOptions, type ClientToolHandler, type ConversationMessage, DEFAULT_SERVER_URL, type EndReason, FishAgentError, type FishAgentErrorCode, MAX_CLIENT_TOOL_RESULT_BYTES, type SessionStatus, type ToolCallCompletedEvent, type ToolCallFailedEvent, type ToolCallStartedEvent, type TranscriptSegment, type UserTranscriptEvent };
|
package/dist/index.js
CHANGED
|
@@ -135,6 +135,22 @@ var AudioStreamAnalyser = class {
|
|
|
135
135
|
}
|
|
136
136
|
};
|
|
137
137
|
|
|
138
|
+
// src/events.ts
|
|
139
|
+
var AGENT_SESSION_EVENT_NAMES = [
|
|
140
|
+
"connect",
|
|
141
|
+
"disconnect",
|
|
142
|
+
"statusChange",
|
|
143
|
+
"modeChange",
|
|
144
|
+
"userTranscript",
|
|
145
|
+
"agentResponseDelta",
|
|
146
|
+
"agentResponse",
|
|
147
|
+
"message",
|
|
148
|
+
"toolCallStarted",
|
|
149
|
+
"toolCallCompleted",
|
|
150
|
+
"toolCallFailed",
|
|
151
|
+
"error"
|
|
152
|
+
];
|
|
153
|
+
|
|
138
154
|
// src/sessionToken.ts
|
|
139
155
|
import {
|
|
140
156
|
SESSION_TRANSPORT_LIVEKIT
|
|
@@ -278,6 +294,8 @@ var TypedEmitter = class {
|
|
|
278
294
|
|
|
279
295
|
// src/session/toolDispatcher.ts
|
|
280
296
|
var DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15e3;
|
|
297
|
+
var MAX_CLIENT_TOOL_RESULT_BYTES = 6e4;
|
|
298
|
+
var textEncoder = new TextEncoder();
|
|
281
299
|
var ClientToolDispatcher = class {
|
|
282
300
|
constructor(tools, timeoutMs, send, onError) {
|
|
283
301
|
this.timeoutMs = timeoutMs;
|
|
@@ -322,14 +340,56 @@ var ClientToolDispatcher = class {
|
|
|
322
340
|
}
|
|
323
341
|
}
|
|
324
342
|
async reply(callId, body) {
|
|
343
|
+
const message = { type: "client_tool.result", callId, ...body };
|
|
344
|
+
const rejected = this.undeliverable(message);
|
|
345
|
+
if (rejected !== void 0) {
|
|
346
|
+
this.onError(new FishAgentError("tool_failed", rejected));
|
|
347
|
+
await this.deliver({ type: "client_tool.result", callId, isError: true, result: rejected });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
325
350
|
try {
|
|
326
|
-
await this.send(
|
|
351
|
+
await this.send(message);
|
|
327
352
|
} catch (error) {
|
|
328
353
|
this.onError(
|
|
329
354
|
new FishAgentError("tool_failed", "Failed to deliver a client tool result", {
|
|
330
355
|
cause: error
|
|
331
356
|
})
|
|
332
357
|
);
|
|
358
|
+
if (body.isError) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
await this.deliver({
|
|
362
|
+
type: "client_tool.result",
|
|
363
|
+
callId,
|
|
364
|
+
isError: true,
|
|
365
|
+
result: `Client tool result could not be delivered: ${String(error)}`
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** Why the message cannot go over the wire as-is, or undefined if it can. */
|
|
370
|
+
undeliverable(message) {
|
|
371
|
+
let serialized;
|
|
372
|
+
try {
|
|
373
|
+
serialized = JSON.stringify(message);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return `Client tool result is not JSON-serializable: ${String(error)}`;
|
|
376
|
+
}
|
|
377
|
+
const bytes = textEncoder.encode(serialized).byteLength;
|
|
378
|
+
if (bytes > MAX_CLIENT_TOOL_RESULT_BYTES) {
|
|
379
|
+
return `Client tool result is too large to send (${bytes} bytes serialized; limit ${MAX_CLIENT_TOOL_RESULT_BYTES}). Return a summary or a reference instead.`;
|
|
380
|
+
}
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
/** Last-resort send of an error result; a failure here is reported, not retried. */
|
|
384
|
+
async deliver(message) {
|
|
385
|
+
try {
|
|
386
|
+
await this.send(message);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
this.onError(
|
|
389
|
+
new FishAgentError("tool_failed", "Failed to deliver a client tool error result", {
|
|
390
|
+
cause: error
|
|
391
|
+
})
|
|
392
|
+
);
|
|
333
393
|
}
|
|
334
394
|
}
|
|
335
395
|
withTimeout(promise, toolName) {
|
|
@@ -412,6 +472,29 @@ request_fn = async function() {
|
|
|
412
472
|
|
|
413
473
|
// src/session/agentSession.ts
|
|
414
474
|
var AGENT_JOIN_TIMEOUT_MS = 15e3;
|
|
475
|
+
var EVENT_NAMES = new Set(AGENT_SESSION_EVENT_NAMES);
|
|
476
|
+
function resolveCallbackSubscriptions(callbacks) {
|
|
477
|
+
const subscriptions = [];
|
|
478
|
+
for (const [key, callback] of Object.entries(callbacks ?? {})) {
|
|
479
|
+
if (callback === void 0) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const event = /^on[A-Z]/.test(key) ? key.charAt(2).toLowerCase() + key.slice(3) : void 0;
|
|
483
|
+
if (event === void 0 || !EVENT_NAMES.has(event)) {
|
|
484
|
+
const expected = AGENT_SESSION_EVENT_NAMES.map(
|
|
485
|
+
(name) => `on${name.charAt(0).toUpperCase()}${name.slice(1)}`
|
|
486
|
+
).join(", ");
|
|
487
|
+
throw new TypeError(
|
|
488
|
+
`Unknown callback "${key}". Callback keys are event names prefixed with "on": ${expected}.`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (typeof callback !== "function") {
|
|
492
|
+
throw new TypeError(`Callback "${key}" must be a function, got ${typeof callback}.`);
|
|
493
|
+
}
|
|
494
|
+
subscriptions.push([event, callback]);
|
|
495
|
+
}
|
|
496
|
+
return subscriptions;
|
|
497
|
+
}
|
|
415
498
|
var startWith;
|
|
416
499
|
var _status, _mode, _endReason, _sessionId, _micMuted, _endedByClient, _ending, _transport, _tools, _output, _outputAnalyser, _inputAnalyser, _wakeLock, _agentSegments, _transcript, _transcriptIndex, _typedMessageCount, _agentPresent, _pendingMessages, _joinTimer, _AgentSession_instances, endOnce_fn, _AgentSession_static, startWith_fn, asStartError_fn, asMicError_fn, asDeviceError_fn, sendClientEvent_fn, setStatus_fn, setMode_fn, handleAgentPresent_fn, abandonAgentWait_fn, handleConnectionState_fn, finalize_fn, handleAgentEvent_fn, recordSegment_fn, handleTranscription_fn, recordTypedUserMessage_fn;
|
|
417
500
|
var _AgentSession = class _AgentSession extends TypedEmitter {
|
|
@@ -632,6 +715,7 @@ startWith_fn = async function(options, createTransport) {
|
|
|
632
715
|
if (options.audio?.outputDeviceId !== void 0) {
|
|
633
716
|
assertOutputSelectionSupported();
|
|
634
717
|
}
|
|
718
|
+
const callbackSubscriptions = resolveCallbackSubscriptions(options.callbacks);
|
|
635
719
|
const transport = createTransport();
|
|
636
720
|
if (options.microphone !== false) {
|
|
637
721
|
transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });
|
|
@@ -648,8 +732,7 @@ startWith_fn = async function(options, createTransport) {
|
|
|
648
732
|
await transport.disconnect().catch(() => void 0);
|
|
649
733
|
throw error;
|
|
650
734
|
}
|
|
651
|
-
for (const [
|
|
652
|
-
const event = key.charAt(2).toLowerCase() + key.slice(3);
|
|
735
|
+
for (const [event, callback] of callbackSubscriptions) {
|
|
653
736
|
session.on(event, callback);
|
|
654
737
|
}
|
|
655
738
|
__privateSet(session, _transport, transport);
|
|
@@ -929,6 +1012,7 @@ var AgentSession = _AgentSession;
|
|
|
929
1012
|
export {
|
|
930
1013
|
AgentSession,
|
|
931
1014
|
DEFAULT_SERVER_URL,
|
|
932
|
-
FishAgentError
|
|
1015
|
+
FishAgentError,
|
|
1016
|
+
MAX_CLIENT_TOOL_RESULT_BYTES
|
|
933
1017
|
};
|
|
934
1018
|
//# sourceMappingURL=index.js.map
|
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 /** 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/audio/output.ts","../src/audio/analysis.ts","../src/events.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 type { ToolCallSource } from \"@fishaudio/agent-protocol\";\nimport type { FishAgentError } from \"./errors.js\";\n\nexport type SessionStatus = \"connecting\" | \"connected\" | \"reconnecting\" | \"ended\";\n\nexport type AgentMode = \"listening\" | \"thinking\" | \"speaking\";\n\nexport type EndReason = \"user_hangup\" | \"agent_hangup\" | \"connection_lost\";\n\nexport interface UserTranscriptEvent {\n /** Groups updates of one utterance; interim updates replace, they never append. */\n segmentId: string;\n /** Full text of the utterance so far. */\n text: string;\n final: boolean;\n}\n\nexport interface AgentResponseDeltaEvent {\n /** Groups streaming updates of one agent speech segment. */\n segmentId: string;\n delta: string;\n /** Accumulated segment text including this delta. */\n text: string;\n}\n\nexport interface AgentResponseEvent {\n segmentId: string;\n text: string;\n}\n\n/** One transcript segment; `text` is the whole segment so far, refined in place. */\nexport interface TranscriptSegment {\n segmentId: string;\n role: \"user\" | \"agent\";\n text: string;\n final: boolean;\n}\n\nexport interface ConversationMessage {\n role: \"user\" | \"agent\";\n text: string;\n}\n\n/**\n * Tool-call lifecycle, forwarded for every tool the agent runs (including client\n * tools, which complete once this client returns their result). `callId` pairs one\n * started event with exactly one completed/failed event; terminal events repeat\n * name/source so a listener attached mid-call can still render a full item.\n * Emission is a session-creation option (`toolEvents`/`tool_events`, default true).\n */\nexport interface ToolCallStartedEvent {\n callId: string;\n toolName: string;\n source: ToolCallSource;\n /** Tool arguments as a JSON string; parse only when `inputTruncated` is false. */\n input: string;\n inputTruncated: boolean;\n}\n\nexport interface ToolCallCompletedEvent {\n callId: string;\n toolName: string;\n source: ToolCallSource;\n /** Tool result as a JSON string; parse only when `outputTruncated` is false. */\n output: string;\n outputTruncated: boolean;\n}\n\nexport interface ToolCallFailedEvent {\n callId: string;\n toolName: string;\n source: ToolCallSource;\n error: string;\n}\n\nexport interface AgentSessionEvents {\n connect: (event: { sessionId: string }) => void;\n disconnect: (event: { reason: EndReason }) => void;\n statusChange: (status: SessionStatus) => void;\n modeChange: (mode: AgentMode) => void;\n userTranscript: (event: UserTranscriptEvent) => void;\n agentResponseDelta: (event: AgentResponseDeltaEvent) => void;\n agentResponse: (event: AgentResponseEvent) => void;\n /** Finalized messages only, in conversation order — the \"just give me a chat log\" event. */\n message: (message: ConversationMessage) => void;\n toolCallStarted: (event: ToolCallStartedEvent) => void;\n toolCallCompleted: (event: ToolCallCompletedEvent) => void;\n toolCallFailed: (event: ToolCallFailedEvent) => void;\n error: (error: FishAgentError) => void;\n}\n\nexport type AgentSessionCallbacks = {\n [K in keyof AgentSessionEvents as `on${Capitalize<K>}`]: AgentSessionEvents[K];\n};\n\n/** Every event name, for runtime validation of the `callbacks` shorthand. */\nexport const AGENT_SESSION_EVENT_NAMES = [\n \"connect\",\n \"disconnect\",\n \"statusChange\",\n \"modeChange\",\n \"userTranscript\",\n \"agentResponseDelta\",\n \"agentResponse\",\n \"message\",\n \"toolCallStarted\",\n \"toolCallCompleted\",\n \"toolCallFailed\",\n \"error\",\n] as const satisfies readonly (keyof AgentSessionEvents)[];\n\n// Compile-time check that the list above stays exhaustive.\ntype MissingEventName = Exclude<keyof AgentSessionEvents, (typeof AGENT_SESSION_EVENT_NAMES)[number]>;\nconst _assertEventNamesExhaustive: MissingEventName extends never ? true : never = true;\nvoid _assertEventNamesExhaustive;\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\n/**\n * Upper bound on the serialized `client_tool.result` message. The realtime\n * data channel rejects packets above 64 000 bytes; this leaves headroom for\n * the packet envelope. Larger results are replaced by an error result so the\n * agent hears back instead of waiting out its server-side timeout.\n */\nexport const MAX_CLIENT_TOOL_RESULT_BYTES = 60_000;\n\nconst textEncoder = new TextEncoder();\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 const message: ClientSessionMessage = { type: \"client_tool.result\", callId, ...body };\n const rejected = this.undeliverable(message);\n if (rejected !== undefined) {\n this.onError(new FishAgentError(\"tool_failed\", rejected));\n await this.deliver({ type: \"client_tool.result\", callId, isError: true, result: rejected });\n return;\n }\n try {\n await this.send(message);\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool result\", {\n cause: error,\n }),\n );\n if (body.isError) {\n return;\n }\n // The agent is still waiting on this call. Trade the lost result for a\n // short error result so it can recover in conversation now, rather than\n // after its server-side tool timeout.\n await this.deliver({\n type: \"client_tool.result\",\n callId,\n isError: true,\n result: `Client tool result could not be delivered: ${String(error)}`,\n });\n }\n }\n\n /** Why the message cannot go over the wire as-is, or undefined if it can. */\n private undeliverable(message: ClientSessionMessage): string | undefined {\n let serialized: string;\n try {\n serialized = JSON.stringify(message);\n } catch (error) {\n return `Client tool result is not JSON-serializable: ${String(error)}`;\n }\n const bytes = textEncoder.encode(serialized).byteLength;\n if (bytes > MAX_CLIENT_TOOL_RESULT_BYTES) {\n return (\n `Client tool result is too large to send (${bytes} bytes serialized; ` +\n `limit ${MAX_CLIENT_TOOL_RESULT_BYTES}). Return a summary or a reference instead.`\n );\n }\n return undefined;\n }\n\n /** Last-resort send of an error result; a failure here is reported, not retried. */\n private async deliver(message: ClientSessionMessage): Promise<void> {\n try {\n await this.send(message);\n } catch (error) {\n this.onError(\n new FishAgentError(\"tool_failed\", \"Failed to deliver a client tool error 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 {\n AGENT_SESSION_EVENT_NAMES,\n type AgentMode,\n type AgentSessionCallbacks,\n type AgentSessionEvents,\n type EndReason,\n type SessionStatus,\n type 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\nconst EVENT_NAMES = new Set<string>(AGENT_SESSION_EVENT_NAMES);\n\n/**\n * Maps the `callbacks` shorthand (`onUserTranscript`) to event names\n * (`userTranscript`). Unknown keys throw: silently subscribing to a\n * non-existent event would hide typos and the bare event names people\n * reach for first (`userTranscript` instead of `onUserTranscript`).\n */\nfunction resolveCallbackSubscriptions(\n callbacks: Partial<AgentSessionCallbacks> | undefined,\n): Array<[keyof AgentSessionEvents, unknown]> {\n const subscriptions: Array<[keyof AgentSessionEvents, unknown]> = [];\n for (const [key, callback] of Object.entries(callbacks ?? {})) {\n if (callback === undefined) {\n continue;\n }\n const event = /^on[A-Z]/.test(key) ? key.charAt(2).toLowerCase() + key.slice(3) : undefined;\n if (event === undefined || !EVENT_NAMES.has(event)) {\n const expected = AGENT_SESSION_EVENT_NAMES.map(\n (name) => `on${name.charAt(0).toUpperCase()}${name.slice(1)}`,\n ).join(\", \");\n throw new TypeError(\n `Unknown callback \"${key}\". Callback keys are event names prefixed with \"on\": ${expected}.`,\n );\n }\n if (typeof callback !== \"function\") {\n throw new TypeError(`Callback \"${key}\" must be a function, got ${typeof callback}.`);\n }\n subscriptions.push([event as keyof AgentSessionEvents, callback]);\n }\n return subscriptions;\n}\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 // Validated before anything with side effects: a typo in a callback name\n // is a programming error and must not cost a microphone prompt or a\n // server session to discover.\n const callbackSubscriptions = resolveCallbackSubscriptions(options.callbacks);\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 [event, callback] of callbackSubscriptions) {\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fishaudio/agent-client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Fish Audio voice-agent client SDK for browsers: sessions, realtime audio, transcripts, client tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fish-audio",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"url": "git+https://github.com/fishaudio/fish-agent-sdk-web.git",
|
|
18
18
|
"directory": "packages/client"
|
|
19
19
|
},
|
|
20
|
-
"homepage": "https://
|
|
20
|
+
"homepage": "https://docs.fish.audio/agents/deploy/web-sdk",
|
|
21
21
|
"bugs": "https://github.com/fishaudio/fish-agent-sdk-web/issues",
|
|
22
22
|
"type": "module",
|
|
23
23
|
"sideEffects": false,
|
|
@@ -47,19 +47,18 @@
|
|
|
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
|
-
},
|
|
56
50
|
"dependencies": {
|
|
57
|
-
"
|
|
58
|
-
"
|
|
51
|
+
"livekit-client": "^2.20.0",
|
|
52
|
+
"@fishaudio/agent-protocol": "^0.2.1"
|
|
59
53
|
},
|
|
60
54
|
"devDependencies": {
|
|
61
55
|
"tsup": "^8.5.0",
|
|
62
56
|
"typescript": "^5.9.3",
|
|
63
57
|
"vitest": "^4.0.5"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"typecheck": "tsc --noEmit"
|
|
64
63
|
}
|
|
65
|
-
}
|
|
64
|
+
}
|