twilio-agent-connect 1.0.3 → 2.0.1

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.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
- voicePublicDomain: z.string().max(253, "Hostname too long (max 253 characters)").regex(
45
- /^[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])?)*$/,
46
- 'Invalid hostname format. Must be a hostname without protocol, port, or path (e.g., "abc123.ngrok.app", "localhost", or "192.168.1.100")'
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
- conversationRelayConfig: ConversationRelayConfigSchema,
748
- actionUrl: z.url().optional()
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 WebSocket connections (domain only, without protocol/port/path, e.g., 'abc123.ngrok.app')
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],
@@ -1047,6 +1341,8 @@ var TACConfig = class _TACConfig {
1047
1341
  /**
1048
1342
  * Whether Conversation Orchestrator is configured.
1049
1343
  * Returns false in voice-only mode (no conversationConfigurationId).
1344
+ *
1345
+ * @internal
1050
1346
  */
1051
1347
  isOrchestratorEnabled() {
1052
1348
  return this.conversationConfigurationId !== void 0;
@@ -1150,7 +1446,7 @@ function createLogger(options) {
1150
1446
 
1151
1447
  // package.json
1152
1448
  var package_default = {
1153
- version: "1.0.3"};
1449
+ version: "2.0.1"};
1154
1450
  function buildUserAgent() {
1155
1451
  return `twilio-agent-connect-typescript/${package_default.version}`;
1156
1452
  }
@@ -2441,6 +2737,8 @@ var TAC = class _TAC {
2441
2737
  /**
2442
2738
  * Whether Conversation Orchestrator is configured.
2443
2739
  * Returns false in voice-only mode.
2740
+ *
2741
+ * @internal
2444
2742
  */
2445
2743
  isOrchestratorEnabled() {
2446
2744
  return this.config.isOrchestratorEnabled();
@@ -2656,7 +2954,9 @@ var BaseChannel = class {
2656
2954
  const modeToValidate = providedMode === void 0 ? "never" : providedMode;
2657
2955
  const parseResult = MemoryModeSchema.safeParse(modeToValidate);
2658
2956
  if (!parseResult.success) {
2659
- throw new Error(`Invalid memoryMode: "${modeToValidate}". Must be "always" or "never".`);
2957
+ throw new Error(
2958
+ `Invalid memoryMode: "${modeToValidate}". Must be "always", "once", or "never".`
2959
+ );
2660
2960
  }
2661
2961
  this.memoryMode = parseResult.data;
2662
2962
  const capacity = options?.dedupCapacity ?? 1e4;
@@ -2838,21 +3138,109 @@ var BaseChannel = class {
2838
3138
  return payload !== null && payload !== void 0;
2839
3139
  }
2840
3140
  /**
2841
- * Retrieve memory only when memoryMode === 'always'.
3141
+ * Preprocess webhook before handling event-specific logic.
3142
+ * Handles deduplication, validation, filtering, and data extraction.
3143
+ *
3144
+ * @param payload - Raw webhook payload from Twilio
3145
+ * @param idempotencyToken - Optional idempotency token for deduplication
3146
+ * @returns Preprocessed webhook data, or null if webhook should be skipped
3147
+ */
3148
+ preprocessWebhook(payload, idempotencyToken) {
3149
+ this.logger.debug({ operation: "webhook_processing" }, "Processing webhook");
3150
+ if (idempotencyToken && this.isDuplicateWebhook(idempotencyToken)) {
3151
+ this.logger.debug({ idempotency_token: idempotencyToken }, "Skipping duplicate webhook");
3152
+ return null;
3153
+ }
3154
+ if (!this.validateWebhookPayload(payload)) {
3155
+ throw new Error("Invalid webhook payload");
3156
+ }
3157
+ const webhookData = payload;
3158
+ const eventType = webhookData.eventType;
3159
+ const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
3160
+ if (!this.isEventForThisChannel(webhookData)) {
3161
+ this.logger.debug(
3162
+ { event_type: eventType, channel: this.channelType, conversation_id: conversationId },
3163
+ "Ignoring event for different channel type"
3164
+ );
3165
+ return null;
3166
+ }
3167
+ return { webhookData, eventType, conversationId };
3168
+ }
3169
+ /**
3170
+ * Extract conversation ID from webhook payload with type validation.
3171
+ *
3172
+ * Extracts conversationId from webhookData.data?.conversationId || webhookData.data?.id,
3173
+ * validates the value is a non-empty string, and ensures it passes isConversationId check.
3174
+ *
3175
+ * This prevents invalid IDs from causing downstream issues like incorrect Map key
3176
+ * matching in CONVERSATION_UPDATED self-filtering or propagating malformed IDs.
3177
+ */
3178
+ extractConversationId(payload) {
3179
+ if (!payload || typeof payload !== "object") {
3180
+ return null;
3181
+ }
3182
+ const webhookData = payload;
3183
+ const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
3184
+ if (conversationId && typeof conversationId === "string" && isConversationId(conversationId)) {
3185
+ return conversationId;
3186
+ }
3187
+ return null;
3188
+ }
3189
+ /**
3190
+ * Extract profile ID from webhook payload with type validation.
3191
+ *
3192
+ * Extracts profileId from webhookData.data?.profileId,
3193
+ * validates the value is a non-empty string, and ensures it passes isProfileId check.
3194
+ *
3195
+ * This prevents invalid IDs from propagating downstream and ensures consistent
3196
+ * validation across all channel types.
3197
+ */
3198
+ extractProfileId(payload) {
3199
+ if (!payload || typeof payload !== "object") {
3200
+ return null;
3201
+ }
3202
+ const webhookData = payload;
3203
+ const profileId = webhookData.data?.profileId;
3204
+ if (profileId && typeof profileId === "string" && isProfileId(profileId)) {
3205
+ return profileId;
3206
+ }
3207
+ return null;
3208
+ }
3209
+ /**
3210
+ * Retrieve memory according to the channel's memoryMode.
2842
3211
  *
2843
3212
  * This method handles the common logic for memory retrieval across all channels,
2844
- * including error handling and debug logging. If memoryMode is 'never',
2845
- * automatic memory retrieval is skipped.
3213
+ * including error handling and debug logging.
3214
+ *
3215
+ * Modes:
3216
+ * - "always": Fetch with the provided query on every message.
3217
+ * - "once": Fetch once with an empty query and cache the result on the
3218
+ * session. Subsequent calls reuse the cache until it is invalidated on the
3219
+ * INACTIVE transition.
3220
+ * - "never": Skip retrieval.
3221
+ *
3222
+ * Memory retrieval failures are logged and swallowed so message processing
3223
+ * continues without memory context.
2846
3224
  */
2847
3225
  async retrieveMemoryIfEnabled(session, query) {
2848
- if (this.memoryMode !== "always") {
3226
+ if (this.memoryMode !== "always" && this.memoryMode !== "once") {
2849
3227
  return void 0;
2850
3228
  }
3229
+ if (this.memoryMode === "once" && session.cachedMemory !== void 0) {
3230
+ this.logger.debug({ conversation_id: session.conversationId }, "Reusing cached memory");
3231
+ return session.cachedMemory;
3232
+ }
2851
3233
  try {
2852
- const memory = await this.tac.retrieveMemory(session, query);
3234
+ const memory = await this.tac.retrieveMemory(
3235
+ session,
3236
+ this.memoryMode === "always" ? query : void 0
3237
+ );
3238
+ if (this.memoryMode === "once") {
3239
+ session.cachedMemory = memory;
3240
+ }
2853
3241
  this.logger.debug(
2854
- { conversation_id: session.conversationId },
2855
- "Memory retrieved successfully"
3242
+ { conversation_id: session.conversationId, memory_mode: this.memoryMode },
3243
+ "Memory retrieved"
2856
3244
  );
2857
3245
  return memory;
2858
3246
  } catch (error) {
@@ -2863,6 +3251,25 @@ var BaseChannel = class {
2863
3251
  return void 0;
2864
3252
  }
2865
3253
  }
3254
+ /**
3255
+ * Invalidate cached memory for "once" mode when a conversation becomes
3256
+ * INACTIVE. Conversation Orchestrator updates memory on the INACTIVE
3257
+ * transition, so the next message re-fetches fresh memory. No-op for other
3258
+ * memory modes.
3259
+ */
3260
+ invalidateCachedMemory(conversationId) {
3261
+ if (this.memoryMode !== "once") {
3262
+ return;
3263
+ }
3264
+ const session = this.activeConversations.get(conversationId);
3265
+ if (session?.cachedMemory !== void 0) {
3266
+ session.cachedMemory = void 0;
3267
+ this.logger.debug(
3268
+ { conversation_id: conversationId },
3269
+ "Invalidated cached memory on INACTIVE status"
3270
+ );
3271
+ }
3272
+ }
2866
3273
  /**
2867
3274
  * Cleanup resources when shutting down
2868
3275
  */
@@ -2945,33 +3352,12 @@ var MessagingChannel = class extends BaseChannel {
2945
3352
  * Process messaging channel webhook from Conversation Orchestrator
2946
3353
  */
2947
3354
  async processWebhook(payload, idempotencyToken) {
2948
- this.logger.debug({ operation: "webhook_processing" }, "Processing webhook");
2949
3355
  try {
2950
- if (idempotencyToken && this.isDuplicateWebhook(idempotencyToken)) {
2951
- this.logger.debug({ idempotency_token: idempotencyToken }, "Skipping duplicate webhook");
2952
- return;
2953
- }
2954
- if (!this.validateWebhookPayload(payload)) {
2955
- throw new Error("Invalid webhook payload");
2956
- }
2957
- const webhookData = payload;
2958
- const eventType = webhookData.eventType;
2959
- const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
2960
- if (!this.isEventForThisChannel(webhookData)) {
2961
- this.logger.debug(
2962
- { event_type: eventType, channel: this.channelType, conversation_id: conversationId },
2963
- "Ignoring event for different channel type"
2964
- );
3356
+ const result = this.preprocessWebhook(payload, idempotencyToken);
3357
+ if (!result) {
2965
3358
  return;
2966
3359
  }
2967
- this.logger.info(
2968
- {
2969
- event_type: eventType,
2970
- raw_event_type: webhookData.eventType,
2971
- conversation_id: conversationId
2972
- },
2973
- "Processing webhook event"
2974
- );
3360
+ const { webhookData, eventType, conversationId } = result;
2975
3361
  switch (eventType) {
2976
3362
  case "CONVERSATION_CREATED":
2977
3363
  this.logger.debug(
@@ -3006,10 +3392,6 @@ var MessagingChannel = class extends BaseChannel {
3006
3392
  if (idempotencyToken) {
3007
3393
  this.removeWebhookToken(idempotencyToken);
3008
3394
  }
3009
- this.logger.error(
3010
- { err: error, operation: "webhook_processing" },
3011
- "Webhook processing error"
3012
- );
3013
3395
  this.handleError(error instanceof Error ? error : new Error(String(error)), { payload });
3014
3396
  }
3015
3397
  }
@@ -3161,43 +3543,15 @@ var MessagingChannel = class extends BaseChannel {
3161
3543
  throw new Error("Missing conversation ID in conversation.updated event");
3162
3544
  }
3163
3545
  if (payload.data?.status === "CLOSED") {
3164
- this.logger.info(
3546
+ this.logger.debug(
3165
3547
  { conversation_id: conversationId, status: payload.data.status },
3166
3548
  "Conversation closed, cleaning up"
3167
3549
  );
3168
3550
  await this.endConversation(conversationId);
3551
+ } else if (payload.data?.status === "INACTIVE") {
3552
+ this.invalidateCachedMemory(conversationId);
3169
3553
  }
3170
3554
  }
3171
- /**
3172
- * Extract conversation ID from webhook payload
3173
- */
3174
- extractConversationId(payload) {
3175
- const webhookData = payload;
3176
- const conversationId = webhookData.data?.conversationId || webhookData.data?.id;
3177
- if (conversationId && isConversationId(conversationId)) {
3178
- return conversationId;
3179
- }
3180
- return null;
3181
- }
3182
- /**
3183
- * Extract profile ID from webhook payload
3184
- */
3185
- extractProfileId(payload) {
3186
- const webhookData = payload;
3187
- const profileId = webhookData.data?.profileId;
3188
- if (profileId && isProfileId(profileId)) {
3189
- this.logger.debug(
3190
- { profile_id: profileId, conversation_id: webhookData.data?.conversationId },
3191
- "Extracted profile ID from webhook payload"
3192
- );
3193
- return profileId;
3194
- }
3195
- this.logger.debug(
3196
- { conversation_id: webhookData.data?.conversationId },
3197
- "Profile ID missing or invalid in webhook payload"
3198
- );
3199
- return null;
3200
- }
3201
3555
  /**
3202
3556
  * Validate messaging channel webhook payload structure
3203
3557
  */
@@ -4068,7 +4422,24 @@ var ChatChannel = class extends MessagingChannel {
4068
4422
  });
4069
4423
  }
4070
4424
  };
4071
- var VoiceChannel = class extends BaseChannel {
4425
+
4426
+ // packages/core/src/util/handoff-urls.ts
4427
+ function studioExecutionsUrl(flowSid) {
4428
+ return `https://studio.twilio.com/v2/Flows/${flowSid}/Executions`;
4429
+ }
4430
+ function studioVoiceHandoffUrl(accountSid, flowSid) {
4431
+ return `https://webhooks.twilio.com/v1/Accounts/${accountSid}/Flows/${flowSid}?Trigger=incomingCall`;
4432
+ }
4433
+
4434
+ // packages/core/src/channels/voice.ts
4435
+ var DEFAULT_WELCOME_GREETING = "Hello! How can I assist you today?";
4436
+ function stringifyParameterValue(value) {
4437
+ if (typeof value === "object") {
4438
+ return JSON.stringify(value);
4439
+ }
4440
+ return String(value);
4441
+ }
4442
+ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4072
4443
  webSocketConnections;
4073
4444
  voiceCallbacks;
4074
4445
  streamTasks;
@@ -4077,8 +4448,11 @@ var VoiceChannel = class extends BaseChannel {
4077
4448
  callSidToConversationId;
4078
4449
  MAX_INITIALIZATION_RETRIES = 3;
4079
4450
  twilioClient;
4451
+ voiceConfig;
4452
+ onInboundCallTwimlHandler;
4080
4453
  constructor(tac, options) {
4081
4454
  super(tac, options);
4455
+ this.voiceConfig = options ?? {};
4082
4456
  this.webSocketConnections = /* @__PURE__ */ new Map();
4083
4457
  this.voiceCallbacks = {};
4084
4458
  this.streamTasks = /* @__PURE__ */ new Map();
@@ -4086,6 +4460,56 @@ var VoiceChannel = class extends BaseChannel {
4086
4460
  this.initializationRetries = /* @__PURE__ */ new Map();
4087
4461
  this.callSidToConversationId = /* @__PURE__ */ new Map();
4088
4462
  }
4463
+ /**
4464
+ * Register a callback that produces per-call overrides for the TwiML inside
4465
+ * `<ConversationRelay>` on inbound calls.
4466
+ *
4467
+ * The callback receives a framework-neutral {@link TwiMLRequest} (parsed from
4468
+ * the Twilio webhook form) and returns {@link TwiMLOptions}. Fields the
4469
+ * callback explicitly sets override `defaultTwimlOptions` and TAC defaults;
4470
+ * unset fields fall through.
4471
+ *
4472
+ * @example
4473
+ * ```typescript
4474
+ * voiceChannel.onInboundCallTwiml(async req => {
4475
+ * if (req.callerCountry === 'MX') {
4476
+ * return { language: 'es-MX', welcomeGreeting: '¡Hola!' };
4477
+ * }
4478
+ * return {};
4479
+ * });
4480
+ * ```
4481
+ *
4482
+ * Outbound calls don't use this — pass per-call TwiML via
4483
+ * `InitiateVoiceConversationOptions.twimlOptions` directly.
4484
+ */
4485
+ onInboundCallTwiml(callback) {
4486
+ this.onInboundCallTwimlHandler = callback;
4487
+ }
4488
+ /**
4489
+ * Resolve the public WebSocket URL from `TACConfig.voicePublicDomain` +
4490
+ * `TACConfig.voiceWebsocketPath`. Throws if `voicePublicDomain` isn't set.
4491
+ */
4492
+ resolveWebsocketUrl(action) {
4493
+ if (this.config.voicePublicDomain) {
4494
+ return `wss://${this.config.voicePublicDomain}${this.config.voiceWebsocketPath}`;
4495
+ }
4496
+ throw new Error(
4497
+ `${action} needs a WebSocket URL. Set TWILIO_VOICE_PUBLIC_DOMAIN (or TACConfig.voicePublicDomain).`
4498
+ );
4499
+ }
4500
+ /**
4501
+ * Resolve the default `<Connect action=...>` cleanup URL.
4502
+ *
4503
+ * Returns undefined if `voicePublicDomain` isn't set; that's fine because
4504
+ * actionUrl has higher-priority layers (customizer, twimlOptions, Studio
4505
+ * handoff) above this fallback.
4506
+ */
4507
+ resolveDefaultActionUrl() {
4508
+ if (this.config.voicePublicDomain) {
4509
+ return `https://${this.config.voicePublicDomain}${this.config.voiceActionPath}`;
4510
+ }
4511
+ return void 0;
4512
+ }
4089
4513
  getTwilioClient() {
4090
4514
  if (!this.twilioClient) {
4091
4515
  this.twilioClient = twilio(this.config.apiKey, this.config.apiSecret, {
@@ -4124,12 +4548,67 @@ var VoiceChannel = class extends BaseChannel {
4124
4548
  }
4125
4549
  }
4126
4550
  /**
4127
- * Process webhook - Voice channel doesn't use traditional webhooks,
4128
- * but this method is required by the base class
4551
+ * Process conversation webhooks for cleanup.
4552
+ *
4553
+ * Voice channel processes CONVERSATION_UPDATED events:
4554
+ * - CLOSED status: Clean up local session state
4555
+ *
4556
+ * Note: Conversation tracking uses instance-local memory. In multi-instance
4557
+ * deployments, webhooks may route to a different instance, preventing cleanup.
4558
+ *
4559
+ * @param payload - Raw webhook event data from Twilio
4560
+ * @param idempotencyToken - Optional Twilio idempotency token from request headers
4129
4561
  */
4130
- processWebhook(_payload) {
4131
- this.logger.warn("processWebhook called but Voice channel uses WebSocket connections");
4132
- return Promise.resolve();
4562
+ async processWebhook(payload, idempotencyToken) {
4563
+ try {
4564
+ const result = this.preprocessWebhook(payload, idempotencyToken);
4565
+ if (!result) {
4566
+ return;
4567
+ }
4568
+ const { webhookData, eventType, conversationId } = result;
4569
+ switch (eventType) {
4570
+ case "CONVERSATION_UPDATED":
4571
+ this.logger.debug(
4572
+ { conversation_id: conversationId, status: webhookData.data?.status },
4573
+ "Handling CONVERSATION_UPDATED"
4574
+ );
4575
+ await this.handleConversationUpdated(webhookData);
4576
+ break;
4577
+ default:
4578
+ this.logger.debug(
4579
+ {
4580
+ event_type: eventType,
4581
+ raw_event_type: webhookData.eventType,
4582
+ conversation_id: conversationId
4583
+ },
4584
+ "Unhandled event type - this event will be ignored"
4585
+ );
4586
+ }
4587
+ this.logger.debug({ event_type: eventType }, "Webhook processing completed");
4588
+ } catch (error) {
4589
+ if (idempotencyToken) {
4590
+ this.removeWebhookToken(idempotencyToken);
4591
+ }
4592
+ this.handleError(error instanceof Error ? error : new Error(String(error)), { payload });
4593
+ }
4594
+ }
4595
+ /**
4596
+ * Handle conversation updated event
4597
+ */
4598
+ async handleConversationUpdated(payload) {
4599
+ const conversationId = this.extractConversationId(payload);
4600
+ if (!conversationId) {
4601
+ throw new Error("Missing conversation ID in conversation.updated event");
4602
+ }
4603
+ if (payload.data?.status === "CLOSED") {
4604
+ this.logger.debug(
4605
+ { conversation_id: conversationId, status: payload.data.status },
4606
+ "Conversation closed, cleaning up"
4607
+ );
4608
+ await this.endConversation(conversationId);
4609
+ } else if (payload.data?.status === "INACTIVE") {
4610
+ this.invalidateCachedMemory(conversationId);
4611
+ }
4133
4612
  }
4134
4613
  /**
4135
4614
  * Get active WebSocket connection for a conversation
@@ -4369,7 +4848,9 @@ var VoiceChannel = class extends BaseChannel {
4369
4848
  }
4370
4849
  }
4371
4850
  /**
4372
- * Handle WebSocket disconnection
4851
+ * Handle WebSocket disconnection. In orchestrated mode the conversation stays
4852
+ * tracked until the CLOSED webhook (so a follow-up call can reuse it); in
4853
+ * voice-only mode there is no such webhook, so it ends here.
4373
4854
  */
4374
4855
  async handleWebSocketDisconnect(conversationId) {
4375
4856
  this.cancelStreamTask(conversationId);
@@ -4378,7 +4859,9 @@ var VoiceChannel = class extends BaseChannel {
4378
4859
  if (this.voiceCallbacks.onWebSocketDisconnected) {
4379
4860
  this.voiceCallbacks.onWebSocketDisconnected({ conversationId });
4380
4861
  }
4381
- await this.endConversation(conversationId);
4862
+ if (!this.tac.isOrchestratorEnabled()) {
4863
+ await this.endConversation(conversationId);
4864
+ }
4382
4865
  }
4383
4866
  /**
4384
4867
  * Send voice response via WebSocket
@@ -4483,24 +4966,134 @@ var VoiceChannel = class extends BaseChannel {
4483
4966
  // Incoming Call Handling
4484
4967
  // =========================================================================
4485
4968
  /**
4486
- * Handle incoming voice call - generate TwiML to connect to ConversationRelay
4969
+ * Generate the TwiML response for an incoming voice call.
4970
+ *
4971
+ * ConversationRelay automatically handles conversation creation and
4972
+ * participant management via the `conversationConfiguration` parameter.
4487
4973
  *
4488
- * ConversationRelay will create the conversation automatically. The conversation
4489
- * will be initialized on the first prompt using the callSid.
4974
+ * The WebSocket URL and default session-cleanup action URL are derived from
4975
+ * `TACConfig.voicePublicDomain` + `TACConfig.voiceWebsocketPath` /
4976
+ * `voiceActionPath`.
4490
4977
  *
4491
- * @param options - Options for handling the incoming call
4492
- * @returns TwiML XML string with ConversationRelay configuration
4978
+ * TwiML fields are merged per-field, highest precedence first:
4979
+ * 1. Output of the customizer registered via
4980
+ * `VoiceChannel.onInboundCallTwiml(...)` if configured and `twimlRequest`
4981
+ * is given. (Application-owned.)
4982
+ * 2. `VoiceChannelConfig.defaultTwimlOptions` — per-channel defaults.
4983
+ * 3. `hostTwimlOptions` — per-call transport facts supplied by the host (the
4984
+ * code owning the route), e.g. a per-call `websocketUrl` with an affinity
4985
+ * token.
4986
+ * 4. TAC defaults: a fixed default welcomeGreeting, `conversationConfiguration`
4987
+ * from `TACConfig`, `actionUrl` resolved via Studio handoff (when
4988
+ * `studioHandoffFlowSid` is configured), else derived from
4989
+ * `TACConfig.voicePublicDomain` + `voiceActionPath`, and the `websocketUrl`
4990
+ * derived from `TACConfig.voicePublicDomain` + `voiceWebsocketPath`.
4991
+ *
4992
+ * Fields not set at a layer fall through to lower layers. Arrays (`languages`)
4993
+ * and nested objects (`customParameters`) replace wholesale when set at a
4994
+ * higher-priority layer. `websocketUrl` falls back to the `TACConfig`-derived
4995
+ * URL if unset at every layer.
4996
+ *
4997
+ * @param twimlRequest - Parsed Twilio webhook fields. Passed to the customizer
4998
+ * if one is configured on the channel.
4999
+ * @param options - Additional per-call inputs.
5000
+ * @param options.hostTwimlOptions - Per-call TwiML supplied by a custom
5001
+ * in-process host (e.g. an affinity-routed deployment injecting a per-call
5002
+ * `websocketUrl`), layered below `defaultTwimlOptions` and the application
5003
+ * customizer but above the TAC defaults.
5004
+ * @returns TwiML XML string for call connection.
4493
5005
  */
4494
- handleIncomingCall(options) {
4495
- const { actionUrl, conversationRelayConfig } = options;
4496
- const conversationConfiguration = this.tac.isOrchestratorEnabled() ? conversationRelayConfig.conversationConfiguration ?? this.config.conversationConfigurationId : void 0;
4497
- return this.connectConversationRelay(
4498
- {
4499
- ...conversationRelayConfig,
4500
- ...conversationConfiguration !== void 0 && { conversationConfiguration }
4501
- },
4502
- actionUrl ? { actionUrl } : void 0
4503
- );
5006
+ async handleIncomingCall(twimlRequest, options) {
5007
+ let customized;
5008
+ if (this.onInboundCallTwimlHandler && twimlRequest) {
5009
+ customized = await this.onInboundCallTwimlHandler(twimlRequest);
5010
+ }
5011
+ const merged = this.buildTwimlOptions(options?.hostTwimlOptions, customized);
5012
+ const websocketUrl = merged.websocketUrl ?? this.resolveWebsocketUrl("handleIncomingCall");
5013
+ return this.generateTwiml(websocketUrl, merged);
5014
+ }
5015
+ /**
5016
+ * Layer TwiML options, lowest precedence first: TAC defaults → `host`
5017
+ * (calling host's per-call values) → channel `defaultTwimlOptions` → `perCall`
5018
+ * (application customizer output for inbound, or
5019
+ * `InitiateVoiceConversationOptions.twimlOptions` for outbound).
5020
+ */
5021
+ buildTwimlOptions(host, perCall) {
5022
+ const merged = {
5023
+ welcomeGreeting: DEFAULT_WELCOME_GREETING,
5024
+ ...this.tac.isOrchestratorEnabled() && this.config.conversationConfigurationId !== void 0 ? { conversationConfiguration: this.config.conversationConfigurationId } : {}
5025
+ };
5026
+ const resolvedActionUrl = this.resolveActionUrl(host, perCall);
5027
+ if (resolvedActionUrl !== void 0) {
5028
+ merged.actionUrl = resolvedActionUrl;
5029
+ }
5030
+ if (host) {
5031
+ this.overlayFields(merged, host);
5032
+ }
5033
+ if (this.voiceConfig.defaultTwimlOptions) {
5034
+ this.overlayFields(merged, this.voiceConfig.defaultTwimlOptions);
5035
+ }
5036
+ if (perCall) {
5037
+ this.overlayFields(merged, perCall);
5038
+ }
5039
+ return merged;
5040
+ }
5041
+ /**
5042
+ * Apply fields explicitly present on `source` onto `target`.
5043
+ *
5044
+ * Nested objects (`customParameters`), arrays (`languages`), and dicts
5045
+ * (`extra`) replace wholesale — there's no per-key merging.
5046
+ *
5047
+ * `actionUrl` is skipped here on purpose — it's resolved once via
5048
+ * `resolveActionUrl` looking at every layer at once, and that resolved value
5049
+ * is written into `target` before this overlay runs. Letting it through here
5050
+ * would let a higher-priority layer that didn't set actionUrl silently clobber
5051
+ * a lower layer that did.
5052
+ *
5053
+ * "Explicitly present" is detected via key presence (`key in source`), which
5054
+ * mirrors Python's `model_fields_set`: a key set to `undefined` is still
5055
+ * "present" and overrides lower layers, while an absent key falls through.
5056
+ */
5057
+ overlayFields(target, source) {
5058
+ for (const key of Object.keys(source)) {
5059
+ if (key === "actionUrl") {
5060
+ continue;
5061
+ }
5062
+ target[key] = source[key];
5063
+ }
5064
+ }
5065
+ /**
5066
+ * Resolve the TwiML `<Connect action=...>` URL.
5067
+ *
5068
+ * Precedence (highest to lowest):
5069
+ * 1. application customizer
5070
+ * 2. channel `defaultTwimlOptions`
5071
+ * 3. `host` (calling host's per-call options)
5072
+ * 4. Studio handoff (when `studioHandoffFlowSid` is configured)
5073
+ * 5. Channel default — derived from `TACConfig.voicePublicDomain` +
5074
+ * `TACConfig.voiceActionPath`.
5075
+ *
5076
+ * User-expressed intent (Studio handoff is configured explicitly on
5077
+ * `TACConfig`) beats the SDK's generated cleanup default.
5078
+ *
5079
+ * Explicit `actionUrl: undefined` on a layer (key present, value undefined)
5080
+ * suppresses `<Connect action=...>` entirely — all lower layers are skipped.
5081
+ * `actionUrl` left absent (key not present) falls through to the next layer.
5082
+ */
5083
+ resolveActionUrl(host, customized) {
5084
+ if (customized && "actionUrl" in customized) {
5085
+ return customized.actionUrl;
5086
+ }
5087
+ if (this.voiceConfig.defaultTwimlOptions && "actionUrl" in this.voiceConfig.defaultTwimlOptions) {
5088
+ return this.voiceConfig.defaultTwimlOptions.actionUrl;
5089
+ }
5090
+ if (host && "actionUrl" in host) {
5091
+ return host.actionUrl;
5092
+ }
5093
+ if (this.config.studioHandoffFlowSid) {
5094
+ return studioVoiceHandoffUrl(this.config.accountSid, this.config.studioHandoffFlowSid);
5095
+ }
5096
+ return this.resolveDefaultActionUrl();
4504
5097
  }
4505
5098
  // =========================================================================
4506
5099
  // Outbound Call Handling
@@ -4513,10 +5106,16 @@ var VoiceChannel = class extends BaseChannel {
4513
5106
  * conversation during passive hydration. The session is initialized lazily
4514
5107
  * on the first prompt when the conversation is discovered by callSid.
4515
5108
  *
4516
- * `conversationRelayConfig.url` must be the publicly accessible WebSocket
4517
- * endpoint (e.g., `wss://your-domain.ngrok.app/ws`). Unlike inbound calls
4518
- * where TACServer sets this automatically, outbound calls require it
4519
- * explicitly since there is no incoming HTTP request to derive the host from.
5109
+ * TwiML fields are merged per-field, highest precedence first:
5110
+ * 1. `options.twimlOptions` — per-call overrides
5111
+ * 2. `VoiceChannelConfig.defaultTwimlOptions` — channel-wide defaults
5112
+ * 3. TAC defaults: welcome greeting, `conversationConfiguration` from
5113
+ * `TACConfig`, and `actionUrl` from Studio handoff (if configured), else
5114
+ * derived from `TACConfig.voicePublicDomain` + `voiceActionPath`.
5115
+ *
5116
+ * The WebSocket URL is derived from `TACConfig.voicePublicDomain` +
5117
+ * `TACConfig.voiceWebsocketPath`, unless overridden per-call via
5118
+ * `options.websocketUrl`.
4520
5119
  */
4521
5120
  async initiateOutboundConversation(options) {
4522
5121
  const validated = InitiateVoiceConversationOptionsSchema.parse(options);
@@ -4526,16 +5125,9 @@ var VoiceChannel = class extends BaseChannel {
4526
5125
  "Initiating outbound voice conversation"
4527
5126
  );
4528
5127
  try {
4529
- const conversationConfiguration = this.tac.isOrchestratorEnabled() ? validated.conversationRelayConfig.conversationConfiguration ?? this.config.conversationConfigurationId : void 0;
4530
- const twiml = this.connectConversationRelay(
4531
- {
4532
- ...validated.conversationRelayConfig,
4533
- ...conversationConfiguration !== void 0 && { conversationConfiguration }
4534
- },
4535
- {
4536
- ...validated.actionUrl ? { actionUrl: validated.actionUrl } : {}
4537
- }
4538
- );
5128
+ const merged = this.buildTwimlOptions(void 0, validated.twimlOptions);
5129
+ const websocketUrl = validated.websocketUrl ?? merged.websocketUrl ?? this.resolveWebsocketUrl("initiateOutboundConversation");
5130
+ const twiml = this.generateTwiml(websocketUrl, merged);
4539
5131
  const client = this.getTwilioClient();
4540
5132
  const call = await client.calls.create({
4541
5133
  to: validated.to,
@@ -4562,7 +5154,8 @@ var VoiceChannel = class extends BaseChannel {
4562
5154
  // ConversationRelay Callback Handling
4563
5155
  // =========================================================================
4564
5156
  /**
4565
- * Handle ConversationRelay callback from Twilio
5157
+ * Handle ConversationRelay callback from Twilio. Cleans up on call completion
5158
+ * in voice-only mode; in orchestrated mode the CO webhook owns cleanup.
4566
5159
  *
4567
5160
  * @param payload - Callback payload from Twilio
4568
5161
  * @returns Response with status, content, and content type
@@ -4579,7 +5172,7 @@ var VoiceChannel = class extends BaseChannel {
4579
5172
  );
4580
5173
  return { status: 403, content: "Forbidden", contentType: "text/plain" };
4581
5174
  }
4582
- if (payload.CallStatus === "completed") {
5175
+ if (payload.CallStatus === "completed" && !this.tac.isOrchestratorEnabled()) {
4583
5176
  const conversationId = this.callSidToConversationId.get(payload.CallSid);
4584
5177
  if (conversationId) {
4585
5178
  this.callSidToConversationId.delete(payload.CallSid);
@@ -4642,6 +5235,103 @@ var VoiceChannel = class extends BaseChannel {
4642
5235
  // =========================================================================
4643
5236
  // ConversationRelay TwiML Generation
4644
5237
  // =========================================================================
5238
+ /**
5239
+ * Field names on {@link TwiMLOptions} that map directly to `<ConversationRelay>`
5240
+ * attributes (camelCase, emitted as-is). Excludes the fields handled specially
5241
+ * by {@link generateTwiml}: websocketUrl (resolved through the layered merge and
5242
+ * emitted as the `url` attribute), actionUrl, languages, customParameters, extra.
5243
+ */
5244
+ static RELAY_ATTR_FIELDS = [
5245
+ "welcomeGreeting",
5246
+ "welcomeGreetingInterruptible",
5247
+ "conversationConfiguration",
5248
+ "language",
5249
+ "ttsLanguage",
5250
+ "transcriptionLanguage",
5251
+ "voice",
5252
+ "ttsProvider",
5253
+ "transcriptionProvider",
5254
+ "speechModel",
5255
+ "elevenlabsTextNormalization",
5256
+ "eotThreshold",
5257
+ "partialPrompts",
5258
+ "deepgramSmartFormat",
5259
+ "speechTimeout",
5260
+ "interruptible",
5261
+ "interruptSensitivity",
5262
+ "reportInputDuringAgentSpeech",
5263
+ "ignoreBackchannel",
5264
+ "preemptible",
5265
+ "dtmfDetection",
5266
+ "hints",
5267
+ "events",
5268
+ "debug",
5269
+ "intelligenceService"
5270
+ ];
5271
+ /**
5272
+ * Generate TwiML XML for ConversationRelay from a merged {@link TwiMLOptions}.
5273
+ *
5274
+ * This is the low-level emitter used by `handleIncomingCall` and
5275
+ * `initiateOutboundConversation` after layering. It mirrors the Python SDK's
5276
+ * `generate_twiml`. The WebSocket URL may be passed as `websocketUrl` or via
5277
+ * `options.websocketUrl` (the explicit argument wins when both are given), so a
5278
+ * channel-less caller can pass everything in one object.
5279
+ *
5280
+ * @param websocketUrl - Public WebSocket URL (e.g. 'wss://example.ngrok.app/ws').
5281
+ * Optional if `options.websocketUrl` is set.
5282
+ * @param options - Merged TwiMLOptions to emit.
5283
+ * @returns TwiML XML string ready to return to Twilio.
5284
+ * @throws {Error} if no WebSocket URL is provided via either source.
5285
+ */
5286
+ generateTwiml(websocketUrl, options) {
5287
+ const resolvedWebsocketUrl = websocketUrl || options.websocketUrl;
5288
+ if (!resolvedWebsocketUrl) {
5289
+ throw new Error(
5290
+ "generateTwiml requires a WebSocket URL \u2014 pass it explicitly or set options.websocketUrl."
5291
+ );
5292
+ }
5293
+ const response = new VoiceResponse();
5294
+ const connect = response.connect(options.actionUrl ? { action: options.actionUrl } : {});
5295
+ const relayAttrs = { url: resolvedWebsocketUrl };
5296
+ for (const field of _VoiceChannel.RELAY_ATTR_FIELDS) {
5297
+ let value = options[field];
5298
+ if (value === void 0) {
5299
+ continue;
5300
+ }
5301
+ if (field === "interruptible" && typeof value === "boolean") {
5302
+ value = value ? "any" : "none";
5303
+ }
5304
+ relayAttrs[field] = value;
5305
+ }
5306
+ if (options.extra) {
5307
+ for (const [key, value] of Object.entries(options.extra)) {
5308
+ if (key === "url") {
5309
+ this.logger.warn(
5310
+ "Ignoring `url` in TwiMLOptions.extra; set `websocketUrl` to override the ConversationRelay URL."
5311
+ );
5312
+ continue;
5313
+ }
5314
+ relayAttrs[key] = value;
5315
+ }
5316
+ }
5317
+ const relay = connect.conversationRelay(
5318
+ relayAttrs
5319
+ );
5320
+ if (options.languages && options.languages.length > 0) {
5321
+ for (const lang of options.languages) {
5322
+ const langAttrs = this.filterUnsetValues(lang);
5323
+ relay.language(langAttrs);
5324
+ }
5325
+ }
5326
+ if (options.customParameters) {
5327
+ for (const [name, value] of Object.entries(options.customParameters)) {
5328
+ if (value !== null && value !== void 0) {
5329
+ relay.parameter({ name, value: stringifyParameterValue(value) });
5330
+ }
5331
+ }
5332
+ }
5333
+ return response.toString();
5334
+ }
4645
5335
  /**
4646
5336
  * Generate TwiML to connect a call to ConversationRelay.
4647
5337
  * Validates configuration with Zod before generating TwiML.
@@ -4689,18 +5379,6 @@ var VoiceChannel = class extends BaseChannel {
4689
5379
  }
4690
5380
  return filtered;
4691
5381
  }
4692
- /**
4693
- * Extract conversation ID - Not applicable for Voice channel
4694
- */
4695
- extractConversationId(_payload) {
4696
- return null;
4697
- }
4698
- /**
4699
- * Extract profile ID - Not applicable for Voice channel
4700
- */
4701
- extractProfileId(_payload) {
4702
- return null;
4703
- }
4704
5382
  /**
4705
5383
  * Cleanup channel state on shutdown
4706
5384
  *
@@ -4717,14 +5395,6 @@ var VoiceChannel = class extends BaseChannel {
4717
5395
  }
4718
5396
  };
4719
5397
 
4720
- // packages/core/src/util/handoff-urls.ts
4721
- function studioExecutionsUrl(flowSid) {
4722
- return `https://studio.twilio.com/v2/Flows/${flowSid}/Executions`;
4723
- }
4724
- function studioVoiceHandoffUrl(accountSid, flowSid) {
4725
- return `https://webhooks.twilio.com/v1/Accounts/${accountSid}/Flows/${flowSid}?Trigger=incomingCall`;
4726
- }
4727
-
4728
5398
  // packages/core/src/lib/conversation-session-helpers.ts
4729
5399
  function formatTraitValue(value) {
4730
5400
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
@@ -5297,49 +5967,56 @@ var DEFAULT_CONFIG = {
5297
5967
  host: "0.0.0.0",
5298
5968
  port: 8e3,
5299
5969
  webhookPaths: {
5300
- messaging: "/webhook",
5301
- twiml: "/twiml",
5302
- ws: "/ws",
5303
- conversationRelayCallback: "/conversation-relay-callback"
5304
- },
5305
- conversationRelayConfig: {
5306
- welcomeGreeting: "Hello! How can I assist you today?"
5970
+ conversation: "/webhook",
5971
+ twiml: "/twiml"
5307
5972
  }
5308
5973
  };
5309
5974
  var TACServer = class {
5310
5975
  fastify;
5311
5976
  tac;
5312
5977
  config;
5313
- /** All enabled messaging channels — webhooks are fanned out to each one */
5978
+ /** All enabled messaging channels */
5314
5979
  messagingChannels;
5315
5980
  /** Voice channel instance */
5316
5981
  voiceChannel;
5982
+ /** All channels that need webhook processing (voice + messaging) */
5983
+ webhookChannels;
5317
5984
  constructor(tac, config = {}) {
5318
5985
  this.tac = tac;
5986
+ const webhookPaths = { ...DEFAULT_CONFIG.webhookPaths, ...config.webhookPaths };
5987
+ if (config.webhookPaths?.messaging) {
5988
+ console.warn(
5989
+ 'TACServer: The "webhookPaths.messaging" field is deprecated and will be removed in a future version. Please update your configuration to use "webhookPaths.conversation" instead.'
5990
+ );
5991
+ if (!config.webhookPaths.conversation) {
5992
+ webhookPaths.conversation = config.webhookPaths.messaging;
5993
+ }
5994
+ }
5319
5995
  this.config = {
5320
5996
  ...DEFAULT_CONFIG,
5321
5997
  ...config,
5322
- // Deep merge webhookPaths to preserve defaults while allowing overrides
5323
- webhookPaths: {
5324
- ...DEFAULT_CONFIG.webhookPaths,
5325
- ...config.webhookPaths
5326
- },
5327
- // Deep merge conversationRelayConfig to preserve defaults while allowing overrides
5328
- conversationRelayConfig: {
5329
- ...DEFAULT_CONFIG.conversationRelayConfig,
5330
- ...config.conversationRelayConfig
5331
- }
5998
+ webhookPaths
5332
5999
  };
5333
6000
  this.voiceChannel = config.voiceChannel ?? tac.getChannel("voice");
6001
+ if (this.voiceChannel && !tac.getConfig().voicePublicDomain) {
6002
+ throw new Error(
6003
+ "Voice channel is configured but TACConfig.voicePublicDomain is not set. Set it directly or via the TWILIO_VOICE_PUBLIC_DOMAIN env var."
6004
+ );
6005
+ }
5334
6006
  this.messagingChannels = config.messagingChannels ?? [
5335
6007
  tac.getChannel("sms"),
5336
6008
  tac.getChannel("rcs"),
5337
6009
  tac.getChannel("chat"),
5338
6010
  tac.getChannel("whatsapp")
5339
6011
  ].filter((ch) => ch != null);
5340
- if (this.messagingChannels.length === 0) {
6012
+ this.webhookChannels = [];
6013
+ if (this.voiceChannel) {
6014
+ this.webhookChannels.push(this.voiceChannel);
6015
+ }
6016
+ this.webhookChannels.push(...this.messagingChannels);
6017
+ if (this.webhookChannels.length === 0) {
5341
6018
  console.warn(
5342
- 'TACServer: No messaging channels configured. Messaging webhooks will be disabled. Register a MessagingChannel (e.g., "sms", "rcs", "chat", or "whatsapp") with TAC to enable messaging.'
6019
+ "TACServer: No channels configured for webhook processing. Register channels with TAC to enable webhooks."
5343
6020
  );
5344
6021
  }
5345
6022
  if (config.fastifyInstance) {
@@ -5403,18 +6080,18 @@ var TACServer = class {
5403
6080
  await this.validateRequestSignature(request, reply);
5404
6081
  }
5405
6082
  };
5406
- if (this.messagingChannels.length > 0) {
6083
+ if (this.webhookChannels.length > 0) {
5407
6084
  this.fastify.post(
5408
- this.config.webhookPaths.messaging || "/webhook",
6085
+ this.config.webhookPaths.conversation || "/webhook",
5409
6086
  validateSignature,
5410
6087
  async (request, reply) => {
5411
6088
  const rawHeader = request.headers["i-twilio-idempotency-token"];
5412
6089
  const idempotencyToken = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
5413
- for (const channel of this.messagingChannels) {
6090
+ for (const channel of this.webhookChannels) {
5414
6091
  channel.processWebhook(request.body, idempotencyToken).catch((err) => {
5415
6092
  this.fastify.log.error(
5416
6093
  { err, channel: channel.channelType },
5417
- "Messaging webhook processing error"
6094
+ "Webhook processing error"
5418
6095
  );
5419
6096
  });
5420
6097
  }
@@ -5432,18 +6109,9 @@ var TACServer = class {
5432
6109
  return;
5433
6110
  }
5434
6111
  const voiceChannel = this.voiceChannel;
5435
- const protocol = this.getForwardedProto(request);
5436
- const host = this.getForwardedHost(request);
5437
- const websocketUrl = `${protocol === "https" ? "wss" : "ws"}://${host}${this.config.webhookPaths.ws || "/ws"}`;
5438
- const tacConfig = this.tac.getConfig();
5439
- const actionUrl = tacConfig.studioHandoffFlowSid ? studioVoiceHandoffUrl(tacConfig.accountSid, tacConfig.studioHandoffFlowSid) : `${protocol}://${host}${this.config.webhookPaths.conversationRelayCallback || "/conversation-relay-callback"}`;
5440
- const twiml = voiceChannel.handleIncomingCall({
5441
- actionUrl,
5442
- conversationRelayConfig: {
5443
- url: websocketUrl,
5444
- ...this.config.conversationRelayConfig
5445
- }
5446
- });
6112
+ const formDict = request.body ?? {};
6113
+ const twimlRequest = twiMLRequestFromForm(formDict);
6114
+ const twiml = await voiceChannel.handleIncomingCall(twimlRequest);
5447
6115
  await reply.type("application/xml").send(twiml);
5448
6116
  } catch (error) {
5449
6117
  this.fastify.log.error(
@@ -5457,7 +6125,7 @@ var TACServer = class {
5457
6125
  }
5458
6126
  );
5459
6127
  this.fastify.post(
5460
- this.config.webhookPaths.conversationRelayCallback || "/conversation-relay-callback",
6128
+ this.tac.getConfig().voiceActionPath,
5461
6129
  validateSignature,
5462
6130
  async (request, reply) => {
5463
6131
  try {
@@ -5488,7 +6156,7 @@ var TACServer = class {
5488
6156
  );
5489
6157
  await this.fastify.register((fastify) => {
5490
6158
  fastify.get(
5491
- this.config.webhookPaths.ws || "/ws",
6159
+ this.tac.getConfig().voiceWebsocketPath,
5492
6160
  { websocket: true },
5493
6161
  (socket, request) => {
5494
6162
  const signature = request.headers["x-twilio-signature"];
@@ -5605,10 +6273,10 @@ var TACServer = class {
5605
6273
  {
5606
6274
  host: this.config.host,
5607
6275
  port: this.config.port,
5608
- messaging_webhook: this.config.webhookPaths.messaging,
6276
+ conversation_webhook: this.config.webhookPaths.conversation,
5609
6277
  twiml_webhook: this.config.webhookPaths.twiml,
5610
- ws_websocket: this.config.webhookPaths.ws,
5611
- conversation_relay_callback: this.config.webhookPaths.conversationRelayCallback,
6278
+ ws_websocket: this.tac.getConfig().voiceWebsocketPath,
6279
+ conversation_relay_callback: this.tac.getConfig().voiceActionPath,
5612
6280
  ...this.config.webhookPaths.cintel && {
5613
6281
  cintel_webhook: this.config.webhookPaths.cintel
5614
6282
  }
@@ -5672,6 +6340,6 @@ var TACServer = class {
5672
6340
  }
5673
6341
  };
5674
6342
 
5675
- 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 };
6343
+ 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 };
5676
6344
  //# sourceMappingURL=index.js.map
5677
6345
  //# sourceMappingURL=index.js.map