twilio-agent-connect 2.0.0 → 2.1.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
@@ -33,6 +33,8 @@ var voicePathSchema = (defaultPath) => z.preprocess((v) => {
33
33
  const trimmed = v.trim();
34
34
  return trimmed.length === 0 ? void 0 : trimmed;
35
35
  }, z.string().startsWith("/", 'Path must start with "/"').default(defaultPath));
36
+ var CallEventKindSchema = z.enum(["status", "amd", "recording"]);
37
+ var CALL_EVENT_KINDS = CallEventKindSchema.options;
36
38
  var TACConfigSchema = z.object({
37
39
  accountSid: z.string().min(1, "Twilio Account SID is required"),
38
40
  authToken: z.string().min(1, "Twilio Auth Token is required"),
@@ -88,8 +90,14 @@ var TACConfigSchema = z.object({
88
90
  * Must start with '/'.
89
91
  */
90
92
  voiceActionPath: voicePathSchema("/conversation-relay-callback"),
93
+ /**
94
+ * Base path for the call-event callbacks (status, async AMD, recording).
95
+ * TACServer registers one route per callback under it — `<base>/status`,
96
+ * `<base>/amd`, `<base>/recording` — so the route identifies the event. Same
97
+ * role as voiceActionPath. Must start with '/'.
98
+ */
99
+ voiceCallEventPath: voicePathSchema("/twilio/call-events"),
91
100
  cintelConfigurationId: z.string().optional(),
92
- cintelObservationOperatorSid: z.string().optional(),
93
101
  cintelSummaryOperatorSid: z.string().optional(),
94
102
  region: z.string().max(63, "Invalid Twilio region format (must be a valid DNS label)").regex(
95
103
  /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/,
@@ -124,8 +132,8 @@ var EnvironmentVariables = {
124
132
  TWILIO_VOICE_PUBLIC_DOMAIN: "TWILIO_VOICE_PUBLIC_DOMAIN",
125
133
  TWILIO_VOICE_WEBSOCKET_PATH: "TWILIO_VOICE_WEBSOCKET_PATH",
126
134
  TWILIO_VOICE_ACTION_PATH: "TWILIO_VOICE_ACTION_PATH",
135
+ TWILIO_VOICE_CALL_EVENT_PATH: "TWILIO_VOICE_CALL_EVENT_PATH",
127
136
  TWILIO_TAC_CI_CONFIGURATION_ID: "TWILIO_TAC_CI_CONFIGURATION_ID",
128
- TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID: "TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID",
129
137
  TWILIO_TAC_CI_SUMMARY_OPERATOR_SID: "TWILIO_TAC_CI_SUMMARY_OPERATOR_SID",
130
138
  TWILIO_REGION: "TWILIO_REGION",
131
139
  TWILIO_STUDIO_HANDOFF_FLOW_SID: "TWILIO_STUDIO_HANDOFF_FLOW_SID"
@@ -232,6 +240,13 @@ var AuthorInfoSchema = z.object({
232
240
  });
233
241
  var ConversationSessionSchema = z.object({
234
242
  conversationId: z.string().min(1, "Conversation ID is required"),
243
+ /**
244
+ * Twilio Call SID on the Voice channel, unset on messaging. The correlation
245
+ * key for call events (`VoiceChannel.onCallStatus` / `onAmd` / `onRecording`)
246
+ * and `endCall`. Equals `conversationId` in relay-only mode; look the session
247
+ * up the other way with `VoiceChannel.getConversationSessionByCallSid`.
248
+ */
249
+ callSid: z.string().optional(),
235
250
  profileId: z.string().optional(),
236
251
  serviceId: z.string().optional(),
237
252
  channel: ChannelTypeSchema,
@@ -548,7 +563,13 @@ var MemoryChannelTypeSchema = z.enum([
548
563
  "API",
549
564
  "SYSTEM"
550
565
  ]);
551
- var MemoryParticipantTypeSchema = z.enum(["HUMAN_AGENT", "CUSTOMER", "AI_AGENT", "AGENT"]);
566
+ var MemoryParticipantTypeSchema = z.enum([
567
+ "HUMAN_AGENT",
568
+ "CUSTOMER",
569
+ "AI_AGENT",
570
+ "AGENT",
571
+ "UNKNOWN"
572
+ ]);
552
573
  var MemoryDeliveryStatusSchema = z.enum([
553
574
  "INITIATED",
554
575
  "IN_PROGRESS",
@@ -640,11 +661,17 @@ var EMPTY_MEMORY_RESPONSE = {
640
661
  summaries: [],
641
662
  communications: []
642
663
  };
643
- var CreateObservationResponseSchema = z.object({
664
+ var ObservationCreateRequestSchema = z.object({
644
665
  content: z.string(),
645
666
  source: z.string(),
646
667
  occurredAt: z.string(),
647
- conversationIds: z.array(z.string())
668
+ conversationIds: z.array(z.string()).optional()
669
+ });
670
+ var CreateObservationsRequestSchema = z.object({
671
+ observations: z.array(ObservationCreateRequestSchema)
672
+ });
673
+ var CreateObservationResponseSchema = z.object({
674
+ message: z.string()
648
675
  });
649
676
  var CreateConversationSummariesResponseSchema = z.object({
650
677
  message: z.string()
@@ -1026,10 +1053,193 @@ var ConversationRelayCallbackPayloadSchema = z.object({
1026
1053
  SessionStatus: z.string().optional(),
1027
1054
  SessionDuration: z.string().optional()
1028
1055
  });
1056
+ function splitCallEventForm(form, aliases) {
1057
+ const known = {};
1058
+ const extra = {};
1059
+ for (const [key, value] of Object.entries(form)) {
1060
+ const alias = aliases[key];
1061
+ if (alias) {
1062
+ known[alias] = value;
1063
+ } else {
1064
+ extra[key] = value;
1065
+ }
1066
+ }
1067
+ return { known, extra };
1068
+ }
1069
+ var CallEventBaseShape = {
1070
+ callSid: z.string().min(1, "CallSid is required"),
1071
+ accountSid: z.string().optional(),
1072
+ /** Any other Twilio webhook fields not surfaced above. */
1073
+ extra: z.record(z.string(), z.string()).default({})
1074
+ };
1075
+ var CALL_EVENT_BASE_ALIASES = {
1076
+ CallSid: "callSid",
1077
+ AccountSid: "accountSid"
1078
+ };
1079
+ var UNREACHED_CALL_STATUSES = ["busy", "no-answer", "failed", "canceled"];
1080
+ var CallStatusEventSchema = z.object({
1081
+ ...CallEventBaseShape,
1082
+ callStatus: z.string().optional(),
1083
+ callDuration: z.string().optional(),
1084
+ sipResponseCode: z.string().optional()
1085
+ }).transform((event) => ({
1086
+ ...event,
1087
+ /** Call ended without reaching the callee — i.e. worth a retry. */
1088
+ isUnreached: UNREACHED_CALL_STATUSES.includes(event.callStatus ?? "")
1089
+ }));
1090
+ var CALL_STATUS_EVENT_ALIASES = {
1091
+ ...CALL_EVENT_BASE_ALIASES,
1092
+ CallStatus: "callStatus",
1093
+ CallDuration: "callDuration",
1094
+ SipResponseCode: "sipResponseCode"
1095
+ };
1096
+ function callStatusEventFromForm(form) {
1097
+ const { known, extra } = splitCallEventForm(form, CALL_STATUS_EVENT_ALIASES);
1098
+ return CallStatusEventSchema.parse({ ...known, extra });
1099
+ }
1100
+ var AmdEventSchema = z.object({
1101
+ ...CallEventBaseShape,
1102
+ answeredBy: z.string().optional(),
1103
+ machineDetectionDuration: z.string().optional()
1104
+ }).transform((event) => ({
1105
+ ...event,
1106
+ /**
1107
+ * A machine answered — any `machine_*` value, either mode. `unknown`
1108
+ * (detection timed out) is false, so a call is never hung up on a guess.
1109
+ */
1110
+ isMachine: (event.answeredBy ?? "").startsWith("machine")
1111
+ }));
1112
+ var AMD_EVENT_ALIASES = {
1113
+ ...CALL_EVENT_BASE_ALIASES,
1114
+ AnsweredBy: "answeredBy",
1115
+ MachineDetectionDuration: "machineDetectionDuration"
1116
+ };
1117
+ function amdEventFromForm(form) {
1118
+ const { known, extra } = splitCallEventForm(form, AMD_EVENT_ALIASES);
1119
+ return AmdEventSchema.parse({ ...known, extra });
1120
+ }
1121
+ var RecordingEventSchema = z.object({
1122
+ ...CallEventBaseShape,
1123
+ recordingSid: z.string().optional(),
1124
+ recordingUrl: z.string().optional(),
1125
+ recordingStatus: z.string().optional(),
1126
+ recordingDuration: z.string().optional()
1127
+ });
1128
+ var RECORDING_EVENT_ALIASES = {
1129
+ ...CALL_EVENT_BASE_ALIASES,
1130
+ RecordingSid: "recordingSid",
1131
+ RecordingUrl: "recordingUrl",
1132
+ RecordingStatus: "recordingStatus",
1133
+ RecordingDuration: "recordingDuration"
1134
+ };
1135
+ function recordingEventFromForm(form) {
1136
+ const { known, extra } = splitCallEventForm(form, RECORDING_EVENT_ALIASES);
1137
+ return RecordingEventSchema.parse({ ...known, extra });
1138
+ }
1139
+ var CALLS_CREATE_PARAMS = [
1140
+ "applicationSid",
1141
+ "asyncAmd",
1142
+ "asyncAmdStatusCallback",
1143
+ "asyncAmdStatusCallbackMethod",
1144
+ "byoc",
1145
+ "callerId",
1146
+ "callReason",
1147
+ "callToken",
1148
+ "clientNotificationUrl",
1149
+ "fallbackMethod",
1150
+ "fallbackUrl",
1151
+ "from",
1152
+ "machineDetection",
1153
+ "machineDetectionSilenceTimeout",
1154
+ "machineDetectionSpeechEndThreshold",
1155
+ "machineDetectionSpeechThreshold",
1156
+ "machineDetectionTimeout",
1157
+ "method",
1158
+ "record",
1159
+ "recordingChannels",
1160
+ "recordingStatusCallback",
1161
+ "recordingStatusCallbackEvent",
1162
+ "recordingStatusCallbackMethod",
1163
+ "recordingTrack",
1164
+ "sendDigits",
1165
+ "sipAuthPassword",
1166
+ "sipAuthUsername",
1167
+ "statusCallback",
1168
+ "statusCallbackEvent",
1169
+ "statusCallbackMethod",
1170
+ "timeLimit",
1171
+ "timeout",
1172
+ "to",
1173
+ "trim",
1174
+ "twiml",
1175
+ "url"
1176
+ ];
1177
+ var RESERVED_CALL_PARAMS = ["to", "from", "twiml", "url", "applicationSid"];
1178
+ var ACCEPTED_CALL_PARAMS = new Set(CALLS_CREATE_PARAMS);
1179
+ function isAsyncAmdEnabled(value) {
1180
+ if (typeof value === "boolean") return value;
1181
+ if (typeof value === "string") return value.trim().toLowerCase() === "true";
1182
+ return false;
1183
+ }
1184
+ var CallOptionsObjectSchema = z.looseObject({
1185
+ machineDetection: z.enum(["Enable", "DetectMessageEnd"]).optional(),
1186
+ asyncAmd: z.union([z.boolean(), z.string()]).optional(),
1187
+ asyncAmdStatusCallback: z.string().optional(),
1188
+ asyncAmdStatusCallbackMethod: z.string().optional(),
1189
+ machineDetectionTimeout: z.number().int().optional(),
1190
+ machineDetectionSpeechThreshold: z.number().int().optional(),
1191
+ machineDetectionSpeechEndThreshold: z.number().int().optional(),
1192
+ machineDetectionSilenceTimeout: z.number().int().optional(),
1193
+ record: z.boolean().optional(),
1194
+ recordingStatusCallback: z.string().optional(),
1195
+ recordingStatusCallbackEvent: z.array(z.string()).optional(),
1196
+ recordingChannels: z.string().optional(),
1197
+ recordingTrack: z.string().optional(),
1198
+ statusCallback: z.string().optional(),
1199
+ statusCallbackEvent: z.array(z.string()).optional(),
1200
+ statusCallbackMethod: z.string().optional(),
1201
+ timeout: z.number().int().optional()
1202
+ }).superRefine((value, ctx) => {
1203
+ const supplied = Object.keys(value);
1204
+ const conflict = supplied.filter(
1205
+ (key) => RESERVED_CALL_PARAMS.includes(key)
1206
+ );
1207
+ if (conflict.length > 0) {
1208
+ ctx.addIssue({
1209
+ code: "custom",
1210
+ message: `callOptions may not set TAC-owned call parameters: ${conflict.sort().join(", ")}. TAC builds the call and its TwiML.`
1211
+ });
1212
+ }
1213
+ const unknown = supplied.filter((key) => !ACCEPTED_CALL_PARAMS.has(key));
1214
+ if (unknown.length > 0) {
1215
+ ctx.addIssue({
1216
+ code: "custom",
1217
+ message: `callOptions has parameters Twilio's calls.create() does not accept: ${unknown.sort().join(", ")}. Check for a typo, or upgrade the twilio package.`
1218
+ });
1219
+ }
1220
+ if (Boolean(value.machineDetection) !== isAsyncAmdEnabled(value.asyncAmd)) {
1221
+ ctx.addIssue({
1222
+ code: "custom",
1223
+ message: `AMD requires both machineDetection and asyncAmd; got machineDetection=${JSON.stringify(value.machineDetection)}, asyncAmd=${JSON.stringify(value.asyncAmd)}.`
1224
+ });
1225
+ }
1226
+ });
1227
+ var CallOptionsSchema = CallOptionsObjectSchema.transform(
1228
+ (value) => value
1229
+ );
1230
+ function callOptionsToCreateParams(options) {
1231
+ const params = {};
1232
+ for (const [key, value] of Object.entries(options)) {
1233
+ if (value === void 0) continue;
1234
+ params[key] = key === "asyncAmd" && typeof value === "boolean" ? String(value) : value;
1235
+ }
1236
+ return params;
1237
+ }
1029
1238
  var InitiateVoiceConversationOptionsSchema = z.object({
1030
1239
  to: z.string().min(1, "Recipient phone number is required"),
1031
1240
  websocketUrl: z.url().optional(),
1032
- twimlOptions: TwiMLOptionsSchema.optional()
1241
+ twimlOptions: TwiMLOptionsSchema.optional(),
1242
+ callOptions: CallOptionsSchema.optional()
1033
1243
  }).strict();
1034
1244
  var JSONSchemaSchema = z.object({
1035
1245
  type: z.enum(["object", "string", "number", "boolean", "array"]),
@@ -1047,6 +1257,11 @@ var OpenAIToolSchema = z.object({
1047
1257
  parameters: JSONSchemaSchema
1048
1258
  })
1049
1259
  });
1260
+ var AnthropicToolSchema = z.object({
1261
+ name: z.string(),
1262
+ description: z.string(),
1263
+ input_schema: JSONSchemaSchema
1264
+ });
1050
1265
  var ToolExecutionResultSchema = z.object({
1051
1266
  success: z.boolean(),
1052
1267
  data: z.any().optional(),
@@ -1101,7 +1316,6 @@ var OperatorProcessingResultSchema = z.object({
1101
1316
  });
1102
1317
  var ConversationIntelligenceConfigSchema = z.object({
1103
1318
  configurationId: z.string(),
1104
- observationOperatorSid: z.string().optional(),
1105
1319
  summaryOperatorSid: z.string().optional()
1106
1320
  });
1107
1321
  var ConversationSummaryItemSchema = z.object({
@@ -1152,8 +1366,9 @@ var TACConfig = class _TACConfig {
1152
1366
  voiceWebsocketPath;
1153
1367
  /** Path the ConversationRelay action callback is served at (default '/conversation-relay-callback'). */
1154
1368
  voiceActionPath;
1369
+ /** Base path the call-event callbacks are served under (default '/twilio/call-events'). */
1370
+ voiceCallEventPath;
1155
1371
  cintelConfigurationId;
1156
- cintelObservationOperatorSid;
1157
1372
  cintelSummaryOperatorSid;
1158
1373
  /** Optional Twilio region subdomain for API routing (e.g. transforms base URLs to `https://{product}.{region}.twilio.com`) */
1159
1374
  region;
@@ -1187,12 +1402,10 @@ var TACConfig = class _TACConfig {
1187
1402
  }
1188
1403
  this.voiceWebsocketPath = validatedConfig.voiceWebsocketPath;
1189
1404
  this.voiceActionPath = validatedConfig.voiceActionPath;
1405
+ this.voiceCallEventPath = validatedConfig.voiceCallEventPath;
1190
1406
  if (validatedConfig.cintelConfigurationId) {
1191
1407
  this.cintelConfigurationId = validatedConfig.cintelConfigurationId;
1192
1408
  }
1193
- if (validatedConfig.cintelObservationOperatorSid) {
1194
- this.cintelObservationOperatorSid = validatedConfig.cintelObservationOperatorSid;
1195
- }
1196
1409
  if (validatedConfig.cintelSummaryOperatorSid) {
1197
1410
  this.cintelSummaryOperatorSid = validatedConfig.cintelSummaryOperatorSid;
1198
1411
  }
@@ -1219,6 +1432,7 @@ var TACConfig = class _TACConfig {
1219
1432
  * - TWILIO_VOICE_PUBLIC_DOMAIN: Public domain for voice routes (required for voice; domain only, without protocol/port/path, e.g., 'abc123.ngrok.app')
1220
1433
  * - TWILIO_VOICE_WEBSOCKET_PATH: Path for the voice WebSocket (default: /ws)
1221
1434
  * - TWILIO_VOICE_ACTION_PATH: Path for the ConversationRelay action callback (default: /conversation-relay-callback)
1435
+ * - TWILIO_VOICE_CALL_EVENT_PATH: Base path for the call-event callbacks — status, async AMD, recording (default: /twilio/call-events)
1222
1436
  * - TWILIO_REGION: Twilio region subdomain for API routing (e.g. transforms base URLs to `https://{product}.{region}.twilio.com`)
1223
1437
  * - TWILIO_STUDIO_HANDOFF_FLOW_SID: Studio Flow SID used by createStudioHandoffTool for human handoff
1224
1438
  * - TWILIO_RCS_SENDER_ID: RCS Sender ID for the RCS channel
@@ -1330,17 +1544,37 @@ var TACConfig = class _TACConfig {
1330
1544
  voicePublicDomain: process.env[EnvironmentVariables.TWILIO_VOICE_PUBLIC_DOMAIN],
1331
1545
  voiceWebsocketPath: process.env[EnvironmentVariables.TWILIO_VOICE_WEBSOCKET_PATH] || void 0,
1332
1546
  voiceActionPath: process.env[EnvironmentVariables.TWILIO_VOICE_ACTION_PATH] || void 0,
1547
+ voiceCallEventPath: process.env[EnvironmentVariables.TWILIO_VOICE_CALL_EVENT_PATH] || void 0,
1333
1548
  cintelConfigurationId: process.env[EnvironmentVariables.TWILIO_TAC_CI_CONFIGURATION_ID],
1334
- cintelObservationOperatorSid: process.env[EnvironmentVariables.TWILIO_TAC_CI_OBSERVATION_OPERATOR_SID],
1335
1549
  cintelSummaryOperatorSid: process.env[EnvironmentVariables.TWILIO_TAC_CI_SUMMARY_OPERATOR_SID],
1336
1550
  region: process.env[EnvironmentVariables.TWILIO_REGION],
1337
1551
  studioHandoffFlowSid: process.env[EnvironmentVariables.TWILIO_STUDIO_HANDOFF_FLOW_SID]
1338
1552
  };
1339
1553
  return new _TACConfig(rawConfig);
1340
1554
  }
1555
+ /**
1556
+ * Path a call-event callback is served at.
1557
+ *
1558
+ * Single source of truth: the voice channel builds callback URLs from this
1559
+ * and TACServer registers routes at it, so the two can't drift.
1560
+ */
1561
+ callEventPath(kind) {
1562
+ return `${this.voiceCallEventPath.replace(/\/+$/, "")}/${kind}`;
1563
+ }
1564
+ /**
1565
+ * Public URL for a call-event callback, or `undefined` without a public domain.
1566
+ */
1567
+ callEventUrl(kind) {
1568
+ if (!this.voicePublicDomain) {
1569
+ return void 0;
1570
+ }
1571
+ return `https://${this.voicePublicDomain}${this.callEventPath(kind)}`;
1572
+ }
1341
1573
  /**
1342
1574
  * Whether Conversation Orchestrator is configured.
1343
1575
  * Returns false in voice-only mode (no conversationConfigurationId).
1576
+ *
1577
+ * @internal
1344
1578
  */
1345
1579
  isOrchestratorEnabled() {
1346
1580
  return this.conversationConfigurationId !== void 0;
@@ -1425,6 +1659,13 @@ function maskAddress(address) {
1425
1659
  if (address.length <= 1) return "***";
1426
1660
  return `${address[0]}***`;
1427
1661
  }
1662
+ function redactTwimlParameters(twiml) {
1663
+ if (!twiml) return "";
1664
+ const parameterValueRe = /(<Parameter\b[^>]*?\bvalue=)(["'])(.*?)\2/gi;
1665
+ return twiml.replace(parameterValueRe, (_match, prefix, quote) => {
1666
+ return `${prefix}${quote}***${quote}`;
1667
+ });
1668
+ }
1428
1669
 
1429
1670
  // packages/core/src/lib/logger.ts
1430
1671
  function piiLogMethod(args, method) {
@@ -1444,7 +1685,7 @@ function createLogger(options) {
1444
1685
 
1445
1686
  // package.json
1446
1687
  var package_default = {
1447
- version: "2.0.0"};
1688
+ version: "2.1.0"};
1448
1689
  function buildUserAgent() {
1449
1690
  return `twilio-agent-connect-typescript/${package_default.version}`;
1450
1691
  }
@@ -1525,7 +1766,7 @@ var BaseClient = class {
1525
1766
  };
1526
1767
 
1527
1768
  // packages/core/src/clients/memory.ts
1528
- var MemoryClient = class extends BaseClient {
1769
+ var MemoryClient = class _MemoryClient extends BaseClient {
1529
1770
  storeId;
1530
1771
  constructor(config, storeId, logger) {
1531
1772
  const baseUrl = config.region ? `https://memory.${config.region}.twilio.com` : "https://memory.twilio.com";
@@ -1578,28 +1819,46 @@ var MemoryClient = class extends BaseClient {
1578
1819
  },
1579
1820
  "Raw memory response received"
1580
1821
  );
1581
- const validatedResponse = MemoryRetrievalResponseSchema.safeParse(data);
1582
- if (!validatedResponse.success) {
1822
+ if (data === null || typeof data !== "object") {
1583
1823
  this.logger.warn(
1584
1824
  {
1585
1825
  profile_id: profileId,
1586
- memory_store_id: this.storeId,
1587
- validation_errors: validatedResponse.error.issues
1826
+ memory_store_id: this.storeId
1588
1827
  },
1589
1828
  "Invalid memory response format"
1590
1829
  );
1591
1830
  return EMPTY_MEMORY_RESPONSE;
1592
1831
  }
1832
+ const response = data;
1833
+ const observations = this.parseItems(
1834
+ response.observations,
1835
+ ObservationInfoSchema,
1836
+ "observation",
1837
+ profileId
1838
+ );
1839
+ const summaries = this.parseItems(
1840
+ response.summaries,
1841
+ SummaryInfoSchema,
1842
+ "summary",
1843
+ profileId
1844
+ );
1845
+ const communications = this.parseItems(
1846
+ response.communications,
1847
+ MemoryCommunicationSchema,
1848
+ "communication",
1849
+ profileId
1850
+ );
1593
1851
  this.logger.debug(
1594
1852
  {
1595
1853
  memory_store_id: this.storeId,
1596
1854
  profile_id: profileId,
1597
- observation_count: validatedResponse.data.observations.length,
1598
- summary_count: validatedResponse.data.summaries.length
1855
+ observation_count: observations.length,
1856
+ summary_count: summaries.length,
1857
+ communication_count: communications.length
1599
1858
  },
1600
1859
  "Memory retrieval succeeded"
1601
1860
  );
1602
- return validatedResponse.data;
1861
+ return { observations, summaries, communications };
1603
1862
  } catch (error) {
1604
1863
  this.logger.warn(
1605
1864
  {
@@ -1612,6 +1871,67 @@ var MemoryClient = class extends BaseClient {
1612
1871
  return EMPTY_MEMORY_RESPONSE;
1613
1872
  }
1614
1873
  }
1874
+ /** Cap on per-item validation warnings logged per response, to avoid flooding logs. */
1875
+ static MAX_INVALID_ITEM_LOGS = 10;
1876
+ /**
1877
+ * Validate an array of memory items one at a time, dropping (and logging)
1878
+ * any that fail validation while keeping the rest.
1879
+ *
1880
+ * Per-item warnings are capped at {@link MemoryClient.MAX_INVALID_ITEM_LOGS}
1881
+ * and followed by a single summary warning with the total `invalid_count`.
1882
+ */
1883
+ parseItems(value, schema, itemType, profileId) {
1884
+ if (value === void 0) {
1885
+ return [];
1886
+ }
1887
+ if (!Array.isArray(value)) {
1888
+ this.logger.warn(
1889
+ {
1890
+ profile_id: profileId,
1891
+ memory_store_id: this.storeId,
1892
+ item_type: itemType,
1893
+ received_type: typeof value
1894
+ },
1895
+ "Expected an array of memory items but received a non-array value"
1896
+ );
1897
+ return [];
1898
+ }
1899
+ const parsed = [];
1900
+ let invalidCount = 0;
1901
+ for (const [index, item] of value.entries()) {
1902
+ const result = schema.safeParse(item);
1903
+ if (result.success) {
1904
+ parsed.push(result.data);
1905
+ continue;
1906
+ }
1907
+ invalidCount += 1;
1908
+ if (invalidCount <= _MemoryClient.MAX_INVALID_ITEM_LOGS) {
1909
+ this.logger.warn(
1910
+ {
1911
+ profile_id: profileId,
1912
+ memory_store_id: this.storeId,
1913
+ item_type: itemType,
1914
+ item_index: index,
1915
+ validation_errors: result.error.issues
1916
+ },
1917
+ "Dropping invalid memory item"
1918
+ );
1919
+ }
1920
+ }
1921
+ if (invalidCount > 0) {
1922
+ this.logger.warn(
1923
+ {
1924
+ profile_id: profileId,
1925
+ memory_store_id: this.storeId,
1926
+ item_type: itemType,
1927
+ invalid_count: invalidCount,
1928
+ total_count: value.length
1929
+ },
1930
+ "Dropped invalid memory items"
1931
+ );
1932
+ }
1933
+ return parsed;
1934
+ }
1615
1935
  /**
1616
1936
  * Find profiles that contain a specific identifier value
1617
1937
  *
@@ -1699,27 +2019,30 @@ var MemoryClient = class extends BaseClient {
1699
2019
  }
1700
2020
  }
1701
2021
  /**
1702
- * Create an observation for a profile
2022
+ * Create an observation for a profile.
2023
+ *
2024
+ * The Memory API Observations endpoint is a batch create: the observation is
2025
+ * wrapped in an `observations` array and `occurredAt` is required, so it
2026
+ * defaults to the current time (ISO 8601) when omitted or blank.
1703
2027
  *
1704
2028
  * @param profileId - The profile ID to create the observation for
1705
2029
  * @param content - The observation content
1706
2030
  * @param source - Source of the observation (default: 'conversation-intelligence')
1707
2031
  * @param conversationIds - Optional array of conversation IDs associated with this observation
1708
- * @param occurredAt - Optional timestamp when the observation occurred
1709
- * @returns Promise containing the created observation
2032
+ * @param occurredAt - Timestamp when the observation occurred (ISO 8601); defaults to now when omitted or blank
2033
+ * @returns Promise containing the API confirmation message
1710
2034
  */
1711
2035
  async createObservation(profileId, content, source = "conversation-intelligence", conversationIds, occurredAt) {
1712
2036
  const url = `/v1/Stores/${this.storeId}/Profiles/${profileId}/Observations`;
1713
- const requestBody = {
2037
+ const observation = {
1714
2038
  content,
1715
- source
2039
+ source,
2040
+ occurredAt: occurredAt && occurredAt.trim() ? occurredAt : (/* @__PURE__ */ new Date()).toISOString()
1716
2041
  };
1717
2042
  if (conversationIds && conversationIds.length > 0) {
1718
- requestBody.conversationIds = conversationIds;
1719
- }
1720
- if (occurredAt) {
1721
- requestBody.occurredAt = occurredAt;
2043
+ observation.conversationIds = conversationIds;
1722
2044
  }
2045
+ const requestBody = { observations: [observation] };
1723
2046
  try {
1724
2047
  const data = await this.makeRequest(url, "POST", requestBody);
1725
2048
  return CreateObservationResponseSchema.parse(data);
@@ -2128,22 +2451,6 @@ function generateContent(operatorResult) {
2128
2451
  const jsonString = JSON.stringify(result);
2129
2452
  return jsonString === "{}" || jsonString === "[]" ? void 0 : jsonString;
2130
2453
  }
2131
- function parseObservationsContent(jsonContent) {
2132
- try {
2133
- const parsed = JSON.parse(jsonContent);
2134
- if (typeof parsed === "object" && parsed !== null && "observations" in parsed) {
2135
- const observations = parsed.observations;
2136
- if (Array.isArray(observations)) {
2137
- return observations.filter(
2138
- (obs) => typeof obs === "string" && obs.trim() !== ""
2139
- );
2140
- }
2141
- }
2142
- return [];
2143
- } catch {
2144
- return [];
2145
- }
2146
- }
2147
2454
  function parseSummariesContent(jsonContent) {
2148
2455
  try {
2149
2456
  const parsed = JSON.parse(jsonContent);
@@ -2281,13 +2588,11 @@ var OperatorResultProcessor = class {
2281
2588
  */
2282
2589
  async processOperatorResult(event, operatorResult) {
2283
2590
  const operatorSid = operatorResult.operator.id;
2284
- const isObservationOperator = this.config.observationOperatorSid === operatorSid;
2285
2591
  const isSummaryOperator = this.config.summaryOperatorSid === operatorSid;
2286
- if (!isObservationOperator && !isSummaryOperator) {
2592
+ if (!isSummaryOperator) {
2287
2593
  this.logger.debug(
2288
2594
  {
2289
2595
  operator_sid: operatorSid,
2290
- observation_operator_sid: this.config.observationOperatorSid,
2291
2596
  summary_operator_sid: this.config.summaryOperatorSid
2292
2597
  },
2293
2598
  "Skipping unconfigured operator"
@@ -2325,75 +2630,7 @@ var OperatorResultProcessor = class {
2325
2630
  createdCount: 0
2326
2631
  };
2327
2632
  }
2328
- if (isObservationOperator) {
2329
- return this.processObservationEvent(event, operatorResult, content, profileIds);
2330
- } else {
2331
- return this.processSummaryEvent(event, operatorResult, content, profileIds);
2332
- }
2333
- }
2334
- /**
2335
- * Process an observation operator result
2336
- */
2337
- async processObservationEvent(event, operatorResult, content, profileIds) {
2338
- const observations = parseObservationsContent(content);
2339
- if (observations.length === 0) {
2340
- this.logger.debug(
2341
- { operator_sid: operatorResult.operator.id },
2342
- "No observations found in content"
2343
- );
2344
- return {
2345
- success: true,
2346
- eventType: "observation",
2347
- skipped: true,
2348
- skipReason: "No observations found in operator result content",
2349
- createdCount: 0
2350
- };
2351
- }
2352
- let createdCount = 0;
2353
- for (const profileId of profileIds) {
2354
- for (const observation of observations) {
2355
- try {
2356
- await this.memoryClient.createObservation(
2357
- profileId,
2358
- observation,
2359
- "conversation-intelligence",
2360
- [event.conversationId],
2361
- operatorResult.dateCreated
2362
- );
2363
- createdCount++;
2364
- this.logger.debug(
2365
- {
2366
- profile_id: profileId,
2367
- conversation_id: event.conversationId,
2368
- observation_preview: observation.substring(0, 100)
2369
- },
2370
- "Created observation"
2371
- );
2372
- } catch (error) {
2373
- this.logger.error(
2374
- {
2375
- err: error,
2376
- profile_id: profileId,
2377
- conversation_id: event.conversationId
2378
- },
2379
- "Failed to create observation"
2380
- );
2381
- return {
2382
- success: false,
2383
- eventType: "observation",
2384
- skipped: false,
2385
- error: `Failed to create observation: ${error instanceof Error ? error.message : String(error)}`,
2386
- createdCount
2387
- };
2388
- }
2389
- }
2390
- }
2391
- return {
2392
- success: true,
2393
- eventType: "observation",
2394
- skipped: false,
2395
- createdCount
2396
- };
2633
+ return this.processSummaryEvent(event, operatorResult, content, profileIds);
2397
2634
  }
2398
2635
  /**
2399
2636
  * Process a summary operator result
@@ -2515,7 +2752,6 @@ var TAC = class _TAC {
2515
2752
  tac.memoryClient,
2516
2753
  {
2517
2754
  configurationId: tac.config.cintelConfigurationId,
2518
- observationOperatorSid: tac.config.cintelObservationOperatorSid,
2519
2755
  summaryOperatorSid: tac.config.cintelSummaryOperatorSid
2520
2756
  },
2521
2757
  tac.logger.child({ component: "cintel" })
@@ -2735,6 +2971,8 @@ var TAC = class _TAC {
2735
2971
  /**
2736
2972
  * Whether Conversation Orchestrator is configured.
2737
2973
  * Returns false in voice-only mode.
2974
+ *
2975
+ * @internal
2738
2976
  */
2739
2977
  isOrchestratorEnabled() {
2740
2978
  return this.config.isOrchestratorEnabled();
@@ -3498,10 +3736,16 @@ var MessagingChannel = class extends BaseChannel {
3498
3736
  if (!session.aiAgentInfo) {
3499
3737
  const resolved = await this.reconcileParticipants(conversationId);
3500
3738
  if (!resolved) {
3501
- this.logger.warn(
3502
- { conversation_id: conversationId },
3503
- "Reconciliation failed; skipping callback for this inbound"
3739
+ const error = new Error(
3740
+ `Participant reconciliation failed for conversation ${conversationId}; inbound message dropped because no sendable participants could be resolved.`
3504
3741
  );
3742
+ error.name = "ParticipantReconciliationError";
3743
+ this.handleError(error, {
3744
+ conversation_id: conversationId,
3745
+ channel: this.channelType,
3746
+ dropped_inbound: true,
3747
+ error_code: "participant_reconciliation_failed"
3748
+ });
3505
3749
  return;
3506
3750
  }
3507
3751
  const [agentParticipant, customerParticipant] = resolved;
@@ -4446,6 +4690,9 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4446
4690
  twilioClient;
4447
4691
  voiceConfig;
4448
4692
  onInboundCallTwimlHandler;
4693
+ onCallStatusHandler;
4694
+ onAmdHandler;
4695
+ onRecordingHandler;
4449
4696
  constructor(tac, options) {
4450
4697
  super(tac, options);
4451
4698
  this.voiceConfig = options ?? {};
@@ -4481,6 +4728,74 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4481
4728
  onInboundCallTwiml(callback) {
4482
4729
  this.onInboundCallTwimlHandler = callback;
4483
4730
  }
4731
+ /**
4732
+ * Register a handler for Twilio `statusCallback` webhooks.
4733
+ *
4734
+ * This is the Calls-API status callback (call disposition), not the
4735
+ * ConversationRelay session callback — see
4736
+ * {@link handleConversationRelayCallback}.
4737
+ *
4738
+ * Registering does two things: it stores the handler, and it makes later
4739
+ * outbound calls pass `statusCallback` to `calls.create`. With no handler
4740
+ * registered TAC omits that parameter, so Twilio has nowhere to post and the
4741
+ * event never arrives.
4742
+ *
4743
+ * Twilio reports only the terminal event by default, which covers every
4744
+ * disposition; set `CallOptions.statusCallbackEvent` for ringing/answered.
4745
+ *
4746
+ * @example
4747
+ * ```typescript
4748
+ * voiceChannel.onCallStatus(async event => {
4749
+ * if (event.isUnreached) {
4750
+ * // queue a retry
4751
+ * }
4752
+ * });
4753
+ * ```
4754
+ */
4755
+ onCallStatus(callback) {
4756
+ this.onCallStatusHandler = callback;
4757
+ }
4758
+ /**
4759
+ * Register a handler for Twilio `asyncAmdStatusCallback` webhooks.
4760
+ *
4761
+ * Registering makes later outbound calls pass `asyncAmdStatusCallback` to
4762
+ * `calls.create`; without a handler TAC omits it and Twilio has nowhere to
4763
+ * post the result. It does not enable detection — that's per-call, via
4764
+ * `CallOptions.machineDetection` and `asyncAmd`, both of which are required
4765
+ * for this to fire (at most once per call).
4766
+ *
4767
+ * @example
4768
+ * ```typescript
4769
+ * voiceChannel.onAmd(async event => {
4770
+ * if (event.isMachine) {
4771
+ * await voiceChannel.endCall(event.callSid); // voicemail → hang up
4772
+ * }
4773
+ * });
4774
+ * ```
4775
+ */
4776
+ onAmd(callback) {
4777
+ this.onAmdHandler = callback;
4778
+ }
4779
+ /**
4780
+ * Register a handler for Twilio `recordingStatusCallback` webhooks.
4781
+ *
4782
+ * Registering makes later outbound calls pass `recordingStatusCallback` to
4783
+ * `calls.create`; without a handler TAC omits it and Twilio has nowhere to
4784
+ * post. It does not start recording — that's `CallOptions.record`, which is
4785
+ * required for this to fire.
4786
+ *
4787
+ * @example
4788
+ * ```typescript
4789
+ * voiceChannel.onRecording(async event => {
4790
+ * if (event.recordingStatus === 'completed') {
4791
+ * // store event.recordingUrl
4792
+ * }
4793
+ * });
4794
+ * ```
4795
+ */
4796
+ onRecording(callback) {
4797
+ this.onRecordingHandler = callback;
4798
+ }
4484
4799
  /**
4485
4800
  * Resolve the public WebSocket URL from `TACConfig.voicePublicDomain` +
4486
4801
  * `TACConfig.voiceWebsocketPath`. Throws if `voicePublicDomain` isn't set.
@@ -4671,6 +4986,7 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4671
4986
  this.webSocketConnections.set(conversationId, ws);
4672
4987
  this.callSidToConversationId.set(callSid, conversationId);
4673
4988
  const session = this.startConversation(conversationId);
4989
+ session.callSid = callSid;
4674
4990
  if (fromNumber) {
4675
4991
  session.authorInfo = { address: fromNumber };
4676
4992
  }
@@ -4708,6 +5024,7 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
4708
5024
  this.webSocketConnections.set(conversationId, ws);
4709
5025
  this.callSidToConversationId.set(callSid, conversationId);
4710
5026
  const session = this.startConversation(conversationId, profileId);
5027
+ session.callSid = callSid;
4711
5028
  if (customerAddress) {
4712
5029
  session.authorInfo = {
4713
5030
  address: customerAddress
@@ -5094,6 +5411,66 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5094
5411
  // =========================================================================
5095
5412
  // Outbound Call Handling
5096
5413
  // =========================================================================
5414
+ /**
5415
+ * Overlay `perCall` onto `VoiceChannelConfig.defaultCallOptions`.
5416
+ *
5417
+ * Per-field via key presence, the same convention {@link overlayFields} uses
5418
+ * for TwiML options — so a per-call `{ machineDetection: undefined }`
5419
+ * explicitly clears the channel default rather than falling through to it.
5420
+ *
5421
+ * The result is always validated, for two reasons: a combination only
5422
+ * reachable by layering — per-call clearing `machineDetection` while the
5423
+ * default set `asyncAmd` — must still fail instead of reaching Twilio, and
5424
+ * `VoiceChannelConfig` is a plain interface, so `defaultCallOptions` has had
5425
+ * no runtime validation of its own.
5426
+ */
5427
+ mergeCallOptions(perCall) {
5428
+ const defaults = this.voiceConfig.defaultCallOptions;
5429
+ if (!defaults && !perCall) {
5430
+ return void 0;
5431
+ }
5432
+ const merged = {};
5433
+ for (const [key, value] of Object.entries(defaults ?? {})) {
5434
+ if (value !== void 0) {
5435
+ merged[key] = value;
5436
+ }
5437
+ }
5438
+ for (const key of Object.keys(perCall ?? {})) {
5439
+ merged[key] = perCall[key];
5440
+ }
5441
+ return CallOptionsSchema.parse(merged);
5442
+ }
5443
+ /**
5444
+ * Build the extra arguments for `client.calls.create`.
5445
+ *
5446
+ * Layers, highest precedence first: this call's `callOptions`,
5447
+ * `VoiceChannelConfig.defaultCallOptions`, then callback URLs derived from
5448
+ * `voicePublicDomain` + `voiceCallEventPath`.
5449
+ *
5450
+ * A URL is derived only when its handler is registered. That's a deliberate
5451
+ * deviation from `websocketUrl` / `actionUrl`, which derive unconditionally:
5452
+ * those are load-bearing, so a wrong one fails loudly on the first call,
5453
+ * whereas an unwanted call-event URL fails as silent 11200 alerts for a
5454
+ * feature nobody asked for. Set the URLs in `defaultCallOptions` when TAC
5455
+ * isn't serving the routes.
5456
+ */
5457
+ buildCallParams(callOptions) {
5458
+ const merged = this.mergeCallOptions(callOptions);
5459
+ const params = merged ? callOptionsToCreateParams(merged) : {};
5460
+ const wiring = [
5461
+ ["status", "statusCallback", this.onCallStatusHandler],
5462
+ ["amd", "asyncAmdStatusCallback", this.onAmdHandler],
5463
+ ["recording", "recordingStatusCallback", this.onRecordingHandler]
5464
+ ];
5465
+ for (const [kind, param, handler] of wiring) {
5466
+ if (!handler) continue;
5467
+ const url = this.config.callEventUrl(kind);
5468
+ if (url !== void 0 && params[param] === void 0) {
5469
+ params[param] = url;
5470
+ }
5471
+ }
5472
+ return params;
5473
+ }
5097
5474
  /**
5098
5475
  * Initiate an outbound voice conversation
5099
5476
  *
@@ -5109,6 +5486,12 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5109
5486
  * `TACConfig`, and `actionUrl` from Studio handoff (if configured), else
5110
5487
  * derived from `TACConfig.voicePublicDomain` + `voiceActionPath`.
5111
5488
  *
5489
+ * Calls-API parameters merge the same way:
5490
+ * 1. `options.callOptions` — per-call overrides
5491
+ * 2. `VoiceChannelConfig.defaultCallOptions` — channel-wide defaults
5492
+ * 3. Callback URLs derived from `TACConfig.voicePublicDomain` +
5493
+ * `voiceCallEventPath`, for handlers that are registered
5494
+ *
5112
5495
  * The WebSocket URL is derived from `TACConfig.voicePublicDomain` +
5113
5496
  * `TACConfig.voiceWebsocketPath`, unless overridden per-call via
5114
5497
  * `options.websocketUrl`.
@@ -5124,11 +5507,17 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5124
5507
  const merged = this.buildTwimlOptions(void 0, validated.twimlOptions);
5125
5508
  const websocketUrl = validated.websocketUrl ?? merged.websocketUrl ?? this.resolveWebsocketUrl("initiateOutboundConversation");
5126
5509
  const twiml = this.generateTwiml(websocketUrl, merged);
5510
+ const callParams = this.buildCallParams(validated.callOptions);
5511
+ this.logger.debug(
5512
+ { twiml: redactTwimlParameters(twiml), to: maskAddress(validated.to) },
5513
+ "Outbound call TwiML"
5514
+ );
5127
5515
  const client = this.getTwilioClient();
5128
5516
  const call = await client.calls.create({
5129
5517
  to: validated.to,
5130
5518
  from: fromNumber,
5131
- twiml
5519
+ twiml,
5520
+ ...callParams
5132
5521
  });
5133
5522
  this.logger.info(
5134
5523
  { call_sid: call.sid, to: maskAddress(validated.to) },
@@ -5178,6 +5567,179 @@ var VoiceChannel = class _VoiceChannel extends BaseChannel {
5178
5567
  return { status: 200, content: "OK", contentType: "text/plain" };
5179
5568
  }
5180
5569
  // =========================================================================
5570
+ // Call Event Handling (status callback, async AMD, recording)
5571
+ // =========================================================================
5572
+ /**
5573
+ * Whether a call-webhook payload belongs to the configured account.
5574
+ *
5575
+ * Twilio signature validation already gates the route; this is defense in
5576
+ * depth. A payload with no `AccountSid` is allowed through.
5577
+ *
5578
+ * Subaccounts: events carry the SID the call was placed on, so configure TAC
5579
+ * with that account or its events get dropped here.
5580
+ */
5581
+ callEventAccountOk(form) {
5582
+ const accountSid = form["AccountSid"];
5583
+ if (accountSid && accountSid !== this.config.accountSid) {
5584
+ this.logger.warn(
5585
+ { expected: this.config.accountSid, received: accountSid },
5586
+ "Call event AccountSid mismatch, ignoring"
5587
+ );
5588
+ return false;
5589
+ }
5590
+ return true;
5591
+ }
5592
+ /**
5593
+ * Parse a call-event webhook form and dispatch it to its handler.
5594
+ *
5595
+ * Returns 400 when the payload can't be parsed (no `CallSid`) or the handler
5596
+ * throws — better than handing Twilio a 200 for an event that wasn't
5597
+ * processed. Everything else, including no handler registered and an
5598
+ * account mismatch, is a 200 no-op.
5599
+ */
5600
+ async dispatchCallEvent(kind, form, handler, parse, logFields) {
5601
+ const ok = { status: 200, content: "OK", contentType: "text/plain" };
5602
+ if (!handler || !this.callEventAccountOk(form)) {
5603
+ return ok;
5604
+ }
5605
+ try {
5606
+ const event = parse(form);
5607
+ this.logger.debug(logFields(event), `Call ${kind} event received`);
5608
+ await handler(event);
5609
+ } catch (error) {
5610
+ this.logger.error({ err: error, kind }, "Failed to process call event callback");
5611
+ return { status: 400, content: "Bad Request", contentType: "text/plain" };
5612
+ }
5613
+ return ok;
5614
+ }
5615
+ /**
5616
+ * Handle a Twilio `statusCallback` webhook.
5617
+ *
5618
+ * The developer routes the request here (`TACServer` does this automatically
5619
+ * for its `/status` call-event route). Parsed into a {@link CallStatusEvent}
5620
+ * and dispatched to the {@link onCallStatus} handler. No-op if no handler is
5621
+ * registered.
5622
+ *
5623
+ * @param form - Raw form data from the webhook request.
5624
+ */
5625
+ async handleCallStatusEvent(form) {
5626
+ return this.dispatchCallEvent(
5627
+ "status",
5628
+ form,
5629
+ this.onCallStatusHandler,
5630
+ callStatusEventFromForm,
5631
+ (event) => ({ call_sid: event.callSid, call_status: event.callStatus })
5632
+ );
5633
+ }
5634
+ /**
5635
+ * Handle a Twilio `asyncAmdStatusCallback` webhook.
5636
+ *
5637
+ * The developer routes the request here (`TACServer` does this automatically
5638
+ * for its `/amd` call-event route). Parsed into an {@link AmdEvent} and
5639
+ * dispatched to the {@link onAmd} handler. No-op if no handler is registered.
5640
+ *
5641
+ * @param form - Raw form data from the webhook request.
5642
+ */
5643
+ async handleAmdEvent(form) {
5644
+ return this.dispatchCallEvent("amd", form, this.onAmdHandler, amdEventFromForm, (event) => ({
5645
+ call_sid: event.callSid,
5646
+ answered_by: event.answeredBy
5647
+ }));
5648
+ }
5649
+ /**
5650
+ * Handle a Twilio `recordingStatusCallback` webhook.
5651
+ *
5652
+ * The developer routes the request here (`TACServer` does this automatically
5653
+ * for its `/recording` call-event route). Parsed into a
5654
+ * {@link RecordingEvent} and dispatched to the {@link onRecording} handler.
5655
+ * No-op if no handler is registered.
5656
+ *
5657
+ * @param form - Raw form data from the webhook request.
5658
+ */
5659
+ async handleRecordingEvent(form) {
5660
+ return this.dispatchCallEvent(
5661
+ "recording",
5662
+ form,
5663
+ this.onRecordingHandler,
5664
+ recordingEventFromForm,
5665
+ (event) => ({ call_sid: event.callSid, recording_status: event.recordingStatus })
5666
+ );
5667
+ }
5668
+ /**
5669
+ * Hang up a call and clean up its ConversationRelay session.
5670
+ *
5671
+ * Works on `callSid` alone, in any mode and before a session exists, so it's
5672
+ * safe from a call-event handler that fires before the first prompt. Session
5673
+ * cleanup no-ops if no tracked session matches.
5674
+ *
5675
+ * Does not throw — hanging up an already-ended call is routine (the callee
5676
+ * hangs up while AMD is still resolving), and handlers shouldn't have to
5677
+ * guard against it.
5678
+ *
5679
+ * @param callSid - Twilio Call SID (from a call event, the outbound result, or
5680
+ * `ConversationSession.callSid`).
5681
+ * @returns True if Twilio accepted the hangup, false if it failed (logged).
5682
+ * Session cleanup runs either way.
5683
+ */
5684
+ async endCall(callSid) {
5685
+ const client = this.getTwilioClient();
5686
+ let hungUp = true;
5687
+ try {
5688
+ await client.calls(callSid).update({ status: "completed" });
5689
+ } catch (error) {
5690
+ hungUp = false;
5691
+ this.logger.error({ err: error, call_sid: callSid }, "Failed to hang up call");
5692
+ }
5693
+ const session = this.getConversationSessionByCallSid(callSid);
5694
+ if (session) {
5695
+ await this.endConversation(session.conversationId);
5696
+ }
5697
+ return hungUp;
5698
+ }
5699
+ /**
5700
+ * Look up the active voice session for a Twilio Call SID.
5701
+ *
5702
+ * Out-of-band code holding a CallSid — a dashboard route, an operator action,
5703
+ * a call-event handler — can't reach the session-facing methods, which are
5704
+ * keyed by conversation id: the Orchestrator conversation id in orchestrator
5705
+ * mode, the CallSid only in ConversationRelay-only mode.
5706
+ *
5707
+ * Sessions are created on the caller's first prompt, not at WebSocket setup,
5708
+ * so this returns `undefined` for a call that connected but hasn't been spoken
5709
+ * into. That includes `onAmd` under `machineDetection: 'Enable'`, which fires
5710
+ * before the first prompt by design — hang up with {@link endCall}, which
5711
+ * needs no session.
5712
+ *
5713
+ * At the other end, orchestrator mode keeps the session until Conversation
5714
+ * Orchestrator's CLOSED webhook, so it outlives the call and `onCallStatus` /
5715
+ * `onRecording` do resolve. Relay-only mode tears down on the
5716
+ * ConversationRelay callback instead, which races them.
5717
+ *
5718
+ * @example
5719
+ * ```typescript
5720
+ * async function nudge(callSid: string): Promise<void> {
5721
+ * const session = voiceChannel.getConversationSessionByCallSid(callSid);
5722
+ * if (session) {
5723
+ * await voiceChannel.sendResponse(session.conversationId, 'Still there?');
5724
+ * }
5725
+ * }
5726
+ * ```
5727
+ *
5728
+ * @param callSid - Twilio Call SID, e.g. from
5729
+ * `InitiateVoiceConversationResult.callSid` or a call event.
5730
+ * @returns The session, or `undefined` — no first prompt yet, the call ended,
5731
+ * or it landed on another instance (see the horizontal-scaling note in
5732
+ * CLAUDE.md).
5733
+ */
5734
+ getConversationSessionByCallSid(callSid) {
5735
+ for (const session of this.activeConversations.values()) {
5736
+ if (session.callSid === callSid) {
5737
+ return session;
5738
+ }
5739
+ }
5740
+ return void 0;
5741
+ }
5742
+ // =========================================================================
5181
5743
  // Stream Task Management
5182
5744
  // =========================================================================
5183
5745
  /**
@@ -5967,6 +6529,9 @@ var DEFAULT_CONFIG = {
5967
6529
  twiml: "/twiml"
5968
6530
  }
5969
6531
  };
6532
+ function stripTrailingSlash(path) {
6533
+ return path.replace(/\/+$/, "");
6534
+ }
5970
6535
  var TACServer = class {
5971
6536
  fastify;
5972
6537
  tac;
@@ -5999,6 +6564,7 @@ var TACServer = class {
5999
6564
  "Voice channel is configured but TACConfig.voicePublicDomain is not set. Set it directly or via the TWILIO_VOICE_PUBLIC_DOMAIN env var."
6000
6565
  );
6001
6566
  }
6567
+ this.validateCallEventPaths();
6002
6568
  this.messagingChannels = config.messagingChannels ?? [
6003
6569
  tac.getChannel("sms"),
6004
6570
  tac.getChannel("rcs"),
@@ -6026,6 +6592,38 @@ var TACServer = class {
6026
6592
  });
6027
6593
  }
6028
6594
  }
6595
+ /**
6596
+ * Validate `voiceCallEventPath`, the one path that isn't literal.
6597
+ *
6598
+ * Every other TAC path registers as configured, so a bad value is visible.
6599
+ * This one expands into three sub-paths, hiding a mistake the base path looks
6600
+ * innocent for: a sub-path colliding with another route while the base looks
6601
+ * unrelated (base `/hooks` vs `webhookPaths.twiml = '/hooks/status'`). Both
6602
+ * would register as POST routes and requests would reach the wrong handler.
6603
+ *
6604
+ * The leading-slash requirement is enforced by `TACConfigSchema` at parse
6605
+ * time, so it doesn't need re-checking here.
6606
+ */
6607
+ validateCallEventPaths() {
6608
+ const cfg = this.tac.getConfig();
6609
+ const others = {
6610
+ [stripTrailingSlash(this.config.webhookPaths.twiml || "/twiml")]: "webhookPaths.twiml",
6611
+ [stripTrailingSlash(cfg.voiceActionPath)]: "TACConfig.voiceActionPath",
6612
+ [stripTrailingSlash(this.config.webhookPaths.conversation || "/webhook")]: "webhookPaths.conversation"
6613
+ };
6614
+ if (this.config.webhookPaths.cintel) {
6615
+ others[stripTrailingSlash(this.config.webhookPaths.cintel)] = "webhookPaths.cintel";
6616
+ }
6617
+ for (const kind of CALL_EVENT_KINDS) {
6618
+ const path = cfg.callEventPath(kind);
6619
+ const clash = others[stripTrailingSlash(path)];
6620
+ if (clash !== void 0) {
6621
+ throw new Error(
6622
+ `TACConfig.voiceCallEventPath expands to '${path}', which collides with ${clash}. Both would register as POST routes and requests would reach the wrong handler.`
6623
+ );
6624
+ }
6625
+ }
6626
+ }
6029
6627
  getForwardedProto(request) {
6030
6628
  const raw = request.headers["x-forwarded-proto"];
6031
6629
  return raw?.split(",")[0]?.trim() || "https";
@@ -6150,6 +6748,34 @@ var TACServer = class {
6150
6748
  }
6151
6749
  }
6152
6750
  );
6751
+ const callEventHandlers = {
6752
+ status: (channel, form) => channel.handleCallStatusEvent(form),
6753
+ amd: (channel, form) => channel.handleAmdEvent(form),
6754
+ recording: (channel, form) => channel.handleRecordingEvent(form)
6755
+ };
6756
+ for (const kind of CALL_EVENT_KINDS) {
6757
+ const handle = callEventHandlers[kind];
6758
+ this.fastify.post(
6759
+ this.tac.getConfig().callEventPath(kind),
6760
+ validateSignature,
6761
+ async (request, reply) => {
6762
+ try {
6763
+ if (!this.voiceChannel) {
6764
+ await reply.code(500).send({ error: "Voice channel not available" });
6765
+ return;
6766
+ }
6767
+ const formData = request.body ?? {};
6768
+ const result = await handle(this.voiceChannel, formData);
6769
+ await reply.code(result.status).type(result.contentType).send(result.content);
6770
+ } catch (error) {
6771
+ this.fastify.log.error(
6772
+ `Call ${kind} callback error: ` + (error instanceof Error ? error.message : String(error))
6773
+ );
6774
+ await reply.code(500).send({ error: "Internal server error" });
6775
+ }
6776
+ }
6777
+ );
6778
+ }
6153
6779
  await this.fastify.register((fastify) => {
6154
6780
  fastify.get(
6155
6781
  this.tac.getConfig().voiceWebsocketPath,
@@ -6273,6 +6899,11 @@ var TACServer = class {
6273
6899
  twiml_webhook: this.config.webhookPaths.twiml,
6274
6900
  ws_websocket: this.tac.getConfig().voiceWebsocketPath,
6275
6901
  conversation_relay_callback: this.tac.getConfig().voiceActionPath,
6902
+ ...this.voiceChannel && {
6903
+ call_event_callbacks: CALL_EVENT_KINDS.map(
6904
+ (kind) => this.tac.getConfig().callEventPath(kind)
6905
+ )
6906
+ },
6276
6907
  ...this.config.webhookPaths.cintel && {
6277
6908
  cintel_webhook: this.config.webhookPaths.cintel
6278
6909
  }
@@ -6336,6 +6967,6 @@ var TACServer = class {
6336
6967
  }
6337
6968
  };
6338
6969
 
6339
- export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, CustomParametersSchema, EMPTY_MEMORY_RESPONSE, EnvironmentVariables, ExecutionDetailsSchema, HandoffPayloadSchema, InitiateMessagingConversationOptionsSchema, InitiateVoiceConversationOptionsSchema, IntelligenceConfigurationSchema, InterruptMessageSchema, InterruptModeSchema, JSONSchemaSchema, KnowledgeBaseSchema, KnowledgeBaseStatusSchema, KnowledgeChunkResultSchema, KnowledgeClient, KnowledgeSearchResponseSchema, LanguageAttributesSchema, LanguageConfigSchema, ListCommunicationsResponseSchema, ListConversationsResponseSchema, ListParticipantsResponseSchema, MemoryChannelTypeSchema, MemoryClient, MemoryCommunicationContentSchema, MemoryCommunicationSchema, MemoryDeliveryStatusSchema, MemoryModeSchema, MemoryParticipantSchema, MemoryParticipantTypeSchema, MemoryPromptBuilder, MemoryRetrievalRequestSchema, MemoryRetrievalResponseSchema, MessageDirectionSchema, MessagingChannel, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, SMSChannel, SendMessageActionPayloadSchema, SendMessageActionRequestSchema, SessionInfoSchema, SessionMessageSchema, SetupMessageSchema, StatusCallbackSchema, StatusTimeoutsSchema, SummaryInfoSchema, TAC, TACChannelTypeSchema, TACCommunicationAuthorSchema, TACCommunicationContentSchema, TACCommunicationSchema, TACConfig, TACConfigSchema, TACDeliveryStatusSchema, TACMemoryResponse, TACParticipantTypeSchema, TACServer, TACTool, TextTokenMessageSchema, ToolExecutionResultSchema, TranscriptionSchema, TranscriptionWordSchema, TwiMLOptionsSchema, TwiMLRequestSchema, TwilioMemoryConfigSchema, VoiceChannel, WebSocketMessageSchema, WhatsAppChannel, buildHandoffPayload, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl, twiMLRequestFromForm };
6970
+ export { ActionChannelSettingsSchema, ActionParticipantRefSchema, ActionResponseSchema, ActionTextContentSchema, AmdEventSchema, AnthropicToolSchema, AuthorInfoSchema, BaseChannel, BaseClient, BuiltInTools, CALL_EVENT_KINDS, CallEventKindSchema, CallOptionsSchema, CallStatusEventSchema, CaptureRuleSchema, ChannelSettingsSchema, ChannelTypeSchema, ChatChannel, CintelParticipantSchema, CommunicationContentSchema, CommunicationParticipantSchema, CommunicationSchema, ConversationAddressSchema, ConversationClient, ConversationConfigurationSchema, ConversationGroupingTypeSchema, ConversationIntelligenceConfigSchema, ConversationParticipantSchema, ConversationRelayAttributesSchema, ConversationRelayCallbackPayloadSchema, ConversationRelayConfigSchema, ConversationResponseSchema, ConversationSessionSchema, ConversationSummaryItemSchema, CreateConversationSummariesResponseSchema, CreateObservationResponseSchema, CreateObservationsRequestSchema, CustomParametersSchema, EMPTY_MEMORY_RESPONSE, EnvironmentVariables, ExecutionDetailsSchema, HandoffPayloadSchema, InitiateMessagingConversationOptionsSchema, InitiateVoiceConversationOptionsSchema, IntelligenceConfigurationSchema, InterruptMessageSchema, InterruptModeSchema, JSONSchemaSchema, KnowledgeBaseSchema, KnowledgeBaseStatusSchema, KnowledgeChunkResultSchema, KnowledgeClient, KnowledgeSearchResponseSchema, LanguageAttributesSchema, LanguageConfigSchema, ListCommunicationsResponseSchema, ListConversationsResponseSchema, ListParticipantsResponseSchema, MemoryChannelTypeSchema, MemoryClient, MemoryCommunicationContentSchema, MemoryCommunicationSchema, MemoryDeliveryStatusSchema, MemoryModeSchema, MemoryParticipantSchema, MemoryParticipantTypeSchema, MemoryPromptBuilder, MemoryRetrievalRequestSchema, MemoryRetrievalResponseSchema, MessageDirectionSchema, MessagingChannel, ObservationCreateRequestSchema, ObservationInfoSchema, OpenAIToolSchema, OperatorProcessingResultSchema, OperatorResultEventSchema, OperatorResultProcessor, OperatorResultSchema, OperatorSchema, ParticipantAddressSchema, ParticipantAddressTypeSchema, PendingHandoffDataSchema, ProfileLookupResponseSchema, ProfileResponseSchema, PromptMessageSchema, RCSChannel, RecordingEventSchema, SMSChannel, SendMessageActionPayloadSchema, SendMessageActionRequestSchema, SessionInfoSchema, SessionMessageSchema, SetupMessageSchema, StatusCallbackSchema, StatusTimeoutsSchema, SummaryInfoSchema, TAC, TACChannelTypeSchema, TACCommunicationAuthorSchema, TACCommunicationContentSchema, TACCommunicationSchema, TACConfig, TACConfigSchema, TACDeliveryStatusSchema, TACMemoryResponse, TACParticipantTypeSchema, TACServer, TACTool, TextTokenMessageSchema, ToolExecutionResultSchema, TranscriptionSchema, TranscriptionWordSchema, TwiMLOptionsSchema, TwiMLRequestSchema, TwilioMemoryConfigSchema, VoiceChannel, WebSocketMessageSchema, WhatsAppChannel, amdEventFromForm, buildHandoffPayload, callOptionsToCreateParams, callStatusEventFromForm, createKnowledgeSearchTool, createKnowledgeSearchToolAsync, createKnowledgeTools, createLogger, createMemoryRetrievalTool, createMemoryTools, createMessagingTools, createSendMessageTool, createStudioHandoffTool, defineTool, isConversationId, isParticipantId, isProfileId, maskAddress, maskEmail, maskPhone, postStudioHandoff, recordingEventFromForm, redactTwimlParameters, scrubObject, scrubPii, studioExecutionsUrl, studioVoiceHandoffUrl, twiMLRequestFromForm };
6340
6971
  //# sourceMappingURL=index.js.map
6341
6972
  //# sourceMappingURL=index.js.map