twilio-agent-connect 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,8 +17,10 @@ var TwilioMemoryConfigSchema = z.object({
17
17
  traitGroups: z.array(z.string()).optional(),
18
18
  observationsLimit: z.number().int().min(0).max(100).default(20),
19
19
  summariesLimit: z.number().int().min(0).max(100).default(5),
20
- // API default is 0 (no communications fetched). SDK defaults to 10 for a useful out-of-box experience.
21
- communicationsLimit: z.number().int().min(0).max(100).default(10),
20
+ // 0 matches the Memory API and Python SDK. Above 0, Memory requires a
21
+ // conversationId on every /Recall, forcing the query expansion that
22
+ // `memoryMode: "once"` exists to avoid.
23
+ communicationsLimit: z.number().int().min(0).max(100).default(0),
22
24
  relevanceThreshold: z.number().min(0).max(1).default(0),
23
25
  /**
24
26
  * Trait group name that holds the phone identifier on newly created profiles.
@@ -33,6 +35,8 @@ var voicePathSchema = (defaultPath) => z.preprocess((v) => {
33
35
  const trimmed = v.trim();
34
36
  return trimmed.length === 0 ? void 0 : trimmed;
35
37
  }, z.string().startsWith("/", 'Path must start with "/"').default(defaultPath));
38
+ var CallEventKindSchema = z.enum(["status", "amd", "recording"]);
39
+ var CALL_EVENT_KINDS = CallEventKindSchema.options;
36
40
  var TACConfigSchema = z.object({
37
41
  accountSid: z.string().min(1, "Twilio Account SID is required"),
38
42
  authToken: z.string().min(1, "Twilio Auth Token is required"),
@@ -47,34 +51,28 @@ var TACConfigSchema = z.object({
47
51
  memoryConfig: TwilioMemoryConfigSchema.prefault({}),
48
52
  conversationConfigurationId: z.string().regex(/^conv_configuration_[0-9a-z]{26}$/, "Invalid Conversation Configuration ID format").optional(),
49
53
  /**
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
- }
54
+ * Public domain where voice routes are reachable, optionally including a port
55
+ * and/or base path (e.g. "example.ngrok.app", "example.ngrok.app:8080",
56
+ * or "example.com/server1"). Used by VoiceChannel to construct the public
57
+ * WebSocket URL and ConversationRelay action URL. Required when using the Voice channel.
58
+ *
59
+ * Whitespace, schemes (https://, wss://), and trailing slashes are stripped
60
+ * automatically; anything else is passed through as given. Mirrors the Python
61
+ * SDK's `_normalize_voice_public_domain` — keep the two in step.
62
+ */
63
+ voicePublicDomain: z.preprocess((v) => {
64
+ if (typeof v !== "string") return v;
65
+ let s = v.trim();
66
+ if (s.length === 0) return void 0;
67
+ for (const scheme of ["https://", "http://", "wss://", "ws://"]) {
68
+ if (s.toLowerCase().startsWith(scheme)) {
69
+ s = s.slice(scheme.length);
70
+ break;
69
71
  }
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()
77
- ).optional(),
72
+ }
73
+ s = s.replace(/\/+$/, "");
74
+ return s.length === 0 ? void 0 : s;
75
+ }, z.string().optional()).optional(),
78
76
  /**
79
77
  * Path the voice WebSocket is served at. Combined with voicePublicDomain to
80
78
  * build the public WebSocket URL the voice channel hands to Twilio in TwiML;
@@ -88,8 +86,14 @@ var TACConfigSchema = z.object({
88
86
  * Must start with '/'.
89
87
  */
90
88
  voiceActionPath: voicePathSchema("/conversation-relay-callback"),
89
+ /**
90
+ * Base path for the call-event callbacks (status, async AMD, recording).
91
+ * TACServer registers one route per callback under it — `<base>/status`,
92
+ * `<base>/amd`, `<base>/recording` — so the route identifies the event. Same
93
+ * role as voiceActionPath. Must start with '/'.
94
+ */
95
+ voiceCallEventPath: voicePathSchema("/twilio/call-events"),
91
96
  cintelConfigurationId: z.string().optional(),
92
- cintelObservationOperatorSid: z.string().optional(),
93
97
  cintelSummaryOperatorSid: z.string().optional(),
94
98
  region: z.string().max(63, "Invalid Twilio region format (must be a valid DNS label)").regex(
95
99
  /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/,
@@ -124,8 +128,8 @@ var EnvironmentVariables = {
124
128
  TWILIO_VOICE_PUBLIC_DOMAIN: "TWILIO_VOICE_PUBLIC_DOMAIN",
125
129
  TWILIO_VOICE_WEBSOCKET_PATH: "TWILIO_VOICE_WEBSOCKET_PATH",
126
130
  TWILIO_VOICE_ACTION_PATH: "TWILIO_VOICE_ACTION_PATH",
131
+ TWILIO_VOICE_CALL_EVENT_PATH: "TWILIO_VOICE_CALL_EVENT_PATH",
127
132
  TWILIO_TAC_CI_CONFIGURATION_ID: "TWILIO_TAC_CI_CONFIGURATION_ID",
128
- TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID: "TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID",
129
133
  TWILIO_TAC_CI_SUMMARY_OPERATOR_SID: "TWILIO_TAC_CI_SUMMARY_OPERATOR_SID",
130
134
  TWILIO_REGION: "TWILIO_REGION",
131
135
  TWILIO_STUDIO_HANDOFF_FLOW_SID: "TWILIO_STUDIO_HANDOFF_FLOW_SID"
@@ -232,6 +236,13 @@ var AuthorInfoSchema = z.object({
232
236
  });
233
237
  var ConversationSessionSchema = z.object({
234
238
  conversationId: z.string().min(1, "Conversation ID is required"),
239
+ /**
240
+ * Twilio Call SID on the Voice channel, unset on messaging. The correlation
241
+ * key for call events (`VoiceChannel.onCallStatus` / `onAmd` / `onRecording`)
242
+ * and `endCall`. Equals `conversationId` in relay-only mode; look the session
243
+ * up the other way with `VoiceChannel.getConversationSessionByCallSid`.
244
+ */
245
+ callSid: z.string().optional(),
235
246
  profileId: z.string().optional(),
236
247
  serviceId: z.string().optional(),
237
248
  channel: ChannelTypeSchema,
@@ -548,7 +559,13 @@ var MemoryChannelTypeSchema = z.enum([
548
559
  "API",
549
560
  "SYSTEM"
550
561
  ]);
551
- var MemoryParticipantTypeSchema = z.enum(["HUMAN_AGENT", "CUSTOMER", "AI_AGENT", "AGENT"]);
562
+ var MemoryParticipantTypeSchema = z.enum([
563
+ "HUMAN_AGENT",
564
+ "CUSTOMER",
565
+ "AI_AGENT",
566
+ "AGENT",
567
+ "UNKNOWN"
568
+ ]);
552
569
  var MemoryDeliveryStatusSchema = z.enum([
553
570
  "INITIATED",
554
571
  "IN_PROGRESS",
@@ -640,11 +657,17 @@ var EMPTY_MEMORY_RESPONSE = {
640
657
  summaries: [],
641
658
  communications: []
642
659
  };
643
- var CreateObservationResponseSchema = z.object({
660
+ var ObservationCreateRequestSchema = z.object({
644
661
  content: z.string(),
645
662
  source: z.string(),
646
663
  occurredAt: z.string(),
647
- conversationIds: z.array(z.string())
664
+ conversationIds: z.array(z.string()).optional()
665
+ });
666
+ var CreateObservationsRequestSchema = z.object({
667
+ observations: z.array(ObservationCreateRequestSchema)
668
+ });
669
+ var CreateObservationResponseSchema = z.object({
670
+ message: z.string()
648
671
  });
649
672
  var CreateConversationSummariesResponseSchema = z.object({
650
673
  message: z.string()
@@ -1026,10 +1049,193 @@ var ConversationRelayCallbackPayloadSchema = z.object({
1026
1049
  SessionStatus: z.string().optional(),
1027
1050
  SessionDuration: z.string().optional()
1028
1051
  });
1052
+ function splitCallEventForm(form, aliases) {
1053
+ const known = {};
1054
+ const extra = {};
1055
+ for (const [key, value] of Object.entries(form)) {
1056
+ const alias = aliases[key];
1057
+ if (alias) {
1058
+ known[alias] = value;
1059
+ } else {
1060
+ extra[key] = value;
1061
+ }
1062
+ }
1063
+ return { known, extra };
1064
+ }
1065
+ var CallEventBaseShape = {
1066
+ callSid: z.string().min(1, "CallSid is required"),
1067
+ accountSid: z.string().optional(),
1068
+ /** Any other Twilio webhook fields not surfaced above. */
1069
+ extra: z.record(z.string(), z.string()).default({})
1070
+ };
1071
+ var CALL_EVENT_BASE_ALIASES = {
1072
+ CallSid: "callSid",
1073
+ AccountSid: "accountSid"
1074
+ };
1075
+ var UNREACHED_CALL_STATUSES = ["busy", "no-answer", "failed", "canceled"];
1076
+ var CallStatusEventSchema = z.object({
1077
+ ...CallEventBaseShape,
1078
+ callStatus: z.string().optional(),
1079
+ callDuration: z.string().optional(),
1080
+ sipResponseCode: z.string().optional()
1081
+ }).transform((event) => ({
1082
+ ...event,
1083
+ /** Call ended without reaching the callee — i.e. worth a retry. */
1084
+ isUnreached: UNREACHED_CALL_STATUSES.includes(event.callStatus ?? "")
1085
+ }));
1086
+ var CALL_STATUS_EVENT_ALIASES = {
1087
+ ...CALL_EVENT_BASE_ALIASES,
1088
+ CallStatus: "callStatus",
1089
+ CallDuration: "callDuration",
1090
+ SipResponseCode: "sipResponseCode"
1091
+ };
1092
+ function callStatusEventFromForm(form) {
1093
+ const { known, extra } = splitCallEventForm(form, CALL_STATUS_EVENT_ALIASES);
1094
+ return CallStatusEventSchema.parse({ ...known, extra });
1095
+ }
1096
+ var AmdEventSchema = z.object({
1097
+ ...CallEventBaseShape,
1098
+ answeredBy: z.string().optional(),
1099
+ machineDetectionDuration: z.string().optional()
1100
+ }).transform((event) => ({
1101
+ ...event,
1102
+ /**
1103
+ * A machine answered — any `machine_*` value, either mode. `unknown`
1104
+ * (detection timed out) is false, so a call is never hung up on a guess.
1105
+ */
1106
+ isMachine: (event.answeredBy ?? "").startsWith("machine")
1107
+ }));
1108
+ var AMD_EVENT_ALIASES = {
1109
+ ...CALL_EVENT_BASE_ALIASES,
1110
+ AnsweredBy: "answeredBy",
1111
+ MachineDetectionDuration: "machineDetectionDuration"
1112
+ };
1113
+ function amdEventFromForm(form) {
1114
+ const { known, extra } = splitCallEventForm(form, AMD_EVENT_ALIASES);
1115
+ return AmdEventSchema.parse({ ...known, extra });
1116
+ }
1117
+ var RecordingEventSchema = z.object({
1118
+ ...CallEventBaseShape,
1119
+ recordingSid: z.string().optional(),
1120
+ recordingUrl: z.string().optional(),
1121
+ recordingStatus: z.string().optional(),
1122
+ recordingDuration: z.string().optional()
1123
+ });
1124
+ var RECORDING_EVENT_ALIASES = {
1125
+ ...CALL_EVENT_BASE_ALIASES,
1126
+ RecordingSid: "recordingSid",
1127
+ RecordingUrl: "recordingUrl",
1128
+ RecordingStatus: "recordingStatus",
1129
+ RecordingDuration: "recordingDuration"
1130
+ };
1131
+ function recordingEventFromForm(form) {
1132
+ const { known, extra } = splitCallEventForm(form, RECORDING_EVENT_ALIASES);
1133
+ return RecordingEventSchema.parse({ ...known, extra });
1134
+ }
1135
+ var CALLS_CREATE_PARAMS = [
1136
+ "applicationSid",
1137
+ "asyncAmd",
1138
+ "asyncAmdStatusCallback",
1139
+ "asyncAmdStatusCallbackMethod",
1140
+ "byoc",
1141
+ "callerId",
1142
+ "callReason",
1143
+ "callToken",
1144
+ "clientNotificationUrl",
1145
+ "fallbackMethod",
1146
+ "fallbackUrl",
1147
+ "from",
1148
+ "machineDetection",
1149
+ "machineDetectionSilenceTimeout",
1150
+ "machineDetectionSpeechEndThreshold",
1151
+ "machineDetectionSpeechThreshold",
1152
+ "machineDetectionTimeout",
1153
+ "method",
1154
+ "record",
1155
+ "recordingChannels",
1156
+ "recordingStatusCallback",
1157
+ "recordingStatusCallbackEvent",
1158
+ "recordingStatusCallbackMethod",
1159
+ "recordingTrack",
1160
+ "sendDigits",
1161
+ "sipAuthPassword",
1162
+ "sipAuthUsername",
1163
+ "statusCallback",
1164
+ "statusCallbackEvent",
1165
+ "statusCallbackMethod",
1166
+ "timeLimit",
1167
+ "timeout",
1168
+ "to",
1169
+ "trim",
1170
+ "twiml",
1171
+ "url"
1172
+ ];
1173
+ var RESERVED_CALL_PARAMS = ["to", "from", "twiml", "url", "applicationSid"];
1174
+ var ACCEPTED_CALL_PARAMS = new Set(CALLS_CREATE_PARAMS);
1175
+ function isAsyncAmdEnabled(value) {
1176
+ if (typeof value === "boolean") return value;
1177
+ if (typeof value === "string") return value.trim().toLowerCase() === "true";
1178
+ return false;
1179
+ }
1180
+ var CallOptionsObjectSchema = z.looseObject({
1181
+ machineDetection: z.enum(["Enable", "DetectMessageEnd"]).optional(),
1182
+ asyncAmd: z.union([z.boolean(), z.string()]).optional(),
1183
+ asyncAmdStatusCallback: z.string().optional(),
1184
+ asyncAmdStatusCallbackMethod: z.string().optional(),
1185
+ machineDetectionTimeout: z.number().int().optional(),
1186
+ machineDetectionSpeechThreshold: z.number().int().optional(),
1187
+ machineDetectionSpeechEndThreshold: z.number().int().optional(),
1188
+ machineDetectionSilenceTimeout: z.number().int().optional(),
1189
+ record: z.boolean().optional(),
1190
+ recordingStatusCallback: z.string().optional(),
1191
+ recordingStatusCallbackEvent: z.array(z.string()).optional(),
1192
+ recordingChannels: z.string().optional(),
1193
+ recordingTrack: z.string().optional(),
1194
+ statusCallback: z.string().optional(),
1195
+ statusCallbackEvent: z.array(z.string()).optional(),
1196
+ statusCallbackMethod: z.string().optional(),
1197
+ timeout: z.number().int().optional()
1198
+ }).superRefine((value, ctx) => {
1199
+ const supplied = Object.keys(value);
1200
+ const conflict = supplied.filter(
1201
+ (key) => RESERVED_CALL_PARAMS.includes(key)
1202
+ );
1203
+ if (conflict.length > 0) {
1204
+ ctx.addIssue({
1205
+ code: "custom",
1206
+ message: `callOptions may not set TAC-owned call parameters: ${conflict.sort().join(", ")}. TAC builds the call and its TwiML.`
1207
+ });
1208
+ }
1209
+ const unknown = supplied.filter((key) => !ACCEPTED_CALL_PARAMS.has(key));
1210
+ if (unknown.length > 0) {
1211
+ ctx.addIssue({
1212
+ code: "custom",
1213
+ message: `callOptions has parameters Twilio's calls.create() does not accept: ${unknown.sort().join(", ")}. Check for a typo, or upgrade the twilio package.`
1214
+ });
1215
+ }
1216
+ if (Boolean(value.machineDetection) !== isAsyncAmdEnabled(value.asyncAmd)) {
1217
+ ctx.addIssue({
1218
+ code: "custom",
1219
+ message: `AMD requires both machineDetection and asyncAmd; got machineDetection=${JSON.stringify(value.machineDetection)}, asyncAmd=${JSON.stringify(value.asyncAmd)}.`
1220
+ });
1221
+ }
1222
+ });
1223
+ var CallOptionsSchema = CallOptionsObjectSchema.transform(
1224
+ (value) => value
1225
+ );
1226
+ function callOptionsToCreateParams(options) {
1227
+ const params = {};
1228
+ for (const [key, value] of Object.entries(options)) {
1229
+ if (value === void 0) continue;
1230
+ params[key] = key === "asyncAmd" && typeof value === "boolean" ? String(value) : value;
1231
+ }
1232
+ return params;
1233
+ }
1029
1234
  var InitiateVoiceConversationOptionsSchema = z.object({
1030
1235
  to: z.string().min(1, "Recipient phone number is required"),
1031
1236
  websocketUrl: z.url().optional(),
1032
- twimlOptions: TwiMLOptionsSchema.optional()
1237
+ twimlOptions: TwiMLOptionsSchema.optional(),
1238
+ callOptions: CallOptionsSchema.optional()
1033
1239
  }).strict();
1034
1240
  var JSONSchemaSchema = z.object({
1035
1241
  type: z.enum(["object", "string", "number", "boolean", "array"]),
@@ -1047,6 +1253,11 @@ var OpenAIToolSchema = z.object({
1047
1253
  parameters: JSONSchemaSchema
1048
1254
  })
1049
1255
  });
1256
+ var AnthropicToolSchema = z.object({
1257
+ name: z.string(),
1258
+ description: z.string(),
1259
+ input_schema: JSONSchemaSchema
1260
+ });
1050
1261
  var ToolExecutionResultSchema = z.object({
1051
1262
  success: z.boolean(),
1052
1263
  data: z.any().optional(),
@@ -1101,7 +1312,6 @@ var OperatorProcessingResultSchema = z.object({
1101
1312
  });
1102
1313
  var ConversationIntelligenceConfigSchema = z.object({
1103
1314
  configurationId: z.string(),
1104
- observationOperatorSid: z.string().optional(),
1105
1315
  summaryOperatorSid: z.string().optional()
1106
1316
  });
1107
1317
  var ConversationSummaryItemSchema = z.object({
@@ -1152,8 +1362,9 @@ var TACConfig = class _TACConfig {
1152
1362
  voiceWebsocketPath;
1153
1363
  /** Path the ConversationRelay action callback is served at (default '/conversation-relay-callback'). */
1154
1364
  voiceActionPath;
1365
+ /** Base path the call-event callbacks are served under (default '/twilio/call-events'). */
1366
+ voiceCallEventPath;
1155
1367
  cintelConfigurationId;
1156
- cintelObservationOperatorSid;
1157
1368
  cintelSummaryOperatorSid;
1158
1369
  /** Optional Twilio region subdomain for API routing (e.g. transforms base URLs to `https://{product}.{region}.twilio.com`) */
1159
1370
  region;
@@ -1187,12 +1398,10 @@ var TACConfig = class _TACConfig {
1187
1398
  }
1188
1399
  this.voiceWebsocketPath = validatedConfig.voiceWebsocketPath;
1189
1400
  this.voiceActionPath = validatedConfig.voiceActionPath;
1401
+ this.voiceCallEventPath = validatedConfig.voiceCallEventPath;
1190
1402
  if (validatedConfig.cintelConfigurationId) {
1191
1403
  this.cintelConfigurationId = validatedConfig.cintelConfigurationId;
1192
1404
  }
1193
- if (validatedConfig.cintelObservationOperatorSid) {
1194
- this.cintelObservationOperatorSid = validatedConfig.cintelObservationOperatorSid;
1195
- }
1196
1405
  if (validatedConfig.cintelSummaryOperatorSid) {
1197
1406
  this.cintelSummaryOperatorSid = validatedConfig.cintelSummaryOperatorSid;
1198
1407
  }
@@ -1216,9 +1425,10 @@ var TACConfig = class _TACConfig {
1216
1425
  * Optional environment variables:
1217
1426
  * - TWILIO_WHATSAPP_NUMBER: WhatsApp number for WhatsApp channel (e.g., 'whatsapp:+1234567890')
1218
1427
  * - TWILIO_CONVERSATION_CONFIGURATION_ID: Conversation Orchestrator configuration ID (enables orchestrated mode)
1219
- * - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice; domain only, without protocol/port/path, e.g., 'abc123.ngrok.app')
1428
+ * - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice; a port and/or base path are allowed, e.g., 'abc123.ngrok.app', 'example.ngrok.app:8080', or 'example.com/server1')
1220
1429
  * - TWILIO_VOICE_WEBSOCKET_PATH: Path for the voice WebSocket (default: /ws)
1221
1430
  * - TWILIO_VOICE_ACTION_PATH: Path for the ConversationRelay action callback (default: /conversation-relay-callback)
1431
+ * - TWILIO_VOICE_CALL_EVENT_PATH: Base path for the call-event callbacks — status, async AMD, recording (default: /twilio/call-events)
1222
1432
  * - TWILIO_REGION: Twilio region subdomain for API routing (e.g. transforms base URLs to `https://{product}.{region}.twilio.com`)
1223
1433
  * - TWILIO_STUDIO_HANDOFF_FLOW_SID: Studio Flow SID used by createStudioHandoffTool for human handoff
1224
1434
  * - TWILIO_RCS_SENDER_ID: RCS Sender ID for the RCS channel
@@ -1330,14 +1540,32 @@ var TACConfig = class _TACConfig {
1330
1540
  voicePublicDomain: process.env[EnvironmentVariables.TWILIO_VOICE_PUBLIC_DOMAIN],
1331
1541
  voiceWebsocketPath: process.env[EnvironmentVariables.TWILIO_VOICE_WEBSOCKET_PATH] || void 0,
1332
1542
  voiceActionPath: process.env[EnvironmentVariables.TWILIO_VOICE_ACTION_PATH] || void 0,
1543
+ voiceCallEventPath: process.env[EnvironmentVariables.TWILIO_VOICE_CALL_EVENT_PATH] || void 0,
1333
1544
  cintelConfigurationId: process.env[EnvironmentVariables.TWILIO_TAC_CI_CONFIGURATION_ID],
1334
- cintelObservationOperatorSid: process.env[EnvironmentVariables.TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID],
1335
1545
  cintelSummaryOperatorSid: process.env[EnvironmentVariables.TWILIO_TAC_CI_SUMMARY_OPERATOR_SID],
1336
1546
  region: process.env[EnvironmentVariables.TWILIO_REGION],
1337
1547
  studioHandoffFlowSid: process.env[EnvironmentVariables.TWILIO_STUDIO_HANDOFF_FLOW_SID]
1338
1548
  };
1339
1549
  return new _TACConfig(rawConfig);
1340
1550
  }
1551
+ /**
1552
+ * Path a call-event callback is served at.
1553
+ *
1554
+ * Single source of truth: the voice channel builds callback URLs from this
1555
+ * and TACServer registers routes at it, so the two can't drift.
1556
+ */
1557
+ callEventPath(kind) {
1558
+ return `${this.voiceCallEventPath.replace(/\/+$/, "")}/${kind}`;
1559
+ }
1560
+ /**
1561
+ * Public URL for a call-event callback, or `undefined` without a public domain.
1562
+ */
1563
+ callEventUrl(kind) {
1564
+ if (!this.voicePublicDomain) {
1565
+ return void 0;
1566
+ }
1567
+ return `https://${this.voicePublicDomain}${this.callEventPath(kind)}`;
1568
+ }
1341
1569
  /**
1342
1570
  * Whether Conversation Orchestrator is configured.
1343
1571
  * Returns false in voice-only mode (no conversationConfigurationId).
@@ -1427,6 +1655,13 @@ function maskAddress(address) {
1427
1655
  if (address.length <= 1) return "***";
1428
1656
  return `${address[0]}***`;
1429
1657
  }
1658
+ function redactTwimlParameters(twiml) {
1659
+ if (!twiml) return "";
1660
+ const parameterValueRe = /(<Parameter\b[^>]*?\bvalue=)(["'])(.*?)\2/gi;
1661
+ return twiml.replace(parameterValueRe, (_match, prefix, quote) => {
1662
+ return `${prefix}${quote}***${quote}`;
1663
+ });
1664
+ }
1430
1665
 
1431
1666
  // packages/core/src/lib/logger.ts
1432
1667
  function piiLogMethod(args, method) {
@@ -1446,7 +1681,7 @@ function createLogger(options) {
1446
1681
 
1447
1682
  // package.json
1448
1683
  var package_default = {
1449
- version: "2.0.1"};
1684
+ version: "2.2.0"};
1450
1685
  function buildUserAgent() {
1451
1686
  return `twilio-agent-connect-typescript/${package_default.version}`;
1452
1687
  }
@@ -1527,7 +1762,7 @@ var BaseClient = class {
1527
1762
  };
1528
1763
 
1529
1764
  // packages/core/src/clients/memory.ts
1530
- var MemoryClient = class extends BaseClient {
1765
+ var MemoryClient = class _MemoryClient extends BaseClient {
1531
1766
  storeId;
1532
1767
  constructor(config, storeId, logger) {
1533
1768
  const baseUrl = config.region ? `https://memory.${config.region}.twilio.com` : "https://memory.twilio.com";
@@ -1580,28 +1815,48 @@ var MemoryClient = class extends BaseClient {
1580
1815
  },
1581
1816
  "Raw memory response received"
1582
1817
  );
1583
- const validatedResponse = MemoryRetrievalResponseSchema.safeParse(data);
1584
- if (!validatedResponse.success) {
1818
+ if (data === null || typeof data !== "object") {
1585
1819
  this.logger.warn(
1586
1820
  {
1587
1821
  profile_id: profileId,
1588
- memory_store_id: this.storeId,
1589
- validation_errors: validatedResponse.error.issues
1822
+ memory_store_id: this.storeId
1590
1823
  },
1591
1824
  "Invalid memory response format"
1592
1825
  );
1593
1826
  return EMPTY_MEMORY_RESPONSE;
1594
1827
  }
1828
+ const response = data;
1829
+ const observations = this.parseItems(
1830
+ response.observations,
1831
+ ObservationInfoSchema,
1832
+ "observation",
1833
+ profileId
1834
+ );
1835
+ const summaries = this.parseItems(
1836
+ response.summaries,
1837
+ SummaryInfoSchema,
1838
+ "summary",
1839
+ profileId
1840
+ );
1841
+ const communications = this.parseItems(
1842
+ response.communications,
1843
+ MemoryCommunicationSchema,
1844
+ "communication",
1845
+ profileId
1846
+ );
1595
1847
  this.logger.debug(
1596
1848
  {
1597
1849
  memory_store_id: this.storeId,
1598
1850
  profile_id: profileId,
1599
- observation_count: validatedResponse.data.observations.length,
1600
- summary_count: validatedResponse.data.summaries.length
1851
+ observation_count: observations.length,
1852
+ summary_count: summaries.length,
1853
+ communication_count: communications.length,
1854
+ // Server-side query time; dominates /Recall when expansion runs.
1855
+ query_time_ms: response.meta?.queryTime
1601
1856
  },
1602
1857
  "Memory retrieval succeeded"
1603
1858
  );
1604
- return validatedResponse.data;
1859
+ return { observations, summaries, communications };
1605
1860
  } catch (error) {
1606
1861
  this.logger.warn(
1607
1862
  {
@@ -1614,6 +1869,67 @@ var MemoryClient = class extends BaseClient {
1614
1869
  return EMPTY_MEMORY_RESPONSE;
1615
1870
  }
1616
1871
  }
1872
+ /** Cap on per-item validation warnings logged per response, to avoid flooding logs. */
1873
+ static MAX_INVALID_ITEM_LOGS = 10;
1874
+ /**
1875
+ * Validate an array of memory items one at a time, dropping (and logging)
1876
+ * any that fail validation while keeping the rest.
1877
+ *
1878
+ * Per-item warnings are capped at {@link MemoryClient.MAX_INVALID_ITEM_LOGS}
1879
+ * and followed by a single summary warning with the total `invalid_count`.
1880
+ */
1881
+ parseItems(value, schema, itemType, profileId) {
1882
+ if (value === void 0) {
1883
+ return [];
1884
+ }
1885
+ if (!Array.isArray(value)) {
1886
+ this.logger.warn(
1887
+ {
1888
+ profile_id: profileId,
1889
+ memory_store_id: this.storeId,
1890
+ item_type: itemType,
1891
+ received_type: typeof value
1892
+ },
1893
+ "Expected an array of memory items but received a non-array value"
1894
+ );
1895
+ return [];
1896
+ }
1897
+ const parsed = [];
1898
+ let invalidCount = 0;
1899
+ for (const [index, item] of value.entries()) {
1900
+ const result = schema.safeParse(item);
1901
+ if (result.success) {
1902
+ parsed.push(result.data);
1903
+ continue;
1904
+ }
1905
+ invalidCount += 1;
1906
+ if (invalidCount <= _MemoryClient.MAX_INVALID_ITEM_LOGS) {
1907
+ this.logger.warn(
1908
+ {
1909
+ profile_id: profileId,
1910
+ memory_store_id: this.storeId,
1911
+ item_type: itemType,
1912
+ item_index: index,
1913
+ validation_errors: result.error.issues
1914
+ },
1915
+ "Dropping invalid memory item"
1916
+ );
1917
+ }
1918
+ }
1919
+ if (invalidCount > 0) {
1920
+ this.logger.warn(
1921
+ {
1922
+ profile_id: profileId,
1923
+ memory_store_id: this.storeId,
1924
+ item_type: itemType,
1925
+ invalid_count: invalidCount,
1926
+ total_count: value.length
1927
+ },
1928
+ "Dropped invalid memory items"
1929
+ );
1930
+ }
1931
+ return parsed;
1932
+ }
1617
1933
  /**
1618
1934
  * Find profiles that contain a specific identifier value
1619
1935
  *
@@ -1701,27 +2017,30 @@ var MemoryClient = class extends BaseClient {
1701
2017
  }
1702
2018
  }
1703
2019
  /**
1704
- * Create an observation for a profile
2020
+ * Create an observation for a profile.
2021
+ *
2022
+ * The Memory API Observations endpoint is a batch create: the observation is
2023
+ * wrapped in an `observations` array and `occurredAt` is required, so it
2024
+ * defaults to the current time (ISO 8601) when omitted or blank.
1705
2025
  *
1706
2026
  * @param profileId - The profile ID to create the observation for
1707
2027
  * @param content - The observation content
1708
2028
  * @param source - Source of the observation (default: 'conversation-intelligence')
1709
2029
  * @param conversationIds - Optional array of conversation IDs associated with this observation
1710
- * @param occurredAt - Optional timestamp when the observation occurred
1711
- * @returns Promise containing the created observation
2030
+ * @param occurredAt - Timestamp when the observation occurred (ISO 8601); defaults to now when omitted or blank
2031
+ * @returns Promise containing the API confirmation message
1712
2032
  */
1713
2033
  async createObservation(profileId, content, source = "conversation-intelligence", conversationIds, occurredAt) {
1714
2034
  const url = `/v1/Stores/${this.storeId}/Profiles/${profileId}/Observations`;
1715
- const requestBody = {
2035
+ const observation = {
1716
2036
  content,
1717
- source
2037
+ source,
2038
+ occurredAt: occurredAt && occurredAt.trim() ? occurredAt : (/* @__PURE__ */ new Date()).toISOString()
1718
2039
  };
1719
2040
  if (conversationIds && conversationIds.length > 0) {
1720
- requestBody.conversationIds = conversationIds;
1721
- }
1722
- if (occurredAt) {
1723
- requestBody.occurredAt = occurredAt;
2041
+ observation.conversationIds = conversationIds;
1724
2042
  }
2043
+ const requestBody = { observations: [observation] };
1725
2044
  try {
1726
2045
  const data = await this.makeRequest(url, "POST", requestBody);
1727
2046
  return CreateObservationResponseSchema.parse(data);
@@ -2130,22 +2449,6 @@ function generateContent(operatorResult) {
2130
2449
  const jsonString = JSON.stringify(result);
2131
2450
  return jsonString === "{}" || jsonString === "[]" ? void 0 : jsonString;
2132
2451
  }
2133
- function parseObservationsContent(jsonContent) {
2134
- try {
2135
- const parsed = JSON.parse(jsonContent);
2136
- if (typeof parsed === "object" && parsed !== null && "observations" in parsed) {
2137
- const observations = parsed.observations;
2138
- if (Array.isArray(observations)) {
2139
- return observations.filter(
2140
- (obs) => typeof obs === "string" && obs.trim() !== ""
2141
- );
2142
- }
2143
- }
2144
- return [];
2145
- } catch {
2146
- return [];
2147
- }
2148
- }
2149
2452
  function parseSummariesContent(jsonContent) {
2150
2453
  try {
2151
2454
  const parsed = JSON.parse(jsonContent);
@@ -2283,13 +2586,11 @@ var OperatorResultProcessor = class {
2283
2586
  */
2284
2587
  async processOperatorResult(event, operatorResult) {
2285
2588
  const operatorSid = operatorResult.operator.id;
2286
- const isObservationOperator = this.config.observationOperatorSid === operatorSid;
2287
2589
  const isSummaryOperator = this.config.summaryOperatorSid === operatorSid;
2288
- if (!isObservationOperator && !isSummaryOperator) {
2590
+ if (!isSummaryOperator) {
2289
2591
  this.logger.debug(
2290
2592
  {
2291
2593
  operator_sid: operatorSid,
2292
- observation_operator_sid: this.config.observationOperatorSid,
2293
2594
  summary_operator_sid: this.config.summaryOperatorSid
2294
2595
  },
2295
2596
  "Skipping unconfigured operator"
@@ -2327,75 +2628,7 @@ var OperatorResultProcessor = class {
2327
2628
  createdCount: 0
2328
2629
  };
2329
2630
  }
2330
- if (isObservationOperator) {
2331
- return this.processObservationEvent(event, operatorResult, content, profileIds);
2332
- } else {
2333
- return this.processSummaryEvent(event, operatorResult, content, profileIds);
2334
- }
2335
- }
2336
- /**
2337
- * Process an observation operator result
2338
- */
2339
- async processObservationEvent(event, operatorResult, content, profileIds) {
2340
- const observations = parseObservationsContent(content);
2341
- if (observations.length === 0) {
2342
- this.logger.debug(
2343
- { operator_sid: operatorResult.operator.id },
2344
- "No observations found in content"
2345
- );
2346
- return {
2347
- success: true,
2348
- eventType: "observation",
2349
- skipped: true,
2350
- skipReason: "No observations found in operator result content",
2351
- createdCount: 0
2352
- };
2353
- }
2354
- let createdCount = 0;
2355
- for (const profileId of profileIds) {
2356
- for (const observation of observations) {
2357
- try {
2358
- await this.memoryClient.createObservation(
2359
- profileId,
2360
- observation,
2361
- "conversation-intelligence",
2362
- [event.conversationId],
2363
- operatorResult.dateCreated
2364
- );
2365
- createdCount++;
2366
- this.logger.debug(
2367
- {
2368
- profile_id: profileId,
2369
- conversation_id: event.conversationId,
2370
- observation_preview: observation.substring(0, 100)
2371
- },
2372
- "Created observation"
2373
- );
2374
- } catch (error) {
2375
- this.logger.error(
2376
- {
2377
- err: error,
2378
- profile_id: profileId,
2379
- conversation_id: event.conversationId
2380
- },
2381
- "Failed to create observation"
2382
- );
2383
- return {
2384
- success: false,
2385
- eventType: "observation",
2386
- skipped: false,
2387
- error: `Failed to create observation: ${error instanceof Error ? error.message : String(error)}`,
2388
- createdCount
2389
- };
2390
- }
2391
- }
2392
- }
2393
- return {
2394
- success: true,
2395
- eventType: "observation",
2396
- skipped: false,
2397
- createdCount
2398
- };
2631
+ return this.processSummaryEvent(event, operatorResult, content, profileIds);
2399
2632
  }
2400
2633
  /**
2401
2634
  * Process a summary operator result
@@ -2517,7 +2750,6 @@ var TAC = class _TAC {
2517
2750
  tac.memoryClient,
2518
2751
  {
2519
2752
  configurationId: tac.config.cintelConfigurationId,
2520
- observationOperatorSid: tac.config.cintelObservationOperatorSid,
2521
2753
  summaryOperatorSid: tac.config.cintelSummaryOperatorSid
2522
2754
  },
2523
2755
  tac.logger.child({ component: "cintel" })
@@ -2811,6 +3043,10 @@ var TAC = class _TAC {
2811
3043
  *
2812
3044
  * @param session - Conversation session context
2813
3045
  * @param query - Optional semantic search query
3046
+ * @param conversationId - Passed through to `/Recall` as-is. Sending one
3047
+ * without a `query` makes Memory infer one from that conversation's history
3048
+ * — an expensive server-side step — so leave it unset when there is no
3049
+ * per-turn topic (e.g. `"once"` mode's cache-priming fetch).
2814
3050
  * @returns Promise containing TACMemoryResponse wrapper providing unified access to memory data.
2815
3051
  *
2816
3052
  * Attempts to retrieve from Memory API first:
@@ -2821,7 +3057,7 @@ var TAC = class _TAC {
2821
3057
  * - observations and summaries are empty arrays
2822
3058
  * - communications have basic fields only (no author name/type)
2823
3059
  */
2824
- async retrieveMemory(session, query) {
3060
+ async retrieveMemory(session, query, conversationId) {
2825
3061
  if (!this.isOrchestratorEnabled()) {
2826
3062
  return new TACMemoryResponse([]);
2827
3063
  }
@@ -2868,7 +3104,7 @@ var TAC = class _TAC {
2868
3104
  throw new Error("Memory client is not available");
2869
3105
  }
2870
3106
  const memoryResponse = await this.memoryClient.retrieveMemories(session.profileId, {
2871
- conversationId: session.conversationId,
3107
+ conversationId,
2872
3108
  query,
2873
3109
  observationsLimit: this.config.memoryConfig.observationsLimit,
2874
3110
  summariesLimit: this.config.memoryConfig.summariesLimit,
@@ -3214,9 +3450,9 @@ var BaseChannel = class {
3214
3450
  *
3215
3451
  * Modes:
3216
3452
  * - "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.
3453
+ * - "once": Fetch once with neither a query nor a conversation id (skipping
3454
+ * query expansion) and cache the result on the session. Subsequent calls
3455
+ * reuse the cache until it is invalidated on the INACTIVE transition.
3220
3456
  * - "never": Skip retrieval.
3221
3457
  *
3222
3458
  * Memory retrieval failures are logged and swallowed so message processing
@@ -3231,9 +3467,11 @@ var BaseChannel = class {
3231
3467
  return session.cachedMemory;
3232
3468
  }
3233
3469
  try {
3470
+ const isAlways = this.memoryMode === "always";
3234
3471
  const memory = await this.tac.retrieveMemory(
3235
3472
  session,
3236
- this.memoryMode === "always" ? query : void 0
3473
+ isAlways ? query : void 0,
3474
+ isAlways ? session.conversationId : void 0
3237
3475
  );
3238
3476
  if (this.memoryMode === "once") {
3239
3477
  session.cachedMemory = memory;
@@ -3502,10 +3740,16 @@ var MessagingChannel = class extends BaseChannel {
3502
3740
  if (!session.aiAgentInfo) {
3503
3741
  const resolved = await this.reconcileParticipants(conversationId);
3504
3742
  if (!resolved) {
3505
- this.logger.warn(
3506
- { conversation_id: conversationId },
3507
- "Reconciliation failed; skipping callback for this inbound"
3743
+ const error = new Error(
3744
+ `Participant reconciliation failed for conversation ${conversationId}; inbound message dropped because no sendable participants could be resolved.`
3508
3745
  );
3746
+ error.name = "ParticipantReconciliationError";
3747
+ this.handleError(error, {
3748
+ conversation_id: conversationId,
3749
+ channel: this.channelType,
3750
+ dropped_inbound: true,
3751
+ error_code: "participant_reconciliation_failed"
3752
+ });
3509
3753
  return;
3510
3754
  }
3511
3755
  const [agentParticipant, customerParticipant] = resolved;
@@ -4433,6 +4677,9 @@ function studioVoiceHandoffUrl(accountSid, flowSid) {
4433
4677
 
4434
4678
  // packages/core/src/channels/voice.ts
4435
4679
  var DEFAULT_WELCOME_GREETING = "Hello! How can I assist you today?";
4680
+ var POLL_ATTEMPTS = 10;
4681
+ var POLL_BASE_DELAY_MS = 250;
4682
+ var POLL_MAX_DELAY_MS = 1500;
4436
4683
  function stringifyParameterValue(value) {
4437
4684
  if (typeof value === "object") {
4438
4685
  return JSON.stringify(value);
@@ -4450,6 +4697,9 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4450
4697
  twilioClient;
4451
4698
  voiceConfig;
4452
4699
  onInboundCallTwimlHandler;
4700
+ onCallStatusHandler;
4701
+ onAmdHandler;
4702
+ onRecordingHandler;
4453
4703
  constructor(tac, options) {
4454
4704
  super(tac, options);
4455
4705
  this.voiceConfig = options ?? {};
@@ -4485,6 +4735,74 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4485
4735
  onInboundCallTwiml(callback) {
4486
4736
  this.onInboundCallTwimlHandler = callback;
4487
4737
  }
4738
+ /**
4739
+ * Register a handler for Twilio `statusCallback` webhooks.
4740
+ *
4741
+ * This is the Calls-API status callback (call disposition), not the
4742
+ * ConversationRelay session callback — see
4743
+ * {@link handleConversationRelayCallback}.
4744
+ *
4745
+ * Registering does two things: it stores the handler, and it makes later
4746
+ * outbound calls pass `statusCallback` to `calls.create`. With no handler
4747
+ * registered TAC omits that parameter, so Twilio has nowhere to post and the
4748
+ * event never arrives.
4749
+ *
4750
+ * Twilio reports only the terminal event by default, which covers every
4751
+ * disposition; set `CallOptions.statusCallbackEvent` for ringing/answered.
4752
+ *
4753
+ * @example
4754
+ * ```typescript
4755
+ * voiceChannel.onCallStatus(async event => {
4756
+ * if (event.isUnreached) {
4757
+ * // queue a retry
4758
+ * }
4759
+ * });
4760
+ * ```
4761
+ */
4762
+ onCallStatus(callback) {
4763
+ this.onCallStatusHandler = callback;
4764
+ }
4765
+ /**
4766
+ * Register a handler for Twilio `asyncAmdStatusCallback` webhooks.
4767
+ *
4768
+ * Registering makes later outbound calls pass `asyncAmdStatusCallback` to
4769
+ * `calls.create`; without a handler TAC omits it and Twilio has nowhere to
4770
+ * post the result. It does not enable detection — that's per-call, via
4771
+ * `CallOptions.machineDetection` and `asyncAmd`, both of which are required
4772
+ * for this to fire (at most once per call).
4773
+ *
4774
+ * @example
4775
+ * ```typescript
4776
+ * voiceChannel.onAmd(async event => {
4777
+ * if (event.isMachine) {
4778
+ * await voiceChannel.endCall(event.callSid); // voicemail → hang up
4779
+ * }
4780
+ * });
4781
+ * ```
4782
+ */
4783
+ onAmd(callback) {
4784
+ this.onAmdHandler = callback;
4785
+ }
4786
+ /**
4787
+ * Register a handler for Twilio `recordingStatusCallback` webhooks.
4788
+ *
4789
+ * Registering makes later outbound calls pass `recordingStatusCallback` to
4790
+ * `calls.create`; without a handler TAC omits it and Twilio has nowhere to
4791
+ * post. It does not start recording — that's `CallOptions.record`, which is
4792
+ * required for this to fire.
4793
+ *
4794
+ * @example
4795
+ * ```typescript
4796
+ * voiceChannel.onRecording(async event => {
4797
+ * if (event.recordingStatus === 'completed') {
4798
+ * // store event.recordingUrl
4799
+ * }
4800
+ * });
4801
+ * ```
4802
+ */
4803
+ onRecording(callback) {
4804
+ this.onRecordingHandler = callback;
4805
+ }
4488
4806
  /**
4489
4807
  * Resolve the public WebSocket URL from `TACConfig.voicePublicDomain` +
4490
4808
  * `TACConfig.voiceWebsocketPath`. Throws if `voicePublicDomain` isn't set.
@@ -4616,6 +4934,57 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4616
4934
  getWebsocket(conversationId) {
4617
4935
  return this.webSocketConnections.get(conversationId) || null;
4618
4936
  }
4937
+ /**
4938
+ * Poll Conversation Orchestrator for the conversation ConversationRelay
4939
+ * created for `callSid`, then register the local session and WebSocket.
4940
+ * Runs in the background from `setup`, so those can exist before the
4941
+ * caller speaks.
4942
+ */
4943
+ async initializeOrchestratedConversation(callSid, fromNumber, ws) {
4944
+ if (!this.conversationClient) {
4945
+ throw new Error("Conversation client is required in orchestrated mode");
4946
+ }
4947
+ let conversations = [];
4948
+ for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
4949
+ conversations = await this.conversationClient.listConversations({
4950
+ channelId: callSid,
4951
+ status: ["ACTIVE"]
4952
+ });
4953
+ if (conversations.length === 1) break;
4954
+ if (attempt < POLL_ATTEMPTS - 1) {
4955
+ this.logger.debug(
4956
+ { call_sid: callSid, attempt: attempt + 1, found: conversations.length },
4957
+ "Conversation not ready yet, polling again"
4958
+ );
4959
+ const delayMs = Math.min(POLL_BASE_DELAY_MS * 2 ** attempt, POLL_MAX_DELAY_MS);
4960
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
4961
+ }
4962
+ }
4963
+ if (conversations.length !== 1) {
4964
+ throw new Error(
4965
+ `Expected exactly 1 conversation for callSid ${callSid}, but found ${conversations.length} after ${POLL_ATTEMPTS} attempts`
4966
+ );
4967
+ }
4968
+ const conversation = conversations[0];
4969
+ const conversationId = conversation.id;
4970
+ const participants = await this.conversationClient.listParticipants(conversationId);
4971
+ const customerParticipant = participants.find((p) => p.type === "CUSTOMER");
4972
+ const customerAddress = customerParticipant?.addresses?.find((a) => a.channel === "VOICE")?.address ?? fromNumber ?? void 0;
4973
+ const profileId = customerParticipant?.profileId ? customerParticipant.profileId : void 0;
4974
+ this.webSocketConnections.set(conversationId, ws);
4975
+ this.callSidToConversationId.set(callSid, conversationId);
4976
+ const session = this.startConversation(conversationId, profileId);
4977
+ session.callSid = callSid;
4978
+ if (customerAddress) {
4979
+ session.authorInfo = {
4980
+ address: customerAddress
4981
+ };
4982
+ }
4983
+ if (this.voiceCallbacks.onWebSocketConnected) {
4984
+ this.voiceCallbacks.onWebSocketConnected({ conversationId });
4985
+ }
4986
+ return conversationId;
4987
+ }
4619
4988
  /**
4620
4989
  * Handle WebSocket connection from ConversationRelay
4621
4990
  */
@@ -4624,6 +4993,7 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4624
4993
  let callSid = null;
4625
4994
  let fromNumber = null;
4626
4995
  let initializationFailed = false;
4996
+ let initPromise = null;
4627
4997
  ws.on("message", (data) => {
4628
4998
  (async () => {
4629
4999
  try {
@@ -4646,6 +5016,14 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4646
5016
  case "setup":
4647
5017
  callSid = message.callSid;
4648
5018
  fromNumber = message.from;
5019
+ if (this.tac.isOrchestratorEnabled()) {
5020
+ this.logger.debug(
5021
+ { call_sid: callSid },
5022
+ "Starting background conversation initialization"
5023
+ );
5024
+ initPromise = this.initializeOrchestratedConversation(callSid, fromNumber, ws);
5025
+ void initPromise.catch(() => void 0);
5026
+ }
4649
5027
  if (this.voiceCallbacks.onSetup) {
4650
5028
  this.voiceCallbacks.onSetup({
4651
5029
  callSid,
@@ -4675,52 +5053,25 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4675
5053
  this.webSocketConnections.set(conversationId, ws);
4676
5054
  this.callSidToConversationId.set(callSid, conversationId);
4677
5055
  const session = this.startConversation(conversationId);
5056
+ session.callSid = callSid;
4678
5057
  if (fromNumber) {
4679
5058
  session.authorInfo = { address: fromNumber };
4680
5059
  }
4681
- } else {
4682
- if (!this.conversationClient) {
4683
- throw new Error("Conversation client is required in orchestrated mode");
4684
- }
4685
- const POLL_ATTEMPTS = 5;
4686
- const POLL_DELAY_MS = 500;
4687
- let conversations = [];
4688
- for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
4689
- conversations = await this.conversationClient.listConversations({
4690
- channelId: callSid
4691
- });
4692
- if (conversations.length === 1) break;
4693
- if (attempt < POLL_ATTEMPTS - 1) {
4694
- this.logger.debug(
4695
- { call_sid: callSid, attempt: attempt + 1, found: conversations.length },
4696
- "Conversation not ready yet, polling again"
4697
- );
4698
- await new Promise((resolve) => setTimeout(resolve, POLL_DELAY_MS));
4699
- }
4700
- }
4701
- if (conversations.length !== 1) {
4702
- throw new Error(
4703
- `Expected exactly 1 conversation for callSid ${callSid}, but found ${conversations.length} after ${POLL_ATTEMPTS} attempts`
4704
- );
5060
+ if (this.voiceCallbacks.onWebSocketConnected) {
5061
+ this.voiceCallbacks.onWebSocketConnected({ conversationId });
4705
5062
  }
4706
- const conversation = conversations[0];
4707
- conversationId = conversation.id;
4708
- const participants = await this.conversationClient.listParticipants(conversationId);
4709
- const customerParticipant = participants.find((p) => p.type === "CUSTOMER");
4710
- const customerAddress = customerParticipant?.addresses?.find((a) => a.channel === "VOICE")?.address ?? fromNumber ?? void 0;
4711
- const profileId = customerParticipant?.profileId ? customerParticipant.profileId : void 0;
4712
- this.webSocketConnections.set(conversationId, ws);
4713
- this.callSidToConversationId.set(callSid, conversationId);
4714
- const session = this.startConversation(conversationId, profileId);
4715
- if (customerAddress) {
4716
- session.authorInfo = {
4717
- address: customerAddress
4718
- };
5063
+ } else {
5064
+ initPromise ??= this.initializeOrchestratedConversation(
5065
+ callSid,
5066
+ fromNumber,
5067
+ ws
5068
+ );
5069
+ try {
5070
+ conversationId = await initPromise;
5071
+ } finally {
5072
+ initPromise = null;
4719
5073
  }
4720
5074
  }
4721
- if (this.voiceCallbacks.onWebSocketConnected) {
4722
- this.voiceCallbacks.onWebSocketConnected({ conversationId });
4723
- }
4724
5075
  initializationFailed = false;
4725
5076
  this.initializationRetries.delete(callSid);
4726
5077
  this.logger.info(
@@ -4777,6 +5128,19 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4777
5128
  });
4778
5129
  });
4779
5130
  ws.on("close", () => {
5131
+ const pendingInit = initPromise;
5132
+ initPromise = null;
5133
+ if (pendingInit && !conversationId) {
5134
+ void pendingInit.then(async (adoptedId) => {
5135
+ await this.handleWebSocketDisconnect(adoptedId);
5136
+ if (callSid) this.callSidToConversationId.delete(callSid);
5137
+ }).catch((err) => {
5138
+ this.logger.error(
5139
+ { err, call_sid: callSid },
5140
+ "Background conversation initialization failed after the call ended"
5141
+ );
5142
+ });
5143
+ }
4780
5144
  if (conversationId) {
4781
5145
  void this.handleWebSocketDisconnect(conversationId).catch((err) => {
4782
5146
  this.logger.error(
@@ -5098,13 +5462,73 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5098
5462
  // =========================================================================
5099
5463
  // Outbound Call Handling
5100
5464
  // =========================================================================
5465
+ /**
5466
+ * Overlay `perCall` onto `VoiceChannelConfig.defaultCallOptions`.
5467
+ *
5468
+ * Per-field via key presence, the same convention {@link overlayFields} uses
5469
+ * for TwiML options — so a per-call `{ machineDetection: undefined }`
5470
+ * explicitly clears the channel default rather than falling through to it.
5471
+ *
5472
+ * The result is always validated, for two reasons: a combination only
5473
+ * reachable by layering — per-call clearing `machineDetection` while the
5474
+ * default set `asyncAmd` — must still fail instead of reaching Twilio, and
5475
+ * `VoiceChannelConfig` is a plain interface, so `defaultCallOptions` has had
5476
+ * no runtime validation of its own.
5477
+ */
5478
+ mergeCallOptions(perCall) {
5479
+ const defaults = this.voiceConfig.defaultCallOptions;
5480
+ if (!defaults && !perCall) {
5481
+ return void 0;
5482
+ }
5483
+ const merged = {};
5484
+ for (const [key, value] of Object.entries(defaults ?? {})) {
5485
+ if (value !== void 0) {
5486
+ merged[key] = value;
5487
+ }
5488
+ }
5489
+ for (const key of Object.keys(perCall ?? {})) {
5490
+ merged[key] = perCall[key];
5491
+ }
5492
+ return CallOptionsSchema.parse(merged);
5493
+ }
5494
+ /**
5495
+ * Build the extra arguments for `client.calls.create`.
5496
+ *
5497
+ * Layers, highest precedence first: this call's `callOptions`,
5498
+ * `VoiceChannelConfig.defaultCallOptions`, then callback URLs derived from
5499
+ * `voicePublicDomain` + `voiceCallEventPath`.
5500
+ *
5501
+ * A URL is derived only when its handler is registered. That's a deliberate
5502
+ * deviation from `websocketUrl` / `actionUrl`, which derive unconditionally:
5503
+ * those are load-bearing, so a wrong one fails loudly on the first call,
5504
+ * whereas an unwanted call-event URL fails as silent 11200 alerts for a
5505
+ * feature nobody asked for. Set the URLs in `defaultCallOptions` when TAC
5506
+ * isn't serving the routes.
5507
+ */
5508
+ buildCallParams(callOptions) {
5509
+ const merged = this.mergeCallOptions(callOptions);
5510
+ const params = merged ? callOptionsToCreateParams(merged) : {};
5511
+ const wiring = [
5512
+ ["status", "statusCallback", this.onCallStatusHandler],
5513
+ ["amd", "asyncAmdStatusCallback", this.onAmdHandler],
5514
+ ["recording", "recordingStatusCallback", this.onRecordingHandler]
5515
+ ];
5516
+ for (const [kind, param, handler] of wiring) {
5517
+ if (!handler) continue;
5518
+ const url = this.config.callEventUrl(kind);
5519
+ if (url !== void 0 && params[param] === void 0) {
5520
+ params[param] = url;
5521
+ }
5522
+ }
5523
+ return params;
5524
+ }
5101
5525
  /**
5102
5526
  * Initiate an outbound voice conversation
5103
5527
  *
5104
5528
  * Places an outbound call with inline TwiML that connects to ConversationRelay.
5105
5529
  * The conversationConfiguration attribute tells CO to create and manage the
5106
- * conversation during passive hydration. The session is initialized lazily
5107
- * on the first prompt when the conversation is discovered by callSid.
5530
+ * conversation during passive hydration. The session is initialized when the
5531
+ * background callSid lookup started at WebSocket setup finds it.
5108
5532
  *
5109
5533
  * TwiML fields are merged per-field, highest precedence first:
5110
5534
  * 1. `options.twimlOptions` — per-call overrides
@@ -5113,6 +5537,12 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5113
5537
  * `TACConfig`, and `actionUrl` from Studio handoff (if configured), else
5114
5538
  * derived from `TACConfig.voicePublicDomain` + `voiceActionPath`.
5115
5539
  *
5540
+ * Calls-API parameters merge the same way:
5541
+ * 1. `options.callOptions` — per-call overrides
5542
+ * 2. `VoiceChannelConfig.defaultCallOptions` — channel-wide defaults
5543
+ * 3. Callback URLs derived from `TACConfig.voicePublicDomain` +
5544
+ * `voiceCallEventPath`, for handlers that are registered
5545
+ *
5116
5546
  * The WebSocket URL is derived from `TACConfig.voicePublicDomain` +
5117
5547
  * `TACConfig.voiceWebsocketPath`, unless overridden per-call via
5118
5548
  * `options.websocketUrl`.
@@ -5128,11 +5558,17 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5128
5558
  const merged = this.buildTwimlOptions(void 0, validated.twimlOptions);
5129
5559
  const websocketUrl = validated.websocketUrl ?? merged.websocketUrl ?? this.resolveWebsocketUrl("initiateOutboundConversation");
5130
5560
  const twiml = this.generateTwiml(websocketUrl, merged);
5561
+ const callParams = this.buildCallParams(validated.callOptions);
5562
+ this.logger.debug(
5563
+ { twiml: redactTwimlParameters(twiml), to: maskAddress(validated.to) },
5564
+ "Outbound call TwiML"
5565
+ );
5131
5566
  const client = this.getTwilioClient();
5132
5567
  const call = await client.calls.create({
5133
5568
  to: validated.to,
5134
5569
  from: fromNumber,
5135
- twiml
5570
+ twiml,
5571
+ ...callParams
5136
5572
  });
5137
5573
  this.logger.info(
5138
5574
  { call_sid: call.sid, to: maskAddress(validated.to) },
@@ -5182,6 +5618,177 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5182
5618
  return { status: 200, content: "OK", contentType: "text/plain" };
5183
5619
  }
5184
5620
  // =========================================================================
5621
+ // Call Event Handling (status callback, async AMD, recording)
5622
+ // =========================================================================
5623
+ /**
5624
+ * Whether a call-webhook payload belongs to the configured account.
5625
+ *
5626
+ * Twilio signature validation already gates the route; this is defense in
5627
+ * depth. A payload with no `AccountSid` is allowed through.
5628
+ *
5629
+ * Subaccounts: events carry the SID the call was placed on, so configure TAC
5630
+ * with that account or its events get dropped here.
5631
+ */
5632
+ callEventAccountOk(form) {
5633
+ const accountSid = form["AccountSid"];
5634
+ if (accountSid && accountSid !== this.config.accountSid) {
5635
+ this.logger.warn(
5636
+ { expected: this.config.accountSid, received: accountSid },
5637
+ "Call event AccountSid mismatch, ignoring"
5638
+ );
5639
+ return false;
5640
+ }
5641
+ return true;
5642
+ }
5643
+ /**
5644
+ * Parse a call-event webhook form and dispatch it to its handler.
5645
+ *
5646
+ * Returns 400 when the payload can't be parsed (no `CallSid`) or the handler
5647
+ * throws — better than handing Twilio a 200 for an event that wasn't
5648
+ * processed. Everything else, including no handler registered and an
5649
+ * account mismatch, is a 200 no-op.
5650
+ */
5651
+ async dispatchCallEvent(kind, form, handler, parse, logFields) {
5652
+ const ok = { status: 200, content: "OK", contentType: "text/plain" };
5653
+ if (!handler || !this.callEventAccountOk(form)) {
5654
+ return ok;
5655
+ }
5656
+ try {
5657
+ const event = parse(form);
5658
+ this.logger.debug(logFields(event), `Call ${kind} event received`);
5659
+ await handler(event);
5660
+ } catch (error) {
5661
+ this.logger.error({ err: error, kind }, "Failed to process call event callback");
5662
+ return { status: 400, content: "Bad Request", contentType: "text/plain" };
5663
+ }
5664
+ return ok;
5665
+ }
5666
+ /**
5667
+ * Handle a Twilio `statusCallback` webhook.
5668
+ *
5669
+ * The developer routes the request here (`TACServer` does this automatically
5670
+ * for its `/status` call-event route). Parsed into a {@link CallStatusEvent}
5671
+ * and dispatched to the {@link onCallStatus} handler. No-op if no handler is
5672
+ * registered.
5673
+ *
5674
+ * @param form - Raw form data from the webhook request.
5675
+ */
5676
+ async handleCallStatusEvent(form) {
5677
+ return this.dispatchCallEvent(
5678
+ "status",
5679
+ form,
5680
+ this.onCallStatusHandler,
5681
+ callStatusEventFromForm,
5682
+ (event) => ({ call_sid: event.callSid, call_status: event.callStatus })
5683
+ );
5684
+ }
5685
+ /**
5686
+ * Handle a Twilio `asyncAmdStatusCallback` webhook.
5687
+ *
5688
+ * The developer routes the request here (`TACServer` does this automatically
5689
+ * for its `/amd` call-event route). Parsed into an {@link AmdEvent} and
5690
+ * dispatched to the {@link onAmd} handler. No-op if no handler is registered.
5691
+ *
5692
+ * @param form - Raw form data from the webhook request.
5693
+ */
5694
+ async handleAmdEvent(form) {
5695
+ return this.dispatchCallEvent("amd", form, this.onAmdHandler, amdEventFromForm, (event) => ({
5696
+ call_sid: event.callSid,
5697
+ answered_by: event.answeredBy
5698
+ }));
5699
+ }
5700
+ /**
5701
+ * Handle a Twilio `recordingStatusCallback` webhook.
5702
+ *
5703
+ * The developer routes the request here (`TACServer` does this automatically
5704
+ * for its `/recording` call-event route). Parsed into a
5705
+ * {@link RecordingEvent} and dispatched to the {@link onRecording} handler.
5706
+ * No-op if no handler is registered.
5707
+ *
5708
+ * @param form - Raw form data from the webhook request.
5709
+ */
5710
+ async handleRecordingEvent(form) {
5711
+ return this.dispatchCallEvent(
5712
+ "recording",
5713
+ form,
5714
+ this.onRecordingHandler,
5715
+ recordingEventFromForm,
5716
+ (event) => ({ call_sid: event.callSid, recording_status: event.recordingStatus })
5717
+ );
5718
+ }
5719
+ /**
5720
+ * Hang up a call and clean up its ConversationRelay session.
5721
+ *
5722
+ * Works on `callSid` alone, whether or not a session exists yet. No-ops the
5723
+ * session cleanup if none is tracked.
5724
+ *
5725
+ * Does not throw — hanging up an already-ended call is routine (the callee
5726
+ * hangs up while AMD is still resolving), and handlers shouldn't have to
5727
+ * guard against it.
5728
+ *
5729
+ * @param callSid - Twilio Call SID (from a call event, the outbound result, or
5730
+ * `ConversationSession.callSid`).
5731
+ * @returns True if Twilio accepted the hangup, false if it failed (logged).
5732
+ * Session cleanup runs either way.
5733
+ */
5734
+ async endCall(callSid) {
5735
+ const client = this.getTwilioClient();
5736
+ let hungUp = true;
5737
+ try {
5738
+ await client.calls(callSid).update({ status: "completed" });
5739
+ } catch (error) {
5740
+ hungUp = false;
5741
+ this.logger.error({ err: error, call_sid: callSid }, "Failed to hang up call");
5742
+ }
5743
+ const session = this.getConversationSessionByCallSid(callSid);
5744
+ if (session) {
5745
+ await this.endConversation(session.conversationId);
5746
+ }
5747
+ return hungUp;
5748
+ }
5749
+ /**
5750
+ * Look up the active voice session for a Twilio Call SID.
5751
+ *
5752
+ * Out-of-band code holding a CallSid — a dashboard route, an operator action,
5753
+ * a call-event handler — can't reach the session-facing methods, which are
5754
+ * keyed by conversation id: the Orchestrator conversation id in orchestrator
5755
+ * mode, the CallSid only in ConversationRelay-only mode.
5756
+ *
5757
+ * Relay-only mode creates the session on the first prompt; orchestrated
5758
+ * mode creates it when the lookup started at setup finishes, so it may
5759
+ * exist before the caller speaks — including before `onAmd` fires. Treat it
5760
+ * as racy and hang up with {@link endCall}, which needs no session.
5761
+ *
5762
+ * At the other end, orchestrator mode keeps the session until Conversation
5763
+ * Orchestrator's CLOSED webhook, so it outlives the call and `onCallStatus` /
5764
+ * `onRecording` do resolve. Relay-only mode tears down on the
5765
+ * ConversationRelay callback instead, which races them.
5766
+ *
5767
+ * @example
5768
+ * ```typescript
5769
+ * async function nudge(callSid: string): Promise<void> {
5770
+ * const session = voiceChannel.getConversationSessionByCallSid(callSid);
5771
+ * if (session) {
5772
+ * await voiceChannel.sendResponse(session.conversationId, 'Still there?');
5773
+ * }
5774
+ * }
5775
+ * ```
5776
+ *
5777
+ * @param callSid - Twilio Call SID, e.g. from
5778
+ * `InitiateVoiceConversationResult.callSid` or a call event.
5779
+ * @returns The session, or `undefined` — not created yet, the call ended, or
5780
+ * it landed on another instance (see the horizontal-scaling note in
5781
+ * CLAUDE.md).
5782
+ */
5783
+ getConversationSessionByCallSid(callSid) {
5784
+ for (const session of this.activeConversations.values()) {
5785
+ if (session.callSid === callSid) {
5786
+ return session;
5787
+ }
5788
+ }
5789
+ return void 0;
5790
+ }
5791
+ // =========================================================================
5185
5792
  // Stream Task Management
5186
5793
  // =========================================================================
5187
5794
  /**
@@ -5971,6 +6578,9 @@ var DEFAULT_CONFIG = {
5971
6578
  twiml: "/twiml"
5972
6579
  }
5973
6580
  };
6581
+ function stripTrailingSlash(path) {
6582
+ return path.replace(/\/+$/, "");
6583
+ }
5974
6584
  var TACServer = class {
5975
6585
  fastify;
5976
6586
  tac;
@@ -6003,6 +6613,7 @@ var TACServer = class {
6003
6613
  "Voice channel is configured but TACConfig.voicePublicDomain is not set. Set it directly or via the TWILIO_VOICE_PUBLIC_DOMAIN env var."
6004
6614
  );
6005
6615
  }
6616
+ this.validateCallEventPaths();
6006
6617
  this.messagingChannels = config.messagingChannels ?? [
6007
6618
  tac.getChannel("sms"),
6008
6619
  tac.getChannel("rcs"),
@@ -6030,6 +6641,38 @@ var TACServer = class {
6030
6641
  });
6031
6642
  }
6032
6643
  }
6644
+ /**
6645
+ * Validate `voiceCallEventPath`, the one path that isn't literal.
6646
+ *
6647
+ * Every other TAC path registers as configured, so a bad value is visible.
6648
+ * This one expands into three sub-paths, hiding a mistake the base path looks
6649
+ * innocent for: a sub-path colliding with another route while the base looks
6650
+ * unrelated (base `/hooks` vs `webhookPaths.twiml = '/hooks/status'`). Both
6651
+ * would register as POST routes and requests would reach the wrong handler.
6652
+ *
6653
+ * The leading-slash requirement is enforced by `TACConfigSchema` at parse
6654
+ * time, so it doesn't need re-checking here.
6655
+ */
6656
+ validateCallEventPaths() {
6657
+ const cfg = this.tac.getConfig();
6658
+ const others = {
6659
+ [stripTrailingSlash(this.config.webhookPaths.twiml || "/twiml")]: "webhookPaths.twiml",
6660
+ [stripTrailingSlash(cfg.voiceActionPath)]: "TACConfig.voiceActionPath",
6661
+ [stripTrailingSlash(this.config.webhookPaths.conversation || "/webhook")]: "webhookPaths.conversation"
6662
+ };
6663
+ if (this.config.webhookPaths.cintel) {
6664
+ others[stripTrailingSlash(this.config.webhookPaths.cintel)] = "webhookPaths.cintel";
6665
+ }
6666
+ for (const kind of CALL_EVENT_KINDS) {
6667
+ const path = cfg.callEventPath(kind);
6668
+ const clash = others[stripTrailingSlash(path)];
6669
+ if (clash !== void 0) {
6670
+ throw new Error(
6671
+ `TACConfig.voiceCallEventPath expands to '${path}', which collides with ${clash}. Both would register as POST routes and requests would reach the wrong handler.`
6672
+ );
6673
+ }
6674
+ }
6675
+ }
6033
6676
  getForwardedProto(request) {
6034
6677
  const raw = request.headers["x-forwarded-proto"];
6035
6678
  return raw?.split(",")[0]?.trim() || "https";
@@ -6154,6 +6797,34 @@ var TACServer = class {
6154
6797
  }
6155
6798
  }
6156
6799
  );
6800
+ const callEventHandlers = {
6801
+ status: (channel, form) => channel.handleCallStatusEvent(form),
6802
+ amd: (channel, form) => channel.handleAmdEvent(form),
6803
+ recording: (channel, form) => channel.handleRecordingEvent(form)
6804
+ };
6805
+ for (const kind of CALL_EVENT_KINDS) {
6806
+ const handle = callEventHandlers[kind];
6807
+ this.fastify.post(
6808
+ this.tac.getConfig().callEventPath(kind),
6809
+ validateSignature,
6810
+ async (request, reply) => {
6811
+ try {
6812
+ if (!this.voiceChannel) {
6813
+ await reply.code(500).send({ error: "Voice channel not available" });
6814
+ return;
6815
+ }
6816
+ const formData = request.body ?? {};
6817
+ const result = await handle(this.voiceChannel, formData);
6818
+ await reply.code(result.status).type(result.contentType).send(result.content);
6819
+ } catch (error) {
6820
+ this.fastify.log.error(
6821
+ `Call ${kind} callback error: ` + (error instanceof Error ? error.message : String(error))
6822
+ );
6823
+ await reply.code(500).send({ error: "Internal server error" });
6824
+ }
6825
+ }
6826
+ );
6827
+ }
6157
6828
  await this.fastify.register((fastify) => {
6158
6829
  fastify.get(
6159
6830
  this.tac.getConfig().voiceWebsocketPath,
@@ -6277,6 +6948,11 @@ var TACServer = class {
6277
6948
  twiml_webhook: this.config.webhookPaths.twiml,
6278
6949
  ws_websocket: this.tac.getConfig().voiceWebsocketPath,
6279
6950
  conversation_relay_callback: this.tac.getConfig().voiceActionPath,
6951
+ ...this.voiceChannel && {
6952
+ call_event_callbacks: CALL_EVENT_KINDS.map(
6953
+ (kind) => this.tac.getConfig().callEventPath(kind)
6954
+ )
6955
+ },
6280
6956
  ...this.config.webhookPaths.cintel && {
6281
6957
  cintel_webhook: this.config.webhookPaths.cintel
6282
6958
  }
@@ -6340,6 +7016,6 @@ var TACServer = class {
6340
7016
  }
6341
7017
  };
6342
7018
 
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 };
7019
+ export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AmdEventSchema, AnthropicToolSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CALL_EVENT_KINDS, CallEventKindSchema, CallOptionsSchema, CallStatusEventSchema, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, CreateObservationsRequestSchema, CustomParametersSchema, EMPTY_MEMORY_RESPONSE, EnvironmentVariables, ExecutionDetailsSchema, HandoffPayloadSchema, InitiateMessagingConversationOptionsSchema, InitiateVoiceConversationOptionsSchema, IntelligenceConfigurationSchema, InterruptMessageSchema, InterruptModeSchema, JSONSchemaSchema, KnowledgeBaseSchema, KnowledgeBaseStatusSchema, KnowledgeChunkResultSchema, KnowledgeClient, KnowledgeSearchResponseSchema, LanguageAttributesSchema, LanguageConfigSchema, ListCommunicationsResponseSchema, ListConversationsResponseSchema, ListParticipantsResponseSchema, MemoryChannelTypeSchema, MemoryClient, MemoryCommunicationContentSchema, MemoryCommunicationSchema, MemoryDeliveryStatusSchema, MemoryModeSchema, MemoryParticipantSchema, MemoryParticipantTypeSchema, MemoryPromptBuilder, MemoryRetrievalRequestSchema, MemoryRetrievalResponseSchema, MessageDirectionSchema, MessagingChannel, ObservationCreateRequestSchema, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, RecordingEventSchema, SMSChannel, SendMessageActionPayloadSchema, SendMessageActionRequestSchema, SessionInfoSchema, SessionMessageSchema, SetupMessageSchema, StatusCallbackSchema, StatusTimeoutsSchema, SummaryInfoSchema, TAC, TACChannelTypeSchema, TACCommunicationAuthorSchema, TACCommunicationContentSchema, TACCommunicationSchema, TACConfig, TACConfigSchema, TACDeliveryStatusSchema, TACMemoryResponse, TACParticipantTypeSchema, TACServer, TACTool, TextTokenMessageSchema, ToolExecutionResultSchema, TranscriptionSchema, TranscriptionWordSchema, TwiMLOptionsSchema, TwiMLRequestSchema, TwilioMemoryConfigSchema, VoiceChannel, WebSocketMessageSchema, WhatsAppChannel, amdEventFromForm, buildHandoffPayload, callOptionsToCreateParams, callStatusEventFromForm, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, recordingEventFromForm, redactTwimlParameters, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl, twiMLRequestFromForm };
6344
7020
  //# sourceMappingURL=index.js.map
6345
7021
  //# sourceMappingURL=index.js.map