twilio-agent-connect 1.0.2 → 2.0.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 +1013 -615
- package/dist/index.js +901 -226
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,6 +28,11 @@ var TwilioMemoryConfigSchema = z.object({
|
|
|
28
28
|
/** Trait field name within `phoneTraitGroup` that holds the phone identifier. */
|
|
29
29
|
phoneTraitField: z.string().default("phone")
|
|
30
30
|
});
|
|
31
|
+
var voicePathSchema = (defaultPath) => z.preprocess((v) => {
|
|
32
|
+
if (typeof v !== "string") return v;
|
|
33
|
+
const trimmed = v.trim();
|
|
34
|
+
return trimmed.length === 0 ? void 0 : trimmed;
|
|
35
|
+
}, z.string().startsWith("/", 'Path must start with "/"').default(defaultPath));
|
|
31
36
|
var TACConfigSchema = z.object({
|
|
32
37
|
accountSid: z.string().min(1, "Twilio Account SID is required"),
|
|
33
38
|
authToken: z.string().min(1, "Twilio Auth Token is required"),
|
|
@@ -41,10 +46,48 @@ var TACConfigSchema = z.object({
|
|
|
41
46
|
whatsappNumber: z.string().regex(/^whatsapp:\+\d+$/, "WhatsApp number must be in format: whatsapp:+1234567890").optional(),
|
|
42
47
|
memoryConfig: TwilioMemoryConfigSchema.prefault({}),
|
|
43
48
|
conversationConfigurationId: z.string().regex(/^conv_configuration_[0-9a-z]{26}$/, "Invalid Conversation Configuration ID format").optional(),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Public domain where voice routes are reachable (e.g. "abc123.ngrok.app").
|
|
51
|
+
* Used by VoiceChannel to construct the public WebSocket URL and
|
|
52
|
+
* ConversationRelay action URL. Required when using the Voice channel.
|
|
53
|
+
*
|
|
54
|
+
* Schemes (https://, wss://), surrounding whitespace, and trailing slashes
|
|
55
|
+
* are stripped automatically before validation — a naive copy-paste from a
|
|
56
|
+
* browser address bar like "https://example.ngrok.app/" is normalized to
|
|
57
|
+
* "example.ngrok.app" rather than rejected.
|
|
58
|
+
*/
|
|
59
|
+
voicePublicDomain: z.preprocess(
|
|
60
|
+
(v) => {
|
|
61
|
+
if (typeof v !== "string") return v;
|
|
62
|
+
let s = v.trim();
|
|
63
|
+
if (s.length === 0) return void 0;
|
|
64
|
+
for (const scheme of ["https://", "http://", "wss://", "ws://"]) {
|
|
65
|
+
if (s.toLowerCase().startsWith(scheme)) {
|
|
66
|
+
s = s.slice(scheme.length);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
s = s.replace(/\/+$/, "");
|
|
71
|
+
return s.length === 0 ? void 0 : s;
|
|
72
|
+
},
|
|
73
|
+
z.string().max(253, "Hostname too long (max 253 characters)").regex(
|
|
74
|
+
/^[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])?)*$/,
|
|
75
|
+
'Invalid hostname format. Must be a hostname without protocol, port, or path (e.g., "abc123.ngrok.app", "localhost", or "192.168.1.100")'
|
|
76
|
+
).optional()
|
|
47
77
|
).optional(),
|
|
78
|
+
/**
|
|
79
|
+
* Path the voice WebSocket is served at. Combined with voicePublicDomain to
|
|
80
|
+
* build the public WebSocket URL the voice channel hands to Twilio in TwiML;
|
|
81
|
+
* TACServer also registers its WebSocket route at this path. Override only if
|
|
82
|
+
* you mount the route at a non-default path. Must start with '/'.
|
|
83
|
+
*/
|
|
84
|
+
voiceWebsocketPath: voicePathSchema("/ws"),
|
|
85
|
+
/**
|
|
86
|
+
* Path the ConversationRelay action callback is served at. Same role as
|
|
87
|
+
* voiceWebsocketPath but for the `<Connect action=...>` cleanup callback.
|
|
88
|
+
* Must start with '/'.
|
|
89
|
+
*/
|
|
90
|
+
voiceActionPath: voicePathSchema("/conversation-relay-callback"),
|
|
48
91
|
cintelConfigurationId: z.string().optional(),
|
|
49
92
|
cintelObservationOperatorSid: z.string().optional(),
|
|
50
93
|
cintelSummaryOperatorSid: z.string().optional(),
|
|
@@ -79,6 +122,8 @@ var EnvironmentVariables = {
|
|
|
79
122
|
TWILIO_MEMORY_PHONE_TRAIT_FIELD: "TWILIO_MEMORY_PHONE_TRAIT_FIELD",
|
|
80
123
|
TWILIO_CONVERSATION_CONFIGURATION_ID: "TWILIO_CONVERSATION_CONFIGURATION_ID",
|
|
81
124
|
TWILIO_VOICE_PUBLIC_DOMAIN: "TWILIO_VOICE_PUBLIC_DOMAIN",
|
|
125
|
+
TWILIO_VOICE_WEBSOCKET_PATH: "TWILIO_VOICE_WEBSOCKET_PATH",
|
|
126
|
+
TWILIO_VOICE_ACTION_PATH: "TWILIO_VOICE_ACTION_PATH",
|
|
82
127
|
TWILIO_TAC_CI_CONFIGURATION_ID: "TWILIO_TAC_CI_CONFIGURATION_ID",
|
|
83
128
|
TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID: "TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID",
|
|
84
129
|
TWILIO_TAC_CI_SUMMARY_OPERATOR_SID: "TWILIO_TAC_CI_SUMMARY_OPERATOR_SID",
|
|
@@ -204,7 +249,12 @@ var ConversationSessionSchema = z.object({
|
|
|
204
249
|
* Pending handoff payload set by the handoff tool. Voice channel sends
|
|
205
250
|
* this as a WS "end" message after the LLM's final response.
|
|
206
251
|
*/
|
|
207
|
-
pendingHandoffData: PendingHandoffDataSchema.optional()
|
|
252
|
+
pendingHandoffData: PendingHandoffDataSchema.optional(),
|
|
253
|
+
/**
|
|
254
|
+
* Cached memory for "once" mode. Set on the first retrieval, cleared when
|
|
255
|
+
* the conversation becomes INACTIVE. Not persisted/serialized.
|
|
256
|
+
*/
|
|
257
|
+
cachedMemory: z.custom().optional()
|
|
208
258
|
});
|
|
209
259
|
function isConversationId(value) {
|
|
210
260
|
return value.length > 0;
|
|
@@ -486,7 +536,7 @@ var TACMemoryResponse = class {
|
|
|
486
536
|
return lines.length > 2 ? lines.join("\n") : null;
|
|
487
537
|
}
|
|
488
538
|
};
|
|
489
|
-
var MemoryModeSchema = z.enum(["always", "never"]);
|
|
539
|
+
var MemoryModeSchema = z.enum(["always", "never", "once"]);
|
|
490
540
|
var MessageDirectionSchema = z.enum(["inbound", "outbound"]);
|
|
491
541
|
var MemoryChannelTypeSchema = z.enum([
|
|
492
542
|
"VOICE",
|
|
@@ -710,6 +760,240 @@ var ConversationRelayConfigSchema = ConversationRelayAttributesSchema.extend({
|
|
|
710
760
|
/** Optional language configurations as child <Language> elements */
|
|
711
761
|
languages: z.array(LanguageAttributesSchema).optional()
|
|
712
762
|
});
|
|
763
|
+
var InterruptModeSchema = z.enum(["none", "dtmf", "speech", "any"]);
|
|
764
|
+
var LanguageConfigSchema = z.object({
|
|
765
|
+
/**
|
|
766
|
+
* Language code, e.g. 'es-MX'. Can be 'multi' for automatic language
|
|
767
|
+
* detection (requires ElevenLabs TTS and Deepgram STT).
|
|
768
|
+
*/
|
|
769
|
+
code: z.string(),
|
|
770
|
+
/** TTS voice name for this language */
|
|
771
|
+
voice: z.string().optional(),
|
|
772
|
+
/** TTS provider, e.g. 'google' */
|
|
773
|
+
ttsProvider: z.string().optional(),
|
|
774
|
+
/** Transcription provider, e.g. 'deepgram' */
|
|
775
|
+
transcriptionProvider: z.string().optional(),
|
|
776
|
+
/** Speech model for STT. Choices vary by transcriptionProvider. */
|
|
777
|
+
speechModel: z.string().optional()
|
|
778
|
+
});
|
|
779
|
+
var TwiMLOptionsSchema = z.object({
|
|
780
|
+
/** Custom parameters to pass to ConversationRelay as `<Parameter>` children */
|
|
781
|
+
customParameters: CustomParametersSchema.optional(),
|
|
782
|
+
/** Initial greeting message for the caller */
|
|
783
|
+
welcomeGreeting: z.string().optional(),
|
|
784
|
+
/**
|
|
785
|
+
* What caller input can interrupt the welcome greeting.
|
|
786
|
+
* Defaults to 'any' on Twilio.
|
|
787
|
+
*/
|
|
788
|
+
welcomeGreetingInterruptible: InterruptModeSchema.optional(),
|
|
789
|
+
/**
|
|
790
|
+
* URL for Twilio to request when the call ends (`<Connect action>`). Set to
|
|
791
|
+
* a non-empty URL, or leave unset. An explicit `undefined` suppresses the
|
|
792
|
+
* action entirely (see `VoiceChannel`'s actionUrl resolution); an empty
|
|
793
|
+
* string is rejected so it can't silently drop the action.
|
|
794
|
+
*/
|
|
795
|
+
actionUrl: z.string().min(1, "actionUrl must not be empty").optional(),
|
|
796
|
+
/**
|
|
797
|
+
* Conversation Service SID. When set, ConversationRelay will manage
|
|
798
|
+
* conversation creation and participants.
|
|
799
|
+
*/
|
|
800
|
+
conversationConfiguration: z.string().optional(),
|
|
801
|
+
/**
|
|
802
|
+
* ConversationRelay WebSocket URL (the `<ConversationRelay url=...>`
|
|
803
|
+
* attribute). Leave unset (the default) to use the URL the channel derives
|
|
804
|
+
* from `TACConfig.voicePublicDomain` + `voiceWebsocketPath`. Set it only for
|
|
805
|
+
* a per-call URL — e.g. an affinity-routed host that appends a token to the
|
|
806
|
+
* upgrade URL — typically from an `onInboundCallTwiml` customizer. Layers
|
|
807
|
+
* per-field like every other field. Must be non-empty when set.
|
|
808
|
+
*/
|
|
809
|
+
websocketUrl: z.string().min(1, "websocketUrl must not be empty").optional(),
|
|
810
|
+
// Language, TTS, STT
|
|
811
|
+
/**
|
|
812
|
+
* Language for both STT and TTS, e.g. 'en-US'. Equivalent to setting both
|
|
813
|
+
* ttsLanguage and transcriptionLanguage.
|
|
814
|
+
*/
|
|
815
|
+
language: z.string().optional(),
|
|
816
|
+
/** TTS language code; overrides `language` for TTS. */
|
|
817
|
+
ttsLanguage: z.string().optional(),
|
|
818
|
+
/**
|
|
819
|
+
* STT language code; overrides `language` for transcription. Can be
|
|
820
|
+
* 'multi' for automatic language detection (Deepgram only).
|
|
821
|
+
*/
|
|
822
|
+
transcriptionLanguage: z.string().optional(),
|
|
823
|
+
/** TTS voice name (choices vary by ttsProvider) */
|
|
824
|
+
voice: z.string().optional(),
|
|
825
|
+
/** TTS provider: 'Google', 'Amazon', or 'ElevenLabs'. Defaults to 'ElevenLabs'. */
|
|
826
|
+
ttsProvider: z.string().optional(),
|
|
827
|
+
/**
|
|
828
|
+
* STT provider: 'Google' or 'Deepgram'. Defaults to 'Deepgram' (or 'Google'
|
|
829
|
+
* for accounts that used ConversationRelay before 2025-09-12).
|
|
830
|
+
*/
|
|
831
|
+
transcriptionProvider: z.string().optional(),
|
|
832
|
+
/** Speech model for STT. Choices vary by transcriptionProvider. */
|
|
833
|
+
speechModel: z.string().optional(),
|
|
834
|
+
/**
|
|
835
|
+
* Text normalization for ElevenLabs TTS. Defaults to 'off'. 'auto' behaves
|
|
836
|
+
* like 'off' for ConversationRelay calls.
|
|
837
|
+
*/
|
|
838
|
+
elevenlabsTextNormalization: z.enum(["on", "auto", "off"]).optional(),
|
|
839
|
+
// Turn detection / interruption
|
|
840
|
+
/**
|
|
841
|
+
* Confidence required to finish a turn. Only applies with Deepgram + flux
|
|
842
|
+
* speech model. Twilio enforces the accepted range — see ConversationRelay docs.
|
|
843
|
+
*/
|
|
844
|
+
eotThreshold: z.number().optional(),
|
|
845
|
+
/**
|
|
846
|
+
* Send unfinalized prompts and eager end-of-turn events (last=false). Only
|
|
847
|
+
* applies with Deepgram + flux speech model.
|
|
848
|
+
*/
|
|
849
|
+
partialPrompts: z.boolean().optional(),
|
|
850
|
+
/**
|
|
851
|
+
* Use Deepgram Smart Format for transcription output. Defaults to true when
|
|
852
|
+
* transcriptionProvider='Deepgram'.
|
|
853
|
+
*/
|
|
854
|
+
deepgramSmartFormat: z.boolean().optional(),
|
|
855
|
+
/**
|
|
856
|
+
* Silence (ms) after speech before finalizing the prompt. Integer
|
|
857
|
+
* milliseconds or the literal 'auto' (the platform default). Twilio enforces
|
|
858
|
+
* the accepted range — see ConversationRelay docs.
|
|
859
|
+
*/
|
|
860
|
+
speechTimeout: z.union([z.number().int(), z.literal("auto")]).optional(),
|
|
861
|
+
/**
|
|
862
|
+
* What caller input interrupts TTS playback. Boolean accepted for backward
|
|
863
|
+
* compat: true='any', false='none'. Defaults to 'any'.
|
|
864
|
+
*/
|
|
865
|
+
interruptible: z.union([InterruptModeSchema, z.boolean()]).optional(),
|
|
866
|
+
/** How easily caller speech triggers an interrupt. Defaults to 'high'. */
|
|
867
|
+
interruptSensitivity: z.enum(["high", "medium", "low"]).optional(),
|
|
868
|
+
/**
|
|
869
|
+
* What caller input gets reported while the agent is speaking (independent
|
|
870
|
+
* of whether playback is interrupted). Defaults to 'none' since May 2025.
|
|
871
|
+
*/
|
|
872
|
+
reportInputDuringAgentSpeech: InterruptModeSchema.optional(),
|
|
873
|
+
/**
|
|
874
|
+
* Filter short conversational feedback ('yeah', 'uh-huh', …) so it doesn't
|
|
875
|
+
* interrupt the agent. Defaults to false.
|
|
876
|
+
*/
|
|
877
|
+
ignoreBackchannel: z.boolean().optional(),
|
|
878
|
+
/**
|
|
879
|
+
* Allow text tokens from the next talk cycle to interrupt the current one.
|
|
880
|
+
* Defaults to false.
|
|
881
|
+
*/
|
|
882
|
+
preemptible: z.boolean().optional(),
|
|
883
|
+
/** Emit DTMF keypress events over the WebSocket. */
|
|
884
|
+
dtmfDetection: z.boolean().optional(),
|
|
885
|
+
// Recognition hints / events / debug / intelligence
|
|
886
|
+
/**
|
|
887
|
+
* Comma-separated words/phrases likely to appear in speech. Capitalize
|
|
888
|
+
* proper nouns.
|
|
889
|
+
*/
|
|
890
|
+
hints: z.string().optional(),
|
|
891
|
+
/** Space-separated event subscriptions, e.g. 'speaker-events tokens-played'. */
|
|
892
|
+
events: z.string().optional(),
|
|
893
|
+
/**
|
|
894
|
+
* Debug subscription, e.g. 'debugging'. Note: 'speaker-events' and
|
|
895
|
+
* 'tokens-played' have moved to the `events` attribute — only use them here
|
|
896
|
+
* for backward compatibility.
|
|
897
|
+
*/
|
|
898
|
+
debug: z.string().optional(),
|
|
899
|
+
/**
|
|
900
|
+
* Conversation Intelligence (classic) Service SID or unique name for
|
|
901
|
+
* persisting transcripts and running Language Operators.
|
|
902
|
+
*/
|
|
903
|
+
intelligenceService: z.string().optional(),
|
|
904
|
+
// Nested <Language> children
|
|
905
|
+
/** Additional `<Language>` children for multi-language support */
|
|
906
|
+
languages: z.array(LanguageConfigSchema).optional(),
|
|
907
|
+
/**
|
|
908
|
+
* Escape hatch for ConversationRelay attributes not yet typed on this model.
|
|
909
|
+
* Keys are emitted as-is on `<ConversationRelay>`; Twilio's SDK converts
|
|
910
|
+
* snake_case to camelCase, lowercases bools to 'true'/'false', and
|
|
911
|
+
* stringifies numbers. Prefer a typed field when one exists — use `extra`
|
|
912
|
+
* only for newly-added Twilio attributes not yet in this SDK.
|
|
913
|
+
*/
|
|
914
|
+
extra: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional()
|
|
915
|
+
}).strict().superRefine((value, ctx) => {
|
|
916
|
+
if (!value.extra) return;
|
|
917
|
+
const typed = new Set(Object.keys(TwiMLOptionsShape).filter((k) => k !== "extra"));
|
|
918
|
+
const shadowed = Object.keys(value.extra).filter((k) => typed.has(k)).sort();
|
|
919
|
+
if (shadowed.length > 0) {
|
|
920
|
+
ctx.addIssue({
|
|
921
|
+
code: "custom",
|
|
922
|
+
path: ["extra"],
|
|
923
|
+
message: `TwiMLOptions.extra keys [${shadowed.join(", ")}] shadow typed fields. Set the typed field directly instead of using \`extra\`.`
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
var TwiMLOptionsShape = {
|
|
928
|
+
customParameters: true,
|
|
929
|
+
welcomeGreeting: true,
|
|
930
|
+
welcomeGreetingInterruptible: true,
|
|
931
|
+
actionUrl: true,
|
|
932
|
+
conversationConfiguration: true,
|
|
933
|
+
websocketUrl: true,
|
|
934
|
+
language: true,
|
|
935
|
+
ttsLanguage: true,
|
|
936
|
+
transcriptionLanguage: true,
|
|
937
|
+
voice: true,
|
|
938
|
+
ttsProvider: true,
|
|
939
|
+
transcriptionProvider: true,
|
|
940
|
+
speechModel: true,
|
|
941
|
+
elevenlabsTextNormalization: true,
|
|
942
|
+
eotThreshold: true,
|
|
943
|
+
partialPrompts: true,
|
|
944
|
+
deepgramSmartFormat: true,
|
|
945
|
+
speechTimeout: true,
|
|
946
|
+
interruptible: true,
|
|
947
|
+
interruptSensitivity: true,
|
|
948
|
+
reportInputDuringAgentSpeech: true,
|
|
949
|
+
ignoreBackchannel: true,
|
|
950
|
+
preemptible: true,
|
|
951
|
+
dtmfDetection: true,
|
|
952
|
+
hints: true,
|
|
953
|
+
events: true,
|
|
954
|
+
debug: true,
|
|
955
|
+
intelligenceService: true,
|
|
956
|
+
languages: true,
|
|
957
|
+
extra: true
|
|
958
|
+
};
|
|
959
|
+
var TwiMLRequestSchema = z.object({
|
|
960
|
+
from: z.string().optional(),
|
|
961
|
+
to: z.string().optional(),
|
|
962
|
+
callSid: z.string().optional(),
|
|
963
|
+
callerCountry: z.string().optional(),
|
|
964
|
+
callerState: z.string().optional(),
|
|
965
|
+
callerCity: z.string().optional(),
|
|
966
|
+
direction: z.string().optional(),
|
|
967
|
+
/**
|
|
968
|
+
* Any other fields from the Twilio webhook not captured above. Values are
|
|
969
|
+
* always strings here (webhook form fields are url-encoded), unlike
|
|
970
|
+
* TwiMLOptions.extra which accepts string | boolean | number for emitted
|
|
971
|
+
* TwiML attributes.
|
|
972
|
+
*/
|
|
973
|
+
extra: z.record(z.string(), z.string()).default({})
|
|
974
|
+
});
|
|
975
|
+
var TWIML_REQUEST_FORM_ALIASES = {
|
|
976
|
+
From: "from",
|
|
977
|
+
To: "to",
|
|
978
|
+
CallSid: "callSid",
|
|
979
|
+
CallerCountry: "callerCountry",
|
|
980
|
+
CallerState: "callerState",
|
|
981
|
+
CallerCity: "callerCity",
|
|
982
|
+
Direction: "direction"
|
|
983
|
+
};
|
|
984
|
+
function twiMLRequestFromForm(form) {
|
|
985
|
+
const known = {};
|
|
986
|
+
const extra = {};
|
|
987
|
+
for (const [key, value] of Object.entries(form)) {
|
|
988
|
+
const alias = TWIML_REQUEST_FORM_ALIASES[key];
|
|
989
|
+
if (alias) {
|
|
990
|
+
known[alias] = value;
|
|
991
|
+
} else {
|
|
992
|
+
extra[key] = value;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return TwiMLRequestSchema.parse({ ...known, extra });
|
|
996
|
+
}
|
|
713
997
|
var ConversationRelayCallbackPayloadSchema = z.object({
|
|
714
998
|
// Core Twilio identifiers (required)
|
|
715
999
|
AccountSid: z.string(),
|
|
@@ -744,9 +1028,9 @@ var ConversationRelayCallbackPayloadSchema = z.object({
|
|
|
744
1028
|
});
|
|
745
1029
|
var InitiateVoiceConversationOptionsSchema = z.object({
|
|
746
1030
|
to: z.string().min(1, "Recipient phone number is required"),
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
});
|
|
1031
|
+
websocketUrl: z.url().optional(),
|
|
1032
|
+
twimlOptions: TwiMLOptionsSchema.optional()
|
|
1033
|
+
}).strict();
|
|
750
1034
|
var JSONSchemaSchema = z.object({
|
|
751
1035
|
type: z.enum(["object", "string", "number", "boolean", "array"]),
|
|
752
1036
|
properties: z.record(z.string(), z.any()).optional(),
|
|
@@ -864,6 +1148,10 @@ var TACConfig = class _TACConfig {
|
|
|
864
1148
|
memoryConfig;
|
|
865
1149
|
conversationConfigurationId;
|
|
866
1150
|
voicePublicDomain;
|
|
1151
|
+
/** Path the voice WebSocket is served at (default '/ws'). */
|
|
1152
|
+
voiceWebsocketPath;
|
|
1153
|
+
/** Path the ConversationRelay action callback is served at (default '/conversation-relay-callback'). */
|
|
1154
|
+
voiceActionPath;
|
|
867
1155
|
cintelConfigurationId;
|
|
868
1156
|
cintelObservationOperatorSid;
|
|
869
1157
|
cintelSummaryOperatorSid;
|
|
@@ -897,6 +1185,8 @@ var TACConfig = class _TACConfig {
|
|
|
897
1185
|
if (validatedConfig.voicePublicDomain) {
|
|
898
1186
|
this.voicePublicDomain = validatedConfig.voicePublicDomain;
|
|
899
1187
|
}
|
|
1188
|
+
this.voiceWebsocketPath = validatedConfig.voiceWebsocketPath;
|
|
1189
|
+
this.voiceActionPath = validatedConfig.voiceActionPath;
|
|
900
1190
|
if (validatedConfig.cintelConfigurationId) {
|
|
901
1191
|
this.cintelConfigurationId = validatedConfig.cintelConfigurationId;
|
|
902
1192
|
}
|
|
@@ -926,7 +1216,9 @@ var TACConfig = class _TACConfig {
|
|
|
926
1216
|
* Optional environment variables:
|
|
927
1217
|
* - TWILIO_WHATSAPP_NUMBER: WhatsApp number for WhatsApp channel (e.g., 'whatsapp:+1234567890')
|
|
928
1218
|
* - TWILIO_CONVERSATION_CONFIGURATION_ID: Conversation Orchestrator configuration ID (enables orchestrated mode)
|
|
929
|
-
* - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice
|
|
1219
|
+
* - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice; domain only, without protocol/port/path, e.g., 'abc123.ngrok.app')
|
|
1220
|
+
* - TWILIO_VOICE_WEBSOCKET_PATH: Path for the voice WebSocket (default: /ws)
|
|
1221
|
+
* - TWILIO_VOICE_ACTION_PATH: Path for the ConversationRelay action callback (default: /conversation-relay-callback)
|
|
930
1222
|
* - TWILIO_REGION: Twilio region subdomain for API routing (e.g. transforms base URLs to `https://{product}.{region}.twilio.com`)
|
|
931
1223
|
* - TWILIO_STUDIO_HANDOFF_FLOW_SID: Studio Flow SID used by createStudioHandoffTool for human handoff
|
|
932
1224
|
* - TWILIO_RCS_SENDER_ID: RCS Sender ID for the RCS channel
|
|
@@ -1036,6 +1328,8 @@ var TACConfig = class _TACConfig {
|
|
|
1036
1328
|
},
|
|
1037
1329
|
conversationConfigurationId: process.env[EnvironmentVariables.TWILIO_CONVERSATION_CONFIGURATION_ID] || void 0,
|
|
1038
1330
|
voicePublicDomain: process.env[EnvironmentVariables.TWILIO_VOICE_PUBLIC_DOMAIN],
|
|
1331
|
+
voiceWebsocketPath: process.env[EnvironmentVariables.TWILIO_VOICE_WEBSOCKET_PATH] || void 0,
|
|
1332
|
+
voiceActionPath: process.env[EnvironmentVariables.TWILIO_VOICE_ACTION_PATH] || void 0,
|
|
1039
1333
|
cintelConfigurationId: process.env[EnvironmentVariables.TWILIO_TAC_CI_CONFIGURATION_ID],
|
|
1040
1334
|
cintelObservationOperatorSid: process.env[EnvironmentVariables.TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID],
|
|
1041
1335
|
cintelSummaryOperatorSid: process.env[EnvironmentVariables.TWILIO_TAC_CI_SUMMARY_OPERATOR_SID],
|
|
@@ -1150,7 +1444,7 @@ function createLogger(options) {
|
|
|
1150
1444
|
|
|
1151
1445
|
// package.json
|
|
1152
1446
|
var package_default = {
|
|
1153
|
-
version: "
|
|
1447
|
+
version: "2.0.0"};
|
|
1154
1448
|
function buildUserAgent() {
|
|
1155
1449
|
return `twilio-agent-connect-typescript/${package_default.version}`;
|
|
1156
1450
|
}
|
|
@@ -2643,6 +2937,8 @@ var BaseChannel = class {
|
|
|
2643
2937
|
activeConversations;
|
|
2644
2938
|
callbacks;
|
|
2645
2939
|
memoryMode;
|
|
2940
|
+
processedWebhookTokens;
|
|
2941
|
+
maxTrackedTokens;
|
|
2646
2942
|
constructor(tac, options) {
|
|
2647
2943
|
this.tac = tac;
|
|
2648
2944
|
this.config = tac.getConfig();
|
|
@@ -2654,9 +2950,17 @@ var BaseChannel = class {
|
|
|
2654
2950
|
const modeToValidate = providedMode === void 0 ? "never" : providedMode;
|
|
2655
2951
|
const parseResult = MemoryModeSchema.safeParse(modeToValidate);
|
|
2656
2952
|
if (!parseResult.success) {
|
|
2657
|
-
throw new Error(
|
|
2953
|
+
throw new Error(
|
|
2954
|
+
`Invalid memoryMode: "${modeToValidate}". Must be "always", "once", or "never".`
|
|
2955
|
+
);
|
|
2658
2956
|
}
|
|
2659
2957
|
this.memoryMode = parseResult.data;
|
|
2958
|
+
const capacity = options?.dedupCapacity ?? 1e4;
|
|
2959
|
+
if (capacity < 1 || !Number.isInteger(capacity)) {
|
|
2960
|
+
throw new Error("dedupCapacity must be a positive integer");
|
|
2961
|
+
}
|
|
2962
|
+
this.maxTrackedTokens = capacity;
|
|
2963
|
+
this.processedWebhookTokens = /* @__PURE__ */ new Set();
|
|
2660
2964
|
}
|
|
2661
2965
|
/**
|
|
2662
2966
|
* Register event callbacks
|
|
@@ -2774,6 +3078,55 @@ var BaseChannel = class {
|
|
|
2774
3078
|
}
|
|
2775
3079
|
}
|
|
2776
3080
|
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Check if a webhook has already been processed using Twilio's idempotency token.
|
|
3083
|
+
* Uses a sliding window with fixed capacity to track tokens (FIFO eviction).
|
|
3084
|
+
*
|
|
3085
|
+
* This is intentionally a single synchronous check-and-record to prevent race conditions
|
|
3086
|
+
* where a duplicate arrives while the first request is still awaiting async work.
|
|
3087
|
+
*/
|
|
3088
|
+
isDuplicateWebhook(idempotencyToken) {
|
|
3089
|
+
if (this.processedWebhookTokens.has(idempotencyToken)) {
|
|
3090
|
+
return true;
|
|
3091
|
+
}
|
|
3092
|
+
if (this.processedWebhookTokens.size >= this.maxTrackedTokens) {
|
|
3093
|
+
const oldest = this.processedWebhookTokens.values().next().value;
|
|
3094
|
+
this.processedWebhookTokens.delete(oldest);
|
|
3095
|
+
}
|
|
3096
|
+
this.processedWebhookTokens.add(idempotencyToken);
|
|
3097
|
+
return false;
|
|
3098
|
+
}
|
|
3099
|
+
/**
|
|
3100
|
+
* Remove an idempotency token from the deduplication cache.
|
|
3101
|
+
* Used when webhook processing fails and retries should not be blocked.
|
|
3102
|
+
*/
|
|
3103
|
+
removeWebhookToken(idempotencyToken) {
|
|
3104
|
+
this.processedWebhookTokens.delete(idempotencyToken);
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Self-filtering: check if webhook event belongs to this channel.
|
|
3108
|
+
*
|
|
3109
|
+
* - COMMUNICATION_CREATED: require author.channel matches this channel type
|
|
3110
|
+
* - CONVERSATION_UPDATED: only process if conversation is tracked locally
|
|
3111
|
+
* - Other events: pass through
|
|
3112
|
+
*/
|
|
3113
|
+
isEventForThisChannel(webhookData) {
|
|
3114
|
+
const eventType = webhookData.eventType;
|
|
3115
|
+
const authorChannel = webhookData.data?.author?.channel;
|
|
3116
|
+
if (eventType === "COMMUNICATION_CREATED") {
|
|
3117
|
+
if (!authorChannel) {
|
|
3118
|
+
return false;
|
|
3119
|
+
}
|
|
3120
|
+
return authorChannel === this.channelType.toUpperCase();
|
|
3121
|
+
}
|
|
3122
|
+
if (eventType === "CONVERSATION_UPDATED") {
|
|
3123
|
+
const conversationId = this.extractConversationId(webhookData);
|
|
3124
|
+
if (conversationId && !this.activeConversations.has(conversationId)) {
|
|
3125
|
+
return false;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
return true;
|
|
3129
|
+
}
|
|
2777
3130
|
/**
|
|
2778
3131
|
* Validate webhook payload (override in subclasses for specific validation)
|
|
2779
3132
|
*/
|
|
@@ -2781,21 +3134,109 @@ var BaseChannel = class {
|
|
|
2781
3134
|
return payload !== null && payload !== void 0;
|
|
2782
3135
|
}
|
|
2783
3136
|
/**
|
|
2784
|
-
*
|
|
3137
|
+
* Preprocess webhook before handling event-specific logic.
|
|
3138
|
+
* Handles deduplication, validation, filtering, and data extraction.
|
|
3139
|
+
*
|
|
3140
|
+
* @param payload - Raw webhook payload from Twilio
|
|
3141
|
+
* @param idempotencyToken - Optional idempotency token for deduplication
|
|
3142
|
+
* @returns Preprocessed webhook data, or null if webhook should be skipped
|
|
3143
|
+
*/
|
|
3144
|
+
preprocessWebhook(payload, idempotencyToken) {
|
|
3145
|
+
this.logger.debug({ operation: "webhook_processing" }, "Processing webhook");
|
|
3146
|
+
if (idempotencyToken && this.isDuplicateWebhook(idempotencyToken)) {
|
|
3147
|
+
this.logger.debug({ idempotency_token: idempotencyToken }, "Skipping duplicate webhook");
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
if (!this.validateWebhookPayload(payload)) {
|
|
3151
|
+
throw new Error("Invalid webhook payload");
|
|
3152
|
+
}
|
|
3153
|
+
const webhookData = payload;
|
|
3154
|
+
const eventType = webhookData.eventType;
|
|
3155
|
+
const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
|
|
3156
|
+
if (!this.isEventForThisChannel(webhookData)) {
|
|
3157
|
+
this.logger.debug(
|
|
3158
|
+
{ event_type: eventType, channel: this.channelType, conversation_id: conversationId },
|
|
3159
|
+
"Ignoring event for different channel type"
|
|
3160
|
+
);
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
return { webhookData, eventType, conversationId };
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Extract conversation ID from webhook payload with type validation.
|
|
3167
|
+
*
|
|
3168
|
+
* Extracts conversationId from webhookData.data?.conversationId || webhookData.data?.id,
|
|
3169
|
+
* validates the value is a non-empty string, and ensures it passes isConversationId check.
|
|
3170
|
+
*
|
|
3171
|
+
* This prevents invalid IDs from causing downstream issues like incorrect Map key
|
|
3172
|
+
* matching in CONVERSATION_UPDATED self-filtering or propagating malformed IDs.
|
|
3173
|
+
*/
|
|
3174
|
+
extractConversationId(payload) {
|
|
3175
|
+
if (!payload || typeof payload !== "object") {
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
const webhookData = payload;
|
|
3179
|
+
const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
|
|
3180
|
+
if (conversationId && typeof conversationId === "string" && isConversationId(conversationId)) {
|
|
3181
|
+
return conversationId;
|
|
3182
|
+
}
|
|
3183
|
+
return null;
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* Extract profile ID from webhook payload with type validation.
|
|
3187
|
+
*
|
|
3188
|
+
* Extracts profileId from webhookData.data?.profileId,
|
|
3189
|
+
* validates the value is a non-empty string, and ensures it passes isProfileId check.
|
|
3190
|
+
*
|
|
3191
|
+
* This prevents invalid IDs from propagating downstream and ensures consistent
|
|
3192
|
+
* validation across all channel types.
|
|
3193
|
+
*/
|
|
3194
|
+
extractProfileId(payload) {
|
|
3195
|
+
if (!payload || typeof payload !== "object") {
|
|
3196
|
+
return null;
|
|
3197
|
+
}
|
|
3198
|
+
const webhookData = payload;
|
|
3199
|
+
const profileId = webhookData.data?.profileId;
|
|
3200
|
+
if (profileId && typeof profileId === "string" && isProfileId(profileId)) {
|
|
3201
|
+
return profileId;
|
|
3202
|
+
}
|
|
3203
|
+
return null;
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Retrieve memory according to the channel's memoryMode.
|
|
2785
3207
|
*
|
|
2786
3208
|
* This method handles the common logic for memory retrieval across all channels,
|
|
2787
|
-
* including error handling and debug logging.
|
|
2788
|
-
*
|
|
3209
|
+
* including error handling and debug logging.
|
|
3210
|
+
*
|
|
3211
|
+
* Modes:
|
|
3212
|
+
* - "always": Fetch with the provided query on every message.
|
|
3213
|
+
* - "once": Fetch once with an empty query and cache the result on the
|
|
3214
|
+
* session. Subsequent calls reuse the cache until it is invalidated on the
|
|
3215
|
+
* INACTIVE transition.
|
|
3216
|
+
* - "never": Skip retrieval.
|
|
3217
|
+
*
|
|
3218
|
+
* Memory retrieval failures are logged and swallowed so message processing
|
|
3219
|
+
* continues without memory context.
|
|
2789
3220
|
*/
|
|
2790
3221
|
async retrieveMemoryIfEnabled(session, query) {
|
|
2791
|
-
if (this.memoryMode !== "always") {
|
|
3222
|
+
if (this.memoryMode !== "always" && this.memoryMode !== "once") {
|
|
2792
3223
|
return void 0;
|
|
2793
3224
|
}
|
|
3225
|
+
if (this.memoryMode === "once" && session.cachedMemory !== void 0) {
|
|
3226
|
+
this.logger.debug({ conversation_id: session.conversationId }, "Reusing cached memory");
|
|
3227
|
+
return session.cachedMemory;
|
|
3228
|
+
}
|
|
2794
3229
|
try {
|
|
2795
|
-
const memory = await this.tac.retrieveMemory(
|
|
3230
|
+
const memory = await this.tac.retrieveMemory(
|
|
3231
|
+
session,
|
|
3232
|
+
this.memoryMode === "always" ? query : void 0
|
|
3233
|
+
);
|
|
3234
|
+
if (this.memoryMode === "once") {
|
|
3235
|
+
session.cachedMemory = memory;
|
|
3236
|
+
}
|
|
2796
3237
|
this.logger.debug(
|
|
2797
|
-
{ conversation_id: session.conversationId },
|
|
2798
|
-
"Memory retrieved
|
|
3238
|
+
{ conversation_id: session.conversationId, memory_mode: this.memoryMode },
|
|
3239
|
+
"Memory retrieved"
|
|
2799
3240
|
);
|
|
2800
3241
|
return memory;
|
|
2801
3242
|
} catch (error) {
|
|
@@ -2806,6 +3247,25 @@ var BaseChannel = class {
|
|
|
2806
3247
|
return void 0;
|
|
2807
3248
|
}
|
|
2808
3249
|
}
|
|
3250
|
+
/**
|
|
3251
|
+
* Invalidate cached memory for "once" mode when a conversation becomes
|
|
3252
|
+
* INACTIVE. Conversation Orchestrator updates memory on the INACTIVE
|
|
3253
|
+
* transition, so the next message re-fetches fresh memory. No-op for other
|
|
3254
|
+
* memory modes.
|
|
3255
|
+
*/
|
|
3256
|
+
invalidateCachedMemory(conversationId) {
|
|
3257
|
+
if (this.memoryMode !== "once") {
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
const session = this.activeConversations.get(conversationId);
|
|
3261
|
+
if (session?.cachedMemory !== void 0) {
|
|
3262
|
+
session.cachedMemory = void 0;
|
|
3263
|
+
this.logger.debug(
|
|
3264
|
+
{ conversation_id: conversationId },
|
|
3265
|
+
"Invalidated cached memory on INACTIVE status"
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
2809
3269
|
/**
|
|
2810
3270
|
* Cleanup resources when shutting down
|
|
2811
3271
|
*/
|
|
@@ -2823,12 +3283,9 @@ var CHANNEL_IDENTITY_TYPES = {
|
|
|
2823
3283
|
RCS: "rcs",
|
|
2824
3284
|
WHATSAPP: "whatsapp"
|
|
2825
3285
|
};
|
|
2826
|
-
var DEFAULT_DEDUP_CAPACITY = 1e4;
|
|
2827
3286
|
var MessagingChannel = class extends BaseChannel {
|
|
2828
3287
|
conversationClient;
|
|
2829
3288
|
messagingCallbacks;
|
|
2830
|
-
processedTokens = /* @__PURE__ */ new Set();
|
|
2831
|
-
maxTrackedTokens;
|
|
2832
3289
|
/**
|
|
2833
3290
|
* Controls whether reconciliation promotes an UNKNOWN customer-side
|
|
2834
3291
|
* participant to CUSTOMER. Subclasses override to opt out (e.g. chat).
|
|
@@ -2843,28 +3300,6 @@ var MessagingChannel = class extends BaseChannel {
|
|
|
2843
3300
|
}
|
|
2844
3301
|
this.conversationClient = tac.getConversationClient();
|
|
2845
3302
|
this.messagingCallbacks = {};
|
|
2846
|
-
const capacity = config?.dedupCapacity ?? DEFAULT_DEDUP_CAPACITY;
|
|
2847
|
-
if (capacity < 1 || !Number.isInteger(capacity)) {
|
|
2848
|
-
throw new Error("dedupCapacity must be a positive integer");
|
|
2849
|
-
}
|
|
2850
|
-
this.maxTrackedTokens = capacity;
|
|
2851
|
-
}
|
|
2852
|
-
/**
|
|
2853
|
-
* Check if a webhook has already been processed, and if not, record the token immediately.
|
|
2854
|
-
* This is intentionally a single synchronous check-and-record to prevent race conditions
|
|
2855
|
-
* where a duplicate arrives while the first request is still awaiting async work.
|
|
2856
|
-
* Uses a sliding window with FIFO eviction at capacity.
|
|
2857
|
-
*/
|
|
2858
|
-
isDuplicateWebhook(idempotencyToken) {
|
|
2859
|
-
if (this.processedTokens.has(idempotencyToken)) {
|
|
2860
|
-
return true;
|
|
2861
|
-
}
|
|
2862
|
-
if (this.processedTokens.size >= this.maxTrackedTokens) {
|
|
2863
|
-
const oldest = this.processedTokens.values().next().value;
|
|
2864
|
-
this.processedTokens.delete(oldest);
|
|
2865
|
-
}
|
|
2866
|
-
this.processedTokens.add(idempotencyToken);
|
|
2867
|
-
return false;
|
|
2868
3303
|
}
|
|
2869
3304
|
/**
|
|
2870
3305
|
* Check if a message is from the bot itself (2-tier).
|
|
@@ -2910,57 +3345,15 @@ var MessagingChannel = class extends BaseChannel {
|
|
|
2910
3345
|
}
|
|
2911
3346
|
}
|
|
2912
3347
|
/**
|
|
2913
|
-
*
|
|
2914
|
-
* Returns false if the event is clearly for a different channel type.
|
|
2915
|
-
*/
|
|
2916
|
-
isEventForThisChannel(webhookData) {
|
|
2917
|
-
const eventType = webhookData.eventType;
|
|
2918
|
-
const authorChannel = webhookData.data?.author?.channel;
|
|
2919
|
-
if (eventType === "COMMUNICATION_CREATED") {
|
|
2920
|
-
if (!authorChannel) {
|
|
2921
|
-
return false;
|
|
2922
|
-
}
|
|
2923
|
-
return authorChannel === this.channelType.toUpperCase();
|
|
2924
|
-
}
|
|
2925
|
-
if (eventType === "CONVERSATION_UPDATED") {
|
|
2926
|
-
const conversationId = this.extractConversationId(webhookData);
|
|
2927
|
-
if (conversationId && !this.isConversationActive(conversationId)) {
|
|
2928
|
-
return false;
|
|
2929
|
-
}
|
|
2930
|
-
}
|
|
2931
|
-
return true;
|
|
2932
|
-
}
|
|
2933
|
-
/**
|
|
2934
|
-
* Process messaging channel webhook from Twilio Conversations Service
|
|
3348
|
+
* Process messaging channel webhook from Conversation Orchestrator
|
|
2935
3349
|
*/
|
|
2936
3350
|
async processWebhook(payload, idempotencyToken) {
|
|
2937
|
-
this.logger.debug({ operation: "webhook_processing" }, "Processing webhook");
|
|
2938
3351
|
try {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
return;
|
|
2942
|
-
}
|
|
2943
|
-
if (!this.validateWebhookPayload(payload)) {
|
|
2944
|
-
throw new Error("Invalid webhook payload");
|
|
2945
|
-
}
|
|
2946
|
-
const webhookData = payload;
|
|
2947
|
-
const eventType = webhookData.eventType;
|
|
2948
|
-
const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
|
|
2949
|
-
if (!this.isEventForThisChannel(webhookData)) {
|
|
2950
|
-
this.logger.debug(
|
|
2951
|
-
{ event_type: eventType, channel: this.channelType, conversation_id: conversationId },
|
|
2952
|
-
"Ignoring event for different channel type"
|
|
2953
|
-
);
|
|
3352
|
+
const result = this.preprocessWebhook(payload, idempotencyToken);
|
|
3353
|
+
if (!result) {
|
|
2954
3354
|
return;
|
|
2955
3355
|
}
|
|
2956
|
-
|
|
2957
|
-
{
|
|
2958
|
-
event_type: eventType,
|
|
2959
|
-
raw_event_type: webhookData.eventType,
|
|
2960
|
-
conversation_id: conversationId
|
|
2961
|
-
},
|
|
2962
|
-
"Processing webhook event"
|
|
2963
|
-
);
|
|
3356
|
+
const { webhookData, eventType, conversationId } = result;
|
|
2964
3357
|
switch (eventType) {
|
|
2965
3358
|
case "CONVERSATION_CREATED":
|
|
2966
3359
|
this.logger.debug(
|
|
@@ -2993,12 +3386,8 @@ var MessagingChannel = class extends BaseChannel {
|
|
|
2993
3386
|
this.logger.debug({ event_type: eventType }, "Webhook processing completed");
|
|
2994
3387
|
} catch (error) {
|
|
2995
3388
|
if (idempotencyToken) {
|
|
2996
|
-
this.
|
|
3389
|
+
this.removeWebhookToken(idempotencyToken);
|
|
2997
3390
|
}
|
|
2998
|
-
this.logger.error(
|
|
2999
|
-
{ err: error, operation: "webhook_processing" },
|
|
3000
|
-
"Webhook processing error"
|
|
3001
|
-
);
|
|
3002
3391
|
this.handleError(error instanceof Error ? error : new Error(String(error)), { payload });
|
|
3003
3392
|
}
|
|
3004
3393
|
}
|
|
@@ -3150,43 +3539,15 @@ var MessagingChannel = class extends BaseChannel {
|
|
|
3150
3539
|
throw new Error("Missing conversation ID in conversation.updated event");
|
|
3151
3540
|
}
|
|
3152
3541
|
if (payload.data?.status === "CLOSED") {
|
|
3153
|
-
this.logger.
|
|
3542
|
+
this.logger.debug(
|
|
3154
3543
|
{ conversation_id: conversationId, status: payload.data.status },
|
|
3155
3544
|
"Conversation closed, cleaning up"
|
|
3156
3545
|
);
|
|
3157
3546
|
await this.endConversation(conversationId);
|
|
3547
|
+
} else if (payload.data?.status === "INACTIVE") {
|
|
3548
|
+
this.invalidateCachedMemory(conversationId);
|
|
3158
3549
|
}
|
|
3159
3550
|
}
|
|
3160
|
-
/**
|
|
3161
|
-
* Extract conversation ID from webhook payload
|
|
3162
|
-
*/
|
|
3163
|
-
extractConversationId(payload) {
|
|
3164
|
-
const webhookData = payload;
|
|
3165
|
-
const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
|
|
3166
|
-
if (conversationId && isConversationId(conversationId)) {
|
|
3167
|
-
return conversationId;
|
|
3168
|
-
}
|
|
3169
|
-
return null;
|
|
3170
|
-
}
|
|
3171
|
-
/**
|
|
3172
|
-
* Extract profile ID from webhook payload
|
|
3173
|
-
*/
|
|
3174
|
-
extractProfileId(payload) {
|
|
3175
|
-
const webhookData = payload;
|
|
3176
|
-
const profileId = webhookData.data?.profileId;
|
|
3177
|
-
if (profileId && isProfileId(profileId)) {
|
|
3178
|
-
this.logger.debug(
|
|
3179
|
-
{ profile_id: profileId, conversation_id: webhookData.data?.conversationId },
|
|
3180
|
-
"Extracted profile ID from webhook payload"
|
|
3181
|
-
);
|
|
3182
|
-
return profileId;
|
|
3183
|
-
}
|
|
3184
|
-
this.logger.debug(
|
|
3185
|
-
{ conversation_id: webhookData.data?.conversationId },
|
|
3186
|
-
"Profile ID missing or invalid in webhook payload"
|
|
3187
|
-
);
|
|
3188
|
-
return null;
|
|
3189
|
-
}
|
|
3190
3551
|
/**
|
|
3191
3552
|
* Validate messaging channel webhook payload structure
|
|
3192
3553
|
*/
|
|
@@ -4057,7 +4418,24 @@ var ChatChannel = class extends MessagingChannel {
|
|
|
4057
4418
|
});
|
|
4058
4419
|
}
|
|
4059
4420
|
};
|
|
4060
|
-
|
|
4421
|
+
|
|
4422
|
+
// packages/core/src/util/handoff-urls.ts
|
|
4423
|
+
function studioExecutionsUrl(flowSid) {
|
|
4424
|
+
return `https://studio.twilio.com/v2/Flows/${flowSid}/Executions`;
|
|
4425
|
+
}
|
|
4426
|
+
function studioVoiceHandoffUrl(accountSid, flowSid) {
|
|
4427
|
+
return `https://webhooks.twilio.com/v1/Accounts/${accountSid}/Flows/${flowSid}?Trigger=incomingCall`;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
// packages/core/src/channels/voice.ts
|
|
4431
|
+
var DEFAULT_WELCOME_GREETING = "Hello! How can I assist you today?";
|
|
4432
|
+
function stringifyParameterValue(value) {
|
|
4433
|
+
if (typeof value === "object") {
|
|
4434
|
+
return JSON.stringify(value);
|
|
4435
|
+
}
|
|
4436
|
+
return String(value);
|
|
4437
|
+
}
|
|
4438
|
+
var VoiceChannel = class _VoiceChannel extends BaseChannel {
|
|
4061
4439
|
webSocketConnections;
|
|
4062
4440
|
voiceCallbacks;
|
|
4063
4441
|
streamTasks;
|
|
@@ -4066,8 +4444,11 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4066
4444
|
callSidToConversationId;
|
|
4067
4445
|
MAX_INITIALIZATION_RETRIES = 3;
|
|
4068
4446
|
twilioClient;
|
|
4447
|
+
voiceConfig;
|
|
4448
|
+
onInboundCallTwimlHandler;
|
|
4069
4449
|
constructor(tac, options) {
|
|
4070
4450
|
super(tac, options);
|
|
4451
|
+
this.voiceConfig = options ?? {};
|
|
4071
4452
|
this.webSocketConnections = /* @__PURE__ */ new Map();
|
|
4072
4453
|
this.voiceCallbacks = {};
|
|
4073
4454
|
this.streamTasks = /* @__PURE__ */ new Map();
|
|
@@ -4075,6 +4456,56 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4075
4456
|
this.initializationRetries = /* @__PURE__ */ new Map();
|
|
4076
4457
|
this.callSidToConversationId = /* @__PURE__ */ new Map();
|
|
4077
4458
|
}
|
|
4459
|
+
/**
|
|
4460
|
+
* Register a callback that produces per-call overrides for the TwiML inside
|
|
4461
|
+
* `<ConversationRelay>` on inbound calls.
|
|
4462
|
+
*
|
|
4463
|
+
* The callback receives a framework-neutral {@link TwiMLRequest} (parsed from
|
|
4464
|
+
* the Twilio webhook form) and returns {@link TwiMLOptions}. Fields the
|
|
4465
|
+
* callback explicitly sets override `defaultTwimlOptions` and TAC defaults;
|
|
4466
|
+
* unset fields fall through.
|
|
4467
|
+
*
|
|
4468
|
+
* @example
|
|
4469
|
+
* ```typescript
|
|
4470
|
+
* voiceChannel.onInboundCallTwiml(async req => {
|
|
4471
|
+
* if (req.callerCountry === 'MX') {
|
|
4472
|
+
* return { language: 'es-MX', welcomeGreeting: '¡Hola!' };
|
|
4473
|
+
* }
|
|
4474
|
+
* return {};
|
|
4475
|
+
* });
|
|
4476
|
+
* ```
|
|
4477
|
+
*
|
|
4478
|
+
* Outbound calls don't use this — pass per-call TwiML via
|
|
4479
|
+
* `InitiateVoiceConversationOptions.twimlOptions` directly.
|
|
4480
|
+
*/
|
|
4481
|
+
onInboundCallTwiml(callback) {
|
|
4482
|
+
this.onInboundCallTwimlHandler = callback;
|
|
4483
|
+
}
|
|
4484
|
+
/**
|
|
4485
|
+
* Resolve the public WebSocket URL from `TACConfig.voicePublicDomain` +
|
|
4486
|
+
* `TACConfig.voiceWebsocketPath`. Throws if `voicePublicDomain` isn't set.
|
|
4487
|
+
*/
|
|
4488
|
+
resolveWebsocketUrl(action) {
|
|
4489
|
+
if (this.config.voicePublicDomain) {
|
|
4490
|
+
return `wss://${this.config.voicePublicDomain}${this.config.voiceWebsocketPath}`;
|
|
4491
|
+
}
|
|
4492
|
+
throw new Error(
|
|
4493
|
+
`${action} needs a WebSocket URL. Set TWILIO_VOICE_PUBLIC_DOMAIN (or TACConfig.voicePublicDomain).`
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
/**
|
|
4497
|
+
* Resolve the default `<Connect action=...>` cleanup URL.
|
|
4498
|
+
*
|
|
4499
|
+
* Returns undefined if `voicePublicDomain` isn't set; that's fine because
|
|
4500
|
+
* actionUrl has higher-priority layers (customizer, twimlOptions, Studio
|
|
4501
|
+
* handoff) above this fallback.
|
|
4502
|
+
*/
|
|
4503
|
+
resolveDefaultActionUrl() {
|
|
4504
|
+
if (this.config.voicePublicDomain) {
|
|
4505
|
+
return `https://${this.config.voicePublicDomain}${this.config.voiceActionPath}`;
|
|
4506
|
+
}
|
|
4507
|
+
return void 0;
|
|
4508
|
+
}
|
|
4078
4509
|
getTwilioClient() {
|
|
4079
4510
|
if (!this.twilioClient) {
|
|
4080
4511
|
this.twilioClient = twilio(this.config.apiKey, this.config.apiSecret, {
|
|
@@ -4113,12 +4544,67 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4113
4544
|
}
|
|
4114
4545
|
}
|
|
4115
4546
|
/**
|
|
4116
|
-
* Process
|
|
4117
|
-
*
|
|
4547
|
+
* Process conversation webhooks for cleanup.
|
|
4548
|
+
*
|
|
4549
|
+
* Voice channel processes CONVERSATION_UPDATED events:
|
|
4550
|
+
* - CLOSED status: Clean up local session state
|
|
4551
|
+
*
|
|
4552
|
+
* Note: Conversation tracking uses instance-local memory. In multi-instance
|
|
4553
|
+
* deployments, webhooks may route to a different instance, preventing cleanup.
|
|
4554
|
+
*
|
|
4555
|
+
* @param payload - Raw webhook event data from Twilio
|
|
4556
|
+
* @param idempotencyToken - Optional Twilio idempotency token from request headers
|
|
4118
4557
|
*/
|
|
4119
|
-
processWebhook(
|
|
4120
|
-
|
|
4121
|
-
|
|
4558
|
+
async processWebhook(payload, idempotencyToken) {
|
|
4559
|
+
try {
|
|
4560
|
+
const result = this.preprocessWebhook(payload, idempotencyToken);
|
|
4561
|
+
if (!result) {
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
const { webhookData, eventType, conversationId } = result;
|
|
4565
|
+
switch (eventType) {
|
|
4566
|
+
case "CONVERSATION_UPDATED":
|
|
4567
|
+
this.logger.debug(
|
|
4568
|
+
{ conversation_id: conversationId, status: webhookData.data?.status },
|
|
4569
|
+
"Handling CONVERSATION_UPDATED"
|
|
4570
|
+
);
|
|
4571
|
+
await this.handleConversationUpdated(webhookData);
|
|
4572
|
+
break;
|
|
4573
|
+
default:
|
|
4574
|
+
this.logger.debug(
|
|
4575
|
+
{
|
|
4576
|
+
event_type: eventType,
|
|
4577
|
+
raw_event_type: webhookData.eventType,
|
|
4578
|
+
conversation_id: conversationId
|
|
4579
|
+
},
|
|
4580
|
+
"Unhandled event type - this event will be ignored"
|
|
4581
|
+
);
|
|
4582
|
+
}
|
|
4583
|
+
this.logger.debug({ event_type: eventType }, "Webhook processing completed");
|
|
4584
|
+
} catch (error) {
|
|
4585
|
+
if (idempotencyToken) {
|
|
4586
|
+
this.removeWebhookToken(idempotencyToken);
|
|
4587
|
+
}
|
|
4588
|
+
this.handleError(error instanceof Error ? error : new Error(String(error)), { payload });
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
/**
|
|
4592
|
+
* Handle conversation updated event
|
|
4593
|
+
*/
|
|
4594
|
+
async handleConversationUpdated(payload) {
|
|
4595
|
+
const conversationId = this.extractConversationId(payload);
|
|
4596
|
+
if (!conversationId) {
|
|
4597
|
+
throw new Error("Missing conversation ID in conversation.updated event");
|
|
4598
|
+
}
|
|
4599
|
+
if (payload.data?.status === "CLOSED") {
|
|
4600
|
+
this.logger.debug(
|
|
4601
|
+
{ conversation_id: conversationId, status: payload.data.status },
|
|
4602
|
+
"Conversation closed, cleaning up"
|
|
4603
|
+
);
|
|
4604
|
+
await this.endConversation(conversationId);
|
|
4605
|
+
} else if (payload.data?.status === "INACTIVE") {
|
|
4606
|
+
this.invalidateCachedMemory(conversationId);
|
|
4607
|
+
}
|
|
4122
4608
|
}
|
|
4123
4609
|
/**
|
|
4124
4610
|
* Get active WebSocket connection for a conversation
|
|
@@ -4358,7 +4844,9 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4358
4844
|
}
|
|
4359
4845
|
}
|
|
4360
4846
|
/**
|
|
4361
|
-
* Handle WebSocket disconnection
|
|
4847
|
+
* Handle WebSocket disconnection. In orchestrated mode the conversation stays
|
|
4848
|
+
* tracked until the CLOSED webhook (so a follow-up call can reuse it); in
|
|
4849
|
+
* voice-only mode there is no such webhook, so it ends here.
|
|
4362
4850
|
*/
|
|
4363
4851
|
async handleWebSocketDisconnect(conversationId) {
|
|
4364
4852
|
this.cancelStreamTask(conversationId);
|
|
@@ -4367,7 +4855,9 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4367
4855
|
if (this.voiceCallbacks.onWebSocketDisconnected) {
|
|
4368
4856
|
this.voiceCallbacks.onWebSocketDisconnected({ conversationId });
|
|
4369
4857
|
}
|
|
4370
|
-
|
|
4858
|
+
if (!this.tac.isOrchestratorEnabled()) {
|
|
4859
|
+
await this.endConversation(conversationId);
|
|
4860
|
+
}
|
|
4371
4861
|
}
|
|
4372
4862
|
/**
|
|
4373
4863
|
* Send voice response via WebSocket
|
|
@@ -4472,24 +4962,134 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4472
4962
|
// Incoming Call Handling
|
|
4473
4963
|
// =========================================================================
|
|
4474
4964
|
/**
|
|
4475
|
-
*
|
|
4965
|
+
* Generate the TwiML response for an incoming voice call.
|
|
4966
|
+
*
|
|
4967
|
+
* ConversationRelay automatically handles conversation creation and
|
|
4968
|
+
* participant management via the `conversationConfiguration` parameter.
|
|
4969
|
+
*
|
|
4970
|
+
* The WebSocket URL and default session-cleanup action URL are derived from
|
|
4971
|
+
* `TACConfig.voicePublicDomain` + `TACConfig.voiceWebsocketPath` /
|
|
4972
|
+
* `voiceActionPath`.
|
|
4973
|
+
*
|
|
4974
|
+
* TwiML fields are merged per-field, highest precedence first:
|
|
4975
|
+
* 1. Output of the customizer registered via
|
|
4976
|
+
* `VoiceChannel.onInboundCallTwiml(...)` if configured and `twimlRequest`
|
|
4977
|
+
* is given. (Application-owned.)
|
|
4978
|
+
* 2. `VoiceChannelConfig.defaultTwimlOptions` — per-channel defaults.
|
|
4979
|
+
* 3. `hostTwimlOptions` — per-call transport facts supplied by the host (the
|
|
4980
|
+
* code owning the route), e.g. a per-call `websocketUrl` with an affinity
|
|
4981
|
+
* token.
|
|
4982
|
+
* 4. TAC defaults: a fixed default welcomeGreeting, `conversationConfiguration`
|
|
4983
|
+
* from `TACConfig`, `actionUrl` resolved via Studio handoff (when
|
|
4984
|
+
* `studioHandoffFlowSid` is configured), else derived from
|
|
4985
|
+
* `TACConfig.voicePublicDomain` + `voiceActionPath`, and the `websocketUrl`
|
|
4986
|
+
* derived from `TACConfig.voicePublicDomain` + `voiceWebsocketPath`.
|
|
4476
4987
|
*
|
|
4477
|
-
*
|
|
4478
|
-
*
|
|
4988
|
+
* Fields not set at a layer fall through to lower layers. Arrays (`languages`)
|
|
4989
|
+
* and nested objects (`customParameters`) replace wholesale when set at a
|
|
4990
|
+
* higher-priority layer. `websocketUrl` falls back to the `TACConfig`-derived
|
|
4991
|
+
* URL if unset at every layer.
|
|
4479
4992
|
*
|
|
4480
|
-
* @param
|
|
4481
|
-
*
|
|
4993
|
+
* @param twimlRequest - Parsed Twilio webhook fields. Passed to the customizer
|
|
4994
|
+
* if one is configured on the channel.
|
|
4995
|
+
* @param options - Additional per-call inputs.
|
|
4996
|
+
* @param options.hostTwimlOptions - Per-call TwiML supplied by a custom
|
|
4997
|
+
* in-process host (e.g. an affinity-routed deployment injecting a per-call
|
|
4998
|
+
* `websocketUrl`), layered below `defaultTwimlOptions` and the application
|
|
4999
|
+
* customizer but above the TAC defaults.
|
|
5000
|
+
* @returns TwiML XML string for call connection.
|
|
4482
5001
|
*/
|
|
4483
|
-
handleIncomingCall(options) {
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
5002
|
+
async handleIncomingCall(twimlRequest, options) {
|
|
5003
|
+
let customized;
|
|
5004
|
+
if (this.onInboundCallTwimlHandler && twimlRequest) {
|
|
5005
|
+
customized = await this.onInboundCallTwimlHandler(twimlRequest);
|
|
5006
|
+
}
|
|
5007
|
+
const merged = this.buildTwimlOptions(options?.hostTwimlOptions, customized);
|
|
5008
|
+
const websocketUrl = merged.websocketUrl ?? this.resolveWebsocketUrl("handleIncomingCall");
|
|
5009
|
+
return this.generateTwiml(websocketUrl, merged);
|
|
5010
|
+
}
|
|
5011
|
+
/**
|
|
5012
|
+
* Layer TwiML options, lowest precedence first: TAC defaults → `host`
|
|
5013
|
+
* (calling host's per-call values) → channel `defaultTwimlOptions` → `perCall`
|
|
5014
|
+
* (application customizer output for inbound, or
|
|
5015
|
+
* `InitiateVoiceConversationOptions.twimlOptions` for outbound).
|
|
5016
|
+
*/
|
|
5017
|
+
buildTwimlOptions(host, perCall) {
|
|
5018
|
+
const merged = {
|
|
5019
|
+
welcomeGreeting: DEFAULT_WELCOME_GREETING,
|
|
5020
|
+
...this.tac.isOrchestratorEnabled() && this.config.conversationConfigurationId !== void 0 ? { conversationConfiguration: this.config.conversationConfigurationId } : {}
|
|
5021
|
+
};
|
|
5022
|
+
const resolvedActionUrl = this.resolveActionUrl(host, perCall);
|
|
5023
|
+
if (resolvedActionUrl !== void 0) {
|
|
5024
|
+
merged.actionUrl = resolvedActionUrl;
|
|
5025
|
+
}
|
|
5026
|
+
if (host) {
|
|
5027
|
+
this.overlayFields(merged, host);
|
|
5028
|
+
}
|
|
5029
|
+
if (this.voiceConfig.defaultTwimlOptions) {
|
|
5030
|
+
this.overlayFields(merged, this.voiceConfig.defaultTwimlOptions);
|
|
5031
|
+
}
|
|
5032
|
+
if (perCall) {
|
|
5033
|
+
this.overlayFields(merged, perCall);
|
|
5034
|
+
}
|
|
5035
|
+
return merged;
|
|
5036
|
+
}
|
|
5037
|
+
/**
|
|
5038
|
+
* Apply fields explicitly present on `source` onto `target`.
|
|
5039
|
+
*
|
|
5040
|
+
* Nested objects (`customParameters`), arrays (`languages`), and dicts
|
|
5041
|
+
* (`extra`) replace wholesale — there's no per-key merging.
|
|
5042
|
+
*
|
|
5043
|
+
* `actionUrl` is skipped here on purpose — it's resolved once via
|
|
5044
|
+
* `resolveActionUrl` looking at every layer at once, and that resolved value
|
|
5045
|
+
* is written into `target` before this overlay runs. Letting it through here
|
|
5046
|
+
* would let a higher-priority layer that didn't set actionUrl silently clobber
|
|
5047
|
+
* a lower layer that did.
|
|
5048
|
+
*
|
|
5049
|
+
* "Explicitly present" is detected via key presence (`key in source`), which
|
|
5050
|
+
* mirrors Python's `model_fields_set`: a key set to `undefined` is still
|
|
5051
|
+
* "present" and overrides lower layers, while an absent key falls through.
|
|
5052
|
+
*/
|
|
5053
|
+
overlayFields(target, source) {
|
|
5054
|
+
for (const key of Object.keys(source)) {
|
|
5055
|
+
if (key === "actionUrl") {
|
|
5056
|
+
continue;
|
|
5057
|
+
}
|
|
5058
|
+
target[key] = source[key];
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
/**
|
|
5062
|
+
* Resolve the TwiML `<Connect action=...>` URL.
|
|
5063
|
+
*
|
|
5064
|
+
* Precedence (highest to lowest):
|
|
5065
|
+
* 1. application customizer
|
|
5066
|
+
* 2. channel `defaultTwimlOptions`
|
|
5067
|
+
* 3. `host` (calling host's per-call options)
|
|
5068
|
+
* 4. Studio handoff (when `studioHandoffFlowSid` is configured)
|
|
5069
|
+
* 5. Channel default — derived from `TACConfig.voicePublicDomain` +
|
|
5070
|
+
* `TACConfig.voiceActionPath`.
|
|
5071
|
+
*
|
|
5072
|
+
* User-expressed intent (Studio handoff is configured explicitly on
|
|
5073
|
+
* `TACConfig`) beats the SDK's generated cleanup default.
|
|
5074
|
+
*
|
|
5075
|
+
* Explicit `actionUrl: undefined` on a layer (key present, value undefined)
|
|
5076
|
+
* suppresses `<Connect action=...>` entirely — all lower layers are skipped.
|
|
5077
|
+
* `actionUrl` left absent (key not present) falls through to the next layer.
|
|
5078
|
+
*/
|
|
5079
|
+
resolveActionUrl(host, customized) {
|
|
5080
|
+
if (customized && "actionUrl" in customized) {
|
|
5081
|
+
return customized.actionUrl;
|
|
5082
|
+
}
|
|
5083
|
+
if (this.voiceConfig.defaultTwimlOptions && "actionUrl" in this.voiceConfig.defaultTwimlOptions) {
|
|
5084
|
+
return this.voiceConfig.defaultTwimlOptions.actionUrl;
|
|
5085
|
+
}
|
|
5086
|
+
if (host && "actionUrl" in host) {
|
|
5087
|
+
return host.actionUrl;
|
|
5088
|
+
}
|
|
5089
|
+
if (this.config.studioHandoffFlowSid) {
|
|
5090
|
+
return studioVoiceHandoffUrl(this.config.accountSid, this.config.studioHandoffFlowSid);
|
|
5091
|
+
}
|
|
5092
|
+
return this.resolveDefaultActionUrl();
|
|
4493
5093
|
}
|
|
4494
5094
|
// =========================================================================
|
|
4495
5095
|
// Outbound Call Handling
|
|
@@ -4502,10 +5102,16 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4502
5102
|
* conversation during passive hydration. The session is initialized lazily
|
|
4503
5103
|
* on the first prompt when the conversation is discovered by callSid.
|
|
4504
5104
|
*
|
|
4505
|
-
*
|
|
4506
|
-
*
|
|
4507
|
-
*
|
|
4508
|
-
*
|
|
5105
|
+
* TwiML fields are merged per-field, highest precedence first:
|
|
5106
|
+
* 1. `options.twimlOptions` — per-call overrides
|
|
5107
|
+
* 2. `VoiceChannelConfig.defaultTwimlOptions` — channel-wide defaults
|
|
5108
|
+
* 3. TAC defaults: welcome greeting, `conversationConfiguration` from
|
|
5109
|
+
* `TACConfig`, and `actionUrl` from Studio handoff (if configured), else
|
|
5110
|
+
* derived from `TACConfig.voicePublicDomain` + `voiceActionPath`.
|
|
5111
|
+
*
|
|
5112
|
+
* The WebSocket URL is derived from `TACConfig.voicePublicDomain` +
|
|
5113
|
+
* `TACConfig.voiceWebsocketPath`, unless overridden per-call via
|
|
5114
|
+
* `options.websocketUrl`.
|
|
4509
5115
|
*/
|
|
4510
5116
|
async initiateOutboundConversation(options) {
|
|
4511
5117
|
const validated = InitiateVoiceConversationOptionsSchema.parse(options);
|
|
@@ -4515,16 +5121,9 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4515
5121
|
"Initiating outbound voice conversation"
|
|
4516
5122
|
);
|
|
4517
5123
|
try {
|
|
4518
|
-
const
|
|
4519
|
-
const
|
|
4520
|
-
|
|
4521
|
-
...validated.conversationRelayConfig,
|
|
4522
|
-
...conversationConfiguration !== void 0 && { conversationConfiguration }
|
|
4523
|
-
},
|
|
4524
|
-
{
|
|
4525
|
-
...validated.actionUrl ? { actionUrl: validated.actionUrl } : {}
|
|
4526
|
-
}
|
|
4527
|
-
);
|
|
5124
|
+
const merged = this.buildTwimlOptions(void 0, validated.twimlOptions);
|
|
5125
|
+
const websocketUrl = validated.websocketUrl ?? merged.websocketUrl ?? this.resolveWebsocketUrl("initiateOutboundConversation");
|
|
5126
|
+
const twiml = this.generateTwiml(websocketUrl, merged);
|
|
4528
5127
|
const client = this.getTwilioClient();
|
|
4529
5128
|
const call = await client.calls.create({
|
|
4530
5129
|
to: validated.to,
|
|
@@ -4551,7 +5150,8 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4551
5150
|
// ConversationRelay Callback Handling
|
|
4552
5151
|
// =========================================================================
|
|
4553
5152
|
/**
|
|
4554
|
-
* Handle ConversationRelay callback from Twilio
|
|
5153
|
+
* Handle ConversationRelay callback from Twilio. Cleans up on call completion
|
|
5154
|
+
* in voice-only mode; in orchestrated mode the CO webhook owns cleanup.
|
|
4555
5155
|
*
|
|
4556
5156
|
* @param payload - Callback payload from Twilio
|
|
4557
5157
|
* @returns Response with status, content, and content type
|
|
@@ -4568,7 +5168,7 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4568
5168
|
);
|
|
4569
5169
|
return { status: 403, content: "Forbidden", contentType: "text/plain" };
|
|
4570
5170
|
}
|
|
4571
|
-
if (payload.CallStatus === "completed") {
|
|
5171
|
+
if (payload.CallStatus === "completed" && !this.tac.isOrchestratorEnabled()) {
|
|
4572
5172
|
const conversationId = this.callSidToConversationId.get(payload.CallSid);
|
|
4573
5173
|
if (conversationId) {
|
|
4574
5174
|
this.callSidToConversationId.delete(payload.CallSid);
|
|
@@ -4631,6 +5231,103 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4631
5231
|
// =========================================================================
|
|
4632
5232
|
// ConversationRelay TwiML Generation
|
|
4633
5233
|
// =========================================================================
|
|
5234
|
+
/**
|
|
5235
|
+
* Field names on {@link TwiMLOptions} that map directly to `<ConversationRelay>`
|
|
5236
|
+
* attributes (camelCase, emitted as-is). Excludes the fields handled specially
|
|
5237
|
+
* by {@link generateTwiml}: websocketUrl (resolved through the layered merge and
|
|
5238
|
+
* emitted as the `url` attribute), actionUrl, languages, customParameters, extra.
|
|
5239
|
+
*/
|
|
5240
|
+
static RELAY_ATTR_FIELDS = [
|
|
5241
|
+
"welcomeGreeting",
|
|
5242
|
+
"welcomeGreetingInterruptible",
|
|
5243
|
+
"conversationConfiguration",
|
|
5244
|
+
"language",
|
|
5245
|
+
"ttsLanguage",
|
|
5246
|
+
"transcriptionLanguage",
|
|
5247
|
+
"voice",
|
|
5248
|
+
"ttsProvider",
|
|
5249
|
+
"transcriptionProvider",
|
|
5250
|
+
"speechModel",
|
|
5251
|
+
"elevenlabsTextNormalization",
|
|
5252
|
+
"eotThreshold",
|
|
5253
|
+
"partialPrompts",
|
|
5254
|
+
"deepgramSmartFormat",
|
|
5255
|
+
"speechTimeout",
|
|
5256
|
+
"interruptible",
|
|
5257
|
+
"interruptSensitivity",
|
|
5258
|
+
"reportInputDuringAgentSpeech",
|
|
5259
|
+
"ignoreBackchannel",
|
|
5260
|
+
"preemptible",
|
|
5261
|
+
"dtmfDetection",
|
|
5262
|
+
"hints",
|
|
5263
|
+
"events",
|
|
5264
|
+
"debug",
|
|
5265
|
+
"intelligenceService"
|
|
5266
|
+
];
|
|
5267
|
+
/**
|
|
5268
|
+
* Generate TwiML XML for ConversationRelay from a merged {@link TwiMLOptions}.
|
|
5269
|
+
*
|
|
5270
|
+
* This is the low-level emitter used by `handleIncomingCall` and
|
|
5271
|
+
* `initiateOutboundConversation` after layering. It mirrors the Python SDK's
|
|
5272
|
+
* `generate_twiml`. The WebSocket URL may be passed as `websocketUrl` or via
|
|
5273
|
+
* `options.websocketUrl` (the explicit argument wins when both are given), so a
|
|
5274
|
+
* channel-less caller can pass everything in one object.
|
|
5275
|
+
*
|
|
5276
|
+
* @param websocketUrl - Public WebSocket URL (e.g. 'wss://example.ngrok.app/ws').
|
|
5277
|
+
* Optional if `options.websocketUrl` is set.
|
|
5278
|
+
* @param options - Merged TwiMLOptions to emit.
|
|
5279
|
+
* @returns TwiML XML string ready to return to Twilio.
|
|
5280
|
+
* @throws {Error} if no WebSocket URL is provided via either source.
|
|
5281
|
+
*/
|
|
5282
|
+
generateTwiml(websocketUrl, options) {
|
|
5283
|
+
const resolvedWebsocketUrl = websocketUrl || options.websocketUrl;
|
|
5284
|
+
if (!resolvedWebsocketUrl) {
|
|
5285
|
+
throw new Error(
|
|
5286
|
+
"generateTwiml requires a WebSocket URL \u2014 pass it explicitly or set options.websocketUrl."
|
|
5287
|
+
);
|
|
5288
|
+
}
|
|
5289
|
+
const response = new VoiceResponse();
|
|
5290
|
+
const connect = response.connect(options.actionUrl ? { action: options.actionUrl } : {});
|
|
5291
|
+
const relayAttrs = { url: resolvedWebsocketUrl };
|
|
5292
|
+
for (const field of _VoiceChannel.RELAY_ATTR_FIELDS) {
|
|
5293
|
+
let value = options[field];
|
|
5294
|
+
if (value === void 0) {
|
|
5295
|
+
continue;
|
|
5296
|
+
}
|
|
5297
|
+
if (field === "interruptible" && typeof value === "boolean") {
|
|
5298
|
+
value = value ? "any" : "none";
|
|
5299
|
+
}
|
|
5300
|
+
relayAttrs[field] = value;
|
|
5301
|
+
}
|
|
5302
|
+
if (options.extra) {
|
|
5303
|
+
for (const [key, value] of Object.entries(options.extra)) {
|
|
5304
|
+
if (key === "url") {
|
|
5305
|
+
this.logger.warn(
|
|
5306
|
+
"Ignoring `url` in TwiMLOptions.extra; set `websocketUrl` to override the ConversationRelay URL."
|
|
5307
|
+
);
|
|
5308
|
+
continue;
|
|
5309
|
+
}
|
|
5310
|
+
relayAttrs[key] = value;
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
const relay = connect.conversationRelay(
|
|
5314
|
+
relayAttrs
|
|
5315
|
+
);
|
|
5316
|
+
if (options.languages && options.languages.length > 0) {
|
|
5317
|
+
for (const lang of options.languages) {
|
|
5318
|
+
const langAttrs = this.filterUnsetValues(lang);
|
|
5319
|
+
relay.language(langAttrs);
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
if (options.customParameters) {
|
|
5323
|
+
for (const [name, value] of Object.entries(options.customParameters)) {
|
|
5324
|
+
if (value !== null && value !== void 0) {
|
|
5325
|
+
relay.parameter({ name, value: stringifyParameterValue(value) });
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
return response.toString();
|
|
5330
|
+
}
|
|
4634
5331
|
/**
|
|
4635
5332
|
* Generate TwiML to connect a call to ConversationRelay.
|
|
4636
5333
|
* Validates configuration with Zod before generating TwiML.
|
|
@@ -4678,18 +5375,6 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4678
5375
|
}
|
|
4679
5376
|
return filtered;
|
|
4680
5377
|
}
|
|
4681
|
-
/**
|
|
4682
|
-
* Extract conversation ID - Not applicable for Voice channel
|
|
4683
|
-
*/
|
|
4684
|
-
extractConversationId(_payload) {
|
|
4685
|
-
return null;
|
|
4686
|
-
}
|
|
4687
|
-
/**
|
|
4688
|
-
* Extract profile ID - Not applicable for Voice channel
|
|
4689
|
-
*/
|
|
4690
|
-
extractProfileId(_payload) {
|
|
4691
|
-
return null;
|
|
4692
|
-
}
|
|
4693
5378
|
/**
|
|
4694
5379
|
* Cleanup channel state on shutdown
|
|
4695
5380
|
*
|
|
@@ -4706,14 +5391,6 @@ var VoiceChannel = class extends BaseChannel {
|
|
|
4706
5391
|
}
|
|
4707
5392
|
};
|
|
4708
5393
|
|
|
4709
|
-
// packages/core/src/util/handoff-urls.ts
|
|
4710
|
-
function studioExecutionsUrl(flowSid) {
|
|
4711
|
-
return `https://studio.twilio.com/v2/Flows/${flowSid}/Executions`;
|
|
4712
|
-
}
|
|
4713
|
-
function studioVoiceHandoffUrl(accountSid, flowSid) {
|
|
4714
|
-
return `https://webhooks.twilio.com/v1/Accounts/${accountSid}/Flows/${flowSid}?Trigger=incomingCall`;
|
|
4715
|
-
}
|
|
4716
|
-
|
|
4717
5394
|
// packages/core/src/lib/conversation-session-helpers.ts
|
|
4718
5395
|
function formatTraitValue(value) {
|
|
4719
5396
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
@@ -5286,49 +5963,56 @@ var DEFAULT_CONFIG = {
|
|
|
5286
5963
|
host: "0.0.0.0",
|
|
5287
5964
|
port: 8e3,
|
|
5288
5965
|
webhookPaths: {
|
|
5289
|
-
|
|
5290
|
-
twiml: "/twiml"
|
|
5291
|
-
ws: "/ws",
|
|
5292
|
-
conversationRelayCallback: "/conversation-relay-callback"
|
|
5293
|
-
},
|
|
5294
|
-
conversationRelayConfig: {
|
|
5295
|
-
welcomeGreeting: "Hello! How can I assist you today?"
|
|
5966
|
+
conversation: "/webhook",
|
|
5967
|
+
twiml: "/twiml"
|
|
5296
5968
|
}
|
|
5297
5969
|
};
|
|
5298
5970
|
var TACServer = class {
|
|
5299
5971
|
fastify;
|
|
5300
5972
|
tac;
|
|
5301
5973
|
config;
|
|
5302
|
-
/** All enabled messaging channels
|
|
5974
|
+
/** All enabled messaging channels */
|
|
5303
5975
|
messagingChannels;
|
|
5304
5976
|
/** Voice channel instance */
|
|
5305
5977
|
voiceChannel;
|
|
5978
|
+
/** All channels that need webhook processing (voice + messaging) */
|
|
5979
|
+
webhookChannels;
|
|
5306
5980
|
constructor(tac, config = {}) {
|
|
5307
5981
|
this.tac = tac;
|
|
5982
|
+
const webhookPaths = { ...DEFAULT_CONFIG.webhookPaths, ...config.webhookPaths };
|
|
5983
|
+
if (config.webhookPaths?.messaging) {
|
|
5984
|
+
console.warn(
|
|
5985
|
+
'TACServer: The "webhookPaths.messaging" field is deprecated and will be removed in a future version. Please update your configuration to use "webhookPaths.conversation" instead.'
|
|
5986
|
+
);
|
|
5987
|
+
if (!config.webhookPaths.conversation) {
|
|
5988
|
+
webhookPaths.conversation = config.webhookPaths.messaging;
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5308
5991
|
this.config = {
|
|
5309
5992
|
...DEFAULT_CONFIG,
|
|
5310
5993
|
...config,
|
|
5311
|
-
|
|
5312
|
-
webhookPaths: {
|
|
5313
|
-
...DEFAULT_CONFIG.webhookPaths,
|
|
5314
|
-
...config.webhookPaths
|
|
5315
|
-
},
|
|
5316
|
-
// Deep merge conversationRelayConfig to preserve defaults while allowing overrides
|
|
5317
|
-
conversationRelayConfig: {
|
|
5318
|
-
...DEFAULT_CONFIG.conversationRelayConfig,
|
|
5319
|
-
...config.conversationRelayConfig
|
|
5320
|
-
}
|
|
5994
|
+
webhookPaths
|
|
5321
5995
|
};
|
|
5322
5996
|
this.voiceChannel = config.voiceChannel ?? tac.getChannel("voice");
|
|
5997
|
+
if (this.voiceChannel && !tac.getConfig().voicePublicDomain) {
|
|
5998
|
+
throw new Error(
|
|
5999
|
+
"Voice channel is configured but TACConfig.voicePublicDomain is not set. Set it directly or via the TWILIO_VOICE_PUBLIC_DOMAIN env var."
|
|
6000
|
+
);
|
|
6001
|
+
}
|
|
5323
6002
|
this.messagingChannels = config.messagingChannels ?? [
|
|
5324
6003
|
tac.getChannel("sms"),
|
|
5325
6004
|
tac.getChannel("rcs"),
|
|
5326
6005
|
tac.getChannel("chat"),
|
|
5327
6006
|
tac.getChannel("whatsapp")
|
|
5328
6007
|
].filter((ch) => ch != null);
|
|
5329
|
-
|
|
6008
|
+
this.webhookChannels = [];
|
|
6009
|
+
if (this.voiceChannel) {
|
|
6010
|
+
this.webhookChannels.push(this.voiceChannel);
|
|
6011
|
+
}
|
|
6012
|
+
this.webhookChannels.push(...this.messagingChannels);
|
|
6013
|
+
if (this.webhookChannels.length === 0) {
|
|
5330
6014
|
console.warn(
|
|
5331
|
-
|
|
6015
|
+
"TACServer: No channels configured for webhook processing. Register channels with TAC to enable webhooks."
|
|
5332
6016
|
);
|
|
5333
6017
|
}
|
|
5334
6018
|
if (config.fastifyInstance) {
|
|
@@ -5392,18 +6076,18 @@ var TACServer = class {
|
|
|
5392
6076
|
await this.validateRequestSignature(request, reply);
|
|
5393
6077
|
}
|
|
5394
6078
|
};
|
|
5395
|
-
if (this.
|
|
6079
|
+
if (this.webhookChannels.length > 0) {
|
|
5396
6080
|
this.fastify.post(
|
|
5397
|
-
this.config.webhookPaths.
|
|
6081
|
+
this.config.webhookPaths.conversation || "/webhook",
|
|
5398
6082
|
validateSignature,
|
|
5399
6083
|
async (request, reply) => {
|
|
5400
6084
|
const rawHeader = request.headers["i-twilio-idempotency-token"];
|
|
5401
6085
|
const idempotencyToken = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
5402
|
-
for (const channel of this.
|
|
6086
|
+
for (const channel of this.webhookChannels) {
|
|
5403
6087
|
channel.processWebhook(request.body, idempotencyToken).catch((err) => {
|
|
5404
6088
|
this.fastify.log.error(
|
|
5405
6089
|
{ err, channel: channel.channelType },
|
|
5406
|
-
"
|
|
6090
|
+
"Webhook processing error"
|
|
5407
6091
|
);
|
|
5408
6092
|
});
|
|
5409
6093
|
}
|
|
@@ -5421,18 +6105,9 @@ var TACServer = class {
|
|
|
5421
6105
|
return;
|
|
5422
6106
|
}
|
|
5423
6107
|
const voiceChannel = this.voiceChannel;
|
|
5424
|
-
const
|
|
5425
|
-
const
|
|
5426
|
-
const
|
|
5427
|
-
const tacConfig = this.tac.getConfig();
|
|
5428
|
-
const actionUrl = tacConfig.studioHandoffFlowSid ? studioVoiceHandoffUrl(tacConfig.accountSid, tacConfig.studioHandoffFlowSid) : `${protocol}://${host}${this.config.webhookPaths.conversationRelayCallback || "/conversation-relay-callback"}`;
|
|
5429
|
-
const twiml = voiceChannel.handleIncomingCall({
|
|
5430
|
-
actionUrl,
|
|
5431
|
-
conversationRelayConfig: {
|
|
5432
|
-
url: websocketUrl,
|
|
5433
|
-
...this.config.conversationRelayConfig
|
|
5434
|
-
}
|
|
5435
|
-
});
|
|
6108
|
+
const formDict = request.body ?? {};
|
|
6109
|
+
const twimlRequest = twiMLRequestFromForm(formDict);
|
|
6110
|
+
const twiml = await voiceChannel.handleIncomingCall(twimlRequest);
|
|
5436
6111
|
await reply.type("application/xml").send(twiml);
|
|
5437
6112
|
} catch (error) {
|
|
5438
6113
|
this.fastify.log.error(
|
|
@@ -5446,7 +6121,7 @@ var TACServer = class {
|
|
|
5446
6121
|
}
|
|
5447
6122
|
);
|
|
5448
6123
|
this.fastify.post(
|
|
5449
|
-
this.
|
|
6124
|
+
this.tac.getConfig().voiceActionPath,
|
|
5450
6125
|
validateSignature,
|
|
5451
6126
|
async (request, reply) => {
|
|
5452
6127
|
try {
|
|
@@ -5477,7 +6152,7 @@ var TACServer = class {
|
|
|
5477
6152
|
);
|
|
5478
6153
|
await this.fastify.register((fastify) => {
|
|
5479
6154
|
fastify.get(
|
|
5480
|
-
this.
|
|
6155
|
+
this.tac.getConfig().voiceWebsocketPath,
|
|
5481
6156
|
{ websocket: true },
|
|
5482
6157
|
(socket, request) => {
|
|
5483
6158
|
const signature = request.headers["x-twilio-signature"];
|
|
@@ -5594,10 +6269,10 @@ var TACServer = class {
|
|
|
5594
6269
|
{
|
|
5595
6270
|
host: this.config.host,
|
|
5596
6271
|
port: this.config.port,
|
|
5597
|
-
|
|
6272
|
+
conversation_webhook: this.config.webhookPaths.conversation,
|
|
5598
6273
|
twiml_webhook: this.config.webhookPaths.twiml,
|
|
5599
|
-
ws_websocket: this.
|
|
5600
|
-
conversation_relay_callback: this.
|
|
6274
|
+
ws_websocket: this.tac.getConfig().voiceWebsocketPath,
|
|
6275
|
+
conversation_relay_callback: this.tac.getConfig().voiceActionPath,
|
|
5601
6276
|
...this.config.webhookPaths.cintel && {
|
|
5602
6277
|
cintel_webhook: this.config.webhookPaths.cintel
|
|
5603
6278
|
}
|
|
@@ -5661,6 +6336,6 @@ var TACServer = class {
|
|
|
5661
6336
|
}
|
|
5662
6337
|
};
|
|
5663
6338
|
|
|
5664
|
-
export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, CustomParametersSchema, EMPTY_MEMORY_RESPONSE, EnvironmentVariables, ExecutionDetailsSchema, HandoffPayloadSchema, InitiateMessagingConversationOptionsSchema, InitiateVoiceConversationOptionsSchema, IntelligenceConfigurationSchema, InterruptMessageSchema, JSONSchemaSchema, KnowledgeBaseSchema, KnowledgeBaseStatusSchema, KnowledgeChunkResultSchema, KnowledgeClient, KnowledgeSearchResponseSchema, LanguageAttributesSchema, ListCommunicationsResponseSchema, ListConversationsResponseSchema, ListParticipantsResponseSchema, MemoryChannelTypeSchema, MemoryClient, MemoryCommunicationContentSchema, MemoryCommunicationSchema, MemoryDeliveryStatusSchema, MemoryModeSchema, MemoryParticipantSchema, MemoryParticipantTypeSchema, MemoryPromptBuilder, MemoryRetrievalRequestSchema, MemoryRetrievalResponseSchema, MessageDirectionSchema, MessagingChannel, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, 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, TwilioMemoryConfigSchema, VoiceChannel, WebSocketMessageSchema, WhatsAppChannel, buildHandoffPayload, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl };
|
|
6339
|
+
export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, 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, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, 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, buildHandoffPayload, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl, twiMLRequestFromForm };
|
|
5665
6340
|
//# sourceMappingURL=index.js.map
|
|
5666
6341
|
//# sourceMappingURL=index.js.map
|