@absolutejs/voice 0.0.22-beta.623 → 0.0.22-beta.624

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.
@@ -810,6 +810,21 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
810
810
  }) => string | Promise<string>);
811
811
  reason?: string;
812
812
  };
813
+ /** Gentle re-engagement before a silent call is given up on. When the caller
814
+ * makes no progress (no committed turn / user partial) for `afterMs`, the
815
+ * assistant speaks `line` ("still there?") instead of ending, and the close
816
+ * deadline (stuckCallClose) is pushed out — up to `maxReprompts` times (default
817
+ * 1). After the budget is spent the normal close path runs. Caller speech
818
+ * resets the budget. Assistant speech does NOT, so this can only extend a
819
+ * bounded amount. `afterMs` should be < stuckCallClose.afterMs so the nudge
820
+ * fires first. */
821
+ idleReprompt?: {
822
+ afterMs: number;
823
+ line: string | ((input: {
824
+ session: TSession;
825
+ }) => string | Promise<string>);
826
+ maxReprompts?: number;
827
+ };
813
828
  languageStrategy?: VoiceLanguageStrategy;
814
829
  lexicon?: VoiceLexiconEntry[] | VoiceLexiconResolver<TContext>;
815
830
  phraseHints?: VoicePhraseHint[] | VoicePhraseHintResolver<TContext>;
@@ -979,6 +994,21 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
979
994
  }) => string | Promise<string>);
980
995
  reason?: string;
981
996
  };
997
+ /** Gentle re-engagement before a silent call is given up on. When the caller
998
+ * makes no progress (no committed turn / user partial) for `afterMs`, the
999
+ * assistant speaks `line` ("still there?") instead of ending, and the close
1000
+ * deadline (stuckCallClose) is pushed out — up to `maxReprompts` times (default
1001
+ * 1). After the budget is spent the normal close path runs. Caller speech
1002
+ * resets the budget. Assistant speech does NOT, so this can only extend a
1003
+ * bounded amount. `afterMs` should be < stuckCallClose.afterMs so the nudge
1004
+ * fires first. */
1005
+ idleReprompt?: {
1006
+ afterMs: number;
1007
+ line: string | ((input: {
1008
+ session: TSession;
1009
+ }) => string | Promise<string>);
1010
+ maxReprompts?: number;
1011
+ };
982
1012
  stt?: STTAdapter;
983
1013
  realtime?: RealtimeAdapter;
984
1014
  realtimeInputFormat?: AudioFormat;
package/dist/index.js CHANGED
@@ -4231,6 +4231,45 @@ var createVoiceSession = (options) => {
4231
4231
  clearStuckCloseWatchdog();
4232
4232
  stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
4233
4233
  };
4234
+ const idleRepromptConfig = options.idleReprompt;
4235
+ const idleRepromptAfterMs = idleRepromptConfig && idleRepromptConfig.afterMs > 0 ? idleRepromptConfig.afterMs : undefined;
4236
+ const idleRepromptMax = Math.max(1, idleRepromptConfig?.maxReprompts ?? 1);
4237
+ let idleRepromptsUsed = 0;
4238
+ let idleRepromptWatchdog = null;
4239
+ const clearIdleRepromptWatchdog = () => {
4240
+ if (idleRepromptWatchdog) {
4241
+ clearTimeout(idleRepromptWatchdog);
4242
+ idleRepromptWatchdog = null;
4243
+ }
4244
+ };
4245
+ const kickIdleRepromptWatchdog = (resetBudget = false) => {
4246
+ if (idleRepromptAfterMs === undefined) {
4247
+ return;
4248
+ }
4249
+ if (resetBudget) {
4250
+ idleRepromptsUsed = 0;
4251
+ }
4252
+ clearIdleRepromptWatchdog();
4253
+ if (idleRepromptsUsed >= idleRepromptMax) {
4254
+ return;
4255
+ }
4256
+ idleRepromptWatchdog = setTimeout(() => {
4257
+ idleRepromptWatchdog = null;
4258
+ if (idleRepromptConfig === undefined || idleRepromptsUsed >= idleRepromptMax) {
4259
+ return;
4260
+ }
4261
+ runSerial("idle-reprompt", async () => {
4262
+ const snapshot = await readSession();
4263
+ if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
4264
+ return;
4265
+ }
4266
+ idleRepromptsUsed += 1;
4267
+ await speakResolvedLine(idleRepromptConfig.line, snapshot);
4268
+ kickStuckCloseWatchdog();
4269
+ kickIdleRepromptWatchdog();
4270
+ });
4271
+ }, idleRepromptAfterMs);
4272
+ };
4234
4273
  const recordingConfig = options.recording;
