@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 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://github.com/fishaudio/fish-agent-sdk-web/tree/main/packages/react) wraps this SDK in hooks and components.
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://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/quickstart.md) — first call, with or without a backend.
40
- - [Authentication](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/authentication.md) — server-created session tokens vs. public agents.
41
- - [Sessions](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/sessions.md) — `start()` options, the session API, lifecycle and reconnection.
42
- - [Events](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/events.md) — event reference and live transcripts.
43
- - [Client tools](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/client-tools.md) — let the agent call functions in the browser.
44
- - [Customization](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/customization.md) — per-session overrides and dynamic variables.
45
- - [Errors](https://github.com/fishaudio/fish-agent-sdk-web/blob/main/docs/errors.md) — `FishAgentError` codes.
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 (options.microphone !== false) {
183
- await room.localParticipant.setMicrophoneEnabled(true);
201
+ if (micTrack) {
202
+ await room.localParticipant.publishTrack(micTrack);
203
+ this.pendingMicTrack = void 0;
184
204
  this.publishInputStream();
185
205
  }
186
206
  }
@@ -189,6 +209,9 @@ var init_livekit = __esm({
189
209
  void room.disconnect();
190
210
  }
191
211
  async disconnect() {
212
+ const pendingMic = this.pendingMicTrack;
213
+ this.pendingMicTrack = void 0;
214
+ void pendingMic?.then((track) => track.stop()).catch(() => void 0);
192
215
  const room = this.room;
193
216
  this.room = void 0;
194
217
  this.callbacks = void 0;
@@ -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({ type: "client_tool.result", callId, ...body });
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: false` asks the agent to answer this turn in text only (no TTS). */
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 === false ? { type: "user.message", text: trimmed, audio: false } : { type: "user.message", text: trimmed };
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 sessionToken = await resolveSessionToken(options);
900
- const session = new _AgentSession(sessionToken);
901
- if (options.audio?.outputDeviceId !== void 0) {
902
- await __privateGet(session, _output).setSinkId(options.audio.outputDeviceId);
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 [key, callback] of Object.entries(options.callbacks ?? {})) {
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