@absolutejs/voice 0.0.22-beta.477 → 0.0.22-beta.479

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.
@@ -29,6 +29,7 @@ export type VoiceTransferCallToolDestination = {
29
29
  message?: string;
30
30
  metadata?: Record<string, unknown>;
31
31
  target: string;
32
+ transferMode?: "cold" | "warm";
32
33
  };
33
34
  export type VoiceTransferCallToolArgs = {
34
35
  destinationId: string;
@@ -0,0 +1,25 @@
1
+ import type { Transcript, VoiceSessionHandle, VoiceSessionRecord } from "./types";
2
+ export type VoiceAMDDetectorInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
3
+ api: VoiceSessionHandle<TContext, TSession, TResult>;
4
+ audioLevel: number | undefined;
5
+ elapsedSinceFirstAudioMs: number;
6
+ elapsedSinceLastTurnCommitMs: number;
7
+ partialTranscript: string;
8
+ session: TSession;
9
+ transcripts: Transcript[];
10
+ };
11
+ export type VoiceAMDVerdict = {
12
+ metadata?: Record<string, unknown>;
13
+ reason?: string;
14
+ };
15
+ export type VoiceAMDDetector<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
16
+ evaluate: (input: VoiceAMDDetectorInput<TContext, TSession, TResult>) => Promise<VoiceAMDVerdict | undefined> | VoiceAMDVerdict | undefined;
17
+ intervalMs?: number;
18
+ };
19
+ export type MonologueAMDDetectorOptions = {
20
+ intervalMs?: number;
21
+ minMonologueMs?: number;
22
+ reason?: string;
23
+ requireFirstAudio?: boolean;
24
+ };
25
+ export declare const createMonologueAMDDetector: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: MonologueAMDDetectorOptions) => VoiceAMDDetector<TContext, TSession, TResult>;
package/dist/index.d.ts CHANGED
@@ -71,6 +71,8 @@ export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, crea
71
71
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool, } from "./agent";
72
72
  export { createAIVoiceModel } from "./aiVoiceModel";
73
73
  export type { CreateAIVoiceModelOptions } from "./aiVoiceModel";
74
+ export { createMonologueAMDDetector } from "./amdDetector";
75
+ export type { MonologueAMDDetectorOptions, VoiceAMDDetector, VoiceAMDDetectorInput, VoiceAMDVerdict, } from "./amdDetector";
74
76
  export { createVoiceRAGTool } from "./ragTool";
75
77
  export type { VoiceRAGCollectionLike, VoiceRAGQueryResult, VoiceRAGSearchInput, VoiceRAGToolArgs, VoiceRAGToolOptions, VoiceRAGToolResult, } from "./ragTool";
76
78
  export { createVoiceApiRequestTool, createVoiceDTMFTool, createVoiceEndCallTool, createVoiceTransferCallTool, createVoiceVoicemailDetectionTool, } from "./agentTools";
