@fishaudio/agent-client 0.1.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 +132 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +106 -12
- package/dist/index.js.map +1 -1
- package/dist/{livekit-B62YPROC.js → livekit-DBSPD4GR.js} +27 -3
- package/dist/livekit-DBSPD4GR.js.map +1 -0
- package/package.json +3 -3
- package/dist/livekit-B62YPROC.js.map +0 -1
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
|
@@ -65,6 +65,14 @@ __export(livekit_exports, {
|
|
|
65
65
|
function hasMessageType(value) {
|
|
66
66
|
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
67
67
|
}
|
|
68
|
+
function captureMicrophone(inputDeviceId) {
|
|
69
|
+
return (0, import_livekit_client.createLocalAudioTrack)({
|
|
70
|
+
deviceId: inputDeviceId,
|
|
71
|
+
echoCancellation: true,
|
|
72
|
+
noiseSuppression: true,
|
|
73
|
+
autoGainControl: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
68
76
|
var import_livekit_client, import_agent_protocol2, LiveKitTransport;
|
|
69
77
|
var init_livekit = __esm({
|
|
70
78
|
"src/transport/livekit.ts"() {
|
|
@@ -77,6 +85,11 @@ var init_livekit = __esm({
|
|
|
77
85
|
/** Local tracks already wired to re-emit their stream when livekit restarts them. */
|
|
78
86
|
this.restartWiredTracks = /* @__PURE__ */ new WeakSet();
|
|
79
87
|
}
|
|
88
|
+
prepareMicrophone(options) {
|
|
89
|
+
const pending = captureMicrophone(options.inputDeviceId);
|
|
90
|
+
pending.catch(() => void 0);
|
|
91
|
+
this.pendingMicTrack = pending;
|
|
92
|
+
}
|
|
80
93
|
async connect(sessionToken, options) {
|
|
81
94
|
this.callbacks = options.callbacks;
|
|
82
95
|
const room = new import_livekit_client.Room({
|
|
@@ -166,6 +179,12 @@ var init_livekit = __esm({
|
|
|
166
179
|
});
|
|
167
180
|
}
|
|
168
181
|
});
|
|
182
|
+
let micTrack;
|
|
183
|
+
if (options.microphone !== false) {
|
|
184
|
+
const pending = this.pendingMicTrack ?? captureMicrophone(options.inputDeviceId);
|
|
185
|
+
this.pendingMicTrack = pending;
|
|
186
|
+
micTrack = await pending;
|
|
187
|
+
}
|
|
169
188
|
try {
|
|
170
189
|
await room.connect(sessionToken.livekit_url, sessionToken.token);
|
|
171
190
|
} catch (cause) {
|
|
@@ -179,8 +198,9 @@ var init_livekit = __esm({
|
|
|
179
198
|
this.callbacks?.onAgentState(state);
|
|
180
199
|
}
|
|
181
200
|
}
|
|
182
|
-
if (
|
|
183
|
-
await room.localParticipant.
|
|
201
|
+
if (micTrack) {
|
|
202
|
+
await room.localParticipant.publishTrack(micTrack);
|
|
203
|
+
this.pendingMicTrack = void 0;
|
|
184
204
|
this.publishInputStream();
|
|
185
205
|
}
|
|
186
206
|
}
|
|
@@ -189,6 +209,9 @@ var init_livekit = __esm({
|
|
|
189
209
|
void room.disconnect();
|
|
190
210
|
}
|
|
191
211
|
async disconnect() {
|
|
212
|
+
const pendingMic = this.pendingMicTrack;
|
|
213
|
+
this.pendingMicTrack = void 0;
|
|
214
|
+
void pendingMic?.then((track) => track.stop()).catch(() => void 0);
|
|
192
215
|
const room = this.room;
|
|
193
216
|
this.room = void 0;
|
|
194
217
|
this.callbacks = void 0;
|
|
@@ -263,7 +286,8 @@ var src_exports = {};
|
|
|
263
286
|
__export(src_exports, {
|
|
264
287
|
AgentSession: () => AgentSession,
|
|
265
288
|
DEFAULT_SERVER_URL: () => DEFAULT_SERVER_URL,
|
|
266
|
-
FishAgentError: () => FishAgentError
|
|
289
|
+
FishAgentError: () => FishAgentError,
|
|
290
|
+
MAX_CLIENT_TOOL_RESULT_BYTES: () => MAX_CLIENT_TOOL_RESULT_BYTES
|
|
267
291
|
});
|
|
268
292
|
module.exports = __toCommonJS(src_exports);
|
|
269
293
|
|
|
@@ -399,6 +423,22 @@ var AudioStreamAnalyser = class {
|
|
|
399
423
|
// src/session/agentSession.ts
|
|
400
424
|
init_errors();
|
|
401
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
|
+
|
|
402
442
|
// src/sessionToken.ts
|
|
403
443
|
var import_agent_protocol = require("@fishaudio/agent-protocol");
|
|
404
444
|
init_errors();
|
|
@@ -542,6 +582,8 @@ var TypedEmitter = class {
|
|
|
542
582
|
// src/session/toolDispatcher.ts
|
|
543
583
|
init_errors();
|
|
544
584
|
var DEFAULT_CLIENT_TOOL_TIMEOUT_MS = 15e3;
|
|
585
|
+
var MAX_CLIENT_TOOL_RESULT_BYTES = 6e4;
|
|
586
|
+
var textEncoder = new TextEncoder();
|
|
545
587
|
var ClientToolDispatcher = class {
|
|
546
588
|
constructor(tools, timeoutMs, send, onError) {
|
|
547
589
|
this.timeoutMs = timeoutMs;
|
|
@@ -586,14 +628,56 @@ var ClientToolDispatcher = class {
|
|
|
586
628
|
}
|
|
587
629
|
}
|
|
588
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
|
+
}
|
|
589
638
|
try {
|
|
590
|
-
await this.send(
|
|
639
|
+
await this.send(message);
|
|
591
640
|
} catch (error) {
|
|
592
641
|
this.onError(
|
|
593
642
|
new FishAgentError("tool_failed", "Failed to deliver a client tool result", {
|
|
594
643
|
cause: error
|
|
595
644
|
})
|
|
596
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
|
+
);
|
|
597
681
|
}
|
|
598
682
|
}
|
|
599
683
|
withTimeout(promise, toolName) {
|
|
@@ -676,6 +760,29 @@ request_fn = async function() {
|
|
|
676
760
|
|
|
677
761
|
// src/session/agentSession.ts
|
|
678
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
|
+
}
|
|
679
786
|
var startWith;
|
|
680
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;
|
|
681
788
|
var _AgentSession = class _AgentSession extends TypedEmitter {
|
|
@@ -747,13 +854,13 @@ var _AgentSession = class _AgentSession extends TypedEmitter {
|
|
|
747
854
|
return __privateGet(this, _transcript).map((segment) => ({ ...segment }));
|
|
748
855
|
}
|
|
749
856
|
// ---- text & control channel ----
|
|
750
|
-
/** `audio:
|
|
857
|
+
/** Typed turns get a text-only reply by default (no TTS); pass `audio: true` to have the agent speak this turn. */
|
|
751
858
|
sendUserMessage(text, options) {
|
|
752
859
|
const trimmed = text.trim();
|
|
753
860
|
if (!trimmed) {
|
|
754
861
|
return;
|
|
755
862
|
}
|
|
756
|
-
const message = options?.audio ===
|
|
863
|
+
const message = options?.audio === true ? { type: "user.message", text: trimmed } : { type: "user.message", text: trimmed, audio: false };
|
|
757
864
|
if (__privateGet(this, _agentPresent)) {
|
|
758
865
|
__privateMethod(this, _AgentSession_instances, sendClientEvent_fn).call(this, message);
|
|
759
866
|
} else {
|
|
@@ -896,16 +1003,26 @@ startWith_fn = async function(options, createTransport) {
|
|
|
896
1003
|
if (options.audio?.outputDeviceId !== void 0) {
|
|
897
1004
|
assertOutputSelectionSupported();
|
|
898
1005
|
}
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
if (options.
|
|
902
|
-
|
|
1006
|
+
const callbackSubscriptions = resolveCallbackSubscriptions(options.callbacks);
|
|
1007
|
+
const transport = createTransport();
|
|
1008
|
+
if (options.microphone !== false) {
|
|
1009
|
+
transport.prepareMicrophone?.({ inputDeviceId: options.audio?.inputDeviceId });
|
|
1010
|
+
}
|
|
1011
|
+
let sessionToken;
|
|
1012
|
+
let session;
|
|
1013
|
+
try {
|
|
1014
|
+
sessionToken = await resolveSessionToken(options);
|
|
1015
|
+
session = new _AgentSession(sessionToken);
|
|
1016
|
+
if (options.audio?.outputDeviceId !== void 0) {
|
|
1017
|
+
await __privateGet(session, _output).setSinkId(options.audio.outputDeviceId);
|
|
1018
|
+
}
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
await transport.disconnect().catch(() => void 0);
|
|
1021
|
+
throw error;
|
|
903
1022
|
}
|
|
904
|
-
for (const [
|
|
905
|
-
const event = key.charAt(2).toLowerCase() + key.slice(3);
|
|
1023
|
+
for (const [event, callback] of callbackSubscriptions) {
|
|
906
1024
|
session.on(event, callback);
|
|
907
1025
|
}
|
|
908
|
-
const transport = createTransport(sessionToken);
|
|
909
1026
|
__privateSet(session, _transport, transport);
|
|
910
1027
|
if (options.microphone === false) {
|
|
911
1028
|
__privateSet(session, _micMuted, true);
|
|
@@ -1187,6 +1304,7 @@ init_errors();
|
|
|
1187
1304
|
0 && (module.exports = {
|
|
1188
1305
|
AgentSession,
|
|
1189
1306
|
DEFAULT_SERVER_URL,
|
|
1190
|
-
FishAgentError
|
|
1307
|
+
FishAgentError,
|
|
1308
|
+
MAX_CLIENT_TOOL_RESULT_BYTES
|
|
1191
1309
|
});
|
|
1192
1310
|
//# sourceMappingURL=index.cjs.map
|