twilio-agent-connect 2.1.0 → 2.2.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 +36 -22
- package/dist/index.js +140 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -104,8 +104,9 @@ declare const EnvironmentVariables: {
|
|
|
104
104
|
* Memory retrieval mode for channels.
|
|
105
105
|
*
|
|
106
106
|
* - "always": Fetch memory with the message as query on every inbound message.
|
|
107
|
-
* - "once": Fetch memory once at conversation start with
|
|
108
|
-
* cache it. The cache is
|
|
107
|
+
* - "once": Fetch memory once at conversation start with no query (and no
|
|
108
|
+
* conversation id, skipping query expansion) and cache it. The cache is
|
|
109
|
+
* invalidated when the conversation becomes INACTIVE.
|
|
109
110
|
* - "never": Never automatically fetch memory (default).
|
|
110
111
|
*/
|
|
111
112
|
declare const MemoryModeSchema: z.ZodEnum<{
|
|
@@ -1483,8 +1484,8 @@ interface InitiateConversationResult {
|
|
|
1483
1484
|
/**
|
|
1484
1485
|
* Result of initiating an outbound voice conversation.
|
|
1485
1486
|
* Note: conversationId is not included because the conversation is created by
|
|
1486
|
-
* Conversation Orchestrator during passive hydration — the SDK discovers it
|
|
1487
|
-
*
|
|
1487
|
+
* Conversation Orchestrator during passive hydration — the SDK discovers it by
|
|
1488
|
+
* callSid, in the background from WebSocket setup.
|
|
1488
1489
|
*/
|
|
1489
1490
|
interface InitiateVoiceConversationResult {
|
|
1490
1491
|
callSid: string;
|
|
@@ -2481,7 +2482,7 @@ declare class TACConfig {
|
|
|
2481
2482
|
* Optional environment variables:
|
|
2482
2483
|
* - TWILIO_WHATSAPP_NUMBER: WhatsApp number for WhatsApp channel (e.g., 'whatsapp:+1234567890')
|
|
2483
2484
|
* - TWILIO_CONVERSATION_CONFIGURATION_ID: Conversation Orchestrator configuration ID (enables orchestrated mode)
|
|
2484
|
-
* - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice;
|
|
2485
|
+
* - 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')
|
|
2485
2486
|
* - TWILIO_VOICE_WEBSOCKET_PATH: Path for the voice WebSocket (default: /ws)
|
|
2486
2487
|
* - TWILIO_VOICE_ACTION_PATH: Path for the ConversationRelay action callback (default: /conversation-relay-callback)
|
|
2487
2488
|
* - TWILIO_VOICE_CALL_EVENT_PATH: Base path for the call-event callbacks — status, async AMD, recording (default: /twilio/call-events)
|
|
@@ -2846,7 +2847,7 @@ interface BaseChannelOptions {
|
|
|
2846
2847
|
*
|
|
2847
2848
|
* - "never": Memory is not automatically retrieved. Use the memory TAC tool or manually call `tac.retrieveMemory()` in callbacks for conditional retrieval.
|
|
2848
2849
|
* - "always": Memory is automatically retrieved (using the message as query) for every inbound message and available in `onMessageReady` callback.
|
|
2849
|
-
* - "once": Memory is retrieved once at conversation start with
|
|
2850
|
+
* - "once": Memory is retrieved once at conversation start with no query (and no conversation id) and cached on the session. Subsequent messages reuse the cache until the conversation becomes INACTIVE.
|
|
2850
2851
|
*/
|
|
2851
2852
|
memoryMode?: MemoryMode;
|
|
2852
2853
|
/**
|
|
@@ -2984,9 +2985,9 @@ declare abstract class BaseChannel {
|
|
|
2984
2985
|
*
|
|
2985
2986
|
* Modes:
|
|
2986
2987
|
* - "always": Fetch with the provided query on every message.
|
|
2987
|
-
* - "once": Fetch once with
|
|
2988
|
-
*
|
|
2989
|
-
* INACTIVE transition.
|
|
2988
|
+
* - "once": Fetch once with neither a query nor a conversation id (skipping
|
|
2989
|
+
* query expansion) and cache the result on the session. Subsequent calls
|
|
2990
|
+
* reuse the cache until it is invalidated on the INACTIVE transition.
|
|
2990
2991
|
* - "never": Skip retrieval.
|
|
2991
2992
|
*
|
|
2992
2993
|
* Memory retrieval failures are logged and swallowed so message processing
|
|
@@ -3137,6 +3138,10 @@ declare class TAC {
|
|
|
3137
3138
|
*
|
|
3138
3139
|
* @param session - Conversation session context
|
|
3139
3140
|
* @param query - Optional semantic search query
|
|
3141
|
+
* @param conversationId - Passed through to `/Recall` as-is. Sending one
|
|
3142
|
+
* without a `query` makes Memory infer one from that conversation's history
|
|
3143
|
+
* — an expensive server-side step — so leave it unset when there is no
|
|
3144
|
+
* per-turn topic (e.g. `"once"` mode's cache-priming fetch).
|
|
3140
3145
|
* @returns Promise containing TACMemoryResponse wrapper providing unified access to memory data.
|
|
3141
3146
|
*
|
|
3142
3147
|
* Attempts to retrieve from Memory API first:
|
|
@@ -3147,7 +3152,7 @@ declare class TAC {
|
|
|
3147
3152
|
* - observations and summaries are empty arrays
|
|
3148
3153
|
* - communications have basic fields only (no author name/type)
|
|
3149
3154
|
*/
|
|
3150
|
-
retrieveMemory(session: ConversationSession, query?: string): Promise<TACMemoryResponse>;
|
|
3155
|
+
retrieveMemory(session: ConversationSession, query?: string, conversationId?: string): Promise<TACMemoryResponse>;
|
|
3151
3156
|
/**
|
|
3152
3157
|
* Fetch profile information with traits
|
|
3153
3158
|
*
|
|
@@ -3549,6 +3554,10 @@ interface VoiceChannelEvents extends BaseChannelEvents {
|
|
|
3549
3554
|
utteranceUntilInterrupt: string | undefined;
|
|
3550
3555
|
durationUntilInterruptMs: number | undefined;
|
|
3551
3556
|
}) => void;
|
|
3557
|
+
/**
|
|
3558
|
+
* Fired once the session and WebSocket registration exist — in orchestrated
|
|
3559
|
+
* mode possibly before the first prompt, since the lookup starts at setup.
|
|
3560
|
+
*/
|
|
3552
3561
|
onWebSocketConnected?: (data: {
|
|
3553
3562
|
conversationId: ConversationId;
|
|
3554
3563
|
}) => void;
|
|
@@ -3706,6 +3715,13 @@ declare class VoiceChannel extends BaseChannel {
|
|
|
3706
3715
|
* Get active WebSocket connection for a conversation
|
|
3707
3716
|
*/
|
|
3708
3717
|
getWebsocket(conversationId: ConversationId): WebSocket | null;
|
|
3718
|
+
/**
|
|
3719
|
+
* Poll Conversation Orchestrator for the conversation ConversationRelay
|
|
3720
|
+
* created for `callSid`, then register the local session and WebSocket.
|
|
3721
|
+
* Runs in the background from `setup`, so those can exist before the
|
|
3722
|
+
* caller speaks.
|
|
3723
|
+
*/
|
|
3724
|
+
private initializeOrchestratedConversation;
|
|
3709
3725
|
/**
|
|
3710
3726
|
* Handle WebSocket connection from ConversationRelay
|
|
3711
3727
|
*/
|
|
@@ -3860,8 +3876,8 @@ declare class VoiceChannel extends BaseChannel {
|
|
|
3860
3876
|
*
|
|
3861
3877
|
* Places an outbound call with inline TwiML that connects to ConversationRelay.
|
|
3862
3878
|
* The conversationConfiguration attribute tells CO to create and manage the
|
|
3863
|
-
* conversation during passive hydration. The session is initialized
|
|
3864
|
-
*
|
|
3879
|
+
* conversation during passive hydration. The session is initialized when the
|
|
3880
|
+
* background callSid lookup started at WebSocket setup finds it.
|
|
3865
3881
|
*
|
|
3866
3882
|
* TwiML fields are merged per-field, highest precedence first:
|
|
3867
3883
|
* 1. `options.twimlOptions` — per-call overrides
|
|
@@ -3959,9 +3975,8 @@ declare class VoiceChannel extends BaseChannel {
|
|
|
3959
3975
|
/**
|
|
3960
3976
|
* Hang up a call and clean up its ConversationRelay session.
|
|
3961
3977
|
*
|
|
3962
|
-
* Works on `callSid` alone,
|
|
3963
|
-
*
|
|
3964
|
-
* cleanup no-ops if no tracked session matches.
|
|
3978
|
+
* Works on `callSid` alone, whether or not a session exists yet. No-ops the
|
|
3979
|
+
* session cleanup if none is tracked.
|
|
3965
3980
|
*
|
|
3966
3981
|
* Does not throw — hanging up an already-ended call is routine (the callee
|
|
3967
3982
|
* hangs up while AMD is still resolving), and handlers shouldn't have to
|
|
@@ -3981,11 +3996,10 @@ declare class VoiceChannel extends BaseChannel {
|
|
|
3981
3996
|
* keyed by conversation id: the Orchestrator conversation id in orchestrator
|
|
3982
3997
|
* mode, the CallSid only in ConversationRelay-only mode.
|
|
3983
3998
|
*
|
|
3984
|
-
*
|
|
3985
|
-
*
|
|
3986
|
-
*
|
|
3987
|
-
*
|
|
3988
|
-
* needs no session.
|
|
3999
|
+
* Relay-only mode creates the session on the first prompt; orchestrated
|
|
4000
|
+
* mode creates it when the lookup started at setup finishes, so it may
|
|
4001
|
+
* exist before the caller speaks — including before `onAmd` fires. Treat it
|
|
4002
|
+
* as racy and hang up with {@link endCall}, which needs no session.
|
|
3989
4003
|
*
|
|
3990
4004
|
* At the other end, orchestrator mode keeps the session until Conversation
|
|
3991
4005
|
* Orchestrator's CLOSED webhook, so it outlives the call and `onCallStatus` /
|
|
@@ -4004,8 +4018,8 @@ declare class VoiceChannel extends BaseChannel {
|
|
|
4004
4018
|
*
|
|
4005
4019
|
* @param callSid - Twilio Call SID, e.g. from
|
|
4006
4020
|
* `InitiateVoiceConversationResult.callSid` or a call event.
|
|
4007
|
-
* @returns The session, or `undefined` —
|
|
4008
|
-
*
|
|
4021
|
+
* @returns The session, or `undefined` — not created yet, the call ended, or
|
|
4022
|
+
* it landed on another instance (see the horizontal-scaling note in
|
|
4009
4023
|
* CLAUDE.md).
|
|
4010
4024
|
*/
|
|
4011
4025
|
getConversationSessionByCallSid(callSid: string): ConversationSession | undefined;
|
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;
|
|
@@ -1429,7 +1425,7 @@ var TACConfig = class _TACConfig {
|
|
|
1429
1425
|
* Optional environment variables:
|
|
1430
1426
|
* - TWILIO_WHATSAPP_NUMBER: WhatsApp number for WhatsApp channel (e.g., 'whatsapp:+1234567890')
|
|
1431
1427
|
* - TWILIO_CONVERSATION_CONFIGURATION_ID: Conversation Orchestrator configuration ID (enables orchestrated mode)
|
|
1432
|
-
* - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice;
|
|
1428
|
+
* - 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
1429
|
* - TWILIO_VOICE_WEBSOCKET_PATH: Path for the voice WebSocket (default: /ws)
|
|
1434
1430
|
* - TWILIO_VOICE_ACTION_PATH: Path for the ConversationRelay action callback (default: /conversation-relay-callback)
|
|
1435
1431
|
* - TWILIO_VOICE_CALL_EVENT_PATH: Base path for the call-event callbacks — status, async AMD, recording (default: /twilio/call-events)
|
|
@@ -1685,7 +1681,7 @@ function createLogger(options) {
|
|
|
1685
1681
|
|
|
1686
1682
|
// package.json
|
|
1687
1683
|
var package_default = {
|
|
1688
|
-
version: "2.
|
|
1684
|
+
version: "2.2.0"};
|
|
1689
1685
|
function buildUserAgent() {
|
|
1690
1686
|
return `twilio-agent-connect-typescript/${package_default.version}`;
|
|
1691
1687
|
}
|
|
@@ -1854,7 +1850,9 @@ var MemoryClient = class _MemoryClient extends BaseClient {
|
|
|
1854
1850
|
profile_id: profileId,
|
|
1855
1851
|
observation_count: observations.length,
|
|
1856
1852
|
summary_count: summaries.length,
|
|
1857
|
-
communication_count: communications.length
|
|
1853
|
+
communication_count: communications.length,
|
|
1854
|
+
// Server-side query time; dominates /Recall when expansion runs.
|
|
1855
|
+
query_time_ms: response.meta?.queryTime
|
|
1858
1856
|
},
|
|
1859
1857
|
"Memory retrieval succeeded"
|
|
1860
1858
|
);
|
|
@@ -3045,6 +3043,10 @@ var TAC = class _TAC {
|
|
|
3045
3043
|
*
|
|
3046
3044
|
* @param session - Conversation session context
|
|
3047
3045
|
* @param query - Optional semantic search query
|
|
3046
|
+
* @param conversationId - Passed through to `/Recall` as-is. Sending one
|
|
3047
|
+
* without a `query` makes Memory infer one from that conversation's history
|
|
3048
|
+
* — an expensive server-side step — so leave it unset when there is no
|
|
3049
|
+
* per-turn topic (e.g. `"once"` mode's cache-priming fetch).
|
|
3048
3050
|
* @returns Promise containing TACMemoryResponse wrapper providing unified access to memory data.
|
|
3049
3051
|
*
|
|
3050
3052
|
* Attempts to retrieve from Memory API first:
|
|
@@ -3055,7 +3057,7 @@ var TAC = class _TAC {
|
|
|
3055
3057
|
* - observations and summaries are empty arrays
|
|
3056
3058
|
* - communications have basic fields only (no author name/type)
|
|
3057
3059
|
*/
|
|
3058
|
-
async retrieveMemory(session, query) {
|
|
3060
|
+
async retrieveMemory(session, query, conversationId) {
|
|
3059
3061
|
if (!this.isOrchestratorEnabled()) {
|
|
3060
3062
|
return new TACMemoryResponse([]);
|
|
3061
3063
|
}
|
|
@@ -3102,7 +3104,7 @@ var TAC = class _TAC {
|
|
|
3102
3104
|
throw new Error("Memory client is not available");
|
|
3103
3105
|
}
|
|
3104
3106
|
const memoryResponse = await this.memoryClient.retrieveMemories(session.profileId, {
|
|
3105
|
-
conversationId
|
|
3107
|
+
conversationId,
|
|
3106
3108
|
query,
|
|
3107
3109
|
observationsLimit: this.config.memoryConfig.observationsLimit,
|
|
3108
3110
|
summariesLimit: this.config.memoryConfig.summariesLimit,
|
|
@@ -3448,9 +3450,9 @@ var BaseChannel = class {
|
|
|
3448
3450
|
*
|
|
3449
3451
|
* Modes:
|
|
3450
3452
|
* - "always": Fetch with the provided query on every message.
|
|
3451
|
-
* - "once": Fetch once with
|
|
3452
|
-
*
|
|
3453
|
-
* INACTIVE transition.
|
|
3453
|
+
* - "once": Fetch once with neither a query nor a conversation id (skipping
|
|
3454
|
+
* query expansion) and cache the result on the session. Subsequent calls
|
|
3455
|
+
* reuse the cache until it is invalidated on the INACTIVE transition.
|
|
3454
3456
|
* - "never": Skip retrieval.
|
|
3455
3457
|
*
|
|
3456
3458
|
* Memory retrieval failures are logged and swallowed so message processing
|
|
@@ -3465,9 +3467,11 @@ var BaseChannel = class {
|
|
|
3465
3467
|
return session.cachedMemory;
|
|
3466
3468
|
}
|
|
3467
3469
|
try {
|
|
3470
|
+
const isAlways = this.memoryMode === "always";
|
|
3468
3471
|
const memory = await this.tac.retrieveMemory(
|
|
3469
3472
|
session,
|
|
3470
|
-
|
|
3473
|
+
isAlways ? query : void 0,
|
|
3474
|
+
isAlways ? session.conversationId : void 0
|
|
3471
3475
|
);
|
|
3472
3476
|
if (this.memoryMode === "once") {
|
|
3473
3477
|
session.cachedMemory = memory;
|
|
@@ -4673,6 +4677,9 @@ function studioVoiceHandoffUrl(accountSid, flowSid) {
|
|
|
4673
4677
|
|
|
4674
4678
|
// packages/core/src/channels/voice.ts
|
|
4675
4679
|
var DEFAULT_WELCOME_GREETING = "Hello! How can I assist you today?";
|
|
4680
|
+
var POLL_ATTEMPTS = 10;
|
|
4681
|
+
var POLL_BASE_DELAY_MS = 250;
|
|
4682
|
+
var POLL_MAX_DELAY_MS = 1500;
|
|
4676
4683
|
function stringifyParameterValue(value) {
|
|
4677
4684
|
if (typeof value === "object") {
|
|
4678
4685
|
return JSON.stringify(value);
|
|
@@ -4927,6 +4934,57 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4927
4934
|
getWebsocket(conversationId) {
|
|
4928
4935
|
return this.webSocketConnections.get(conversationId) || null;
|
|
4929
4936
|
}
|
|
4937
|
+
/**
|
|
4938
|
+
* Poll Conversation Orchestrator for the conversation ConversationRelay
|
|
4939
|
+
* created for `callSid`, then register the local session and WebSocket.
|
|
4940
|
+
* Runs in the background from `setup`, so those can exist before the
|
|
4941
|
+
* caller speaks.
|
|
4942
|
+
*/
|
|
4943
|
+
async initializeOrchestratedConversation(callSid, fromNumber, ws) {
|
|
4944
|
+
if (!this.conversationClient) {
|
|
4945
|
+
throw new Error("Conversation client is required in orchestrated mode");
|
|
4946
|
+
}
|
|
4947
|
+
let conversations = [];
|
|
4948
|
+
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
|
|
4949
|
+
conversations = await this.conversationClient.listConversations({
|
|
4950
|
+
channelId: callSid,
|
|
4951
|
+
status: ["ACTIVE"]
|
|
4952
|
+
});
|
|
4953
|
+
if (conversations.length === 1) break;
|
|
4954
|
+
if (attempt < POLL_ATTEMPTS - 1) {
|
|
4955
|
+
this.logger.debug(
|
|
4956
|
+
{ call_sid: callSid, attempt: attempt + 1, found: conversations.length },
|
|
4957
|
+
"Conversation not ready yet, polling again"
|
|
4958
|
+
);
|
|
4959
|
+
const delayMs = Math.min(POLL_BASE_DELAY_MS * 2 ** attempt, POLL_MAX_DELAY_MS);
|
|
4960
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
if (conversations.length !== 1) {
|
|
4964
|
+
throw new Error(
|
|
4965
|
+
`Expected exactly 1 conversation for callSid ${callSid}, but found ${conversations.length} after ${POLL_ATTEMPTS} attempts`
|
|
4966
|
+
);
|
|
4967
|
+
}
|
|
4968
|
+
const conversation = conversations[0];
|
|
4969
|
+
const conversationId = conversation.id;
|
|
4970
|
+
const participants = await this.conversationClient.listParticipants(conversationId);
|
|
4971
|
+
const customerParticipant = participants.find((p) => p.type === "CUSTOMER");
|
|
4972
|
+
const customerAddress = customerParticipant?.addresses?.find((a) => a.channel === "VOICE")?.address ?? fromNumber ?? void 0;
|
|
4973
|
+
const profileId = customerParticipant?.profileId ? customerParticipant.profileId : void 0;
|
|
4974
|
+
this.webSocketConnections.set(conversationId, ws);
|
|
4975
|
+
this.callSidToConversationId.set(callSid, conversationId);
|
|
4976
|
+
const session = this.startConversation(conversationId, profileId);
|
|
4977
|
+
session.callSid = callSid;
|
|
4978
|
+
if (customerAddress) {
|
|
4979
|
+
session.authorInfo = {
|
|
4980
|
+
address: customerAddress
|
|
4981
|
+
};
|
|
4982
|
+
}
|
|
4983
|
+
if (this.voiceCallbacks.onWebSocketConnected) {
|
|
4984
|
+
this.voiceCallbacks.onWebSocketConnected({ conversationId });
|
|
4985
|
+
}
|
|
4986
|
+
return conversationId;
|
|
4987
|
+
}
|
|
4930
4988
|
/**
|
|
4931
4989
|
* Handle WebSocket connection from ConversationRelay
|
|
4932
4990
|
*/
|
|
@@ -4935,6 +4993,7 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4935
4993
|
let callSid = null;
|
|
4936
4994
|
let fromNumber = null;
|
|
4937
4995
|
let initializationFailed = false;
|
|
4996
|
+
let initPromise = null;
|
|
4938
4997
|
ws.on("message", (data) => {
|
|
4939
4998
|
(async () => {
|
|
4940
4999
|
try {
|
|
@@ -4957,6 +5016,14 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4957
5016
|
case "setup":
|
|
4958
5017
|
callSid = message.callSid;
|
|
4959
5018
|
fromNumber = message.from;
|
|
5019
|
+
if (this.tac.isOrchestratorEnabled()) {
|
|
5020
|
+
this.logger.debug(
|
|
5021
|
+
{ call_sid: callSid },
|
|
5022
|
+
"Starting background conversation initialization"
|
|
5023
|
+
);
|
|
5024
|
+
initPromise = this.initializeOrchestratedConversation(callSid, fromNumber, ws);
|
|
5025
|
+
void initPromise.catch(() => void 0);
|
|
5026
|
+
}
|
|
4960
5027
|
if (this.voiceCallbacks.onSetup) {
|
|
4961
5028
|
this.voiceCallbacks.onSetup({
|
|
4962
5029
|
callSid,
|
|
@@ -4990,50 +5057,21 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
4990
5057
|
if (fromNumber) {
|
|
4991
5058
|
session.authorInfo = { address: fromNumber };
|
|
4992
5059
|
}
|
|
4993
|
-
|
|
4994
|
-
|
|
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
|
-
}
|
|
5060
|
+
if (this.voiceCallbacks.onWebSocketConnected) {
|
|
5061
|
+
this.voiceCallbacks.onWebSocketConnected({ conversationId });
|
|
5012
5062
|
}
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
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
|
-
};
|
|
5063
|
+
} else {
|
|
5064
|
+
initPromise ??= this.initializeOrchestratedConversation(
|
|
5065
|
+
callSid,
|
|
5066
|
+
fromNumber,
|
|
5067
|
+
ws
|
|
5068
|
+
);
|
|
5069
|
+
try {
|
|
5070
|
+
conversationId = await initPromise;
|
|
5071
|
+
} finally {
|
|
5072
|
+
initPromise = null;
|
|
5032
5073
|
}
|
|
5033
5074
|
}
|
|
5034
|
-
if (this.voiceCallbacks.onWebSocketConnected) {
|
|
5035
|
-
this.voiceCallbacks.onWebSocketConnected({ conversationId });
|
|
5036
|
-
}
|
|
5037
5075
|
initializationFailed = false;
|
|
5038
5076
|
this.initializationRetries.delete(callSid);
|
|
5039
5077
|
this.logger.info(
|
|
@@ -5090,6 +5128,19 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5090
5128
|
});
|
|
5091
5129
|
});
|
|
5092
5130
|
ws.on("close", () => {
|
|
5131
|
+
const pendingInit = initPromise;
|
|
5132
|
+
initPromise = null;
|
|
5133
|
+
if (pendingInit && !conversationId) {
|
|
5134
|
+
void pendingInit.then(async (adoptedId) => {
|
|
5135
|
+
await this.handleWebSocketDisconnect(adoptedId);
|
|
5136
|
+
if (callSid) this.callSidToConversationId.delete(callSid);
|
|
5137
|
+
}).catch((err) => {
|
|
5138
|
+
this.logger.error(
|
|
5139
|
+
{ err, call_sid: callSid },
|
|
5140
|
+
"Background conversation initialization failed after the call ended"
|
|
5141
|
+
);
|
|
5142
|
+
});
|
|
5143
|
+
}
|
|
5093
5144
|
if (conversationId) {
|
|
5094
5145
|
void this.handleWebSocketDisconnect(conversationId).catch((err) => {
|
|
5095
5146
|
this.logger.error(
|
|
@@ -5476,8 +5527,8 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5476
5527
|
*
|
|
5477
5528
|
* Places an outbound call with inline TwiML that connects to ConversationRelay.
|
|
5478
5529
|
* The conversationConfiguration attribute tells CO to create and manage the
|
|
5479
|
-
* conversation during passive hydration. The session is initialized
|
|
5480
|
-
*
|
|
5530
|
+
* conversation during passive hydration. The session is initialized when the
|
|
5531
|
+
* background callSid lookup started at WebSocket setup finds it.
|
|
5481
5532
|
*
|
|
5482
5533
|
* TwiML fields are merged per-field, highest precedence first:
|
|
5483
5534
|
* 1. `options.twimlOptions` — per-call overrides
|
|
@@ -5668,9 +5719,8 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5668
5719
|
/**
|
|
5669
5720
|
* Hang up a call and clean up its ConversationRelay session.
|
|
5670
5721
|
*
|
|
5671
|
-
* Works on `callSid` alone,
|
|
5672
|
-
*
|
|
5673
|
-
* cleanup no-ops if no tracked session matches.
|
|
5722
|
+
* Works on `callSid` alone, whether or not a session exists yet. No-ops the
|
|
5723
|
+
* session cleanup if none is tracked.
|
|
5674
5724
|
*
|
|
5675
5725
|
* Does not throw — hanging up an already-ended call is routine (the callee
|
|
5676
5726
|
* hangs up while AMD is still resolving), and handlers shouldn't have to
|
|
@@ -5704,11 +5754,10 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5704
5754
|
* keyed by conversation id: the Orchestrator conversation id in orchestrator
|
|
5705
5755
|
* mode, the CallSid only in ConversationRelay-only mode.
|
|
5706
5756
|
*
|
|
5707
|
-
*
|
|
5708
|
-
*
|
|
5709
|
-
*
|
|
5710
|
-
*
|
|
5711
|
-
* needs no session.
|
|
5757
|
+
* Relay-only mode creates the session on the first prompt; orchestrated
|
|
5758
|
+
* mode creates it when the lookup started at setup finishes, so it may
|
|
5759
|
+
* exist before the caller speaks — including before `onAmd` fires. Treat it
|
|
5760
|
+
* as racy and hang up with {@link endCall}, which needs no session.
|
|
5712
5761
|
*
|
|
5713
5762
|
* At the other end, orchestrator mode keeps the session until Conversation
|
|
5714
5763
|
* Orchestrator's CLOSED webhook, so it outlives the call and `onCallStatus` /
|
|
@@ -5727,8 +5776,8 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
|
5727
5776
|
*
|
|
5728
5777
|
* @param callSid - Twilio Call SID, e.g. from
|
|
5729
5778
|
* `InitiateVoiceConversationResult.callSid` or a call event.
|
|
5730
|
-
* @returns The session, or `undefined` —
|
|
5731
|
-
*
|
|
5779
|
+
* @returns The session, or `undefined` — not created yet, the call ended, or
|
|
5780
|
+
* it landed on another instance (see the horizontal-scaling note in
|
|
5732
5781
|
* CLAUDE.md).
|
|
5733
5782
|
*/
|
|
5734
5783
|
getConversationSessionByCallSid(callSid) {
|