@@ -120,7 +122,7 @@ export { buildVoiceTraceDeliveryReport, createVoiceTraceDeliveryHTMLHandler, cre
120
122
  export { createVoiceTraceTimelineRoutes, renderVoiceTraceTimelineHTML, renderVoiceTraceTimelineSessionHTML, summarizeVoiceTraceTimeline, } from "./traceTimeline";
121
123
  export { createVoiceSQLiteAuditEventStore, createVoiceSQLiteAuditSinkDeliveryStore, createVoiceSQLiteCampaignStore, createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTelephonyWebhookIdempotencyStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore, } from "./sqliteStore";
122
124
  export { createVoicePostgresAuditEventStore, createVoicePostgresAuditSinkDeliveryStore, createVoicePostgresCampaignStore, createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore, } from "./postgresStore";
123
- export { createVoiceS3ReviewStore } from "./s3Store";
125
+ export { createVoiceS3RecordingStore, createVoiceS3ReviewStore } from "./s3Store";
124
126
  export { createVoiceMemoryStore } from "./memoryStore";
125
127
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks, } from "./opsSinks";
126
128
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature, } from "./opsWebhook";
package/dist/index.js CHANGED
@@ -3682,6 +3682,90 @@ var createVoiceSession = (options) => {
3682
3682
  const currentTurnAudio = [];
3683
3683
  let fallbackAttemptsForCurrentTurn = 0;
3684
3684
  let fallbackReplayAudioMsForCurrentTurn = 0;
3685
+ const amdDetector = options.amd;
3686
+ let amdEvaluationTimer = null;
3687
+ let amdFired = false;
3688
+ let amdFirstAudioAt;
3689
+ let amdLastTurnCommitAt;
3690
+ let amdLastAudioLevel;
3691
+ const clearAmdEvaluationTimer = () => {
3692
+ if (amdEvaluationTimer) {
3693
+ clearInterval(amdEvaluationTimer);
3694
+ amdEvaluationTimer = null;
3695
+ }
3696
+ };
3697
+ const evaluateAmd = async () => {
3698
+ if (!amdDetector || amdFired) {
3699
+ return;
3700
+ }
3701
+ let snapshot;
3702
+ try {
3703
+ snapshot = await readSession();
3704
+ } catch {
3705
+ return;
3706
+ }
3707
+ const now = Date.now();
3708
+ const verdict = await Promise.resolve(amdDetector.evaluate({
3709
+ api,
3710
+ audioLevel: amdLastAudioLevel,
3711
+ elapsedSinceFirstAudioMs: amdFirstAudioAt === undefined ? 0 : now - amdFirstAudioAt,
3712
+ elapsedSinceLastTurnCommitMs: amdLastTurnCommitAt === undefined ? 0 : now - amdLastTurnCommitAt,
3713
+ partialTranscript: snapshot.currentTurn.partialText,
3714
+ session: snapshot,
3715
+ transcripts: [
3716
+ ...snapshot.transcripts,
3717
+ ...snapshot.currentTurn.transcripts
3718
+ ]
3719
+ }));
3720
+ if (!verdict || amdFired) {
3721
+ return;
3722
+ }
3723
+ amdFired = true;
3724
+ clearAmdEvaluationTimer();
3725
+ try {
3726
+ await api.markVoicemail({
3727
+ metadata: verdict.metadata
3728
+ });
3729
+ } catch (error) {
3730
+ logger.warn("voice amd markVoicemail failed", {
3731
+ error: toError(error).message,
3732
+ sessionId: options.id
3733
+ });
3734
+ }
3735
+ };
3736
+ const startAmdEvaluationTimer = () => {
3737
+ if (!amdDetector || amdEvaluationTimer || amdFired) {
3738
+ return;
3739
+ }
3740
+ const intervalMs = amdDetector.intervalMs ?? 1000;
3741
+ amdEvaluationTimer = setInterval(() => {
3742
+ evaluateAmd();
3743
+ }, intervalMs);
3744
+ };
3745
+ const callSilenceTimeoutMs = options.callSilenceTimeoutMs && options.callSilenceTimeoutMs > 0 ? options.callSilenceTimeoutMs : undefined;
3746
+ let callSilenceWatchdog = null;
3747
+ let callSilenceFired = false;
3748
+ const clearCallSilenceWatchdog = () => {
3749
+ if (callSilenceWatchdog) {
3750
+ clearTimeout(callSilenceWatchdog);
3751
+ callSilenceWatchdog = null;
3752
+ }
3753
+ };
3754
+ const fireCallSilenceTimeout = () => {
3755
+ callSilenceWatchdog = null;
3756
+ if (callSilenceFired) {
3757
+ return;
3758
+ }
3759
+ callSilenceFired = true;
3760
+ api.close("silence-timeout");
3761
+ };
3762
+ const kickCallSilenceWatchdog = () => {
3763
+ if (callSilenceTimeoutMs === undefined || callSilenceFired) {
3764
+ return;
3765
+ }
3766
+ clearCallSilenceWatchdog();
3767
+ callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
3768
+ };
3685
3769
  const recordingConfig = options.recording;
3686
3770
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
3687
3771
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -3970,6 +4054,7 @@ var createVoiceSession = (options) => {
3970
4054
  const sendAssistantAudio = async (chunk, input) => {
3971
4055
  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));
3972
4056
  captureRecordingChunk("assistant", normalizedChunk, input.format);
4057
+ kickCallSilenceWatchdog();
3973
4058
  await send({
3974
4059
  chunkBase64: encodeBase64(normalizedChunk),
3975
4060
  format: input.format,
@@ -4064,6 +4149,8 @@ var createVoiceSession = (options) => {
4064
4149
  recoverable: false,
4065
4150
  type: "error"
4066
4151
  });
4152
+ clearCallSilenceWatchdog();
4153
+ clearAmdEvaluationTimer();
4067
4154
  await closeTTSSession("failed");
4068
4155
  await closeAdapter("failed");
4069
4156
  await persistRecordings();
@@ -4133,6 +4220,8 @@ var createVoiceSession = (options) => {
4133
4220
  sessionId: options.id,
4134
4221
  type: "complete"
4135
4222
  });
4223
+ clearCallSilenceWatchdog();
4224
+ clearAmdEvaluationTimer();
4136
4225
  await closeTTSSession("complete");
4137
4226
  await closeAdapter("complete");
4138
4227
  await persistRecordings();
@@ -4191,19 +4280,21 @@ var createVoiceSession = (options) => {
4191
4280
  });
