twilio-agent-connect 2.1.0 → 2.3.0
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/index.d.ts +99 -23
- package/dist/index.js +252 -135
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,8 +17,10 @@ var TwilioMemoryConfigSchema = z.object({
|
|
|
17
17
|
traitGroups: z.array(z.string()).optional(),
|
|
18
18
|
observationsLimit: z.number().int().min(0).max(100).default(20),
|
|
19
19
|
summariesLimit: z.number().int().min(0).max(100).default(5),
|
|
20
|
-
//
|
|
21
|
-
|
|
20
|
+
// 0 matches the Memory API and Python SDK. Above 0, Memory requires a
|
|
21
|
+
// conversationId on every /Recall, forcing the query expansion that
|
|
22
|
+
// `memoryMode: "once"` exists to avoid.
|
|
23
|
+
communicationsLimit: z.number().int().min(0).max(100).default(0),
|
|
22
24
|
relevanceThreshold: z.number().min(0).max(1).default(0),
|
|
23
25
|
/**
|
|
24
26
|
* Trait group name that holds the phone identifier on newly created profiles.
|
|
@@ -49,34 +51,28 @@ var TACConfigSchema = z.object({
|
|
|
49
51
|
memoryConfig: TwilioMemoryConfigSchema.prefault({}),
|
|
50
52
|
conversationConfigurationId: z.string().regex(/^conv_configuration_[0-9a-z]{26}$/, "Invalid Conversation Configuration ID format").optional(),
|
|
51
53
|
/**
|
|
52
|
-
* Public domain where voice routes are reachable
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*/
|
|
61
|
-
voicePublicDomain: z.preprocess(
|
|
62
|
-
(v)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
54
|
+
* Public domain where voice routes are reachable, optionally including a port
|
|
55
|
+
* and/or base path (e.g. "example.ngrok.app", "example.ngrok.app:8080",
|
|
56
|
+
* or "example.com/server1"). Used by VoiceChannel to construct the public
|
|
57
|
+
* WebSocket URL and ConversationRelay action URL. Required when using the Voice channel.
|
|
58
|
+
*
|
|
59
|
+
* Whitespace, schemes (https://, wss://), and trailing slashes are stripped
|
|
60
|
+
* automatically; anything else is passed through as given. Mirrors the Python
|
|
61
|
+
* SDK's `_normalize_voice_public_domain` — keep the two in step.
|
|
62
|
+
*/
|
|
63
|
+
voicePublicDomain: z.preprocess((v) => {
|
|
64
|
+
if (typeof v !== "string") return v;
|
|
65
|
+
let s = v.trim();
|
|
66
|
+
if (s.length === 0) return void 0;
|
|
67
|
+
for (const scheme of ["https://", "http://", "wss://", "ws://"]) {
|
|
68
|
+
if (s.toLowerCase().startsWith(scheme)) {
|
|
69
|
+
s = s.slice(scheme.length);
|
|
70
|
+
break;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
|
|
77
|
-
'Invalid hostname format. Must be a hostname without protocol, port, or path (e.g., "abc123.ngrok.app", "localhost", or "192.168.1.100")'
|
|
78
|
-
).optional()
|
|
79
|
-
).optional(),
|
|
72
|
+
}
|
|
73
|
+
s = s.replace(/\/+$/, "");
|
|
74
|
+
return s.length === 0 ? void 0 : s;
|
|
75
|
+
}, z.string().optional()).optional(),
|
|
80
76
|
/**
|
|
81
77
|
* Path the voice WebSocket is served at. Combined with voicePublicDomain to
|
|
82
78
|
* build the public WebSocket URL the voice channel hands to Twilio in TwiML;
|
|
@@ -773,10 +769,16 @@ var InterruptMessageSchema = z.object({
|
|
|
773
769
|
utteranceUntilInterrupt: z.string().optional(),
|
|
774
770
|
durationUntilInterruptMs: z.number().int().nonnegative().optional()
|
|
775
771
|
});
|
|
772
|
+
var DtmfMessageSchema = z.object({
|
|
773
|
+
type: z.literal("dtmf"),
|
|
774
|
+
/** The key pressed: `0`-`9`, `*`, `#`, or `A`-`D`. */
|
|
775
|
+
digit: z.string()
|
|
776
|
+
});
|
|
776
777
|
var WebSocketMessageSchema = z.union([
|
|
777
778
|
SetupMessageSchema,
|
|
778
779
|
PromptMessageSchema,
|
|
779
|
-
InterruptMessageSchema
|
|
780
|
+
InterruptMessageSchema,
|
|
781
|
+
DtmfMessageSchema
|
|
780
782
|
]);
|
|
781
783
|
var TextTokenMessageSchema = z.object({
|
|
782
784
|
type: z.literal("text"),
|
|
@@ -1429,7 +1431,7 @@ var TACConfig = class _TACConfig {
|
|
|
1429
1431
|
* Optional environment variables:
|
|
1430
1432
|
* - TWILIO_WHATSAPP_NUMBER: WhatsApp number for WhatsApp channel (e.g., 'whatsapp:+1234567890')
|
|
1431
1433
|
* - TWILIO_CONVERSATION_CONFIGURATION_ID: Conversation Orchestrator configuration ID (enables orchestrated mode)
|
|
1432
|
-
* - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice;
|
|
1434
|
+
* - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice; a port and/or base path are allowed, e.g., 'abc123.ngrok.app', 'example.ngrok.app:8080', or 'example.com/server1')
|
|
1433
1435
|
* - TWILIO_VOICE_WEBSOCKET_PATH: Path for the voice WebSocket (default: /ws)
|
|
1434
1436
|
* - TWILIO_VOICE_ACTION_PATH: Path for the ConversationRelay action callback (default: /conversation-relay-callback)
|
|
1435
1437
|
* - TWILIO_VOICE_CALL_EVENT_PATH: Base path for the call-event callbacks — status, async AMD, recording (default: /twilio/call-events)
|
|
@@ -1685,7 +1687,7 @@ function createLogger(options) {
|
|
|
1685
1687
|
|
|
1686
1688
|
// package.json
|
|
1687
1689
|
var package_default = {
|
|
1688
|
-
version: "2.
|
|
1690
|
+
version: "2.3.0"};
|
|
1689
1691
|
function buildUserAgent() {
|
|
1690
1692
|
return `twilio-agent-connect-typescript/${package_default.version}`;
|
|
1691
1693
|
}
|
|
@@ -1854,7 +1856,9 @@ var MemoryClient = class _MemoryClient extends BaseClient {
|
|
|
1854
1856
|
profile_id: profileId,
|
|
1855
1857
|
observation_count: observations.length,
|
|
1856
1858
|
summary_count: summaries.length,
|
|
1857
|
-
communication_count: communications.length
|
|
1859
|
+
communication_count: communications.length,
|
|
1860
|
+
// Server-side query time; dominates /Recall when expansion runs.
|
|
1861
|
+
query_time_ms: response.meta?.queryTime
|
|
1858
1862
|
},
|
|
1859
1863
|
"Memory retrieval succeeded"
|
|
1860
1864
|
);
|
|
@@ -3045,6 +3049,10 @@ var TAC = class _TAC {
|
|
|
3045
3049
|
*
|
|
3046
3050
|
* @param session - Conversation session context
|
|
3047
3051
|
* @param query - Optional semantic search query
|
|
3052
|
+
* @param conversationId - Passed through to `/Recall` as-is. Sending one
|
|
3053
|
+
* without a `query` makes Memory infer one from that conversation's history
|
|
3054
|
+
* — an expensive server-side step — so leave it unset when there is no
|
|
3055
|
+
* per-turn topic (e.g. `"once"` mode's cache-priming fetch).
|
|
3048
3056
|
* @returns Promise containing TACMemoryResponse wrapper providing unified access to memory data.
|
|
3049
3057
|
*
|
|
3050
3058
|
* Attempts to retrieve from Memory API first:
|
|
@@ -3055,7 +3063,7 @@ var TAC = class _TAC {
|
|
|
3055
3063
|
* - observations and summaries are empty arrays
|
|
3056
3064
|
* - communications have basic fields only (no author name/type)
|
|
3057
3065
|
*/
|
|
3058
|
-
async retrieveMemory(session, query) {
|
|
3066
|
+
async retrieveMemory(session, query, conversationId) {
|
|
3059
3067
|
if (!this.isOrchestratorEnabled()) {
|
|
3060
3068
|
return new TACMemoryResponse([]);
|
|
3061
3069
|
}
|
|
@@ -3102,7 +3110,7 @@ var TAC = class _TAC {
|
|
|
3102
3110
|
throw new Error("Memory client is not available");
|
|
3103
3111
|
}
|
|
3104
3112
|
const memoryResponse = await this.memoryClient.retrieveMemories(session.profileId, {
|
|
3105
|
-
conversationId
|
|
3113
|
+
conversationId,
|
|
3106
3114
|
query,
|
|
3107
3115
|
observationsLimit: this.config.memoryConfig.observationsLimit,
|
|
3108
3116
|
summariesLimit: this.config.memoryConfig.summariesLimit,
|
|
@@ -3448,9 +3456,9 @@ var BaseChannel = class {
|
|
|
3448
3456
|
*
|
|
3449
3457
|
* Modes:
|
|
3450
3458
|
* - "always": Fetch with the provided query on every message.
|
|
3451
|
-
* - "once": Fetch once with
|
|
3452
|
-
*
|
|
3453
|
-
* INACTIVE transition.
|
|
3459
|
+
* - "once": Fetch once with neither a query nor a conversation id (skipping
|
|
3460
|
+
* query expansion) and cache the result on the session. Subsequent calls
|
|
3461
|
+
* reuse the cache until it is invalidated on the INACTIVE transition.
|
|
3454
3462
|
* - "never": Skip retrieval.
|
|
3455
3463
|
*
|
|
3456
3464
|
* Memory retrieval failures are logged and swallowed so message processing
|
|
@@ -3465,9 +3473,11 @@ var BaseChannel = class {
|
|
|
3465
3473
|
return session.cachedMemory;
|
|
3466
3474
|
}
|
|
3467
3475
|
try {
|
|
3476
|
+
const isAlways = this.memoryMode === "always";
|
|
3468
3477
|
const memory = await this.tac.retrieveMemory(
|
|
3469
3478
|
session,
|
|
3470
|
-
|
|
3479
|
+
isAlways ? query : void 0,
|
|
3480
|
+
isAlways ? session.conversationId : void 0
|
|
3471
3481
|
);
|
|
3472
3482
|
if (this.memoryMode === "once") {
|
|
3473
3483
|
session.cachedMemory = memory;
|
|
@@ -4673,6 +4683,9 @@ function studioVoiceHandoffUrl(accountSid, flowSid) {
|
|
|
4673
4683
|
|
|
4674
4684
|
// packages/core/src/channels/voice.ts
|
|
4675
4685
|
var DEFAULT_WELCOME_GREETING = "Hello! How can I assist you today?";
|
|
4686
|
+
var POLL_ATTEMPTS = 10;
|
|
4687
|
+
var POLL_BASE_DELAY_MS = 250;
|
|
4688
|
+
var POLL_MAX_DELAY_MS = 1500;
|
|
4676
4689
|
function stringifyParameterValue(value) {
|
|
4677
4690
|
if (typeof value === "object") {
|
|
4678
4691
|
return JSON.stringify(value);
|
|
@@ -4796,6 +4809,32 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4796
4809
|
onRecording(callback) {
|
|
4797
4810
|
this.onRecordingHandler = callback;
|
|
4798
4811
|
}
|
|
4812
|
+
/**
|
|
4813
|
+
* Register a handler for DTMF keypresses, called once per key in order.
|
|
4814
|
+
*
|
|
4815
|
+
* Requires `dtmfDetection: true` on the ConversationRelay config — without it
|
|
4816
|
+
* Twilio sends nothing and this never fires. Digits aren't buffered, so
|
|
4817
|
+
* accumulating a multi-digit entry is the handler's job.
|
|
4818
|
+
*
|
|
4819
|
+
* A keypress initializes the conversation just as a prompt does, since a
|
|
4820
|
+
* caller can type without ever speaking; if that fails the digit still
|
|
4821
|
+
* arrives, with `conversationId` and `session` undefined. Keypresses don't
|
|
4822
|
+
* cancel in-flight streaming on their own — that's a separate `interrupt`
|
|
4823
|
+
* message, sent when `interruptible` includes `dtmf`.
|
|
4824
|
+
*
|
|
4825
|
+
* @example
|
|
4826
|
+
* ```typescript
|
|
4827
|
+
* const digits = new Map<string, string>();
|
|
4828
|
+
*
|
|
4829
|
+
* voiceChannel.onDtmf(({ conversationId, digit }) => {
|
|
4830
|
+
* if (!conversationId) return;
|
|
4831
|
+
* digits.set(conversationId, (digits.get(conversationId) ?? '') + digit);
|
|
4832
|
+
* });
|
|
4833
|
+
* ```
|
|
4834
|
+
*/
|
|
4835
|
+
onDtmf(callback) {
|
|
4836
|
+
this.voiceCallbacks.onDtmf = callback;
|
|
4837
|
+
}
|
|
4799
4838
|
/**
|
|
4800
4839
|
* Resolve the public WebSocket URL from `TACConfig.voicePublicDomain` +
|
|
4801
4840
|
* `TACConfig.voiceWebsocketPath`. Throws if `voicePublicDomain` isn't set.
|
|
@@ -4847,6 +4886,9 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4847
4886
|
case "interrupt":
|
|
4848
4887
|
this.voiceCallbacks.onInterrupt = callback;
|
|
4849
4888
|
break;
|
|
4889
|
+
case "dtmf":
|
|
4890
|
+
this.voiceCallbacks.onDtmf = callback;
|
|
4891
|
+
break;
|
|
4850
4892
|
case "webSocketConnected":
|
|
4851
4893
|
this.voiceCallbacks.onWebSocketConnected = callback;
|
|
4852
4894
|
break;
|
|
@@ -4927,6 +4969,57 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4927
4969
|
getWebsocket(conversationId) {
|
|
4928
4970
|
return this.webSocketConnections.get(conversationId) || null;
|
|
4929
4971
|
}
|
|
4972
|
+
/**
|
|
4973
|
+
* Poll Conversation Orchestrator for the conversation ConversationRelay
|
|
4974
|
+
* created for `callSid`, then register the local session and WebSocket.
|
|
4975
|
+
* Runs in the background from `setup`, so those can exist before the
|
|
4976
|
+
* caller speaks.
|
|
4977
|
+
*/
|
|
4978
|
+
async initializeOrchestratedConversation(callSid, fromNumber, ws) {
|
|
4979
|
+
if (!this.conversationClient) {
|
|
4980
|
+
throw new Error("Conversation client is required in orchestrated mode");
|
|
4981
|
+
}
|
|
4982
|
+
let conversations = [];
|
|
4983
|
+
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
|
|
4984
|
+
conversations = await this.conversationClient.listConversations({
|
|
4985
|
+
channelId: callSid,
|
|
4986
|
+
status: ["ACTIVE"]
|
|
4987
|
+
});
|
|
4988
|
+
if (conversations.length === 1) break;
|
|
4989
|
+
if (attempt < POLL_ATTEMPTS - 1) {
|
|
4990
|
+
this.logger.debug(
|
|
4991
|
+
{ call_sid: callSid, attempt: attempt + 1, found: conversations.length },
|
|
4992
|
+
"Conversation not ready yet, polling again"
|
|
4993
|
+
);
|
|
4994
|
+
const delayMs = Math.min(POLL_BASE_DELAY_MS * 2 ** attempt, POLL_MAX_DELAY_MS);
|
|
4995
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
if (conversations.length !== 1) {
|
|
4999
|
+
throw new Error(
|
|
5000
|
+
`Expected exactly 1 conversation for callSid ${callSid}, but found ${conversations.length} after ${POLL_ATTEMPTS} attempts`
|
|
5001
|
+
);
|
|
5002
|
+
}
|
|
5003
|
+
const conversation = conversations[0];
|
|
5004
|
+
const conversationId = conversation.id;
|
|
5005
|
+
const participants = await this.conversationClient.listParticipants(conversationId);
|
|
5006
|
+
const customerParticipant = participants.find((p) => p.type === "CUSTOMER");
|
|
5007
|
+
const customerAddress = customerParticipant?.addresses?.find((a) => a.channel === "VOICE")?.address ?? fromNumber ?? void 0;
|
|
5008
|
+
const profileId = customerParticipant?.profileId ? customerParticipant.profileId : void 0;
|
|
5009
|
+
this.webSocketConnections.set(conversationId, ws);
|
|
5010
|
+
this.callSidToConversationId.set(callSid, conversationId);
|
|
5011
|
+
const session = this.startConversation(conversationId, profileId);
|
|
5012
|
+
session.callSid = callSid;
|
|
5013
|
+
if (customerAddress) {
|
|
5014
|
+
session.authorInfo = {
|
|
5015
|
+
address: customerAddress
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
if (this.voiceCallbacks.onWebSocketConnected) {
|
|
5019
|
+
this.voiceCallbacks.onWebSocketConnected({ conversationId });
|
|
5020
|
+
}
|
|
5021
|
+
return conversationId;
|
|
5022
|
+
}
|
|
4930
5023
|
/**
|
|
4931
5024
|
* Handle WebSocket connection from ConversationRelay
|
|
4932
5025
|
*/
|
|
@@ -4935,6 +5028,65 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4935
5028
|
let callSid = null;
|
|
4936
5029
|
let fromNumber = null;
|
|
4937
5030
|
let initializationFailed = false;
|
|
5031
|
+
let initPromise = null;
|
|
5032
|
+
const ensureConversation = async () => {
|
|
5033
|
+
if (conversationId) {
|
|
5034
|
+
return conversationId;
|
|
5035
|
+
}
|
|
5036
|
+
const sid = callSid;
|
|
5037
|
+
if (!sid) {
|
|
5038
|
+
return null;
|
|
5039
|
+
}
|
|
5040
|
+
const retryCount = this.initializationRetries.get(sid) ?? 0;
|
|
5041
|
+
if (retryCount >= this.MAX_INITIALIZATION_RETRIES) {
|
|
5042
|
+
throw new Error(
|
|
5043
|
+
`Cannot process message - conversation initialization failed after ${retryCount} attempts for callSid ${sid}`
|
|
5044
|
+
);
|
|
5045
|
+
}
|
|
5046
|
+
try {
|
|
5047
|
+
if (initializationFailed) {
|
|
5048
|
+
this.logger.info(
|
|
5049
|
+
{ call_sid: sid, retry_count: retryCount },
|
|
5050
|
+
"Retrying conversation initialization after previous failure"
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
if (!this.tac.isOrchestratorEnabled()) {
|
|
5054
|
+
conversationId = sid;
|
|
5055
|
+
this.webSocketConnections.set(conversationId, ws);
|
|
5056
|
+
this.callSidToConversationId.set(sid, conversationId);
|
|
5057
|
+
const session = this.startConversation(conversationId);
|
|
5058
|
+
session.callSid = sid;
|
|
5059
|
+
if (fromNumber) {
|
|
5060
|
+
session.authorInfo = { address: fromNumber };
|
|
5061
|
+
}
|
|
5062
|
+
if (this.voiceCallbacks.onWebSocketConnected) {
|
|
5063
|
+
this.voiceCallbacks.onWebSocketConnected({ conversationId });
|
|
5064
|
+
}
|
|
5065
|
+
} else {
|
|
5066
|
+
initPromise ??= this.initializeOrchestratedConversation(sid, fromNumber, ws);
|
|
5067
|
+
try {
|
|
5068
|
+
conversationId = await initPromise;
|
|
5069
|
+
} finally {
|
|
5070
|
+
initPromise = null;
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
initializationFailed = false;
|
|
5074
|
+
this.initializationRetries.delete(sid);
|
|
5075
|
+
this.logger.info(
|
|
5076
|
+
{ conversation_id: conversationId, call_sid: sid },
|
|
5077
|
+
"Conversation initialization succeeded"
|
|
5078
|
+
);
|
|
5079
|
+
return conversationId;
|
|
5080
|
+
} catch (err) {
|
|
5081
|
+
initializationFailed = true;
|
|
5082
|
+
this.initializationRetries.set(sid, retryCount + 1);
|
|
5083
|
+
this.logger.error(
|
|
5084
|
+
{ err, call_sid: sid, retry_count: retryCount + 1 },
|
|
5085
|
+
"Conversation initialization failed"
|
|
5086
|
+
);
|
|
5087
|
+
throw err;
|
|
5088
|
+
}
|
|
5089
|
+
};
|
|
4938
5090
|
ws.on("message", (data) => {
|
|
4939
5091
|
(async () => {
|
|
4940
5092
|
try {
|
|
@@ -4957,6 +5109,14 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4957
5109
|
case "setup":
|
|
4958
5110
|
callSid = message.callSid;
|
|
4959
5111
|
fromNumber = message.from;
|
|
5112
|
+
if (this.tac.isOrchestratorEnabled()) {
|
|
5113
|
+
this.logger.debug(
|
|
5114
|
+
{ call_sid: callSid },
|
|
5115
|
+
"Starting background conversation initialization"
|
|
5116
|
+
);
|
|
5117
|
+
initPromise = this.initializeOrchestratedConversation(callSid, fromNumber, ws);
|
|
5118
|
+
void initPromise.catch(() => void 0);
|
|
5119
|
+
}
|
|
4960
5120
|
if (this.voiceCallbacks.onSetup) {
|
|
4961
5121
|
this.voiceCallbacks.onSetup({
|
|
4962
5122
|
callSid,
|
|
@@ -4967,89 +5127,7 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4967
5127
|
}
|
|
4968
5128
|
break;
|
|
4969
5129
|
case "prompt":
|
|
4970
|
-
|
|
4971
|
-
const retryCount = this.initializationRetries.get(callSid) ?? 0;
|
|
4972
|
-
if (retryCount >= this.MAX_INITIALIZATION_RETRIES) {
|
|
4973
|
-
throw new Error(
|
|
4974
|
-
`Cannot process prompt - conversation initialization failed after ${retryCount} attempts for callSid ${callSid}`
|
|
4975
|
-
);
|
|
4976
|
-
}
|
|
4977
|
-
try {
|
|
4978
|
-
if (initializationFailed) {
|
|
4979
|
-
this.logger.info(
|
|
4980
|
-
{ call_sid: callSid, retry_count: retryCount },
|
|
4981
|
-
"Retrying conversation initialization after previous failure"
|
|
4982
|
-
);
|
|
4983
|
-
}
|
|
4984
|
-
if (!this.tac.isOrchestratorEnabled()) {
|
|
4985
|
-
conversationId = callSid;
|
|
4986
|
-
this.webSocketConnections.set(conversationId, ws);
|
|
4987
|
-
this.callSidToConversationId.set(callSid, conversationId);
|
|
4988
|
-
const session = this.startConversation(conversationId);
|
|
4989
|
-
session.callSid = callSid;
|
|
4990
|
-
if (fromNumber) {
|
|
4991
|
-
session.authorInfo = { address: fromNumber };
|
|
4992
|
-
}
|
|
4993
|
-
} else {
|
|
4994
|
-
if (!this.conversationClient) {
|
|
4995
|
-
throw new Error("Conversation client is required in orchestrated mode");
|
|
4996
|
-
}
|
|
4997
|
-
const POLL_ATTEMPTS = 5;
|
|
4998
|
-
const POLL_DELAY_MS = 500;
|
|
4999
|
-
let conversations = [];
|
|
5000
|
-
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
|
|
5001
|
-
conversations = await this.conversationClient.listConversations({
|
|
5002
|
-
channelId: callSid
|
|
5003
|
-
});
|
|
5004
|
-
if (conversations.length === 1) break;
|
|
5005
|
-
if (attempt < POLL_ATTEMPTS - 1) {
|
|
5006
|
-
this.logger.debug(
|
|
5007
|
-
{ call_sid: callSid, attempt: attempt + 1, found: conversations.length },
|
|
5008
|
-
"Conversation not ready yet, polling again"
|
|
5009
|
-
);
|
|
5010
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_DELAY_MS));
|
|
5011
|
-
}
|
|
5012
|
-
}
|
|
5013
|
-
if (conversations.length !== 1) {
|
|
5014
|
-
throw new Error(
|
|
5015
|
-
`Expected exactly 1 conversation for callSid ${callSid}, but found ${conversations.length} after ${POLL_ATTEMPTS} attempts`
|
|
5016
|
-
);
|
|
5017
|
-
}
|
|
5018
|
-
const conversation = conversations[0];
|
|
5019
|
-
conversationId = conversation.id;
|
|
5020
|
-
const participants = await this.conversationClient.listParticipants(conversationId);
|
|
5021
|
-
const customerParticipant = participants.find((p) => p.type === "CUSTOMER");
|
|
5022
|
-
const customerAddress = customerParticipant?.addresses?.find((a) => a.channel === "VOICE")?.address ?? fromNumber ?? void 0;
|
|
5023
|
-
const profileId = customerParticipant?.profileId ? customerParticipant.profileId : void 0;
|
|
5024
|
-
this.webSocketConnections.set(conversationId, ws);
|
|
5025
|
-
this.callSidToConversationId.set(callSid, conversationId);
|
|
5026
|
-
const session = this.startConversation(conversationId, profileId);
|
|
5027
|
-
session.callSid = callSid;
|
|
5028
|
-
if (customerAddress) {
|
|
5029
|
-
session.authorInfo = {
|
|
5030
|
-
address: customerAddress
|
|
5031
|
-
};
|
|
5032
|
-
}
|
|
5033
|
-
}
|
|
5034
|
-
if (this.voiceCallbacks.onWebSocketConnected) {
|
|
5035
|
-
this.voiceCallbacks.onWebSocketConnected({ conversationId });
|
|
5036
|
-
}
|
|
5037
|
-
initializationFailed = false;
|
|
5038
|
-
this.initializationRetries.delete(callSid);
|
|
5039
|
-
this.logger.info(
|
|
5040
|
-
{ conversation_id: conversationId, call_sid: callSid },
|
|
5041
|
-
"Conversation initialization succeeded"
|
|
5042
|
-
);
|
|
5043
|
-
} catch (err) {
|
|
5044
|
-
initializationFailed = true;
|
|
5045
|
-
this.initializationRetries.set(callSid, retryCount + 1);
|
|
5046
|
-
this.logger.error(
|
|
5047
|
-
{ err, call_sid: callSid, retry_count: retryCount + 1 },
|
|
5048
|
-
"Conversation initialization failed"
|
|
5049
|
-
);
|
|
5050
|
-
throw err;
|
|
5051
|
-
}
|
|
5052
|
-
}
|
|
5130
|
+
await ensureConversation();
|
|
5053
5131
|
if (conversationId) {
|
|
5054
5132
|
const previousPrompt = this.promptQueues.get(conversationId) ?? Promise.resolve();
|
|
5055
5133
|
const currentPrompt = previousPrompt.then(() => this.handlePromptMessage(conversationId, message)).catch((err) => {
|
|
@@ -5068,6 +5146,17 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5068
5146
|
this.handleInterruptMessage(conversationId, message);
|
|
5069
5147
|
}
|
|
5070
5148
|
break;
|
|
5149
|
+
case "dtmf":
|
|
5150
|
+
try {
|
|
5151
|
+
await ensureConversation();
|
|
5152
|
+
} catch (err) {
|
|
5153
|
+
this.logger.warn(
|
|
5154
|
+
{ err, call_sid: callSid },
|
|
5155
|
+
"Conversation initialization failed on DTMF keypress, delivering digit without a conversation"
|
|
5156
|
+
);
|
|
5157
|
+
}
|
|
5158
|
+
await this.handleDtmfMessage(conversationId, callSid, message);
|
|
5159
|
+
break;
|
|
5071
5160
|
default:
|
|
5072
5161
|
this.logger.debug(
|
|
5073
5162
|
{
|
|
@@ -5090,6 +5179,19 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5090
5179
|
});
|
|
5091
5180
|
});
|
|
5092
5181
|
ws.on("close", () => {
|
|
5182
|
+
const pendingInit = initPromise;
|
|
5183
|
+
initPromise = null;
|
|
5184
|
+
if (pendingInit && !conversationId) {
|
|
5185
|
+
void pendingInit.then(async (adoptedId) => {
|
|
5186
|
+
await this.handleWebSocketDisconnect(adoptedId);
|
|
5187
|
+
if (callSid) this.callSidToConversationId.delete(callSid);
|
|
5188
|
+
}).catch((err) => {
|
|
5189
|
+
this.logger.error(
|
|
5190
|
+
{ err, call_sid: callSid },
|
|
5191
|
+
"Background conversation initialization failed after the call ended"
|
|
5192
|
+
);
|
|
5193
|
+
});
|
|
5194
|
+
}
|
|
5093
5195
|
if (conversationId) {
|
|
5094
5196
|
void this.handleWebSocketDisconnect(conversationId).catch((err) => {
|
|
5095
5197
|
this.logger.error(
|
|
@@ -5160,6 +5262,23 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5160
5262
|
});
|
|
5161
5263
|
}
|
|
5162
5264
|
}
|
|
5265
|
+
/**
|
|
5266
|
+
* Handle WebSocket DTMF message (caller keypress)
|
|
5267
|
+
*/
|
|
5268
|
+
async handleDtmfMessage(conversationId, callSid, message) {
|
|
5269
|
+
const { digit } = message;
|
|
5270
|
+
this.logger.debug({ conversation_id: conversationId, call_sid: callSid }, "DTMF keypress");
|
|
5271
|
+
if (!this.voiceCallbacks.onDtmf) {
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
const session = conversationId ? this.getConversationSession(conversationId) : void 0;
|
|
5275
|
+
await this.voiceCallbacks.onDtmf({
|
|
5276
|
+
conversationId: conversationId ?? void 0,
|
|
5277
|
+
callSid: callSid ?? void 0,
|
|
5278
|
+
digit,
|
|
5279
|
+
...session !== void 0 && { session }
|
|
5280
|
+
});
|
|
5281
|
+
}
|
|
5163
5282
|
/**
|
|
5164
5283
|
* Handle WebSocket disconnection. In orchestrated mode the conversation stays
|
|
5165
5284
|
* tracked until the CLOSED webhook (so a follow-up call can reuse it); in
|
|
@@ -5476,8 +5595,8 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5476
5595
|
*
|
|
5477
5596
|
* Places an outbound call with inline TwiML that connects to ConversationRelay.
|
|
5478
5597
|
* The conversationConfiguration attribute tells CO to create and manage the
|
|
5479
|
-
* conversation during passive hydration. The session is initialized
|
|
5480
|
-
*
|
|
5598
|
+
* conversation during passive hydration. The session is initialized when the
|
|
5599
|
+
* background callSid lookup started at WebSocket setup finds it.
|
|
5481
5600
|
*
|
|
5482
5601
|
* TwiML fields are merged per-field, highest precedence first:
|
|
5483
5602
|
* 1. `options.twimlOptions` — per-call overrides
|
|
@@ -5668,9 +5787,8 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5668
5787
|
/**
|
|
5669
5788
|
* Hang up a call and clean up its ConversationRelay session.
|
|
5670
5789
|
*
|
|
5671
|
-
* Works on `callSid` alone,
|
|
5672
|
-
*
|
|
5673
|
-
* cleanup no-ops if no tracked session matches.
|
|
5790
|
+
* Works on `callSid` alone, whether or not a session exists yet. No-ops the
|
|
5791
|
+
* session cleanup if none is tracked.
|
|
5674
5792
|
*
|
|
5675
5793
|
* Does not throw — hanging up an already-ended call is routine (the callee
|
|
5676
5794
|
* hangs up while AMD is still resolving), and handlers shouldn't have to
|
|
@@ -5704,11 +5822,10 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5704
5822
|
* keyed by conversation id: the Orchestrator conversation id in orchestrator
|
|
5705
5823
|
* mode, the CallSid only in ConversationRelay-only mode.
|
|
5706
5824
|
*
|
|
5707
|
-
*
|
|
5708
|
-
*
|
|
5709
|
-
*
|
|
5710
|
-
*
|
|
5711
|
-
* needs no session.
|
|
5825
|
+
* Relay-only mode creates the session on the first prompt; orchestrated
|
|
5826
|
+
* mode creates it when the lookup started at setup finishes, so it may
|
|
5827
|
+
* exist before the caller speaks — including before `onAmd` fires. Treat it
|
|
5828
|
+
* as racy and hang up with {@link endCall}, which needs no session.
|
|
5712
5829
|
*
|
|
5713
5830
|
* At the other end, orchestrator mode keeps the session until Conversation
|
|
5714
5831
|
* Orchestrator's CLOSED webhook, so it outlives the call and `onCallStatus` /
|
|
@@ -5727,8 +5844,8 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5727
5844
|
*
|
|
5728
5845
|
* @param callSid - Twilio Call SID, e.g. from
|
|
5729
5846
|
* `InitiateVoiceConversationResult.callSid` or a call event.
|
|
5730
|
-
* @returns The session, or `undefined` —
|
|
5731
|
-
*
|
|
5847
|
+
* @returns The session, or `undefined` — not created yet, the call ended, or
|
|
5848
|
+
* it landed on another instance (see the horizontal-scaling note in
|
|
5732
5849
|
* CLAUDE.md).
|
|
5733
5850
|
*/
|
|
5734
5851
|
getConversationSessionByCallSid(callSid) {
|
|
@@ -6967,6 +7084,6 @@ var TACServer = class {
|
|
|
6967
7084
|
}
|
|
6968
7085
|
};
|
|
6969
7086
|
|
|
6970
|
-
export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AmdEventSchema, AnthropicToolSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CALL_EVENT_KINDS, CallEventKindSchema, CallOptionsSchema, CallStatusEventSchema, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, CreateObservationsRequestSchema, CustomParametersSchema, EMPTY_MEMORY_RESPONSE, EnvironmentVariables, ExecutionDetailsSchema, HandoffPayloadSchema, InitiateMessagingConversationOptionsSchema, InitiateVoiceConversationOptionsSchema, IntelligenceConfigurationSchema, InterruptMessageSchema, InterruptModeSchema, JSONSchemaSchema, KnowledgeBaseSchema, KnowledgeBaseStatusSchema, KnowledgeChunkResultSchema, KnowledgeClient, KnowledgeSearchResponseSchema, LanguageAttributesSchema, LanguageConfigSchema, ListCommunicationsResponseSchema, ListConversationsResponseSchema, ListParticipantsResponseSchema, MemoryChannelTypeSchema, MemoryClient, MemoryCommunicationContentSchema, MemoryCommunicationSchema, MemoryDeliveryStatusSchema, MemoryModeSchema, MemoryParticipantSchema, MemoryParticipantTypeSchema, MemoryPromptBuilder, MemoryRetrievalRequestSchema, MemoryRetrievalResponseSchema, MessageDirectionSchema, MessagingChannel, ObservationCreateRequestSchema, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, RecordingEventSchema, SMSChannel, SendMessageActionPayloadSchema, SendMessageActionRequestSchema, SessionInfoSchema, SessionMessageSchema, SetupMessageSchema, StatusCallbackSchema, StatusTimeoutsSchema, SummaryInfoSchema, TAC, TACChannelTypeSchema, TACCommunicationAuthorSchema, TACCommunicationContentSchema, TACCommunicationSchema, TACConfig, TACConfigSchema, TACDeliveryStatusSchema, TACMemoryResponse, TACParticipantTypeSchema, TACServer, TACTool, TextTokenMessageSchema, ToolExecutionResultSchema, TranscriptionSchema, TranscriptionWordSchema, TwiMLOptionsSchema, TwiMLRequestSchema, TwilioMemoryConfigSchema, VoiceChannel, WebSocketMessageSchema, WhatsAppChannel, amdEventFromForm, buildHandoffPayload, callOptionsToCreateParams, callStatusEventFromForm, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, recordingEventFromForm, redactTwimlParameters, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl, twiMLRequestFromForm };
|
|
7087
|
+
export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AmdEventSchema, AnthropicToolSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CALL_EVENT_KINDS, CallEventKindSchema, CallOptionsSchema, CallStatusEventSchema, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, CreateObservationsRequestSchema, CustomParametersSchema, DtmfMessageSchema, EMPTY_MEMORY_RESPONSE, EnvironmentVariables, ExecutionDetailsSchema, HandoffPayloadSchema, InitiateMessagingConversationOptionsSchema, InitiateVoiceConversationOptionsSchema, IntelligenceConfigurationSchema, InterruptMessageSchema, InterruptModeSchema, JSONSchemaSchema, KnowledgeBaseSchema, KnowledgeBaseStatusSchema, KnowledgeChunkResultSchema, KnowledgeClient, KnowledgeSearchResponseSchema, LanguageAttributesSchema, LanguageConfigSchema, ListCommunicationsResponseSchema, ListConversationsResponseSchema, ListParticipantsResponseSchema, MemoryChannelTypeSchema, MemoryClient, MemoryCommunicationContentSchema, MemoryCommunicationSchema, MemoryDeliveryStatusSchema, MemoryModeSchema, MemoryParticipantSchema, MemoryParticipantTypeSchema, MemoryPromptBuilder, MemoryRetrievalRequestSchema, MemoryRetrievalResponseSchema, MessageDirectionSchema, MessagingChannel, ObservationCreateRequestSchema, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, RecordingEventSchema, SMSChannel, SendMessageActionPayloadSchema, SendMessageActionRequestSchema, SessionInfoSchema, SessionMessageSchema, SetupMessageSchema, StatusCallbackSchema, StatusTimeoutsSchema, SummaryInfoSchema, TAC, TACChannelTypeSchema, TACCommunicationAuthorSchema, TACCommunicationContentSchema, TACCommunicationSchema, TACConfig, TACConfigSchema, TACDeliveryStatusSchema, TACMemoryResponse, TACParticipantTypeSchema, TACServer, TACTool, TextTokenMessageSchema, ToolExecutionResultSchema, TranscriptionSchema, TranscriptionWordSchema, TwiMLOptionsSchema, TwiMLRequestSchema, TwilioMemoryConfigSchema, VoiceChannel, WebSocketMessageSchema, WhatsAppChannel, amdEventFromForm, buildHandoffPayload, callOptionsToCreateParams, callStatusEventFromForm, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, recordingEventFromForm, redactTwimlParameters, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl, twiMLRequestFromForm };
|
|
6971
7088
|
//# sourceMappingURL=index.js.map
|
|
6972
7089
|
//# sourceMappingURL=index.js.map
|