4235
4274
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
4236
4275
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -4728,6 +4767,7 @@ var createVoiceSession = (options) => {
4728
4767
  });
4729
4768
  clearCallSilenceWatchdog();
4730
4769
  clearStuckCloseWatchdog();
4770
+ clearIdleRepromptWatchdog();
4731
4771
  clearAmdEvaluationTimer();
4732
4772
  await closeTTSSession("failed");
4733
4773
  await closeAdapter("failed");
@@ -4838,6 +4878,7 @@ var createVoiceSession = (options) => {
4838
4878
  });
4839
4879
  clearCallSilenceWatchdog();
4840
4880
  clearStuckCloseWatchdog();
4881
+ clearIdleRepromptWatchdog();
4841
4882
  clearAmdEvaluationTimer();
4842
4883
  await closeTTSSession("complete");
4843
4884
  await closeAdapter("complete");
@@ -5317,6 +5358,7 @@ var createVoiceSession = (options) => {
5317
5358
  const handlePartial = async (transcript) => {
5318
5359
  if (transcript.text.trim()) {
5319
5360
  kickStuckCloseWatchdog();
5361
+ kickIdleRepromptWatchdog(true);
5320
5362
  }
5321
5363
  if (activeTTSTurnId !== undefined) {
5322
5364
  const triggeringText = transcript.text.trim();
@@ -5793,6 +5835,7 @@ var createVoiceSession = (options) => {
5793
5835
  const completeTurn = async (session, turn) => {
5794
5836
  console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
5795
5837
  kickStuckCloseWatchdog();
5838
+ kickIdleRepromptWatchdog(true);
5796
5839
  const liveOpsControl = await options.liveOps?.getControl(options.id);
5797
5840
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
5798
5841
  await appendTrace({
@@ -6495,6 +6538,7 @@ var createVoiceSession = (options) => {
6495
6538
  warmTTSSession();
6496
6539
  kickCallSilenceWatchdog();
6497
6540
  kickStuckCloseWatchdog();
6541
+ kickIdleRepromptWatchdog(true);
6498
6542
  startAmdEvaluationTimer();
6499
6543
  if (options.greeting && session.turns.length === 0) {
6500
6544
  await speakResolvedLine(options.greeting, session);
@@ -6641,6 +6685,7 @@ var createVoiceSession = (options) => {
6641
6685
  clearSilenceTimer();
6642
6686
  clearCallSilenceWatchdog();
6643
6687
  clearStuckCloseWatchdog();
6688
+ clearIdleRepromptWatchdog();
6644
6689
  clearAmdEvaluationTimer();
6645
6690
  if (options.noiseSuppressor?.close) {
6646
6691
  try {
@@ -6551,6 +6551,45 @@ var createVoiceSession = (options) => {
6551
6551
  clearStuckCloseWatchdog();
6552
6552
  stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
6553
6553
  };
6554
+ const idleRepromptConfig = options.idleReprompt;
6555
+ const idleRepromptAfterMs = idleRepromptConfig && idleRepromptConfig.afterMs > 0 ? idleRepromptConfig.afterMs : undefined;
6556
+ const idleRepromptMax = Math.max(1, idleRepromptConfig?.maxReprompts ?? 1);
6557
+ let idleRepromptsUsed = 0;
6558
+ let idleRepromptWatchdog = null;
6559
+ const clearIdleRepromptWatchdog = () => {
6560
+ if (idleRepromptWatchdog) {
6561
+ clearTimeout(idleRepromptWatchdog);
6562
+ idleRepromptWatchdog = null;
6563
+ }
6564
+ };
6565
+ const kickIdleRepromptWatchdog = (resetBudget = false) => {
6566
+ if (idleRepromptAfterMs === undefined) {
6567
+ return;
6568
+ }
6569
+ if (resetBudget) {
6570
+ idleRepromptsUsed = 0;
6571
+ }
6572
+ clearIdleRepromptWatchdog();
6573
+ if (idleRepromptsUsed >= idleRepromptMax) {
6574
+ return;
6575
+ }
6576
+ idleRepromptWatchdog = setTimeout(() => {
6577
+ idleRepromptWatchdog = null;
6578
+ if (idleRepromptConfig === undefined || idleRepromptsUsed >= idleRepromptMax) {
6579
+ return;
6580
+ }
6581
+ runSerial("idle-reprompt", async () => {
6582
+ const snapshot = await readSession();
6583
+ if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
6584
+ return;
6585
+ }
6586
+ idleRepromptsUsed += 1;
6587
+ await speakResolvedLine(idleRepromptConfig.line, snapshot);
6588
+ kickStuckCloseWatchdog();
6589
+ kickIdleRepromptWatchdog();
6590
+ });
6591
+ }, idleRepromptAfterMs);
6592
+ };
6554
6593
  const recordingConfig = options.recording;
6555
6594
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
6556
6595
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -7048,6 +7087,7 @@ var createVoiceSession = (options) => {
7048
7087
  });
7049
7088
  clearCallSilenceWatchdog();
7050
7089
  clearStuckCloseWatchdog();
7090
+ clearIdleRepromptWatchdog();
7051
7091
  clearAmdEvaluationTimer();
7052
7092
  await closeTTSSession("failed");
7053
7093
  await closeAdapter("failed");
@@ -7158,6 +7198,7 @@ var createVoiceSession = (options) => {
7158
7198
  });
7159
7199
  clearCallSilenceWatchdog();
7160
7200
  clearStuckCloseWatchdog();
7201
+ clearIdleRepromptWatchdog();
7161
7202
  clearAmdEvaluationTimer();
7162
7203
  await closeTTSSession("complete");
7163
7204
  await closeAdapter("complete");
@@ -7637,6 +7678,7 @@ var createVoiceSession = (options) => {
7637
7678
  const handlePartial = async (transcript) => {
7638
7679
  if (transcript.text.trim()) {
7639
7680
  kickStuckCloseWatchdog();
7681
+ kickIdleRepromptWatchdog(true);
7640
7682
  }
7641
7683
  if (activeTTSTurnId !== undefined) {
7642
7684
  const triggeringText = transcript.text.trim();
@@ -8113,6 +8155,7 @@ var createVoiceSession = (options) => {
8113
8155
  const completeTurn = async (session, turn) => {
8114
8156
  console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
8115
8157
  kickStuckCloseWatchdog();
8158
+ kickIdleRepromptWatchdog(true);
8116
8159
  const liveOpsControl = await options.liveOps?.getControl(options.id);
8117
8160
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
8118
8161
  await appendTrace({
@@ -8815,6 +8858,7 @@ var createVoiceSession = (options) => {
8815
8858
  warmTTSSession();
8816
8859
  kickCallSilenceWatchdog();
8817
8860
  kickStuckCloseWatchdog();
8861
+ kickIdleRepromptWatchdog(true);
8818
8862
  startAmdEvaluationTimer();
8819
8863
  if (options.greeting && session.turns.length === 0) {
8820
8864
  await speakResolvedLine(options.greeting, session);
@@ -8961,6 +9005,7 @@ var createVoiceSession = (options) => {
8961
9005
  clearSilenceTimer();
8962
9006
  clearCallSilenceWatchdog();
8963
9007
  clearStuckCloseWatchdog();
9008
+ clearIdleRepromptWatchdog();
8964
9009
  clearAmdEvaluationTimer();
8965
9010
  if (options.noiseSuppressor?.close) {
8966
9011
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.623",
3
+ "version": "0.0.22-beta.624",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",