4192
4281
  };
4193
4282
  const transferInternal = async (input) => {
4283
+ const transferMetadata = input.transferMode === undefined ? input.metadata : { ...input.metadata ?? {}, transferMode: input.transferMode };
4194
4284
  const session = await writeSession((currentSession) => {
4195
4285
  pushCallLifecycleEvent(currentSession, {
4196
- metadata: input.metadata,
4286
+ metadata: transferMetadata,
4197
4287
  reason: input.reason,
4198
4288
  target: input.target,
4199
4289
  type: "transfer"
4200
4290
  });
4201
4291
  });
4202
4292
  await appendTrace({
4203
- metadata: input.metadata,
4293
+ metadata: transferMetadata,
4204
4294
  payload: {
4205
4295
  reason: input.reason,
4206
4296
  target: input.target,
4297
+ transferMode: input.transferMode,
4207
4298
  type: "transfer"
4208
4299
  },
4209
4300
  session,
@@ -4988,6 +5079,7 @@ var createVoiceSession = (options) => {
4988
5079
  };
4989
5080
  const commitTurnInternal = async (reason = "manual") => {
4990
5081
  clearSilenceTimer();
5082
+ amdLastTurnCommitAt = Date.now();
4991
5083
  const session = await readSession();
4992
5084
  if (session.status === "completed" || session.status === "failed") {
4993
5085
  return;
@@ -5234,6 +5326,8 @@ var createVoiceSession = (options) => {
5234
5326
  resumePendingTurnCommit(session);
5235
5327
  await ensureAdapter();
5236
5328
  warmTTSSession();
5329
+ kickCallSilenceWatchdog();
5330
+ startAmdEvaluationTimer();
5237
5331
  };
5238
5332
  const disconnectInternal = async (event) => {
5239
5333
  clearSilenceTimer();
@@ -5281,12 +5375,17 @@ var createVoiceSession = (options) => {
5281
5375
  const userBytes = conditionedAudio instanceof Uint8Array ? conditionedAudio : conditionedAudio instanceof ArrayBuffer ? new Uint8Array(conditionedAudio) : new Uint8Array(conditionedAudio.buffer, conditionedAudio.byteOffset, conditionedAudio.byteLength);
5282
5376
  captureRecordingChunk("user", userBytes, recordingConfig.userInputFormat);
5283
5377
  }
5378
+ amdLastAudioLevel = audioLevel;
5284
5379
  if (audioLevel >= turnDetection.speechThreshold) {
5380
+ if (amdFirstAudioAt === undefined) {
5381
+ amdFirstAudioAt = Date.now();
5382
+ }
5285
5383
  if (!speechDetected && activeTTSTurnId !== undefined) {
5286
5384
  cancelActiveTTS("barge-in");
5287
5385
  }
5288
5386
  speechDetected = true;
5289
5387
  clearSilenceTimer();
5388
+ kickCallSilenceWatchdog();
5290
5389
  } else if (speechDetected) {
5291
5390
  const currentSession = await readSession();
5292
5391
  const hasTurnText = Boolean(buildTurnText(currentSession.currentTurn.transcripts, currentSession.currentTurn.partialText, {
@@ -5299,44 +5398,50 @@ var createVoiceSession = (options) => {
5299
5398
  }
5300
5399
  await adapter.send(conditionedAudio);
5301
5400
  };
5401
+ const closeInternal = async (reason, disposition = "closed") => {
5402
+ const session = await writeSession((currentSession) => {
5403
+ if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
5404
+ currentSession.lastActivityAt = Date.now();
5405
+ currentSession.status = "completed";
5406
+ pushCallLifecycleEvent(currentSession, {
5407
+ disposition,
5408
+ reason,
5409
+ type: "end"
5410
+ });
5411
+ }
5412
+ });
5413
+ clearSilenceTimer();
5414
+ clearCallSilenceWatchdog();
5415
+ clearAmdEvaluationTimer();
5416
+ await closeTTSSession(reason);
5417
+ await closeAdapter(reason);
5418
+ await persistRecordings();
5419
+ await Promise.resolve(socket.close(1000, reason));
5420
+ if (session.call?.endedAt && session.call.disposition === disposition) {
5421
+ await appendTrace({
5422
+ payload: {
5423
+ disposition,
5424
+ reason,
5425
+ type: "end"
5426
+ },
5427
+ session,
5428
+ type: "call.lifecycle"
5429
+ });
5430
+ await options.route.onCallEnd?.({
5431
+ api,
5432
+ context: options.context,
5433
+ disposition,
5434
+ reason,
5435
+ session
5436
+ });
5437
+ }
5438
+ };
5302
5439
  const api = {
5303
5440
  id: options.id,
5304
5441
  close: async (reason) => {
5305
5442
  await runSerial("api.close", async () => {
5306
- const session = await writeSession((currentSession) => {
5307
- if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
5308
- currentSession.lastActivityAt = Date.now();
5309
- currentSession.status = "completed";
5310
- pushCallLifecycleEvent(currentSession, {
5311
- disposition: "closed",
5312
- reason,
5313
- type: "end"
5314
- });
5315
- }
5316
- });
5317
- clearSilenceTimer();
5318
- await closeTTSSession(reason);
5319
- await closeAdapter(reason);
5320
- await persistRecordings();
5321
- await Promise.resolve(socket.close(1000, reason));
5322
- if (session.call?.endedAt && session.call.disposition === "closed") {
5323
- await appendTrace({
5324
- payload: {
5325
- disposition: "closed",
5326
- reason,
5327
- type: "end"
5328
- },
5329
- session,
5330
- type: "call.lifecycle"
5331
- });
5332
- await options.route.onCallEnd?.({
5333
- api,
5334
- context: options.context,
5335
- disposition: "closed",
5336
- reason,
5337
- session
5338
- });
5339
- }
5443
+ const disposition = reason === "silence-timeout" ? "silence-timeout" : "closed";
5444
+ await closeInternal(reason, disposition);
5340
5445
  });
5341
5446
  },
5342
5447
  commitTurn: async (reason = "manual") => runSerial("api.commitTurn", async () => {
@@ -9082,6 +9187,18 @@ var RECIPE_DEFAULTS = {
9082
9187
  defaultQueue: "transfer-verification",
9083
9188
  description: "Creates transfer verification work for transferred calls and escalation work when the handoff fails.",
9084
9189
  escalationQueue: "transfer-escalations"
9190
+ },
9191
+ "cold-transfer": {
9192
+ completedAction: "Verify the SIP REFER landed and the caller reached the destination without re-introduction.",
9193
+ completedDescription: "The call was cold-transferred (SIP REFER) \u2014 confirm the destination picked up.",
9194
+ completedKind: "transfer-check",
9195
+ completedTitle: "Verify cold transfer",
9196
+ defaultCompletedCreatesTask: false,
9197
+ defaultDueInMs: 5 * 60000,
9198
+ defaultPriority: "normal",
9199
+ defaultQueue: "transfer-verification",
9200
+ description: "Creates verification work for cold-transferred (REFER) calls and escalation work when the handoff fails.",
9201
+ escalationQueue: "transfer-escalations"
9085
9202
  }
9086
9203
  };
9087
9204
  var buildRecipeTask = (input) => {
@@ -9211,7 +9328,7 @@ var resolveVoiceOutcomeRecipe = (name, options = {}) => {
9211
9328
  dueInMs: Math.min(options.dueInMs ?? defaults.defaultDueInMs, 20 * 60000),
9212
9329
  name: `${name}-transfer-check`,
9213
9330
  priority: options.priority ?? defaults.defaultPriority,
9214
- queue: name === "warm-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
9331
+ queue: name === "warm-transfer" || name === "cold-transfer" ? options.queue ?? defaults.defaultQueue : "transfer-verification"
9215
9332
  },
9216
9333
  voicemail: {
9217
9334
  assignee: options.assignee,
@@ -34818,6 +34935,36 @@ var createAIVoiceModel = (options) => ({
34818
34935
  return output;
34819
34936
  }
34820
34937
  });
34938
+ // src/amdDetector.ts
34939
+ var createMonologueAMDDetector = (options = {}) => {
34940
+ const minMonologueMs = options.minMonologueMs ?? 8000;
34941
+ const reason = options.reason ?? "monologue-suspected-voicemail";
34942
+ const requireFirstAudio = options.requireFirstAudio ?? true;
34943
+ return {
34944
+ evaluate: ({
34945
+ elapsedSinceFirstAudioMs,
34946
+ elapsedSinceLastTurnCommitMs,
34947
+ session
34948
+ }) => {
34949
+ if (requireFirstAudio && elapsedSinceFirstAudioMs <= 0) {
34950
+ return;
34951
+ }
34952
+ const noTurnsYet = session.turns.length === 0;
34953
+ const monologueElapsed = noTurnsYet ? elapsedSinceFirstAudioMs : elapsedSinceLastTurnCommitMs;
34954
+ if (monologueElapsed < minMonologueMs) {
34955
+ return;
34956
+ }
34957
+ return {
34958
+ metadata: {
34959
+ detector: "monologue",
34960
+ monologueMs: monologueElapsed
34961
+ },
34962
+ reason
34963
+ };
34964
+ },
34965
+ intervalMs: options.intervalMs ?? 1000
34966
+ };
34967
+ };
34821
34968
  // src/ragTool.ts
34822
34969
  var DEFAULT_TOOL_NAME = "searchKnowledgeBase";
34823
34970
  var DEFAULT_DESCRIPTION = "Search the knowledge base and return short grounded citations. Use this whenever the caller asks a question that may be answered by indexed reference material.";
@@ -34999,7 +35146,8 @@ ${destinationDocs}`,
34999
35146
  metadata: destination.metadata,
35000
35147
  reason: args?.reason,
35001
35148
  result,
35002
- target: destination.target
35149
+ target: destination.target,
35150
+ transferMode: destination.transferMode
35003
35151
  });
35004
35152
  return {
35005
35153
  destinationId: destination.id,
@@ -41255,6 +41403,62 @@ var createVoiceS3ReviewStore = (options) => {
41255
41403
  set
41256
41404
  };
41257
41405
  };
41406
+ var normalizeRecordingKeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/recordings";
41407
+ var recordingWavKey = (prefix, sessionId, channel) => `${prefix}/${encodeURIComponent(sessionId)}_${channel}.wav`;
41408
+ var recordingMetadataKey = (prefix, sessionId, channel) => `${prefix}/${encodeURIComponent(sessionId)}_${channel}.json`;
41409
+ var createVoiceS3RecordingStore = (options) => {
41410
+ const client = options.client ?? new Bun.S3Client(options);
41411
+ const keyPrefix = normalizeRecordingKeyPrefix(options.keyPrefix);
41412
+ const publicUrlBase = options.publicUrlBase?.replace(/\/+$/, "");
41413
+ const getFile = (key) => client.file(key, options);
41414
+ const resolveUrl = (key) => publicUrlBase ? `${publicUrlBase}/${key}` : `s3://${key}`;
41415
+ const put = async (artifact) => {
41416
+ const wavKey = recordingWavKey(keyPrefix, artifact.sessionId, artifact.channel);
41417
+ const metadataKey = recordingMetadataKey(keyPrefix, artifact.sessionId, artifact.channel);
41418
+ const wav = encodePcmAsWav(artifact.audioBytes, artifact.format);
41419
+ await getFile(wavKey).write(wav);
41420
+ const recordingUrl = resolveUrl(wavKey);
41421
+ const metadata = {
41422
+ capturedAt: artifact.capturedAt,
41423
+ channel: artifact.channel,
41424
+ durationMs: artifact.durationMs,
41425
+ format: artifact.format,
41426
+ recordingUrl,
41427
+ sessionId: artifact.sessionId
41428
+ };
41429
+ await getFile(metadataKey).write(JSON.stringify(metadata));
41430
+ return {
41431
+ ...artifact,
41432
+ recordingUrl
41433
+ };
41434
+ };
41435
+ const readMetadata = async (sessionId, channel) => {
41436
+ const metadataKey = recordingMetadataKey(keyPrefix, sessionId, channel);
41437
+ const wavKey = recordingWavKey(keyPrefix, sessionId, channel);
41438
+ const metadataFile = getFile(metadataKey);
41439
+ if (!await metadataFile.exists()) {
41440
+ return;
41441
+ }
41442
+ const meta = JSON.parse(await metadataFile.text());
41443
+ const wavBytes = await getFile(wavKey).bytes();
41444
+ return {
41445
+ audioBytes: wavBytes,
41446
+ capturedAt: meta.capturedAt,
41447
+ channel: meta.channel,
41448
+ durationMs: meta.durationMs,
41449
+ format: meta.format,
41450
+ recordingUrl: meta.recordingUrl,
41451
+ sessionId: meta.sessionId
41452
+ };
41453
+ };
41454
+ const get = (sessionId, channel) => readMetadata(sessionId, channel);
41455
+ const list = async (sessionId) => {
41456
+ const channels = ["assistant", "user"];
41457
+ const records = await Promise.all(channels.map((channel) => readMetadata(sessionId, channel)));
41458
+ return records.filter((record) => record !== undefined);
41459
+ };
41460
+ return { get, list, put };
41461
+ };
41258
41462
  // src/memoryStore.ts
41259
41463
  var createVoiceMemoryStore = () => {
41260
41464
  const sessions = new Map;
@@ -45687,6 +45891,7 @@ export {
45687
45891
  createVoiceSQLiteAuditSinkDeliveryStore,
45688
45892
  createVoiceSQLiteAuditEventStore,
45689
45893
  createVoiceS3ReviewStore,
45894
+ createVoiceS3RecordingStore,
45690
45895
  createVoiceS3DeliverySink,
45691
45896
  createVoiceRoutingDecisionSummary,
45692
45897
  createVoiceReviewSavedEvent,
@@ -45928,6 +46133,7 @@ export {
45928
46133
  createPhraseHintCorrectionHandler,
45929
46134
  createOpenAIVoiceTTS,
45930
46135
  createOpenAIVoiceAssistantModel,
46136
+ createMonologueAMDDetector,
45931
46137
  createMemoryVoiceTelnyxWebhookEventStore,
45932
46138
  createMemoryVoiceTelephonyWebhookIdempotencyStore,
45933
46139
  createMemoryVoicePlivoWebhookNonceStore,
@@ -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;
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;
@@ -5650,6 +5650,90 @@ var createVoiceSession = (options) => {
5650
5650
  const currentTurnAudio = [];
5651
5651
  let fallbackAttemptsForCurrentTurn = 0;
5652
5652
  let fallbackReplayAudioMsForCurrentTurn = 0;
5653
+ const amdDetector = options.amd;
5654
+ let amdEvaluationTimer = null;
5655
+ let amdFired = false;
5656
+ let amdFirstAudioAt;
5657
+ let amdLastTurnCommitAt;
5658
+ let amdLastAudioLevel;
5659
+ const clearAmdEvaluationTimer = () => {
5660
+ if (amdEvaluationTimer) {
5661
+ clearInterval(amdEvaluationTimer);
5662
+ amdEvaluationTimer = null;
5663
+ }
5664
+ };
5665
+ const evaluateAmd = async () => {
5666
+ if (!amdDetector || amdFired) {
5667
+ return;
5668
+ }
5669
+ let snapshot;
5670
+ try {
5671
+ snapshot = await readSession();
5672
+ } catch {
5673
+ return;
5674
+ }
5675
+ const now = Date.now();
5676
+ const verdict = await Promise.resolve(amdDetector.evaluate({
5677
+ api,
5678
+ audioLevel: amdLastAudioLevel,
5679
+ elapsedSinceFirstAudioMs: amdFirstAudioAt === undefined ? 0 : now - amdFirstAudioAt,
5680
+ elapsedSinceLastTurnCommitMs: amdLastTurnCommitAt === undefined ? 0 : now - amdLastTurnCommitAt,
5681
+ partialTranscript: snapshot.currentTurn.partialText,
5682
+ session: snapshot,
5683
+ transcripts: [
5684
+ ...snapshot.transcripts,
5685
+ ...snapshot.currentTurn.transcripts
5686
+ ]
5687
+ }));
5688
+ if (!verdict || amdFired) {
5689
+ return;
5690
+ }
5691
+ amdFired = true;
5692
+ clearAmdEvaluationTimer();
5693
+ try {
5694
+ await api.markVoicemail({
5695
+ metadata: verdict.metadata
5696
+ });
5697
+ } catch (error) {
5698
+ logger.warn("voice amd markVoicemail failed", {
5699
+ error: toError(error).message,
5700
+ sessionId: options.id
5701
+ });
5702
+ }
5703
+ };
5704
+ const startAmdEvaluationTimer = () => {
5705
+ if (!amdDetector || amdEvaluationTimer || amdFired) {
5706
+ return;
5707
+ }
5708
+ const intervalMs = amdDetector.intervalMs ?? 1000;
5709
+ amdEvaluationTimer = setInterval(() => {
5710
+ evaluateAmd();
5711
+ }, intervalMs);
5712
+ };
5713
+ const callSilenceTimeoutMs = options.callSilenceTimeoutMs && options.callSilenceTimeoutMs > 0 ? options.callSilenceTimeoutMs : undefined;
5714
+ let callSilenceWatchdog = null;
5715
+ let callSilenceFired = false;
5716
+ const clearCallSilenceWatchdog = () => {
5717
+ if (callSilenceWatchdog) {
5718
+ clearTimeout(callSilenceWatchdog);
5719
+ callSilenceWatchdog = null;
5720
+ }
5721
+ };
5722
+ const fireCallSilenceTimeout = () => {
5723
+ callSilenceWatchdog = null;
5724
+ if (callSilenceFired) {
5725
+ return;
5726
+ }
5727
+ callSilenceFired = true;
5728
+ api.close("silence-timeout");
5729
+ };
5730
+ const kickCallSilenceWatchdog = () => {
5731
+ if (callSilenceTimeoutMs === undefined || callSilenceFired) {
5732
+ return;
5733
+ }
5734
+ clearCallSilenceWatchdog();
5735
+ callSilenceWatchdog = setTimeout(fireCallSilenceTimeout, callSilenceTimeoutMs);
5736
+ };
5653
5737
  const recordingConfig = options.recording;
5654
5738
  const recordingChannels = new Set(recordingConfig?.channels ?? ["assistant", "user"]);
5655
5739
  const recordingMaxBytes = recordingConfig?.maxBytesPerChannel ?? 50 * 1024 * 1024;
@@ -5938,6 +6022,7 @@ var createVoiceSession = (options) => {
5938
6022
  const sendAssistantAudio = async (chunk, input) => {
5939
6023
  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));
5940
6024
  captureRecordingChunk("assistant", normalizedChunk, input.format);
6025
+ kickCallSilenceWatchdog();
5941
6026
  await send({
5942
6027
  chunkBase64: encodeBase64(normalizedChunk),
5943
6028
  format: input.format,
@@ -6032,6 +6117,8 @@ var createVoiceSession = (options) => {
6032
6117
  recoverable: false,
6033
6118
  type: "error"
6034
6119
  });
6120
+ clearCallSilenceWatchdog();
6121
+ clearAmdEvaluationTimer();
6035
6122
  await closeTTSSession("failed");
6036
6123
  await closeAdapter("failed");
6037
6124
  await persistRecordings();
@@ -6101,6 +6188,8 @@ var createVoiceSession = (options) => {
6101
6188
  sessionId: options.id,
6102
6189
  type: "complete"
6103
6190
  });
6191
+ clearCallSilenceWatchdog();
6192
+ clearAmdEvaluationTimer();
6104
6193
  await closeTTSSession("complete");
6105
6194
  await closeAdapter("complete");
6106
6195
  await persistRecordings();
@@ -6159,19 +6248,21 @@ var createVoiceSession = (options) => {
6159
6248
  });
6160
6249
  };
6161
6250
  const transferInternal = async (input) => {
6251
+ const transferMetadata = input.transferMode === undefined ? input.metadata : { ...input.metadata ?? {}, transferMode: input.transferMode };
6162
6252
  const session = await writeSession((currentSession) => {
6163
6253
  pushCallLifecycleEvent(currentSession, {
6164
- metadata: input.metadata,
6254
+ metadata: transferMetadata,
6165
6255
  reason: input.reason,
6166
6256
  target: input.target,
6167
6257
  type: "transfer"
6168
6258
  });
6169
6259
  });
6170
6260
  await appendTrace({
6171
- metadata: input.metadata,
6261
+ metadata: transferMetadata,
6172
6262
  payload: {
6173
6263
  reason: input.reason,
6174
6264
  target: input.target,
6265
+ transferMode: input.transferMode,
6175
6266
  type: "transfer"
6176
6267
  },
6177
6268
  session,
@@ -6956,6 +7047,7 @@ var createVoiceSession = (options) => {
6956
7047
  };
6957
7048
  const commitTurnInternal = async (reason = "manual") => {
6958
7049
  clearSilenceTimer();
7050
+ amdLastTurnCommitAt = Date.now();
6959
7051
  const session = await readSession();
6960
7052
  if (session.status === "completed" || session.status === "failed") {
6961
7053
  return;
@@ -7202,6 +7294,8 @@ var createVoiceSession = (options) => {
7202
7294
  resumePendingTurnCommit(session);
7203
7295
  await ensureAdapter();
7204
7296
  warmTTSSession();
7297
+ kickCallSilenceWatchdog();
7298
+ startAmdEvaluationTimer();
7205
7299
  };
7206
7300
  const disconnectInternal = async (event) => {
7207
7301
  clearSilenceTimer();
@@ -7249,12 +7343,17 @@ var createVoiceSession = (options) => {
7249
7343
  const userBytes = conditionedAudio instanceof Uint8Array ? conditionedAudio : conditionedAudio instanceof ArrayBuffer ? new Uint8Array(conditionedAudio) : new Uint8Array(conditionedAudio.buffer, conditionedAudio.byteOffset, conditionedAudio.byteLength);
7250
7344
  captureRecordingChunk("user", userBytes, recordingConfig.userInputFormat);
7251
7345
  }
7346
+ amdLastAudioLevel = audioLevel;
7252
7347
  if (audioLevel >= turnDetection.speechThreshold) {
7348
+ if (amdFirstAudioAt === undefined) {
7349
+ amdFirstAudioAt = Date.now();
7350
+ }
7253
7351
  if (!speechDetected && activeTTSTurnId !== undefined) {
7254
7352
  cancelActiveTTS("barge-in");
7255
7353
  }
7256
7354
  speechDetected = true;
7257
7355
  clearSilenceTimer();
7356
+ kickCallSilenceWatchdog();
7258
7357
  } else if (speechDetected) {
7259
7358
  const currentSession = await readSession();
7260
7359
  const hasTurnText = Boolean(buildTurnText(currentSession.currentTurn.transcripts, currentSession.currentTurn.partialText, {
@@ -7267,44 +7366,50 @@ var createVoiceSession = (options) => {
7267
7366
  }
7268
7367
  await adapter.send(conditionedAudio);
7269
7368
  };
7369
+ const closeInternal = async (reason, disposition = "closed") => {
7370
+ const session = await writeSession((currentSession) => {
7371
+ if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
7372
+ currentSession.lastActivityAt = Date.now();
7373
+ currentSession.status = "completed";
7374
+ pushCallLifecycleEvent(currentSession, {
7375
+ disposition,
7376
+ reason,
7377
+ type: "end"
7378
+ });
7379
+ }
7380
+ });
7381
+ clearSilenceTimer();
7382
+ clearCallSilenceWatchdog();
7383
+ clearAmdEvaluationTimer();
7384
+ await closeTTSSession(reason);
7385
+ await closeAdapter(reason);
7386
+ await persistRecordings();
7387
+ await Promise.resolve(socket.close(1000, reason));
7388
+ if (session.call?.endedAt && session.call.disposition === disposition) {
7389
+ await appendTrace({
7390
+ payload: {
7391
+ disposition,
7392
+ reason,
7393
+ type: "end"
7394
+ },
7395
+ session,
7396
+ type: "call.lifecycle"
7397
+ });
7398
+ await options.route.onCallEnd?.({
7399
+ api,
7400
+ context: options.context,
7401
+ disposition,
7402
+ reason,
7403
+ session
7404
+ });
7405
+ }
7406
+ };
7270
7407
  const api = {
7271
7408
  id: options.id,
7272
7409
  close: async (reason) => {
7273
7410
  await runSerial("api.close", async () => {
7274
- const session = await writeSession((currentSession) => {
7275
- if (currentSession.status !== "completed" && currentSession.status !== "failed" && !currentSession.call?.endedAt) {
7276
- currentSession.lastActivityAt = Date.now();
7277
- currentSession.status = "completed";
7278
- pushCallLifecycleEvent(currentSession, {
7279
- disposition: "closed",
7280
- reason,
7281
- type: "end"
7282
- });
7283
- }
7284
- });
7285
- clearSilenceTimer();
7286
- await closeTTSSession(reason);
7287
- await closeAdapter(reason);
7288
- await persistRecordings();
7289
- await Promise.resolve(socket.close(1000, reason));
7290
- if (session.call?.endedAt && session.call.disposition === "closed") {
7291
- await appendTrace({
7292
- payload: {
7293
- disposition: "closed",
7294
- reason,
7295
- type: "end"
7296
- },
7297
- session,
7298
- type: "call.lifecycle"
7299
- });
7300
- await options.route.onCallEnd?.({
7301
- api,
7302
- context: options.context,
7303
- disposition: "closed",
7304
- reason,
7305
- session
7306
- });
7307
- }
7411
+ const disposition = reason === "silence-timeout" ? "silence-timeout" : "closed";
7412
+ await closeInternal(reason, disposition);
7308
7413
  });
7309
7414
  },
7310
7415
  commitTurn: async (reason = "manual") => runSerial("api.commitTurn", async () => {
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>;
@@ -722,6 +724,8 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
722
724
  store: VoiceSessionStore<TSession>;
723
725
  trace?: VoiceTraceEventStore;
724
726
  recording?: VoiceSessionRecordingConfig;
727
+ callSilenceTimeoutMs?: number;
728
+ amd?: import("./amdDetector").VoiceAMDDetector<TContext, TSession, TResult>;
725
729
  reconnect: Required<VoiceReconnectConfig>;
726
730
  phraseHints?: VoicePhraseHint[];
727
731
  sessionMetadata?: Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.477",
3
+ "version": "0.0.22-beta.479",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",