@absolutejs/voice 0.0.22-beta.476 → 0.0.22-beta.478
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/agentTools.d.ts +1 -0
- package/dist/fileStore.d.ts +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +367 -37
- package/dist/outcomeRecipes.d.ts +1 -1
- package/dist/recordingStore.d.ts +21 -0
- package/dist/s3Store.d.ts +15 -0
- package/dist/testing/index.js +231 -35
- package/dist/trace.d.ts +1 -1
- package/dist/types.d.ts +11 -1
- package/package.json +1 -1
package/dist/agentTools.d.ts
CHANGED
package/dist/fileStore.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type StoredVoiceTraceEvent, type VoiceTraceSinkDeliveryRecord, type Voi
|
|
|
7
7
|
import type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMap, VoiceExternalObjectMapStore, VoiceIntegrationEvent, VoiceIntegrationEventStore, VoiceOpsTask, VoiceOpsTaskStore } from "./ops";
|
|
8
8
|
import type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewStore } from "./testing/review";
|
|
9
9
|
import type { VoiceSessionRecord, VoiceSessionStore } from "./types";
|
|
10
|
+
import type { VoiceRecordingStore } from "./recordingStore";
|
|
10
11
|
export type VoiceFileStoreOptions = {
|
|
11
12
|
directory: string;
|
|
12
13
|
pretty?: boolean;
|
|
@@ -50,3 +51,4 @@ export declare const createStoredVoiceIntegrationEvent: <TEvent extends Omit<Voi
|
|
|
50
51
|
export declare const createStoredVoiceExternalObjectMap: <TMapping extends Omit<VoiceExternalObjectMap, "id" | "createdAt" | "updatedAt"> = Omit<VoiceExternalObjectMap, "id" | "createdAt" | "updatedAt">>(mapping: TMapping & {
|
|
51
52
|
at?: number;
|
|
52
53
|
}) => VoiceExternalObjectMap;
|
|
54
|
+
export declare const createVoiceFileRecordingStore: (options: VoiceFileStoreOptions) => VoiceRecordingStore;
|
package/dist/index.d.ts
CHANGED
|
@@ -87,7 +87,9 @@ export { createVoiceTurnQualityHTMLHandler, createVoiceTurnQualityJSONHandler, c
|
|
|
87
87
|
export { assertVoiceOutcomeContractEvidence, createVoiceOutcomeContractHTMLHandler, createVoiceOutcomeContractJSONHandler, createVoiceOutcomeContractRoutes, evaluateVoiceOutcomeContractEvidence, renderVoiceOutcomeContractHTML, runVoiceOutcomeContractSuite, } from "./outcomeContract";
|
|
88
88
|
export { applyVoiceTelephonyOutcome, assertVoiceTelephonyWebhookNormalizationEvidence, createMemoryVoiceTelephonyWebhookIdempotencyStore, createVoiceTelephonyOutcomePolicy, createVoiceTelephonyWebhookHandler, createVoiceTelephonyWebhookRoutes, evaluateVoiceTelephonyWebhookNormalizationEvidence, parseVoiceTelephonyWebhookEvent, resolveVoiceTelephonyOutcome, signVoiceTwilioWebhook, verifyVoiceTwilioWebhookSignature, voiceTelephonyOutcomeToRouteResult, } from "./telephonyOutcome";
|
|
89
89
|
export { assertVoicePhoneCallControlEvidence, assertVoicePhoneAssistantEvidence, createVoicePhoneAgent, evaluateVoicePhoneCallControlEvidence, evaluateVoicePhoneAssistantEvidence, } from "./phoneAgent";
|
|
90
|
-
export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileIncidentBundleStore, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileAuditEventStore, createVoiceFileAuditSinkDeliveryStore, createVoiceFileCampaignStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore, } from "./fileStore";
|
|
90
|
+
export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileIncidentBundleStore, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileAuditEventStore, createVoiceFileAuditSinkDeliveryStore, createVoiceFileCampaignStore, createVoiceFileIntegrationEventStore, createVoiceFileRecordingStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore, } from "./fileStore";
|
|
91
|
+
export { computePcmDurationMs, createVoiceMemoryRecordingStore, encodePcmAsWav, } from "./recordingStore";
|
|
92
|
+
export type { StoredVoiceRecordingArtifact, VoiceRecordingArtifact, VoiceRecordingChannel, VoiceRecordingStore, } from "./recordingStore";
|
|
91
93
|
export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace, } from "./assistantMemory";
|
|
92
94
|
export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderOrchestrationProfile, resolveVoiceProviderRoutingPolicyPreset, createVoiceProviderRouter, } from "./modelAdapters";
|
|
93
95
|
export { createOpenAIVoiceTTS } from "./openaiTTS";
|
|
@@ -118,7 +120,7 @@ export { buildVoiceTraceDeliveryReport, createVoiceTraceDeliveryHTMLHandler, cre
|
|
|
118
120
|
export { createVoiceTraceTimelineRoutes, renderVoiceTraceTimelineHTML, renderVoiceTraceTimelineSessionHTML, summarizeVoiceTraceTimeline, } from "./traceTimeline";
|
|
119
121
|
export { createVoiceSQLiteAuditEventStore, createVoiceSQLiteAuditSinkDeliveryStore, createVoiceSQLiteCampaignStore, createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTelephonyWebhookIdempotencyStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore, } from "./sqliteStore";
|
|
120
122
|
export { createVoicePostgresAuditEventStore, createVoicePostgresAuditSinkDeliveryStore, createVoicePostgresCampaignStore, createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore, } from "./postgresStore";
|
|
121
|
-
export { createVoiceS3ReviewStore } from "./s3Store";
|
|
123
|
+
export { createVoiceS3RecordingStore, createVoiceS3ReviewStore } from "./s3Store";
|
|
122
124
|
export { createVoiceMemoryStore } from "./memoryStore";
|
|
123
125
|
export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks, } from "./opsSinks";
|
|
124
126
|
export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature, } from "./opsWebhook";
|
package/dist/index.js
CHANGED
|
@@ -3372,6 +3372,74 @@ var buildTurnText = (transcripts, partialText, options = {}) => {
|
|
|
3372
3372
|
// src/types.ts
|
|
3373
3373
|
var ttsAdapterSessionCanCancel = (session) => typeof session.cancel === "function";
|
|
3374
3374
|
|
|
3375
|
+
// src/recordingStore.ts
|
|
3376
|
+
var writeUint32LE = (view, offset, value) => {
|
|
3377
|
+
view.setUint32(offset, value, true);
|
|
3378
|
+
};
|
|
3379
|
+
var writeUint16LE = (view, offset, value) => {
|
|
3380
|
+
view.setUint16(offset, value, true);
|
|
3381
|
+
};
|
|
3382
|
+
var writeAscii = (view, offset, value) => {
|
|
3383
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
3384
|
+
view.setUint8(offset + index, value.charCodeAt(index));
|
|
3385
|
+
}
|
|
3386
|
+
};
|
|
3387
|
+
var encodePcmAsWav = (pcm, format) => {
|
|
3388
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
3389
|
+
throw new Error(`encodePcmAsWav only supports raw pcm_s16le input (got container=${format.container}, encoding=${format.encoding})`);
|
|
3390
|
+
}
|
|
3391
|
+
const channels = format.channels;
|
|
3392
|
+
const sampleRate = format.sampleRateHz;
|
|
3393
|
+
const bitsPerSample = 16;
|
|
3394
|
+
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
3395
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
3396
|
+
const dataSize = pcm.byteLength;
|
|
3397
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
3398
|
+
const view = new DataView(buffer);
|
|
3399
|
+
writeAscii(view, 0, "RIFF");
|
|
3400
|
+
writeUint32LE(view, 4, 36 + dataSize);
|
|
3401
|
+
writeAscii(view, 8, "WAVE");
|
|
3402
|
+
writeAscii(view, 12, "fmt ");
|
|
3403
|
+
writeUint32LE(view, 16, 16);
|
|
3404
|
+
writeUint16LE(view, 20, 1);
|
|
3405
|
+
writeUint16LE(view, 22, channels);
|
|
3406
|
+
writeUint32LE(view, 24, sampleRate);
|
|
3407
|
+
writeUint32LE(view, 28, byteRate);
|
|
3408
|
+
writeUint16LE(view, 32, blockAlign);
|
|
3409
|
+
writeUint16LE(view, 34, bitsPerSample);
|
|
3410
|
+
writeAscii(view, 36, "data");
|
|
3411
|
+
writeUint32LE(view, 40, dataSize);
|
|
3412
|
+
const output = new Uint8Array(buffer);
|
|
3413
|
+
output.set(pcm, 44);
|
|
3414
|
+
return output;
|
|
3415
|
+
};
|
|
3416
|
+
var computePcmDurationMs = (pcmByteLength, format) => {
|
|
3417
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
3418
|
+
return 0;
|
|
3419
|
+
}
|
|
3420
|
+
const bytesPerSecond = format.sampleRateHz * format.channels * 2;
|
|
3421
|
+
if (bytesPerSecond === 0) {
|
|
3422
|
+
return 0;
|
|
3423
|
+
}
|
|
3424
|
+
return Math.round(pcmByteLength / bytesPerSecond * 1000);
|
|
3425
|
+
};
|
|
3426
|
+
var createVoiceMemoryRecordingStore = () => {
|
|
3427
|
+
const records = new Map;
|
|
3428
|
+
const key = (sessionId, channel) => `${sessionId}::${channel}`;
|
|
3429
|
+
return {
|
|
3430
|
+
get: async (sessionId, channel) => records.get(key(sessionId, channel)),
|
|
3431
|
+
list: async (sessionId) => Array.from(records.values()).filter((record) => record.sessionId === sessionId),
|
|
3432
|
+
put: async (artifact) => {
|
|
3433
|
+
const stored = {
|
|
3434
|
+
...artifact,
|
|
3435
|
+
recordingUrl: `memory://recording/${artifact.sessionId}/${artifact.channel}.wav`
|
|
3436
|
+
};
|
|
3437
|
+
records.set(key(artifact.sessionId, artifact.channel), stored);
|
|
3438
|
+
return stored;
|
|
3439
|
+
}
|
|
3440
|
+
};
|
|
3441
|
+
};
|
|
3442
|
+
|
|
3375
3443
|
// src/session.ts
|
|
3376
3444
|
var DEFAULT_RECONNECT_TIMEOUT = 30000;
|
|
3377
3445
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
|
|
@@ -3614,6 +3682,63 @@ var createVoiceSession = (options) => {
|
|
|
3614
3682
|
const currentTurnAudio = [];
|
|
3615
3683
|
let fallbackAttemptsForCurrentTurn = 0;
|
|
3616
3684
|
let fallbackReplayAudioMsForCurrentTurn = 0;
|
|
3685
|
+
const callSilenceTimeoutMs = options.callSilenceTimeoutMs && options.callSilenceTimeoutMs > 0 ? options.callSilenceTimeoutMs : undefined;
|
|
3686
|
+
let callSilenceWatchdog = null;
|
|
3687
|
+
let callSilenceFired = false;
|
|
3688
|
+
const clearCallSilenceWatchdog = () => {
|
|
3689
|
+
if (callSilenceWatchdog) {
|
|
3690
|
+
clearTimeout(callSilenceWatchdog);
|
|
3691
|
+
callSilenceWatchdog = null;
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
const fireCallSilenceTimeout = () => {
|
|
3695
|
+
callSilenceWatchdog = null;
|
|
3696
|
+
if (callSilenceFired) {
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
callSilenceFired = true;
|
|
3700
|
+
api.close("silence-timeout");
|
|
3701
|
+
};
|
|
3702
|
+
const kickCallSilenceWatchdog = () => {
|
|
3703
|
+
if (callSilenceTimeoutMs === undefined || callSilenceFired) {
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
clearCallSilenceWatchdog();
|
|
3707
|
+
callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
|
|
3708
|
+
};
|
|
3709
|
+
const recordingConfig = options.recording;
|
|
3710
|
+
const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
|
|
3711
|
+
const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
|
|
3712
|
+
const recordingBuffers = {
|
|
3713
|
+
assistant: [],
|
|
3714
|
+
user: []
|
|
3715
|
+
};
|
|
3716
|
+
const recordingByteTotals = {
|
|
3717
|
+
assistant: 0,
|
|
3718
|
+
user: 0
|
|
3719
|
+
};
|
|
3720
|
+
const recordingFormats = {};
|
|
3721
|
+
let recordingPersisted = false;
|
|
3722
|
+
const captureRecordingChunk = (channel, bytes, format) => {
|
|
3723
|
+
if (!recordingConfig || recordingPersisted) {
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
if (!recordingChannels.has(channel)) {
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
const currentTotal = recordingByteTotals[channel];
|
|
3733
|
+
if (currentTotal >= recordingMaxBytes) {
|
|
3734
|
+
return;
|
|
3735
|
+
}
|
|
3736
|
+
const remaining = recordingMaxBytes - currentTotal;
|
|
3737
|
+
const slice = bytes.byteLength <= remaining ? bytes : bytes.subarray(0, remaining);
|
|
3738
|
+
recordingBuffers[channel].push(new Uint8Array(slice));
|
|
3739
|
+
recordingByteTotals[channel] += slice.byteLength;
|
|
3740
|
+
recordingFormats[channel] = format;
|
|
3741
|
+
};
|
|
3617
3742
|
const pruneTurnAudio = () => {
|
|
3618
3743
|
const replayWindowMs = sttFallback?.replayWindowMs ?? DEFAULT_FALLBACK_REPLAY_MS;
|
|
3619
3744
|
const cutoffAt = Date.now() - replayWindowMs;
|
|
@@ -3792,6 +3917,59 @@ var createVoiceSession = (options) => {
|
|
|
3792
3917
|
});
|
|
3793
3918
|
}
|
|
3794
3919
|
};
|
|
3920
|
+
const persistRecordings = async () => {
|
|
3921
|
+
if (!recordingConfig || recordingPersisted) {
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
recordingPersisted = true;
|
|
3925
|
+
const channels = ["assistant", "user"];
|
|
3926
|
+
for (const channel of channels) {
|
|
3927
|
+
if (!recordingChannels.has(channel)) {
|
|
3928
|
+
continue;
|
|
3929
|
+
}
|
|
3930
|
+
const chunks = recordingBuffers[channel];
|
|
3931
|
+
const format = recordingFormats[channel];
|
|
3932
|
+
if (chunks.length === 0 || !format) {
|
|
3933
|
+
continue;
|
|
3934
|
+
}
|
|
3935
|
+
const totalBytes = recordingByteTotals[channel];
|
|
3936
|
+
const merged = new Uint8Array(totalBytes);
|
|
3937
|
+
let offset = 0;
|
|
3938
|
+
for (const chunk of chunks) {
|
|
3939
|
+
merged.set(chunk, offset);
|
|
3940
|
+
offset += chunk.byteLength;
|
|
3941
|
+
}
|
|
3942
|
+
try {
|
|
3943
|
+
const stored = await recordingConfig.store.put({
|
|
3944
|
+
audioBytes: merged,
|
|
3945
|
+
capturedAt: Date.now(),
|
|
3946
|
+
channel,
|
|
3947
|
+
durationMs: computePcmDurationMs(totalBytes, format),
|
|
3948
|
+
format,
|
|
3949
|
+
sessionId: options.id
|
|
3950
|
+
});
|
|
3951
|
+
await appendTrace({
|
|
3952
|
+
payload: {
|
|
3953
|
+
channel,
|
|
3954
|
+
durationMs: stored.durationMs,
|
|
3955
|
+
recordingUrl: stored.recordingUrl,
|
|
3956
|
+
sessionId: options.id,
|
|
3957
|
+
sizeBytes: merged.byteLength
|
|
3958
|
+
},
|
|
3959
|
+
type: "recording.ready"
|
|
3960
|
+
});
|
|
3961
|
+
} catch (error) {
|
|
3962
|
+
logger.warn("voice recording persist failed", {
|
|
3963
|
+
channel,
|
|
3964
|
+
error: toError(error).message,
|
|
3965
|
+
sessionId: options.id
|
|
3966
|
+
});
|
|
3967
|
+
} finally {
|
|
3968
|
+
recordingBuffers[channel] = [];
|
|
3969
|
+
recordingByteTotals[channel] = 0;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
3795
3973
|
const cancelActiveTTS = async (reason) => {
|
|
3796
3974
|
const activeSession = ttsSession;
|
|
3797
3975
|
const cancelledTurnId = activeTTSTurnId;
|
|
@@ -3815,6 +3993,8 @@ var createVoiceSession = (options) => {
|
|
|
3815
3993
|
};
|
|
3816
3994
|
const sendAssistantAudio = async (chunk, input) => {
|
|
3817
3995
|
const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
3996
|
+
captureRecordingChunk("assistant", normalizedChunk, input.format);
|
|
3997
|
+
kickCallSilenceWatchdog();
|
|
3818
3998
|
await send({
|
|
3819
3999
|
chunkBase64: encodeBase64(normalizedChunk),
|
|
3820
4000
|
format: input.format,
|
|
@@ -3911,6 +4091,7 @@ var createVoiceSession = (options) => {
|
|
|
3911
4091
|
});
|
|
3912
4092
|
await closeTTSSession("failed");
|
|
3913
4093
|
await closeAdapter("failed");
|
|
4094
|
+
await persistRecordings();
|
|
3914
4095
|
speechDetected = false;
|
|
3915
4096
|
rewindFallbackTurnAudio();
|
|
3916
4097
|
await options.route.onError?.({
|
|
@@ -3979,6 +4160,7 @@ var createVoiceSession = (options) => {
|
|
|
3979
4160
|
});
|
|
3980
4161
|
await closeTTSSession("complete");
|
|
3981
4162
|
await closeAdapter("complete");
|
|
4163
|
+
await persistRecordings();
|
|
3982
4164
|
speechDetected = false;
|
|
3983
4165
|
rewindFallbackTurnAudio();
|
|
3984
4166
|
if (disposition === "transferred" && input.target) {
|
|
@@ -4034,19 +4216,21 @@ var createVoiceSession = (options) => {
|
|
|
4034
4216
|
});
|
|
4035
4217
|
};
|
|
4036
4218
|
const transferInternal = async (input) => {
|
|
4219
|
+
const transferMetadata = input.transferMode === undefined ? input.metadata : { ...input.metadata ?? {}, transferMode: input.transferMode };
|
|
4037
4220
|
const session = await writeSession((currentSession) => {
|
|
4038
4221
|
pushCallLifecycleEvent(currentSession, {
|
|
4039
|
-
metadata:
|
|
4222
|
+
metadata: transferMetadata,
|
|
4040
4223
|
reason: input.reason,
|
|
4041
4224
|
target: input.target,
|
|
4042
4225
|
type: "transfer"
|
|
4043
4226
|
});
|
|
4044
4227
|
});
|
|
4045
4228
|
await appendTrace({
|
|
4046
|
-
metadata:
|
|
4229
|
+
metadata: transferMetadata,
|
|
4047
4230
|
payload: {
|
|
4048
4231
|
reason: input.reason,
|
|
4049
4232
|
target: input.target,
|
|
4233
|
+
transferMode: input.transferMode,
|
|
4050
4234
|
type: "transfer"
|
|
4051
4235
|
},
|
|
4052
4236
|
session,
|
|
@@ -5077,6 +5261,7 @@ var createVoiceSession = (options) => {
|
|
|
5077
5261
|
resumePendingTurnCommit(session);
|
|
5078
5262
|
await ensureAdapter();
|
|
5079
5263
|
warmTTSSession();
|
|
5264
|
+
kickCallSilenceWatchdog();
|
|
5080
5265
|
};
|
|
5081
5266
|
const disconnectInternal = async (event) => {
|
|
5082
5267
|
clearSilenceTimer();
|
|
@@ -5120,12 +5305,17 @@ var createVoiceSession = (options) => {
|
|
|
5120
5305
|
if (shouldStoreAudio) {
|
|
5121
5306
|
pushTurnAudio(conditionedAudio);
|
|
5122
5307
|
}
|
|
5308
|
+
if (recordingConfig?.userInputFormat) {
|
|
5309
|
+
const userBytes = conditionedAudio instanceof Uint8Array ? conditionedAudio : conditionedAudio instanceof ArrayBuffer ? new Uint8Array(conditionedAudio) : new Uint8Array(conditionedAudio.buffer, conditionedAudio.byteOffset, conditionedAudio.byteLength);
|
|
5310
|
+
captureRecordingChunk("user", userBytes, recordingConfig.userInputFormat);
|
|
5311
|
+
}
|
|
5123
5312
|
if (audioLevel >= turnDetection.speechThreshold) {
|
|
5124
5313
|
if (!speechDetected && activeTTSTurnId !== undefined) {
|
|
5125
5314
|
cancelActiveTTS("barge-in");
|
|
5126
5315
|
}
|
|
5127
5316
|
speechDetected = true;
|
|
5128
5317
|
clearSilenceTimer();
|
|
5318
|
+
kickCallSilenceWatchdog();
|
|
5129
5319
|
} else if (speechDetected) {
|
|
5130
5320
|
const currentSession = await readSession();
|
|
5131
5321
|
const hasTurnText = Boolean(buildTurnText(currentSession.currentTurn.transcripts, currentSession.currentTurn.partialText, {
|
|
@@ -5138,43 +5328,49 @@ var createVoiceSession = (options) => {
|
|
|
5138
5328
|
}
|
|
5139
5329
|
await adapter.send(conditionedAudio);
|
|
5140
5330
|
};
|
|
5331
|
+
const closeInternal = async (reason, disposition = "closed") => {
|
|
5332
|
+
const session = await writeSession((currentSession) => {
|
|
5333
|
+
if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
|
|
5334
|
+
currentSession.lastActivityAt = Date.now();
|
|
5335
|
+
currentSession.status = "completed";
|
|
5336
|
+
pushCallLifecycleEvent(currentSession, {
|
|
5337
|
+
disposition,
|
|
5338
|
+
reason,
|
|
5339
|
+
type: "end"
|
|
5340
|
+
});
|
|
5341
|
+
}
|
|
5342
|
+
});
|
|
5343
|
+
clearSilenceTimer();
|
|
5344
|
+
clearCallSilenceWatchdog();
|
|
5345
|
+
await closeTTSSession(reason);
|
|
5346
|
+
await closeAdapter(reason);
|
|
5347
|
+
await persistRecordings();
|
|
5348
|
+
await Promise.resolve(socket.close(1000, reason));
|
|
5349
|
+
if (session.call?.endedAt && session.call.disposition === disposition) {
|
|
5350
|
+
await appendTrace({
|
|
5351
|
+
payload: {
|
|
5352
|
+
disposition,
|
|
5353
|
+
reason,
|
|
5354
|
+
type: "end"
|
|
5355
|
+
},
|
|
5356
|
+
session,
|
|
5357
|
+
type: "call.lifecycle"
|
|
5358
|
+
});
|
|
5359
|
+
await options.route.onCallEnd?.({
|
|
5360
|
+
api,
|
|
5361
|
+
context: options.context,
|
|
5362
|
+
disposition,
|
|
5363
|
+
reason,
|
|
5364
|
+
session
|
|
5365
|
+
});
|
|
5366
|
+
}
|
|
5367
|
+
};
|
|
5141
5368
|
const api = {
|
|
5142
5369
|
id: options.id,
|
|
5143
5370
|
close: async (reason) => {
|
|
5144
5371
|
await runSerial("api.close", async () => {
|
|
5145
|
-
const
|
|
5146
|
-
|
|
5147
|
-
currentSession.lastActivityAt = Date.now();
|
|
5148
|
-
currentSession.status = "completed";
|
|
5149
|
-
pushCallLifecycleEvent(currentSession, {
|
|
5150
|
-
disposition: "closed",
|
|
5151
|
-
reason,
|
|
5152
|
-
type: "end"
|
|
5153
|
-
});
|
|
5154
|
-
}
|
|
5155
|
-
});
|
|
5156
|
-
clearSilenceTimer();
|
|
5157
|
-
await closeTTSSession(reason);
|
|
5158
|
-
await closeAdapter(reason);
|
|
5159
|
-
await Promise.resolve(socket.close(1000, reason));
|
|
5160
|
-
if (session.call?.endedAt && session.call.disposition === "closed") {
|
|
5161
|
-
await appendTrace({
|
|
5162
|
-
payload: {
|
|
5163
|
-
disposition: "closed",
|
|
5164
|
-
reason,
|
|
5165
|
-
type: "end"
|
|
5166
|
-
},
|
|
5167
|
-
session,
|
|
5168
|
-
type: "call.lifecycle"
|
|
5169
|
-
});
|
|
5170
|
-
await options.route.onCallEnd?.({
|
|
5171
|
-
api,
|
|
5172
|
-
context: options.context,
|
|
5173
|
-
disposition: "closed",
|
|
5174
|
-
reason,
|
|
5175
|
-
session
|
|
5176
|
-
});
|
|
5177
|
-
}
|
|
5372
|
+
const disposition = reason === "silence-timeout" ? "silence-timeout" : "closed";
|
|
5373
|
+
await closeInternal(reason, disposition);
|
|
5178
5374
|
});
|
|
5179
5375
|
},
|
|
5180
5376
|
commitTurn: async (reason = "manual") => runSerial("api.commitTurn", async () => {
|
|
@@ -8920,6 +9116,18 @@ var RECIPE_DEFAULTS = {
|
|
|
8920
9116
|
defaultQueue: "transfer-verification",
|
|
8921
9117
|
description: "Creates transfer verification work for transferred calls and escalation work when the handoff fails.",
|
|
8922
9118
|
escalationQueue: "transfer-escalations"
|
|
9119
|
+
},
|
|
9120
|
+
"cold-transfer": {
|
|
9121
|
+
completedAction: "Verify the SIP REFER landed and the caller reached the destination without re-introduction.",
|
|
9122
|
+
completedDescription: "The call was cold-transferred (SIP REFER) \u2014 confirm the destination picked up.",
|
|
9123
|
+
completedKind: "transfer-check",
|
|
9124
|
+
completedTitle: "Verify cold transfer",
|
|
9125
|
+
defaultCompletedCreatesTask: false,
|
|
9126
|
+
defaultDueInMs: 5 * 60000,
|
|
9127
|
+
defaultPriority: "normal",
|
|
9128
|
+
defaultQueue: "transfer-verification",
|
|
9129
|
+
description: "Creates verification work for cold-transferred (REFER) calls and escalation work when the handoff fails.",
|
|
9130
|
+
escalationQueue: "transfer-escalations"
|
|
8923
9131
|
}
|
|
8924
9132
|
};
|
|
8925
9133
|
var buildRecipeTask = (input) => {
|
|
@@ -9049,7 +9257,7 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
|
|
|
9049
9257
|
dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 20 * 60000),
|
|
9050
9258
|
name: `${name}-transfer-check`,
|
|
9051
9259
|
priority: options.priority ?? defaults.defaultPriority,
|
|
9052
|
-
queue: name === "warm-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
|
|
9260
|
+
queue: name === "warm-transfer" || name === "cold-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
|
|
9053
9261
|
},
|
|
9054
9262
|
voicemail: {
|
|
9055
9263
|
assignee: options.assignee,
|
|
@@ -34837,7 +35045,8 @@ ${destinationDocs}`,
|
|
|
34837
35045
|
metadata: destination.metadata,
|
|
34838
35046
|
reason: args?.reason,
|
|
34839
35047
|
result,
|
|
34840
|
-
target: destination.target
|
|
35048
|
+
target: destination.target,
|
|
35049
|
+
transferMode: destination.transferMode
|
|
34841
35050
|
});
|
|
34842
35051
|
return {
|
|
34843
35052
|
destinationId: destination.id,
|
|
@@ -37101,6 +37310,66 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
|
|
|
37101
37310
|
sourceId: mapping.sourceId,
|
|
37102
37311
|
sourceType: mapping.sourceType
|
|
37103
37312
|
});
|
|
37313
|
+
var recordingFileName = (sessionId, channel) => `${encodeURIComponent(sessionId)}_${channel}.wav`;
|
|
37314
|
+
var recordingMetadataFileName = (sessionId, channel) => `${encodeURIComponent(sessionId)}_${channel}.json`;
|
|
37315
|
+
var createVoiceFileRecordingStore = (options) => {
|
|
37316
|
+
const ensureDir = async () => {
|
|
37317
|
+
await mkdir4(options.directory, { recursive: true });
|
|
37318
|
+
};
|
|
37319
|
+
const put = async (artifact) => {
|
|
37320
|
+
await ensureDir();
|
|
37321
|
+
const wavPath = join3(options.directory, recordingFileName(artifact.sessionId, artifact.channel));
|
|
37322
|
+
const metadataPath = join3(options.directory, recordingMetadataFileName(artifact.sessionId, artifact.channel));
|
|
37323
|
+
const wav = encodePcmAsWav(artifact.audioBytes, artifact.format);
|
|
37324
|
+
await writeFile(wavPath, wav);
|
|
37325
|
+
const recordingUrl = `file://${wavPath}`;
|
|
37326
|
+
const metadata = {
|
|
37327
|
+
capturedAt: artifact.capturedAt,
|
|
37328
|
+
channel: artifact.channel,
|
|
37329
|
+
durationMs: artifact.durationMs,
|
|
37330
|
+
format: artifact.format,
|
|
37331
|
+
recordingUrl,
|
|
37332
|
+
sessionId: artifact.sessionId
|
|
37333
|
+
};
|
|
37334
|
+
await writeFile(metadataPath, options.pretty ? JSON.stringify(metadata, null, 2) : JSON.stringify(metadata));
|
|
37335
|
+
return {
|
|
37336
|
+
...artifact,
|
|
37337
|
+
recordingUrl
|
|
37338
|
+
};
|
|
37339
|
+
};
|
|
37340
|
+
const readMetadata = async (sessionId, channel) => {
|
|
37341
|
+
const metadataPath = join3(options.directory, recordingMetadataFileName(sessionId, channel));
|
|
37342
|
+
const wavPath = join3(options.directory, recordingFileName(sessionId, channel));
|
|
37343
|
+
try {
|
|
37344
|
+
const [metaText, wavBytes] = await Promise.all([
|
|
37345
|
+
readFile2(metadataPath, "utf8"),
|
|
37346
|
+
readFile2(wavPath)
|
|
37347
|
+
]);
|
|
37348
|
+
const meta = JSON.parse(metaText);
|
|
37349
|
+
return {
|
|
37350
|
+
audioBytes: new Uint8Array(wavBytes.buffer, wavBytes.byteOffset, wavBytes.byteLength),
|
|
37351
|
+
capturedAt: meta.capturedAt,
|
|
37352
|
+
channel: meta.channel,
|
|
37353
|
+
durationMs: meta.durationMs,
|
|
37354
|
+
format: meta.format,
|
|
37355
|
+
recordingUrl: meta.recordingUrl,
|
|
37356
|
+
sessionId: meta.sessionId
|
|
37357
|
+
};
|
|
37358
|
+
} catch (error) {
|
|
37359
|
+
if (error.code === "ENOENT") {
|
|
37360
|
+
return;
|
|
37361
|
+
}
|
|
37362
|
+
throw error;
|
|
37363
|
+
}
|
|
37364
|
+
};
|
|
37365
|
+
const get = (sessionId, channel) => readMetadata(sessionId, channel);
|
|
37366
|
+
const list = async (sessionId) => {
|
|
37367
|
+
const channels = ["assistant", "user"];
|
|
37368
|
+
const records = await Promise.all(channels.map((channel) => readMetadata(sessionId, channel)));
|
|
37369
|
+
return records.filter((record) => record !== undefined);
|
|
37370
|
+
};
|
|
37371
|
+
return { get, list, put };
|
|
37372
|
+
};
|
|
37104
37373
|
// src/modelAdapters.ts
|
|
37105
37374
|
var isVoiceProviderRoutingPolicyPreset = (value) => value === "balanced" || value === "cost-cap" || value === "cost-first" || value === "latency-first" || value === "quality-first";
|
|
37106
37375
|
var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
|
|
@@ -41033,6 +41302,62 @@ var createVoiceS3ReviewStore = (options) => {
|
|
|
41033
41302
|
set
|
|
41034
41303
|
};
|
|
41035
41304
|
};
|
|
41305
|
+
var normalizeRecordingKeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/recordings";
|
|
41306
|
+
var recordingWavKey = (prefix, sessionId, channel) => `${prefix}/${encodeURIComponent(sessionId)}_${channel}.wav`;
|
|
41307
|
+
var recordingMetadataKey = (prefix, sessionId, channel) => `${prefix}/${encodeURIComponent(sessionId)}_${channel}.json`;
|
|
41308
|
+
var createVoiceS3RecordingStore = (options) => {
|
|
41309
|
+
const client = options.client ?? new Bun.S3Client(options);
|
|
41310
|
+
const keyPrefix = normalizeRecordingKeyPrefix(options.keyPrefix);
|
|
41311
|
+
const publicUrlBase = options.publicUrlBase?.replace(/\/+$/, "");
|
|
41312
|
+
const getFile = (key) => client.file(key, options);
|
|
41313
|
+
const resolveUrl = (key) => publicUrlBase ? `${publicUrlBase}/${key}` : `s3://${key}`;
|
|
41314
|
+
const put = async (artifact) => {
|
|
41315
|
+
const wavKey = recordingWavKey(keyPrefix, artifact.sessionId, artifact.channel);
|
|
41316
|
+
const metadataKey = recordingMetadataKey(keyPrefix, artifact.sessionId, artifact.channel);
|
|
41317
|
+
const wav = encodePcmAsWav(artifact.audioBytes, artifact.format);
|
|
41318
|
+
await getFile(wavKey).write(wav);
|
|
41319
|
+
const recordingUrl = resolveUrl(wavKey);
|
|
41320
|
+
const metadata = {
|
|
41321
|
+
capturedAt: artifact.capturedAt,
|
|
41322
|
+
channel: artifact.channel,
|
|
41323
|
+
durationMs: artifact.durationMs,
|
|
41324
|
+
format: artifact.format,
|
|
41325
|
+
recordingUrl,
|
|
41326
|
+
sessionId: artifact.sessionId
|
|
41327
|
+
};
|
|
41328
|
+
await getFile(metadataKey).write(JSON.stringify(metadata));
|
|
41329
|
+
return {
|
|
41330
|
+
...artifact,
|
|
41331
|
+
recordingUrl
|
|
41332
|
+
};
|
|
41333
|
+
};
|
|
41334
|
+
const readMetadata = async (sessionId, channel) => {
|
|
41335
|
+
const metadataKey = recordingMetadataKey(keyPrefix, sessionId, channel);
|
|
41336
|
+
const wavKey = recordingWavKey(keyPrefix, sessionId, channel);
|
|
41337
|
+
const metadataFile = getFile(metadataKey);
|
|
41338
|
+
if (!await metadataFile.exists()) {
|
|
41339
|
+
return;
|
|
41340
|
+
}
|
|
41341
|
+
const meta = JSON.parse(await metadataFile.text());
|
|
41342
|
+
const wavBytes = await getFile(wavKey).bytes();
|
|
41343
|
+
return {
|
|
41344
|
+
audioBytes: wavBytes,
|
|
41345
|
+
capturedAt: meta.capturedAt,
|
|
41346
|
+
channel: meta.channel,
|
|
41347
|
+
durationMs: meta.durationMs,
|
|
41348
|
+
format: meta.format,
|
|
41349
|
+
recordingUrl: meta.recordingUrl,
|
|
41350
|
+
sessionId: meta.sessionId
|
|
41351
|
+
};
|
|
41352
|
+
};
|
|
41353
|
+
const get = (sessionId, channel) => readMetadata(sessionId, channel);
|
|
41354
|
+
const list = async (sessionId) => {
|
|
41355
|
+
const channels = ["assistant", "user"];
|
|
41356
|
+
const records = await Promise.all(channels.map((channel) => readMetadata(sessionId, channel)));
|
|
41357
|
+
return records.filter((record) => record !== undefined);
|
|
41358
|
+
};
|
|
41359
|
+
return { get, list, put };
|
|
41360
|
+
};
|
|
41036
41361
|
// src/memoryStore.ts
|
|
41037
41362
|
var createVoiceMemoryStore = () => {
|
|
41038
41363
|
const sessions = new Map;
|
|
@@ -45361,6 +45686,7 @@ export {
|
|
|
45361
45686
|
evaluateVoiceBrowserCallProfileEvidence,
|
|
45362
45687
|
evaluateVoiceAgentSquadContractEvidence,
|
|
45363
45688
|
encodeTwilioMulawBase64,
|
|
45689
|
+
encodePcmAsWav,
|
|
45364
45690
|
deliverVoiceTraceEventsToSinks,
|
|
45365
45691
|
deliverVoiceObservabilityExport,
|
|
45366
45692
|
deliverVoiceMonitorIssueNotifications,
|
|
@@ -45464,6 +45790,7 @@ export {
|
|
|
45464
45790
|
createVoiceSQLiteAuditSinkDeliveryStore,
|
|
45465
45791
|
createVoiceSQLiteAuditEventStore,
|
|
45466
45792
|
createVoiceS3ReviewStore,
|
|
45793
|
+
createVoiceS3RecordingStore,
|
|
45467
45794
|
createVoiceS3DeliverySink,
|
|
45468
45795
|
createVoiceRoutingDecisionSummary,
|
|
45469
45796
|
createVoiceReviewSavedEvent,
|
|
@@ -45574,6 +45901,7 @@ export {
|
|
|
45574
45901
|
createVoiceMemoryTraceSinkDeliveryStore,
|
|
45575
45902
|
createVoiceMemoryTraceEventStore,
|
|
45576
45903
|
createVoiceMemoryStore,
|
|
45904
|
+
createVoiceMemoryRecordingStore,
|
|
45577
45905
|
createVoiceMemoryObservabilityExportDeliveryReceiptStore,
|
|
45578
45906
|
createVoiceMemoryMonitorNotifierDeliveryReceiptStore,
|
|
45579
45907
|
createVoiceMemoryMonitorIssueStore,
|
|
@@ -45620,6 +45948,7 @@ export {
|
|
|
45620
45948
|
createVoiceFileScenarioFixtureStore,
|
|
45621
45949
|
createVoiceFileRuntimeStorage,
|
|
45622
45950
|
createVoiceFileReviewStore,
|
|
45951
|
+
createVoiceFileRecordingStore,
|
|
45623
45952
|
createVoiceFileObservabilityExportDeliveryReceiptStore,
|
|
45624
45953
|
createVoiceFileIntegrationEventStore,
|
|
45625
45954
|
createVoiceFileIncidentBundleStore,
|
|
@@ -45714,6 +46043,7 @@ export {
|
|
|
45714
46043
|
createAnthropicVoiceAssistantModel,
|
|
45715
46044
|
createAIVoiceModel,
|
|
45716
46045
|
conditionAudioChunk,
|
|
46046
|
+
computePcmDurationMs,
|
|
45717
46047
|
completeVoiceOpsTask,
|
|
45718
46048
|
compareVoiceEvalBaseline,
|
|
45719
46049
|
claimVoiceOpsTask,
|
package/dist/outcomeRecipes.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VoiceOpsTaskPriority } from "./ops";
|
|
2
2
|
import type { VoiceRuntimeOpsConfig, VoiceSessionRecord } from "./types";
|
|
3
|
-
export type VoiceOutcomeRecipeName = "appointment-booking" | "lead-qualification" | "support-triage" | "voicemail-callback" | "warm-transfer";
|
|
3
|
+
export type VoiceOutcomeRecipeName = "appointment-booking" | "cold-transfer" | "lead-qualification" | "support-triage" | "voicemail-callback" | "warm-transfer";
|
|
4
4
|
export type VoiceOutcomeRecipeOptions = {
|
|
5
5
|
assignee?: string;
|
|
6
6
|
completedCreatesTask?: boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AudioFormat } from "./types";
|
|
2
|
+
export type VoiceRecordingChannel = "assistant" | "user";
|
|
3
|
+
export type VoiceRecordingArtifact = {
|
|
4
|
+
audioBytes: Uint8Array;
|
|
5
|
+
capturedAt: number;
|
|
6
|
+
channel: VoiceRecordingChannel;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
format: AudioFormat;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
};
|
|
11
|
+
export type StoredVoiceRecordingArtifact = VoiceRecordingArtifact & {
|
|
12
|
+
recordingUrl?: string;
|
|
13
|
+
};
|
|
14
|
+
export type VoiceRecordingStore = {
|
|
15
|
+
get: (sessionId: string, channel: VoiceRecordingChannel) => Promise<StoredVoiceRecordingArtifact | undefined>;
|
|
16
|
+
list: (sessionId: string) => Promise<StoredVoiceRecordingArtifact[]>;
|
|
17
|
+
put: (artifact: VoiceRecordingArtifact) => Promise<StoredVoiceRecordingArtifact>;
|
|
18
|
+
};
|
|
19
|
+
export declare const encodePcmAsWav: (pcm: Uint8Array, format: AudioFormat) => Uint8Array;
|
|
20
|
+
export declare const computePcmDurationMs: (pcmByteLength: number, format: AudioFormat) => number;
|
|
21
|
+
export declare const createVoiceMemoryRecordingStore: () => VoiceRecordingStore;
|
package/dist/s3Store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { S3Client, S3Options } from "bun";
|
|
2
2
|
import type { StoredVoiceCallReviewArtifact, VoiceCallReviewStore } from "./testing/review";
|
|
3
|
+
import type { VoiceRecordingStore } from "./recordingStore";
|
|
3
4
|
export type VoiceS3ReviewStoreFile = {
|
|
4
5
|
delete: () => Promise<void>;
|
|
5
6
|
exists: () => Promise<boolean>;
|
|
@@ -12,3 +13,17 @@ export type VoiceS3ReviewStoreOptions = S3Options & {
|
|
|
12
13
|
keyPrefix?: string;
|
|
13
14
|
};
|
|
14
15
|
export declare const createVoiceS3ReviewStore: <TArtifact extends StoredVoiceCallReviewArtifact = StoredVoiceCallReviewArtifact>(options: VoiceS3ReviewStoreOptions) => VoiceCallReviewStore<TArtifact>;
|
|
16
|
+
export type VoiceS3RecordingStoreFile = {
|
|
17
|
+
delete: () => Promise<void>;
|
|
18
|
+
exists: () => Promise<boolean>;
|
|
19
|
+
text: () => Promise<string>;
|
|
20
|
+
bytes: () => Promise<Uint8Array>;
|
|
21
|
+
write: (data: string | Uint8Array) => Promise<number>;
|
|
22
|
+
};
|
|
23
|
+
export type VoiceS3RecordingStoreClient = Pick<S3Client, "file" | "list">;
|
|
24
|
+
export type VoiceS3RecordingStoreOptions = S3Options & {
|
|
25
|
+
client?: VoiceS3RecordingStoreClient;
|
|
26
|
+
keyPrefix?: string;
|
|
27
|
+
publicUrlBase?: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const createVoiceS3RecordingStore: (options: VoiceS3RecordingStoreOptions) => VoiceRecordingStore;
|
package/dist/testing/index.js
CHANGED
|
@@ -5340,6 +5340,74 @@ var resolveLogger = (logger) => ({
|
|
|
5340
5340
|
// src/types.ts
|
|
5341
5341
|
var ttsAdapterSessionCanCancel = (session) => typeof session.cancel === "function";
|
|
5342
5342
|
|
|
5343
|
+
// src/recordingStore.ts
|
|
5344
|
+
var writeUint32LE = (view, offset, value) => {
|
|
5345
|
+
view.setUint32(offset, value, true);
|
|
5346
|
+
};
|
|
5347
|
+
var writeUint16LE = (view, offset, value) => {
|
|
5348
|
+
view.setUint16(offset, value, true);
|
|
5349
|
+
};
|
|
5350
|
+
var writeAscii = (view, offset, value) => {
|
|
5351
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
5352
|
+
view.setUint8(offset + index, value.charCodeAt(index));
|
|
5353
|
+
}
|
|
5354
|
+
};
|
|
5355
|
+
var encodePcmAsWav = (pcm, format) => {
|
|
5356
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5357
|
+
throw new Error(`encodePcmAsWav only supports raw pcm_s16le input (got container=${format.container}, encoding=${format.encoding})`);
|
|
5358
|
+
}
|
|
5359
|
+
const channels = format.channels;
|
|
5360
|
+
const sampleRate = format.sampleRateHz;
|
|
5361
|
+
const bitsPerSample = 16;
|
|
5362
|
+
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
5363
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
5364
|
+
const dataSize = pcm.byteLength;
|
|
5365
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
5366
|
+
const view = new DataView(buffer);
|
|
5367
|
+
writeAscii(view, 0, "RIFF");
|
|
5368
|
+
writeUint32LE(view, 4, 36 + dataSize);
|
|
5369
|
+
writeAscii(view, 8, "WAVE");
|
|
5370
|
+
writeAscii(view, 12, "fmt ");
|
|
5371
|
+
writeUint32LE(view, 16, 16);
|
|
5372
|
+
writeUint16LE(view, 20, 1);
|
|
5373
|
+
writeUint16LE(view, 22, channels);
|
|
5374
|
+
writeUint32LE(view, 24, sampleRate);
|
|
5375
|
+
writeUint32LE(view, 28, byteRate);
|
|
5376
|
+
writeUint16LE(view, 32, blockAlign);
|
|
5377
|
+
writeUint16LE(view, 34, bitsPerSample);
|
|
5378
|
+
writeAscii(view, 36, "data");
|
|
5379
|
+
writeUint32LE(view, 40, dataSize);
|
|
5380
|
+
const output = new Uint8Array(buffer);
|
|
5381
|
+
output.set(pcm, 44);
|
|
5382
|
+
return output;
|
|
5383
|
+
};
|
|
5384
|
+
var computePcmDurationMs = (pcmByteLength, format) => {
|
|
5385
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5386
|
+
return 0;
|
|
5387
|
+
}
|
|
5388
|
+
const bytesPerSecond = format.sampleRateHz * format.channels * 2;
|
|
5389
|
+
if (bytesPerSecond === 0) {
|
|
5390
|
+
return 0;
|
|
5391
|
+
}
|
|
5392
|
+
return Math.round(pcmByteLength / bytesPerSecond * 1000);
|
|
5393
|
+
};
|
|
5394
|
+
var createVoiceMemoryRecordingStore = () => {
|
|
5395
|
+
const records = new Map;
|
|
5396
|
+
const key = (sessionId, channel) => `${sessionId}::${channel}`;
|
|
5397
|
+
return {
|
|
5398
|
+
get: async (sessionId, channel) => records.get(key(sessionId, channel)),
|
|
5399
|
+
list: async (sessionId) => Array.from(records.values()).filter((record) => record.sessionId === sessionId),
|
|
5400
|
+
put: async (artifact) => {
|
|
5401
|
+
const stored = {
|
|
5402
|
+
...artifact,
|
|
5403
|
+
recordingUrl: `memory://recording/${artifact.sessionId}/${artifact.channel}.wav`
|
|
5404
|
+
};
|
|
5405
|
+
records.set(key(artifact.sessionId, artifact.channel), stored);
|
|
5406
|
+
return stored;
|
|
5407
|
+
}
|
|
5408
|
+
};
|
|
5409
|
+
};
|
|
5410
|
+
|
|
5343
5411
|
// src/session.ts
|
|
5344
5412
|
var DEFAULT_RECONNECT_TIMEOUT = 30000;
|
|
5345
5413
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS2 = 10;
|
|
@@ -5582,6 +5650,63 @@ var createVoiceSession = (options) => {
|
|
|
5582
5650
|
const currentTurnAudio = [];
|
|
5583
5651
|
let fallbackAttemptsForCurrentTurn = 0;
|
|
5584
5652
|
let fallbackReplayAudioMsForCurrentTurn = 0;
|
|
5653
|
+
const callSilenceTimeoutMs = options.callSilenceTimeoutMs && options.callSilenceTimeoutMs > 0 ? options.callSilenceTimeoutMs : undefined;
|
|
5654
|
+
let callSilenceWatchdog = null;
|
|
5655
|
+
let callSilenceFired = false;
|
|
5656
|
+
const clearCallSilenceWatchdog = () => {
|
|
5657
|
+
if (callSilenceWatchdog) {
|
|
5658
|
+
clearTimeout(callSilenceWatchdog);
|
|
5659
|
+
callSilenceWatchdog = null;
|
|
5660
|
+
}
|
|
5661
|
+
};
|
|
5662
|
+
const fireCallSilenceTimeout = () => {
|
|
5663
|
+
callSilenceWatchdog = null;
|
|
5664
|
+
if (callSilenceFired) {
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
callSilenceFired = true;
|
|
5668
|
+
api.close("silence-timeout");
|
|
5669
|
+
};
|
|
5670
|
+
const kickCallSilenceWatchdog = () => {
|
|
5671
|
+
if (callSilenceTimeoutMs === undefined || callSilenceFired) {
|
|
5672
|
+
return;
|
|
5673
|
+
}
|
|
5674
|
+
clearCallSilenceWatchdog();
|
|
5675
|
+
callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
|
|
5676
|
+
};
|
|
5677
|
+
const recordingConfig = options.recording;
|
|
5678
|
+
const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
|
|
5679
|
+
const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
|
|
5680
|
+
const recordingBuffers = {
|
|
5681
|
+
assistant: [],
|
|
5682
|
+
user: []
|
|
5683
|
+
};
|
|
5684
|
+
const recordingByteTotals = {
|
|
5685
|
+
assistant: 0,
|
|
5686
|
+
user: 0
|
|
5687
|
+
};
|
|
5688
|
+
const recordingFormats = {};
|
|
5689
|
+
let recordingPersisted = false;
|
|
5690
|
+
const captureRecordingChunk = (channel, bytes, format) => {
|
|
5691
|
+
if (!recordingConfig || recordingPersisted) {
|
|
5692
|
+
return;
|
|
5693
|
+
}
|
|
5694
|
+
if (!recordingChannels.has(channel)) {
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5697
|
+
if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
|
|
5698
|
+
return;
|
|
5699
|
+
}
|
|
5700
|
+
const currentTotal = recordingByteTotals[channel];
|
|
5701
|
+
if (currentTotal >= recordingMaxBytes) {
|
|
5702
|
+
return;
|
|
5703
|
+
}
|
|
5704
|
+
const remaining = recordingMaxBytes - currentTotal;
|
|
5705
|
+
const slice = bytes.byteLength <= remaining ? bytes : bytes.subarray(0, remaining);
|
|
5706
|
+
recordingBuffers[channel].push(new Uint8Array(slice));
|
|
5707
|
+
recordingByteTotals[channel] += slice.byteLength;
|
|
5708
|
+
recordingFormats[channel] = format;
|
|
5709
|
+
};
|
|
5585
5710
|
const pruneTurnAudio = () => {
|
|
5586
5711
|
const replayWindowMs = sttFallback?.replayWindowMs ?? DEFAULT_FALLBACK_REPLAY_MS;
|
|
5587
5712
|
const cutoffAt = Date.now() - replayWindowMs;
|
|
@@ -5760,6 +5885,59 @@ var createVoiceSession = (options) => {
|
|
|
5760
5885
|
});
|
|
5761
5886
|
}
|
|
5762
5887
|
};
|
|
5888
|
+
const persistRecordings = async () => {
|
|
5889
|
+
if (!recordingConfig || recordingPersisted) {
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5892
|
+
recordingPersisted = true;
|
|
5893
|
+
const channels = ["assistant", "user"];
|
|
5894
|
+
for (const channel of channels) {
|
|
5895
|
+
if (!recordingChannels.has(channel)) {
|
|
5896
|
+
continue;
|
|
5897
|
+
}
|
|
5898
|
+
const chunks = recordingBuffers[channel];
|
|
5899
|
+
const format = recordingFormats[channel];
|
|
5900
|
+
if (chunks.length === 0 || !format) {
|
|
5901
|
+
continue;
|
|
5902
|
+
}
|
|
5903
|
+
const totalBytes = recordingByteTotals[channel];
|
|
5904
|
+
const merged = new Uint8Array(totalBytes);
|
|
5905
|
+
let offset = 0;
|
|
5906
|
+
for (const chunk of chunks) {
|
|
5907
|
+
merged.set(chunk, offset);
|
|
5908
|
+
offset += chunk.byteLength;
|
|
5909
|
+
}
|
|
5910
|
+
try {
|
|
5911
|
+
const stored = await recordingConfig.store.put({
|
|
5912
|
+
audioBytes: merged,
|
|
5913
|
+
capturedAt: Date.now(),
|
|
5914
|
+
channel,
|
|
5915
|
+
durationMs: computePcmDurationMs(totalBytes, format),
|
|
5916
|
+
format,
|
|
5917
|
+
sessionId: options.id
|
|
5918
|
+
});
|
|
5919
|
+
await appendTrace({
|
|
5920
|
+
payload: {
|
|
5921
|
+
channel,
|
|
5922
|
+
durationMs: stored.durationMs,
|
|
5923
|
+
recordingUrl: stored.recordingUrl,
|
|
5924
|
+
sessionId: options.id,
|
|
5925
|
+
sizeBytes: merged.byteLength
|
|
5926
|
+
},
|
|
5927
|
+
type: "recording.ready"
|
|
5928
|
+
});
|
|
5929
|
+
} catch (error) {
|
|
5930
|
+
logger.warn("voice recording persist failed", {
|
|
5931
|
+
channel,
|
|
5932
|
+
error: toError(error).message,
|
|
5933
|
+
sessionId: options.id
|
|
5934
|
+
});
|
|
5935
|
+
} finally {
|
|
5936
|
+
recordingBuffers[channel] = [];
|
|
5937
|
+
recordingByteTotals[channel] = 0;
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
};
|
|
5763
5941
|
const cancelActiveTTS = async (reason) => {
|
|
5764
5942
|
const activeSession = ttsSession;
|
|
5765
5943
|
const cancelledTurnId = activeTTSTurnId;
|
|
@@ -5783,6 +5961,8 @@ var createVoiceSession = (options) => {
|
|
|
5783
5961
|
};
|
|
5784
5962
|
const sendAssistantAudio = async (chunk, input) => {
|
|
5785
5963
|
const normalizedChunk = chunk instanceof Uint8Array ? new Uint8Array(chunk) : chunk instanceof ArrayBuffer ? new Uint8Array(chunk.slice(0)) : new Uint8Array(chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength));
|
|
5964
|
+
captureRecordingChunk("assistant", normalizedChunk, input.format);
|
|
5965
|
+
kickCallSilenceWatchdog();
|
|
5786
5966
|
await send({
|
|
5787
5967
|
chunkBase64: encodeBase64(normalizedChunk),
|
|
5788
5968
|
format: input.format,
|
|
@@ -5879,6 +6059,7 @@ var createVoiceSession = (options) => {
|
|
|
5879
6059
|
});
|
|
5880
6060
|
await closeTTSSession("failed");
|
|
5881
6061
|
await closeAdapter("failed");
|
|
6062
|
+
await persistRecordings();
|
|
5882
6063
|
speechDetected = false;
|
|
5883
6064
|
rewindFallbackTurnAudio();
|
|
5884
6065
|
await options.route.onError?.({
|
|
@@ -5947,6 +6128,7 @@ var createVoiceSession = (options) => {
|
|
|
5947
6128
|
});
|
|
5948
6129
|
await closeTTSSession("complete");
|
|
5949
6130
|
await closeAdapter("complete");
|
|
6131
|
+
await persistRecordings();
|
|
5950
6132
|
speechDetected = false;
|
|
5951
6133
|
rewindFallbackTurnAudio();
|
|
5952
6134
|
if (disposition === "transferred" && input.target) {
|
|
@@ -6002,19 +6184,21 @@ var createVoiceSession = (options) => {
|
|
|
6002
6184
|
});
|
|
6003
6185
|
};
|
|
6004
6186
|
const transferInternal = async (input) => {
|
|
6187
|
+
const transferMetadata = input.transferMode === undefined ? input.metadata : { ...input.metadata ?? {}, transferMode: input.transferMode };
|
|
6005
6188
|
const session = await writeSession((currentSession) => {
|
|
6006
6189
|
pushCallLifecycleEvent(currentSession, {
|
|
6007
|
-
metadata:
|
|
6190
|
+
metadata: transferMetadata,
|
|
6008
6191
|
reason: input.reason,
|
|
6009
6192
|
target: input.target,
|
|
6010
6193
|
type: "transfer"
|
|
6011
6194
|
});
|
|
6012
6195
|
});
|
|
6013
6196
|
await appendTrace({
|
|
6014
|
-
metadata:
|
|
6197
|
+
metadata: transferMetadata,
|
|
6015
6198
|
payload: {
|
|
6016
6199
|
reason: input.reason,
|
|
6017
6200
|
target: input.target,
|
|
6201
|
+
transferMode: input.transferMode,
|
|
6018
6202
|
type: "transfer"
|
|
6019
6203
|
},
|
|
6020
6204
|
session,
|
|
@@ -7045,6 +7229,7 @@ var createVoiceSession = (options) => {
|
|
|
7045
7229
|
resumePendingTurnCommit(session);
|
|
7046
7230
|
await ensureAdapter();
|
|
7047
7231
|
warmTTSSession();
|
|
7232
|
+
kickCallSilenceWatchdog();
|
|
7048
7233
|
};
|
|
7049
7234
|
const disconnectInternal = async (event) => {
|
|
7050
7235
|
clearSilenceTimer();
|
|
@@ -7088,12 +7273,17 @@ var createVoiceSession = (options) => {
|
|
|
7088
7273
|
if (shouldStoreAudio) {
|
|
7089
7274
|
pushTurnAudio(conditionedAudio);
|
|
7090
7275
|
}
|
|
7276
|
+
if (recordingConfig?.userInputFormat) {
|
|
7277
|
+
const userBytes = conditionedAudio instanceof Uint8Array ? conditionedAudio : conditionedAudio instanceof ArrayBuffer ? new Uint8Array(conditionedAudio) : new Uint8Array(conditionedAudio.buffer, conditionedAudio.byteOffset, conditionedAudio.byteLength);
|
|
7278
|
+
captureRecordingChunk("user", userBytes, recordingConfig.userInputFormat);
|
|
7279
|
+
}
|
|
7091
7280
|
if (audioLevel >= turnDetection.speechThreshold) {
|
|
7092
7281
|
if (!speechDetected && activeTTSTurnId !== undefined) {
|
|
7093
7282
|
cancelActiveTTS("barge-in");
|
|
7094
7283
|
}
|
|
7095
7284
|
speechDetected = true;
|
|
7096
7285
|
clearSilenceTimer();
|
|
7286
|
+
kickCallSilenceWatchdog();
|
|
7097
7287
|
} else if (speechDetected) {
|
|
7098
7288
|
const currentSession = await readSession();
|
|
7099
7289
|
const hasTurnText = Boolean(buildTurnText(currentSession.currentTurn.transcripts, currentSession.currentTurn.partialText, {
|
|
@@ -7106,43 +7296,49 @@ var createVoiceSession = (options) => {
|
|
|
7106
7296
|
}
|
|
7107
7297
|
await adapter.send(conditionedAudio);
|
|
7108
7298
|
};
|
|
7299
|
+
const closeInternal = async (reason, disposition = "closed") => {
|
|
7300
|
+
const session = await writeSession((currentSession) => {
|
|
7301
|
+
if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
|
|
7302
|
+
currentSession.lastActivityAt = Date.now();
|
|
7303
|
+
currentSession.status = "completed";
|
|
7304
|
+
pushCallLifecycleEvent(currentSession, {
|
|
7305
|
+
disposition,
|
|
7306
|
+
reason,
|
|
7307
|
+
type: "end"
|
|
7308
|
+
});
|
|
7309
|
+
}
|
|
7310
|
+
});
|
|
7311
|
+
clearSilenceTimer();
|
|
7312
|
+
clearCallSilenceWatchdog();
|
|
7313
|
+
await closeTTSSession(reason);
|
|
7314
|
+
await closeAdapter(reason);
|
|
7315
|
+
await persistRecordings();
|
|
7316
|
+
await Promise.resolve(socket.close(1000, reason));
|
|
7317
|
+
if (session.call?.endedAt && session.call.disposition === disposition) {
|
|
7318
|
+
await appendTrace({
|
|
7319
|
+
payload: {
|
|
7320
|
+
disposition,
|
|
7321
|
+
reason,
|
|
7322
|
+
type: "end"
|
|
7323
|
+
},
|
|
7324
|
+
session,
|
|
7325
|
+
type: "call.lifecycle"
|
|
7326
|
+
});
|
|
7327
|
+
await options.route.onCallEnd?.({
|
|
7328
|
+
api,
|
|
7329
|
+
context: options.context,
|
|
7330
|
+
disposition,
|
|
7331
|
+
reason,
|
|
7332
|
+
session
|
|
7333
|
+
});
|
|
7334
|
+
}
|
|
7335
|
+
};
|
|
7109
7336
|
const api = {
|
|
7110
7337
|
id: options.id,
|
|
7111
7338
|
close: async (reason) => {
|
|
7112
7339
|
await runSerial("api.close", async () => {
|
|
7113
|
-
const
|
|
7114
|
-
|
|
7115
|
-
currentSession.lastActivityAt = Date.now();
|
|
7116
|
-
currentSession.status = "completed";
|
|
7117
|
-
pushCallLifecycleEvent(currentSession, {
|
|
7118
|
-
disposition: "closed",
|
|
7119
|
-
reason,
|
|
7120
|
-
type: "end"
|
|
7121
|
-
});
|
|
7122
|
-
}
|
|
7123
|
-
});
|
|
7124
|
-
clearSilenceTimer();
|
|
7125
|
-
await closeTTSSession(reason);
|
|
7126
|
-
await closeAdapter(reason);
|
|
7127
|
-
await Promise.resolve(socket.close(1000, reason));
|
|
7128
|
-
if (session.call?.endedAt && session.call.disposition === "closed") {
|
|
7129
|
-
await appendTrace({
|
|
7130
|
-
payload: {
|
|
7131
|
-
disposition: "closed",
|
|
7132
|
-
reason,
|
|
7133
|
-
type: "end"
|
|
7134
|
-
},
|
|
7135
|
-
session,
|
|
7136
|
-
type: "call.lifecycle"
|
|
7137
|
-
});
|
|
7138
|
-
await options.route.onCallEnd?.({
|
|
7139
|
-
api,
|
|
7140
|
-
context: options.context,
|
|
7141
|
-
disposition: "closed",
|
|
7142
|
-
reason,
|
|
7143
|
-
session
|
|
7144
|
-
});
|
|
7145
|
-
}
|
|
7340
|
+
const disposition = reason === "silence-timeout" ? "silence-timeout" : "closed";
|
|
7341
|
+
await closeInternal(reason, disposition);
|
|
7146
7342
|
});
|
|
7147
7343
|
},
|
|
7148
7344
|
commitTurn: async (reason = "manual") => runSerial("api.commitTurn", async () => {
|
package/dist/trace.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { S3Client, S3Options } from "bun";
|
|
2
|
-
export type VoiceTraceEventType = "assistant.guardrail" | "assistant.memory" | "assistant.run" | "agent.context" | "agent.handoff" | "agent.model" | "agent.result" | "agent.tool" | "call.handoff" | "call.lifecycle" | "client.barge_in" | "client.browser_media" | "client.live_latency" | "client.reconnect" | "client.telephony_media" | "operator.action" | "provider.decision" | "session.error" | "turn.assistant" | "turn.committed" | "turn.cost" | "turn_latency.stage" | "turn.transcript" | "workflow.contract";
|
|
2
|
+
export type VoiceTraceEventType = "assistant.guardrail" | "assistant.memory" | "assistant.run" | "agent.context" | "agent.handoff" | "agent.model" | "agent.result" | "agent.tool" | "call.handoff" | "call.lifecycle" | "client.barge_in" | "client.browser_media" | "client.live_latency" | "client.reconnect" | "client.telephony_media" | "operator.action" | "provider.decision" | "recording.ready" | "session.error" | "turn.assistant" | "turn.committed" | "turn.cost" | "turn_latency.stage" | "turn.transcript" | "workflow.contract";
|
|
3
3
|
export type VoiceTraceEvent<TPayload extends Record<string, unknown> = Record<string, unknown>> = {
|
|
4
4
|
at: number;
|
|
5
5
|
id?: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -271,7 +271,7 @@ export type VoiceSessionSummary = {
|
|
|
271
271
|
status: VoiceSessionStatus;
|
|
272
272
|
turnCount: number;
|
|
273
273
|
};
|
|
274
|
-
export type VoiceCallDisposition = "completed" | "transferred" | "escalated" | "voicemail" | "no-answer" | "failed" | "closed";
|
|
274
|
+
export type VoiceCallDisposition = "completed" | "transferred" | "escalated" | "voicemail" | "no-answer" | "failed" | "silence-timeout" | "closed";
|
|
275
275
|
export type VoiceCallLifecycleEvent = {
|
|
276
276
|
at: number;
|
|
277
277
|
type: "start" | "end" | "transfer" | "escalation" | "voicemail" | "no-answer";
|
|
@@ -455,6 +455,7 @@ export type VoiceSessionHandle<TContext = unknown, TSession extends VoiceSession
|
|
|
455
455
|
reason?: string;
|
|
456
456
|
result?: TResult;
|
|
457
457
|
target: string;
|
|
458
|
+
transferMode?: "cold" | "warm";
|
|
458
459
|
}) => Promise<void>;
|
|
459
460
|
close: (reason?: string) => Promise<void>;
|
|
460
461
|
snapshot: () => Promise<TSession>;
|
|
@@ -467,6 +468,7 @@ export type VoiceRouteResult<TResult = unknown> = {
|
|
|
467
468
|
metadata?: Record<string, unknown>;
|
|
468
469
|
reason?: string;
|
|
469
470
|
target: string;
|
|
471
|
+
transferMode?: "cold" | "warm";
|
|
470
472
|
};
|
|
471
473
|
escalate?: {
|
|
472
474
|
metadata?: Record<string, unknown>;
|
|
@@ -701,6 +703,12 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
|
|
|
701
703
|
profileSwitchGuard?: VoicePluginProfileSwitchGuardConfig<TContext, TSession, TResult>;
|
|
702
704
|
trace?: VoiceTraceEventStore;
|
|
703
705
|
} & VoiceRouteConfig<TContext, TSession, TResult>;
|
|
706
|
+
export type VoiceSessionRecordingConfig = {
|
|
707
|
+
channels?: ReadonlyArray<"assistant" | "user">;
|
|
708
|
+
maxBytesPerChannel?: number;
|
|
709
|
+
store: import("./recordingStore").VoiceRecordingStore;
|
|
710
|
+
userInputFormat?: AudioFormat;
|
|
711
|
+
};
|
|
704
712
|
export type CreateVoiceSessionOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
|
|
705
713
|
costTelemetry?: VoiceCostTelemetryConfig<TContext, TSession, TResult>;
|
|
706
714
|
id: string;
|
|
@@ -715,6 +723,8 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
|
|
|
715
723
|
sttFallback?: VoiceResolvedSTTFallbackConfig;
|
|
716
724
|
store: VoiceSessionStore<TSession>;
|
|
717
725
|
trace?: VoiceTraceEventStore;
|
|
726
|
+
recording?: VoiceSessionRecordingConfig;
|
|
727
|
+
callSilenceTimeoutMs?: number;
|
|
718
728
|
reconnect: Required<VoiceReconnectConfig>;
|
|
719
729
|
phraseHints?: VoicePhraseHint[];
|
|
720
730
|
sessionMetadata?: Record<string, unknown>;
|