@absolutejs/voice 0.0.22-beta.622 → 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.
- package/dist/core/types.d.ts +51 -0
- package/dist/index.js +99 -0
- package/dist/testing/index.js +98 -0
- package/package.json +1 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -803,6 +803,28 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
|
|
|
803
803
|
sttRecoveryLine?: string | ((input: {
|
|
804
804
|
session: TSession;
|
|
805
805
|
}) => string | Promise<string>);
|
|
806
|
+
stuckCallClose?: {
|
|
807
|
+
afterMs: number;
|
|
808
|
+
line?: string | ((input: {
|
|
809
|
+
session: TSession;
|
|
810
|
+
}) => string | Promise<string>);
|
|
811
|
+
reason?: string;
|
|
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
|
+
};
|
|
806
828
|
languageStrategy?: VoiceLanguageStrategy;
|
|
807
829
|
lexicon?: VoiceLexiconEntry[] | VoiceLexiconResolver<TContext>;
|
|
808
830
|
phraseHints?: VoicePhraseHint[] | VoicePhraseHintResolver<TContext>;
|
|
@@ -958,6 +980,35 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
|
|
|
958
980
|
sttRecoveryLine?: string | ((input: {
|
|
959
981
|
session: TSession;
|
|
960
982
|
}) => string | Promise<string>);
|
|
983
|
+
/** Last-resort GRACEFUL terminal close for a wedged call. If no caller-side
|
|
984
|
+
* progress (committed turn / user partial) lands for `afterMs` on a live call
|
|
985
|
+
* — STT permanently deaf, or the caller left — the assistant speaks `line` and
|
|
986
|
+
* the session COMPLETES (disposition "completed") so onComplete still saves and
|
|
987
|
+
* the call ends with a real goodbye instead of dead air + "abandoned". Reset by
|
|
988
|
+
* real progress (committed turn / user partial / (re)connect), NOT by the
|
|
989
|
+
* assistant's own speech, so STT recovery re-prompts can't defer it forever. */
|
|
990
|
+
stuckCallClose?: {
|
|
991
|
+
afterMs: number;
|
|
992
|
+
line?: string | ((input: {
|
|
993
|
+
session: TSession;
|
|
994
|
+
}) => string | Promise<string>);
|
|
995
|
+
reason?: string;
|
|
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
|
+
};
|
|
961
1012
|
stt?: STTAdapter;
|
|
962
1013
|
realtime?: RealtimeAdapter;
|
|
963
1014
|
realtimeInputFormat?: AudioFormat;
|
package/dist/index.js
CHANGED
|
@@ -4186,6 +4186,90 @@ var createVoiceSession = (options) => {
|
|
|
4186
4186
|
clearCallSilenceWatchdog();
|
|
4187
4187
|
callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
|
|
4188
4188
|
};
|
|
4189
|
+
const stuckCloseConfig = options.stuckCallClose;
|
|
4190
|
+
const stuckCloseAfterMs = stuckCloseConfig && stuckCloseConfig.afterMs > 0 ? stuckCloseConfig.afterMs : undefined;
|
|
4191
|
+
let stuckCloseWatchdog = null;
|
|
4192
|
+
let stuckCloseFired = false;
|
|
4193
|
+
const clearStuckCloseWatchdog = () => {
|
|
4194
|
+
if (stuckCloseWatchdog) {
|
|
4195
|
+
clearTimeout(stuckCloseWatchdog);
|
|
4196
|
+
stuckCloseWatchdog = null;
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
const fireStuckClose = () => {
|
|
4200
|
+
stuckCloseWatchdog = null;
|
|
4201
|
+
if (stuckCloseFired) {
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
stuckCloseFired = true;
|
|
4205
|
+
runSerial("stuck-call-close", async () => {
|
|
4206
|
+
const snapshot = await readSession();
|
|
4207
|
+
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
await appendTrace({
|
|
4211
|
+
payload: {
|
|
4212
|
+
action: "stuck-call-close",
|
|
4213
|
+
reason: `no caller progress for ${stuckCloseAfterMs}ms`
|
|
4214
|
+
},
|
|
4215
|
+
session: snapshot,
|
|
4216
|
+
type: "session.error"
|
|
4217
|
+
});
|
|
4218
|
+
if (stuckCloseConfig?.line) {
|
|
4219
|
+
await speakResolvedLine(stuckCloseConfig.line, snapshot);
|
|
4220
|
+
}
|
|
4221
|
+
await completeInternal(undefined, {
|
|
4222
|
+
disposition: "completed",
|
|
4223
|
+
reason: stuckCloseConfig?.reason ?? "stuck-call-close"
|
|
4224
|
+
});
|
|
4225
|
+
});
|
|
4226
|
+
};
|
|
4227
|
+
const kickStuckCloseWatchdog = () => {
|
|
4228
|
+
if (stuckCloseAfterMs === undefined || stuckCloseFired) {
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
clearStuckCloseWatchdog();
|
|
4232
|
+
stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
|
|
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
|
+
};
|
|
4189
4273
|
const recordingConfig = options.recording;
|
|
4190
4274
|
const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
|
|
4191
4275
|
const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
|
|
@@ -4682,6 +4766,8 @@ var createVoiceSession = (options) => {
|
|
|
4682
4766
|
type: "error"
|
|
4683
4767
|
});
|
|
4684
4768
|
clearCallSilenceWatchdog();
|
|
4769
|
+
clearStuckCloseWatchdog();
|
|
4770
|
+
clearIdleRepromptWatchdog();
|
|
4685
4771
|
clearAmdEvaluationTimer();
|
|
4686
4772
|
await closeTTSSession("failed");
|
|
4687
4773
|
await closeAdapter("failed");
|
|
@@ -4791,6 +4877,8 @@ var createVoiceSession = (options) => {
|
|
|
4791
4877
|
type: "complete"
|
|
4792
4878
|
});
|
|
4793
4879
|
clearCallSilenceWatchdog();
|
|
4880
|
+
clearStuckCloseWatchdog();
|
|
4881
|
+
clearIdleRepromptWatchdog();
|
|
4794
4882
|
clearAmdEvaluationTimer();
|
|
4795
4883
|
await closeTTSSession("complete");
|
|
4796
4884
|
await closeAdapter("complete");
|
|
@@ -5268,6 +5356,10 @@ var createVoiceSession = (options) => {
|
|
|
5268
5356
|
};
|
|
5269
5357
|
};
|
|
5270
5358
|
const handlePartial = async (transcript) => {
|
|
5359
|
+
if (transcript.text.trim()) {
|
|
5360
|
+
kickStuckCloseWatchdog();
|
|
5361
|
+
kickIdleRepromptWatchdog(true);
|
|
5362
|
+
}
|
|
5271
5363
|
if (activeTTSTurnId !== undefined) {
|
|
5272
5364
|
const triggeringText = transcript.text.trim();
|
|
5273
5365
|
if (triggeringText) {
|
|
@@ -5742,6 +5834,8 @@ var createVoiceSession = (options) => {
|
|
|
5742
5834
|
};
|
|
5743
5835
|
const completeTurn = async (session, turn) => {
|
|
5744
5836
|
console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
|
|
5837
|
+
kickStuckCloseWatchdog();
|
|
5838
|
+
kickIdleRepromptWatchdog(true);
|
|
5745
5839
|
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
5746
5840
|
if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
|
|
5747
5841
|
await appendTrace({
|
|
@@ -6443,6 +6537,8 @@ var createVoiceSession = (options) => {
|
|
|
6443
6537
|
await ensureAdapter();
|
|
6444
6538
|
warmTTSSession();
|
|
6445
6539
|
kickCallSilenceWatchdog();
|
|
6540
|
+
kickStuckCloseWatchdog();
|
|
6541
|
+
kickIdleRepromptWatchdog(true);
|
|
6446
6542
|
startAmdEvaluationTimer();
|
|
6447
6543
|
if (options.greeting && session.turns.length === 0) {
|
|
6448
6544
|
await speakResolvedLine(options.greeting, session);
|
|
@@ -6588,6 +6684,8 @@ var createVoiceSession = (options) => {
|
|
|
6588
6684
|
});
|
|
6589
6685
|
clearSilenceTimer();
|
|
6590
6686
|
clearCallSilenceWatchdog();
|
|
6687
|
+
clearStuckCloseWatchdog();
|
|
6688
|
+
clearIdleRepromptWatchdog();
|
|
6591
6689
|
clearAmdEvaluationTimer();
|
|
6592
6690
|
if (options.noiseSuppressor?.close) {
|
|
6593
6691
|
try {
|
|
@@ -39705,6 +39803,7 @@ var voice = (config) => {
|
|
|
39705
39803
|
greeting: config.greeting,
|
|
39706
39804
|
resumeGreeting: config.resumeGreeting,
|
|
39707
39805
|
sttRecoveryLine: config.sttRecoveryLine,
|
|
39806
|
+
stuckCallClose: config.stuckCallClose,
|
|
39708
39807
|
handoff: config.handoff,
|
|
39709
39808
|
languageStrategy: config.languageStrategy,
|
|
39710
39809
|
lexicon,
|
package/dist/testing/index.js
CHANGED
|
@@ -6506,6 +6506,90 @@ var createVoiceSession = (options) => {
|
|
|
6506
6506
|
clearCallSilenceWatchdog();
|
|
6507
6507
|
callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
|
|
6508
6508
|
};
|
|
6509
|
+
const stuckCloseConfig = options.stuckCallClose;
|
|
6510
|
+
const stuckCloseAfterMs = stuckCloseConfig && stuckCloseConfig.afterMs > 0 ? stuckCloseConfig.afterMs : undefined;
|
|
6511
|
+
let stuckCloseWatchdog = null;
|
|
6512
|
+
let stuckCloseFired = false;
|
|
6513
|
+
const clearStuckCloseWatchdog = () => {
|
|
6514
|
+
if (stuckCloseWatchdog) {
|
|
6515
|
+
clearTimeout(stuckCloseWatchdog);
|
|
6516
|
+
stuckCloseWatchdog = null;
|
|
6517
|
+
}
|
|
6518
|
+
};
|
|
6519
|
+
const fireStuckClose = () => {
|
|
6520
|
+
stuckCloseWatchdog = null;
|
|
6521
|
+
if (stuckCloseFired) {
|
|
6522
|
+
return;
|
|
6523
|
+
}
|
|
6524
|
+
stuckCloseFired = true;
|
|
6525
|
+
runSerial("stuck-call-close", async () => {
|
|
6526
|
+
const snapshot = await readSession();
|
|
6527
|
+
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
|
|
6528
|
+
return;
|
|
6529
|
+
}
|
|
6530
|
+
await appendTrace({
|
|
6531
|
+
payload: {
|
|
6532
|
+
action: "stuck-call-close",
|
|
6533
|
+
reason: `no caller progress for ${stuckCloseAfterMs}ms`
|
|
6534
|
+
},
|
|
6535
|
+
session: snapshot,
|
|
6536
|
+
type: "session.error"
|
|
6537
|
+
});
|
|
6538
|
+
if (stuckCloseConfig?.line) {
|
|
6539
|
+
await speakResolvedLine(stuckCloseConfig.line, snapshot);
|
|
6540
|
+
}
|
|
6541
|
+
await completeInternal(undefined, {
|
|
6542
|
+
disposition: "completed",
|
|
6543
|
+
reason: stuckCloseConfig?.reason ?? "stuck-call-close"
|
|
6544
|
+
});
|
|
6545
|
+
});
|
|
6546
|
+
};
|
|
6547
|
+
const kickStuckCloseWatchdog = () => {
|
|
6548
|
+
if (stuckCloseAfterMs === undefined || stuckCloseFired) {
|
|
6549
|
+
return;
|
|
6550
|
+
}
|
|
6551
|
+
clearStuckCloseWatchdog();
|
|
6552
|
+
stuckCloseWatchdog = setTimeout(fireStuckClose, stuckCloseAfterMs);
|
|
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
|
+
};
|
|
6509
6593
|
const recordingConfig = options.recording;
|
|
6510
6594
|
const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
|
|
6511
6595
|
const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
|
|
@@ -7002,6 +7086,8 @@ var createVoiceSession = (options) => {
|
|
|
7002
7086
|
type: "error"
|
|
7003
7087
|
});
|
|
7004
7088
|
clearCallSilenceWatchdog();
|
|
7089
|
+
clearStuckCloseWatchdog();
|
|
7090
|
+
clearIdleRepromptWatchdog();
|
|
7005
7091
|
clearAmdEvaluationTimer();
|
|
7006
7092
|
await closeTTSSession("failed");
|
|
7007
7093
|
await closeAdapter("failed");
|
|
@@ -7111,6 +7197,8 @@ var createVoiceSession = (options) => {
|
|
|
7111
7197
|
type: "complete"
|
|
7112
7198
|
});
|
|
7113
7199
|
clearCallSilenceWatchdog();
|
|
7200
|
+
clearStuckCloseWatchdog();
|
|
7201
|
+
clearIdleRepromptWatchdog();
|
|
7114
7202
|
clearAmdEvaluationTimer();
|
|
7115
7203
|
await closeTTSSession("complete");
|
|
7116
7204
|
await closeAdapter("complete");
|
|
@@ -7588,6 +7676,10 @@ var createVoiceSession = (options) => {
|
|
|
7588
7676
|
};
|
|
7589
7677
|
};
|
|
7590
7678
|
const handlePartial = async (transcript) => {
|
|
7679
|
+
if (transcript.text.trim()) {
|
|
7680
|
+
kickStuckCloseWatchdog();
|
|
7681
|
+
kickIdleRepromptWatchdog(true);
|
|
7682
|
+
}
|
|
7591
7683
|
if (activeTTSTurnId !== undefined) {
|
|
7592
7684
|
const triggeringText = transcript.text.trim();
|
|
7593
7685
|
if (triggeringText) {
|
|
@@ -8062,6 +8154,8 @@ var createVoiceSession = (options) => {
|
|
|
8062
8154
|
};
|
|
8063
8155
|
const completeTurn = async (session, turn) => {
|
|
8064
8156
|
console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
|
|
8157
|
+
kickStuckCloseWatchdog();
|
|
8158
|
+
kickIdleRepromptWatchdog(true);
|
|
8065
8159
|
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
8066
8160
|
if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
|
|
8067
8161
|
await appendTrace({
|
|
@@ -8763,6 +8857,8 @@ var createVoiceSession = (options) => {
|
|
|
8763
8857
|
await ensureAdapter();
|
|
8764
8858
|
warmTTSSession();
|
|
8765
8859
|
kickCallSilenceWatchdog();
|
|
8860
|
+
kickStuckCloseWatchdog();
|
|
8861
|
+
kickIdleRepromptWatchdog(true);
|
|
8766
8862
|
startAmdEvaluationTimer();
|
|
8767
8863
|
if (options.greeting && session.turns.length === 0) {
|
|
8768
8864
|
await speakResolvedLine(options.greeting, session);
|
|
@@ -8908,6 +9004,8 @@ var createVoiceSession = (options) => {
|
|
|
8908
9004
|
});
|
|
8909
9005
|
clearSilenceTimer();
|
|
8910
9006
|
clearCallSilenceWatchdog();
|
|
9007
|
+
clearStuckCloseWatchdog();
|
|
9008
|
+
clearIdleRepromptWatchdog();
|
|
8911
9009
|
clearAmdEvaluationTimer();
|
|
8912
9010
|
if (options.noiseSuppressor?.close) {
|
|
8913
9011
|
try {
|