@absolutejs/voice 0.0.22-beta.553 → 0.0.22-beta.554

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.
@@ -916,6 +916,13 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
916
916
  fillerPhrases?: ReadonlyArray<string>;
917
917
  /** Milliseconds after turn-commit before the filler fires. Default 250ms — short enough to feel instant, long enough to skip if the LLM is very fast. */
918
918
  fillerDelayMs?: number;
919
+ /**
920
+ * Default spoken ack if the model returns ONLY tool calls (no text) and the
921
+ * turn isn't ending. Without this, the caller hears total silence after
922
+ * their turn and assumes the line dropped. Default is "Sorry, one moment."
923
+ * Set to "" to opt out entirely.
924
+ */
925
+ defaultSilentTurnAck?: string;
919
926
  assistantMode?: import("./assistantMode").VoiceAssistantMode;
920
927
  modalities?: ReadonlyArray<"audio" | "text">;
921
928
  prosody?: VoiceTTSProsody;
package/dist/index.js CHANGED
@@ -5464,6 +5464,53 @@ var createVoiceSession = (options) => {
5464
5464
  });
5465
5465
  }
5466
5466
  }
5467
+ const audioWasSent = Boolean(streamResult?.streamed) || Boolean(output?.assistantText?.trim());
5468
+ const turnIsEnding = Boolean(output?.complete) || Boolean(output?.transfer) || Boolean(output?.escalate) || Boolean(output?.voicemail) || Boolean(output?.noAnswer);
5469
+ if (!audioWasSent && !turnIsEnding) {
5470
+ const fallback = typeof options.defaultSilentTurnAck === "string" ? options.defaultSilentTurnAck : "Sorry, one moment.";
5471
+ if (fallback.trim() && options.tts) {
5472
+ try {
5473
+ const activeTTSSession = await ensureTTSSession();
5474
+ if (activeTTSSession) {
5475
+ fillerToken += 1;
5476
+ if (fillerTimer) {
5477
+ clearTimeout(fillerTimer);
5478
+ fillerTimer = null;
5479
+ }
5480
+ if (fillerActive) {
5481
+ await cancelActiveTTS("filler-superseded").catch(() => {});
5482
+ fillerActive = false;
5483
+ }
5484
+ activeTTSTurnId = turn.id;
5485
+ await activeTTSSession.send(fallback);
5486
+ await appendTrace({
5487
+ payload: {
5488
+ assistantMode: resolveVoiceAssistantMode(options),
5489
+ fallback: true,
5490
+ realtimeConfigured: Boolean(options.realtime),
5491
+ reason: "model-returned-no-text",
5492
+ text: fallback,
5493
+ ttsConfigured: Boolean(options.tts)
5494
+ },
5495
+ session,
5496
+ turnId: turn.id,
5497
+ type: "turn.assistant"
5498
+ });
5499
+ if (options.costAccountant) {
5500
+ options.costAccountant.recordTTS({
5501
+ characters: fallback.length
5502
+ });
5503
+ }
5504
+ }
5505
+ } catch (error) {
5506
+ logger.warn("voice default-silent-turn-ack fallback send failed", {
5507
+ error: toError(error).message,
5508
+ sessionId: options.id,
5509
+ turnId: turn.id
5510
+ });
5511
+ }
5512
+ }
5513
+ }
5467
5514
  if (output?.result !== undefined) {
5468
5515
  await writeSession((currentSession) => {
5469
5516
  setTurnResult(currentSession, turn.id, {
@@ -24608,6 +24655,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
24608
24655
  ...options.semanticTurnDetector ? { semanticTurnDetector: options.semanticTurnDetector } : {},
24609
24656
  ...options.fillerPhrases ? { fillerPhrases: options.fillerPhrases } : {},
24610
24657
  ...options.fillerDelayMs !== undefined ? { fillerDelayMs: options.fillerDelayMs } : {},
24658
+ ...options.defaultSilentTurnAck !== undefined ? { defaultSilentTurnAck: options.defaultSilentTurnAck } : {},
24611
24659
  trace: options.trace,
24612
24660
  tts: options.tts,
24613
24661
  turnDetection
@@ -132,6 +132,13 @@ export type TwilioMediaStreamBridgeOptions<TContext = unknown, TSession extends
132
132
  fillerPhrases?: ReadonlyArray<string>;
133
133
  /** Milliseconds after turn-commit before the filler fires. Default 250ms. */
134
134
  fillerDelayMs?: number;
135
+ /**
136
+ * Default spoken ack if the model returns ONLY tool calls (no text) and
137
+ * the turn isn't ending. Without this, the caller hears silence and
138
+ * assumes the line dropped. Default "Sorry, one moment." — set to ""
139
+ * to opt out. See CreateVoiceSessionOptions for full semantics.
140
+ */
141
+ defaultSilentTurnAck?: string;
135
142
  };
136
143
  export type TwilioMediaStreamBridge = {
137
144
  close: (reason?: string) => Promise<void>;
@@ -7281,6 +7281,53 @@ var createVoiceSession = (options) => {
7281
7281
  });
7282
7282
  }
7283
7283
  }
7284
+ const audioWasSent = Boolean(streamResult?.streamed) || Boolean(output?.assistantText?.trim());
7285
+ const turnIsEnding = Boolean(output?.complete) || Boolean(output?.transfer) || Boolean(output?.escalate) || Boolean(output?.voicemail) || Boolean(output?.noAnswer);
7286
+ if (!audioWasSent && !turnIsEnding) {
7287
+ const fallback = typeof options.defaultSilentTurnAck === "string" ? options.defaultSilentTurnAck : "Sorry, one moment.";
7288
+ if (fallback.trim() && options.tts) {
7289
+ try {
7290
+ const activeTTSSession = await ensureTTSSession();
7291
+ if (activeTTSSession) {
7292
+ fillerToken += 1;
7293
+ if (fillerTimer) {
7294
+ clearTimeout(fillerTimer);
7295
+ fillerTimer = null;
7296
+ }
7297
+ if (fillerActive) {
7298
+ await cancelActiveTTS("filler-superseded").catch(() => {});
7299
+ fillerActive = false;
7300
+ }
7301
+ activeTTSTurnId = turn.id;
7302
+ await activeTTSSession.send(fallback);
7303
+ await appendTrace({
7304
+ payload: {
7305
+ assistantMode: resolveVoiceAssistantMode(options),
7306
+ fallback: true,
7307
+ realtimeConfigured: Boolean(options.realtime),
7308
+ reason: "model-returned-no-text",
7309
+ text: fallback,
7310
+ ttsConfigured: Boolean(options.tts)
7311
+ },
7312
+ session,
7313
+ turnId: turn.id,
7314
+ type: "turn.assistant"
7315
+ });
7316
+ if (options.costAccountant) {
7317
+ options.costAccountant.recordTTS({
7318
+ characters: fallback.length
7319
+ });
7320
+ }
7321
+ }
7322
+ } catch (error) {
7323
+ logger.warn("voice default-silent-turn-ack fallback send failed", {
7324
+ error: toError(error).message,
7325
+ sessionId: options.id,
7326
+ turnId: turn.id
7327
+ });
7328
+ }
7329
+ }
7330
+ }
7284
7331
  if (output?.result !== undefined) {
7285
7332
  await writeSession((currentSession) => {
7286
7333
  setTurnResult(currentSession, turn.id, {
@@ -13144,6 +13191,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
13144
13191
  ...options.semanticTurnDetector ? { semanticTurnDetector: options.semanticTurnDetector } : {},
13145
13192
  ...options.fillerPhrases ? { fillerPhrases: options.fillerPhrases } : {},
13146
13193
  ...options.fillerDelayMs !== undefined ? { fillerDelayMs: options.fillerDelayMs } : {},
13194
+ ...options.defaultSilentTurnAck !== undefined ? { defaultSilentTurnAck: options.defaultSilentTurnAck } : {},
13147
13195
  trace: options.trace,
13148
13196
  tts: options.tts,
13149
13197
  turnDetection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.553",
3
+ "version": "0.0.22-beta.554",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",