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/README.md +1 -0
- package/dist/index.d.ts +603 -37
- package/dist/index.js +878 -202
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,8 +17,10 @@ var TwilioMemoryConfigSchema = z.object({
|
|
|
17
17
|
traitGroups: z.array(z.string()).optional(),
|
|
18
18
|
observationsLimit: z.number().int().min(0).max(100).default(20),
|
|
19
19
|
summariesLimit: z.number().int().min(0).max(100).default(5),
|
|
20
|
-
//
|
|
21
|
-
|
|
20
|
+
// 0 matches the Memory API and Python SDK. Above 0, Memory requires a
|
|
21
|
+
// conversationId on every /Recall, forcing the query expansion that
|
|
22
|
+
// `memoryMode: "once"` exists to avoid.
|
|
23
|
+
communicationsLimit: z.number().int().min(0).max(100).default(0),
|
|
22
24
|
relevanceThreshold: z.number().min(0).max(1).default(0),
|
|
23
25
|
/**
|
|
24
26
|
* Trait group name that holds the phone identifier on newly created profiles.
|
|
@@ -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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
|
-
voicePublicDomain: z.preprocess(
|
|
60
|
-
(v)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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([
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
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:
|
|
1600
|
-
summary_count:
|
|
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
|
|
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 -
|
|
1711
|
-
* @returns Promise containing the
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
3218
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3506
|
-
|
|
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
|
-
|
|
4682
|
-
|
|
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
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
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
|
|
5107
|
-
*
|
|
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
|