@absolutejs/voice 0.0.22-beta.192 → 0.0.22-beta.193
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 +112 -1
- package/dist/campaign.d.ts +132 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +849 -71
- package/dist/latencySlo.d.ts +56 -0
- package/dist/operationsRecord.d.ts +30 -0
- package/dist/productionReadiness.d.ts +12 -0
- package/dist/readinessProfiles.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5652,6 +5652,192 @@ var maybeCompleteCampaign = (record) => {
|
|
|
5652
5652
|
record.campaign.status = "completed";
|
|
5653
5653
|
}
|
|
5654
5654
|
};
|
|
5655
|
+
var normalizeHour = (hour) => {
|
|
5656
|
+
if (!Number.isFinite(hour)) {
|
|
5657
|
+
return 0;
|
|
5658
|
+
}
|
|
5659
|
+
return Math.min(24, Math.max(0, hour));
|
|
5660
|
+
};
|
|
5661
|
+
var getCampaignWindowMinute = (at, offsetMinutes = 0) => {
|
|
5662
|
+
const date = new Date(at + offsetMinutes * 60000);
|
|
5663
|
+
return {
|
|
5664
|
+
dayOfWeek: date.getUTCDay(),
|
|
5665
|
+
minuteOfDay: date.getUTCHours() * 60 + date.getUTCMinutes()
|
|
5666
|
+
};
|
|
5667
|
+
};
|
|
5668
|
+
var isWithinCampaignTimeWindow = (window, at) => {
|
|
5669
|
+
const { dayOfWeek, minuteOfDay } = getCampaignWindowMinute(at, window.timeZoneOffsetMinutes ?? 0);
|
|
5670
|
+
if (window.daysOfWeek && !window.daysOfWeek.includes(dayOfWeek)) {
|
|
5671
|
+
return false;
|
|
5672
|
+
}
|
|
5673
|
+
const start = normalizeHour(window.startHour) * 60;
|
|
5674
|
+
const end = normalizeHour(window.endHour) * 60;
|
|
5675
|
+
if (start === end) {
|
|
5676
|
+
return true;
|
|
5677
|
+
}
|
|
5678
|
+
if (start < end) {
|
|
5679
|
+
return minuteOfDay >= start && minuteOfDay < end;
|
|
5680
|
+
}
|
|
5681
|
+
return minuteOfDay >= start || minuteOfDay < end;
|
|
5682
|
+
};
|
|
5683
|
+
var getCampaignRetryBackoffMs = (policy, attemptNumber) => {
|
|
5684
|
+
const backoff = policy?.backoffMs;
|
|
5685
|
+
if (Array.isArray(backoff)) {
|
|
5686
|
+
return Math.max(0, backoff[Math.min(backoff.length - 1, attemptNumber - 1)] ?? 0);
|
|
5687
|
+
}
|
|
5688
|
+
const value = Math.max(0, backoff ?? 0);
|
|
5689
|
+
if (policy?.maxBackoffMs === undefined) {
|
|
5690
|
+
return value;
|
|
5691
|
+
}
|
|
5692
|
+
return Math.min(value, Math.max(0, policy.maxBackoffMs));
|
|
5693
|
+
};
|
|
5694
|
+
var getLastCampaignAttempt = (record, recipientId) => record.attempts.filter((attempt) => attempt.recipientId === recipientId).sort((left, right) => right.createdAt - left.createdAt)[0];
|
|
5695
|
+
var parseCsvLine = (line) => {
|
|
5696
|
+
const values = [];
|
|
5697
|
+
let current = "";
|
|
5698
|
+
let quoted = false;
|
|
5699
|
+
for (let index = 0;index < line.length; index += 1) {
|
|
5700
|
+
const character = line[index];
|
|
5701
|
+
const next = line[index + 1];
|
|
5702
|
+
if (character === '"' && quoted && next === '"') {
|
|
5703
|
+
current += '"';
|
|
5704
|
+
index += 1;
|
|
5705
|
+
continue;
|
|
5706
|
+
}
|
|
5707
|
+
if (character === '"') {
|
|
5708
|
+
quoted = !quoted;
|
|
5709
|
+
continue;
|
|
5710
|
+
}
|
|
5711
|
+
if (character === "," && !quoted) {
|
|
5712
|
+
values.push(current.trim());
|
|
5713
|
+
current = "";
|
|
5714
|
+
continue;
|
|
5715
|
+
}
|
|
5716
|
+
current += character;
|
|
5717
|
+
}
|
|
5718
|
+
values.push(current.trim());
|
|
5719
|
+
return values;
|
|
5720
|
+
};
|
|
5721
|
+
var parseVoiceCampaignCSVRows = (csv) => {
|
|
5722
|
+
const lines = csv.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
5723
|
+
if (lines.length === 0) {
|
|
5724
|
+
return [];
|
|
5725
|
+
}
|
|
5726
|
+
const headers = parseCsvLine(lines[0] ?? "");
|
|
5727
|
+
return lines.slice(1).map((line) => {
|
|
5728
|
+
const values = parseCsvLine(line);
|
|
5729
|
+
const row = {};
|
|
5730
|
+
headers.forEach((header, index) => {
|
|
5731
|
+
row[header] = values[index] ?? "";
|
|
5732
|
+
});
|
|
5733
|
+
return row;
|
|
5734
|
+
});
|
|
5735
|
+
};
|
|
5736
|
+
var normalizeCampaignPhone = (value) => {
|
|
5737
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
5738
|
+
return;
|
|
5739
|
+
}
|
|
5740
|
+
const raw = String(value).trim();
|
|
5741
|
+
if (!raw) {
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
const hasPlus = raw.startsWith("+");
|
|
5745
|
+
const digits = raw.replace(/\D/g, "");
|
|
5746
|
+
if (digits.length < 8 || digits.length > 15) {
|
|
5747
|
+
return;
|
|
5748
|
+
}
|
|
5749
|
+
return hasPlus ? `+${digits}` : `+${digits}`;
|
|
5750
|
+
};
|
|
5751
|
+
var toCampaignScalar = (value) => {
|
|
5752
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
5753
|
+
return value;
|
|
5754
|
+
}
|
|
5755
|
+
return;
|
|
5756
|
+
};
|
|
5757
|
+
var truthyConsent = (value) => {
|
|
5758
|
+
if (value === true) {
|
|
5759
|
+
return true;
|
|
5760
|
+
}
|
|
5761
|
+
if (typeof value === "number") {
|
|
5762
|
+
return value > 0;
|
|
5763
|
+
}
|
|
5764
|
+
if (typeof value !== "string") {
|
|
5765
|
+
return false;
|
|
5766
|
+
}
|
|
5767
|
+
return ["1", "true", "yes", "y", "consented", "opt-in", "opted-in"].includes(value.trim().toLowerCase());
|
|
5768
|
+
};
|
|
5769
|
+
var importVoiceCampaignRecipients = (options) => {
|
|
5770
|
+
const rows = options.rows ?? (options.csv ? parseVoiceCampaignCSVRows(options.csv) : []);
|
|
5771
|
+
const phoneColumn = options.phoneColumn ?? "phone";
|
|
5772
|
+
const nameColumn = options.nameColumn ?? "name";
|
|
5773
|
+
const idColumn = options.idColumn ?? "id";
|
|
5774
|
+
const consentColumn = options.consentColumn ?? "consent";
|
|
5775
|
+
const dedupe = options.dedupe ?? true;
|
|
5776
|
+
const seenPhones = new Set;
|
|
5777
|
+
const accepted = [];
|
|
5778
|
+
const rejected = [];
|
|
5779
|
+
let duplicates = 0;
|
|
5780
|
+
rows.forEach((row, index) => {
|
|
5781
|
+
const rowNumber = index + 1;
|
|
5782
|
+
const phoneValue = row[phoneColumn];
|
|
5783
|
+
if (phoneValue === undefined || phoneValue === null || phoneValue === "") {
|
|
5784
|
+
rejected.push({
|
|
5785
|
+
code: "missing-phone",
|
|
5786
|
+
message: `Campaign recipient row ${rowNumber} is missing a phone number.`,
|
|
5787
|
+
row: rowNumber,
|
|
5788
|
+
value: phoneValue
|
|
5789
|
+
});
|
|
5790
|
+
return;
|
|
5791
|
+
}
|
|
5792
|
+
const phone = normalizeCampaignPhone(phoneValue);
|
|
5793
|
+
if (!phone) {
|
|
5794
|
+
rejected.push({
|
|
5795
|
+
code: "invalid-phone",
|
|
5796
|
+
message: `Campaign recipient row ${rowNumber} has an invalid phone number.`,
|
|
5797
|
+
row: rowNumber,
|
|
5798
|
+
value: phoneValue
|
|
5799
|
+
});
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
if (options.requireConsent && !truthyConsent(row[consentColumn])) {
|
|
5803
|
+
rejected.push({
|
|
5804
|
+
code: "missing-consent",
|
|
5805
|
+
message: `Campaign recipient row ${rowNumber} is missing required consent.`,
|
|
5806
|
+
row: rowNumber,
|
|
5807
|
+
value: row[consentColumn]
|
|
5808
|
+
});
|
|
5809
|
+
return;
|
|
5810
|
+
}
|
|
5811
|
+
if (dedupe && seenPhones.has(phone)) {
|
|
5812
|
+
duplicates += 1;
|
|
5813
|
+
rejected.push({
|
|
5814
|
+
code: "duplicate",
|
|
5815
|
+
message: `Campaign recipient row ${rowNumber} duplicates ${phone}.`,
|
|
5816
|
+
row: rowNumber,
|
|
5817
|
+
value: phone
|
|
5818
|
+
});
|
|
5819
|
+
return;
|
|
5820
|
+
}
|
|
5821
|
+
seenPhones.add(phone);
|
|
5822
|
+
const variables = Object.fromEntries((options.variableColumns ?? []).map((column) => [column, toCampaignScalar(row[column])]).filter(([, value]) => value !== undefined));
|
|
5823
|
+
const metadata = Object.fromEntries((options.metadataColumns ?? []).map((column) => [column, row[column]]).filter(([, value]) => value !== undefined));
|
|
5824
|
+
const id = toCampaignScalar(row[idColumn]);
|
|
5825
|
+
const name = toCampaignScalar(row[nameColumn]);
|
|
5826
|
+
accepted.push({
|
|
5827
|
+
id: typeof id === "string" ? id : undefined,
|
|
5828
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
5829
|
+
name: typeof name === "string" ? name : undefined,
|
|
5830
|
+
phone,
|
|
5831
|
+
variables: Object.keys(variables).length > 0 ? variables : undefined
|
|
5832
|
+
});
|
|
5833
|
+
});
|
|
5834
|
+
return {
|
|
5835
|
+
accepted,
|
|
5836
|
+
duplicates,
|
|
5837
|
+
rejected,
|
|
5838
|
+
total: rows.length
|
|
5839
|
+
};
|
|
5840
|
+
};
|
|
5655
5841
|
var createVoiceMemoryCampaignStore = () => {
|
|
5656
5842
|
const campaigns = new Map;
|
|
5657
5843
|
return {
|
|
@@ -5812,24 +5998,28 @@ var buildVoiceCampaignObservabilityReport = async (records, options = {}) => {
|
|
|
5812
5998
|
report.stuck.recipients.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
5813
5999
|
return report;
|
|
5814
6000
|
};
|
|
6001
|
+
var runtimeAddRecipients = async (store, record, recipients) => {
|
|
6002
|
+
const at = Date.now();
|
|
6003
|
+
record.recipients.push(...recipients.map((recipient) => ({
|
|
6004
|
+
attempts: 0,
|
|
6005
|
+
createdAt: at,
|
|
6006
|
+
id: recipient.id ?? createId2(),
|
|
6007
|
+
metadata: recipient.metadata,
|
|
6008
|
+
name: recipient.name,
|
|
6009
|
+
phone: recipient.phone,
|
|
6010
|
+
status: "pending",
|
|
6011
|
+
updatedAt: at,
|
|
6012
|
+
variables: recipient.variables
|
|
6013
|
+
})));
|
|
6014
|
+
return saveRecord(store, record);
|
|
6015
|
+
};
|
|
5815
6016
|
var createVoiceCampaign = (options) => {
|
|
5816
6017
|
const { store } = options;
|
|
6018
|
+
const now = options.now ?? Date.now;
|
|
5817
6019
|
return {
|
|
5818
6020
|
addRecipients: async (campaignId, recipients) => {
|
|
5819
6021
|
const record = await ensureRecord(store, campaignId);
|
|
5820
|
-
|
|
5821
|
-
record.recipients.push(...recipients.map((recipient) => ({
|
|
5822
|
-
attempts: 0,
|
|
5823
|
-
createdAt: at,
|
|
5824
|
-
id: recipient.id ?? createId2(),
|
|
5825
|
-
metadata: recipient.metadata,
|
|
5826
|
-
name: recipient.name,
|
|
5827
|
-
phone: recipient.phone,
|
|
5828
|
-
status: "pending",
|
|
5829
|
-
updatedAt: at,
|
|
5830
|
-
variables: recipient.variables
|
|
5831
|
-
})));
|
|
5832
|
-
return saveRecord(store, record);
|
|
6022
|
+
return runtimeAddRecipients(store, record, recipients);
|
|
5833
6023
|
},
|
|
5834
6024
|
cancel: async (campaignId) => {
|
|
5835
6025
|
const record = await ensureRecord(store, campaignId);
|
|
@@ -5874,7 +6064,7 @@ var createVoiceCampaign = (options) => {
|
|
|
5874
6064
|
return saveRecord(store, record);
|
|
5875
6065
|
},
|
|
5876
6066
|
create: async (input) => {
|
|
5877
|
-
const at =
|
|
6067
|
+
const at = now();
|
|
5878
6068
|
const campaign = {
|
|
5879
6069
|
createdAt: at,
|
|
5880
6070
|
description: input.description,
|
|
@@ -5883,6 +6073,7 @@ var createVoiceCampaign = (options) => {
|
|
|
5883
6073
|
maxConcurrentAttempts: input.maxConcurrentAttempts ?? 1,
|
|
5884
6074
|
metadata: input.metadata,
|
|
5885
6075
|
name: input.name,
|
|
6076
|
+
schedule: input.schedule,
|
|
5886
6077
|
status: "draft",
|
|
5887
6078
|
updatedAt: at
|
|
5888
6079
|
};
|
|
@@ -5903,6 +6094,15 @@ var createVoiceCampaign = (options) => {
|
|
|
5903
6094
|
return saveRecord(store, record);
|
|
5904
6095
|
},
|
|
5905
6096
|
get: async (campaignId) => await store.get(campaignId),
|
|
6097
|
+
importRecipients: async (campaignId, input) => {
|
|
6098
|
+
const result = importVoiceCampaignRecipients(input);
|
|
6099
|
+
const record = await ensureRecord(store, campaignId);
|
|
6100
|
+
const updated = result.accepted.length > 0 ? await runtimeAddRecipients(store, record, result.accepted) : record;
|
|
6101
|
+
return {
|
|
6102
|
+
...updated,
|
|
6103
|
+
import: result
|
|
6104
|
+
};
|
|
6105
|
+
},
|
|
5906
6106
|
list: async () => await store.list(),
|
|
5907
6107
|
pause: async (campaignId) => {
|
|
5908
6108
|
const record = await ensureRecord(store, campaignId);
|
|
@@ -5922,6 +6122,7 @@ var createVoiceCampaign = (options) => {
|
|
|
5922
6122
|
const record = await ensureRecord(store, campaignId);
|
|
5923
6123
|
const result = {
|
|
5924
6124
|
attempted: 0,
|
|
6125
|
+
blocked: [],
|
|
5925
6126
|
campaignId,
|
|
5926
6127
|
errors: [],
|
|
5927
6128
|
started: []
|
|
@@ -5929,9 +6130,53 @@ var createVoiceCampaign = (options) => {
|
|
|
5929
6130
|
if (record.campaign.status !== "running") {
|
|
5930
6131
|
return result;
|
|
5931
6132
|
}
|
|
6133
|
+
const at = now();
|
|
6134
|
+
const schedule = record.campaign.schedule;
|
|
6135
|
+
if (schedule?.attemptWindow && !isWithinCampaignTimeWindow(schedule.attemptWindow, at)) {
|
|
6136
|
+
result.blocked.push({
|
|
6137
|
+
reason: "outside-attempt-window"
|
|
6138
|
+
});
|
|
6139
|
+
return result;
|
|
6140
|
+
}
|
|
6141
|
+
if (schedule?.quietHours && isWithinCampaignTimeWindow(schedule.quietHours, at)) {
|
|
6142
|
+
result.blocked.push({
|
|
6143
|
+
reason: "quiet-hours"
|
|
6144
|
+
});
|
|
6145
|
+
return result;
|
|
6146
|
+
}
|
|
5932
6147
|
const capacity = Math.max(0, record.campaign.maxConcurrentAttempts - activeAttemptCount(record));
|
|
5933
|
-
const
|
|
5934
|
-
const
|
|
6148
|
+
const rateLimit = schedule?.rateLimit;
|
|
6149
|
+
const rateRemaining = rateLimit ? Math.max(0, rateLimit.maxAttempts - record.attempts.filter((attempt) => (attempt.startedAt ?? attempt.createdAt) >= at - rateLimit.windowMs).length) : Number.MAX_SAFE_INTEGER;
|
|
6150
|
+
const availableCapacity = Math.min(capacity, rateRemaining);
|
|
6151
|
+
if (capacity > 0 && rateLimit && rateRemaining === 0) {
|
|
6152
|
+
result.blocked.push({
|
|
6153
|
+
reason: "rate-limit"
|
|
6154
|
+
});
|
|
6155
|
+
}
|
|
6156
|
+
const candidates = [];
|
|
6157
|
+
for (const recipient of record.recipients) {
|
|
6158
|
+
if (candidates.length >= availableCapacity) {
|
|
6159
|
+
break;
|
|
6160
|
+
}
|
|
6161
|
+
if (recipient.status !== "queued" && recipient.status !== "pending") {
|
|
6162
|
+
continue;
|
|
6163
|
+
}
|
|
6164
|
+
if (recipient.attempts >= record.campaign.maxAttempts) {
|
|
6165
|
+
continue;
|
|
6166
|
+
}
|
|
6167
|
+
const backoffMs = getCampaignRetryBackoffMs(schedule?.retryPolicy, recipient.attempts);
|
|
6168
|
+
const lastAttempt = getLastCampaignAttempt(record, recipient.id);
|
|
6169
|
+
const retryAt = lastAttempt && recipient.attempts > 0 ? (lastAttempt.completedAt ?? lastAttempt.updatedAt) + backoffMs : undefined;
|
|
6170
|
+
if (retryAt !== undefined && retryAt > at) {
|
|
6171
|
+
result.blocked.push({
|
|
6172
|
+
reason: "retry-backoff",
|
|
6173
|
+
recipientId: recipient.id,
|
|
6174
|
+
until: retryAt
|
|
6175
|
+
});
|
|
6176
|
+
continue;
|
|
6177
|
+
}
|
|
6178
|
+
candidates.push(recipient);
|
|
6179
|
+
}
|
|
5935
6180
|
for (const recipient of candidates) {
|
|
5936
6181
|
const attempt = {
|
|
5937
6182
|
campaignId,
|
|
@@ -5957,12 +6202,13 @@ var createVoiceCampaign = (options) => {
|
|
|
5957
6202
|
attempt.externalCallId = dialerResult.externalCallId;
|
|
5958
6203
|
attempt.metadata = dialerResult.metadata;
|
|
5959
6204
|
attempt.status = dialerResult.status ?? "running";
|
|
5960
|
-
attempt.updatedAt =
|
|
6205
|
+
attempt.updatedAt = now();
|
|
5961
6206
|
} catch (error) {
|
|
5962
|
-
|
|
6207
|
+
const failedAt = now();
|
|
6208
|
+
attempt.completedAt = failedAt;
|
|
5963
6209
|
attempt.error = error instanceof Error ? error.message : String(error);
|
|
5964
6210
|
attempt.status = "failed";
|
|
5965
|
-
attempt.updatedAt =
|
|
6211
|
+
attempt.updatedAt = failedAt;
|
|
5966
6212
|
recipient.error = attempt.error;
|
|
5967
6213
|
recipient.status = recipient.attempts >= record.campaign.maxAttempts ? "failed" : "pending";
|
|
5968
6214
|
result.errors.push({
|
|
@@ -6279,6 +6525,142 @@ var runVoiceCampaignProof = async (options = {}) => {
|
|
|
6279
6525
|
tick
|
|
6280
6526
|
};
|
|
6281
6527
|
};
|
|
6528
|
+
var pushCampaignReadinessCheck = (checks, name, condition, details) => {
|
|
6529
|
+
checks.push({
|
|
6530
|
+
details,
|
|
6531
|
+
name,
|
|
6532
|
+
status: condition ? "pass" : "fail"
|
|
6533
|
+
});
|
|
6534
|
+
};
|
|
6535
|
+
var runVoiceCampaignReadinessProof = async (options = {}) => {
|
|
6536
|
+
const checks = [];
|
|
6537
|
+
const store = options.store ?? createVoiceMemoryCampaignStore();
|
|
6538
|
+
let now = Date.UTC(2026, 0, 5, 8, 0, 0);
|
|
6539
|
+
const runtime = createVoiceCampaign({
|
|
6540
|
+
dialer: ({ attempt }) => ({
|
|
6541
|
+
externalCallId: `campaign-readiness-${attempt.id}`,
|
|
6542
|
+
metadata: {
|
|
6543
|
+
mode: "readiness-proof"
|
|
6544
|
+
},
|
|
6545
|
+
status: "running"
|
|
6546
|
+
}),
|
|
6547
|
+
now: () => now,
|
|
6548
|
+
store
|
|
6549
|
+
});
|
|
6550
|
+
const scheduled = await runtime.create({
|
|
6551
|
+
id: `campaign-readiness-schedule-${crypto.randomUUID()}`,
|
|
6552
|
+
maxConcurrentAttempts: 3,
|
|
6553
|
+
name: "Campaign Readiness Schedule Proof",
|
|
6554
|
+
schedule: {
|
|
6555
|
+
attemptWindow: {
|
|
6556
|
+
endHour: 17,
|
|
6557
|
+
startHour: 9
|
|
6558
|
+
},
|
|
6559
|
+
quietHours: {
|
|
6560
|
+
endHour: 13,
|
|
6561
|
+
startHour: 12
|
|
6562
|
+
},
|
|
6563
|
+
rateLimit: {
|
|
6564
|
+
maxAttempts: 1,
|
|
6565
|
+
windowMs: 60000
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
});
|
|
6569
|
+
const imported = await runtime.importRecipients(scheduled.campaign.id, {
|
|
6570
|
+
csv: `id,name,phone,consent,segment
|
|
6571
|
+
recipient-1,Ada,+15550001001,yes,alpha
|
|
6572
|
+
recipient-duplicate,Grace,+15550001001,yes,beta
|
|
6573
|
+
recipient-bad,Linus,not-a-phone,yes,gamma
|
|
6574
|
+
recipient-no-consent,Barbara,+15550001004,no,delta`,
|
|
6575
|
+
requireConsent: true,
|
|
6576
|
+
variableColumns: ["segment"]
|
|
6577
|
+
});
|
|
6578
|
+
await runtime.enqueue(scheduled.campaign.id);
|
|
6579
|
+
const windowBlocked = await runtime.tick(scheduled.campaign.id);
|
|
6580
|
+
now = Date.UTC(2026, 0, 5, 12, 30, 0);
|
|
6581
|
+
const quietHours = await runtime.tick(scheduled.campaign.id);
|
|
6582
|
+
now = Date.UTC(2026, 0, 5, 14, 0, 0);
|
|
6583
|
+
const allowed = await runtime.tick(scheduled.campaign.id);
|
|
6584
|
+
const rateLimited = await runtime.tick(scheduled.campaign.id);
|
|
6585
|
+
let retryNow = 1000;
|
|
6586
|
+
const retryRuntime = createVoiceCampaign({
|
|
6587
|
+
dialer: () => {
|
|
6588
|
+
throw new Error("carrier busy");
|
|
6589
|
+
},
|
|
6590
|
+
now: () => retryNow,
|
|
6591
|
+
store
|
|
6592
|
+
});
|
|
6593
|
+
const retry = await retryRuntime.create({
|
|
6594
|
+
id: `campaign-readiness-retry-${crypto.randomUUID()}`,
|
|
6595
|
+
maxAttempts: 2,
|
|
6596
|
+
name: "Campaign Readiness Retry Proof",
|
|
6597
|
+
schedule: {
|
|
6598
|
+
retryPolicy: {
|
|
6599
|
+
backoffMs: 5000
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
});
|
|
6603
|
+
await retryRuntime.addRecipients(retry.campaign.id, [
|
|
6604
|
+
{
|
|
6605
|
+
id: "readiness-retry-recipient",
|
|
6606
|
+
phone: "+15550002001"
|
|
6607
|
+
}
|
|
6608
|
+
]);
|
|
6609
|
+
await retryRuntime.enqueue(retry.campaign.id);
|
|
6610
|
+
const retryInitial = await retryRuntime.tick(retry.campaign.id);
|
|
6611
|
+
retryNow = 3000;
|
|
6612
|
+
const retryBackoff = await retryRuntime.tick(retry.campaign.id);
|
|
6613
|
+
retryNow = 6000;
|
|
6614
|
+
const retryAllowed = await retryRuntime.tick(retry.campaign.id);
|
|
6615
|
+
const finalScheduled = await runtime.get(scheduled.campaign.id);
|
|
6616
|
+
const finalRetry = await retryRuntime.get(retry.campaign.id);
|
|
6617
|
+
if (!finalScheduled || !finalRetry) {
|
|
6618
|
+
throw new Error("Campaign readiness proof did not persist campaign records.");
|
|
6619
|
+
}
|
|
6620
|
+
pushCampaignReadinessCheck(checks, "recipient-import-validation", imported.import.accepted.length === 1 && imported.import.rejected.length === 3, {
|
|
6621
|
+
accepted: imported.import.accepted.length,
|
|
6622
|
+
rejected: imported.import.rejected.length
|
|
6623
|
+
});
|
|
6624
|
+
pushCampaignReadinessCheck(checks, "attempt-window-block", windowBlocked.blocked.some((block) => block.reason === "outside-attempt-window"), {
|
|
6625
|
+
blocked: windowBlocked.blocked
|
|
6626
|
+
});
|
|
6627
|
+
pushCampaignReadinessCheck(checks, "quiet-hours-block", quietHours.blocked.some((block) => block.reason === "quiet-hours"), {
|
|
6628
|
+
blocked: quietHours.blocked
|
|
6629
|
+
});
|
|
6630
|
+
pushCampaignReadinessCheck(checks, "allowed-attempt", allowed.attempted === 1, {
|
|
6631
|
+
attempted: allowed.attempted
|
|
6632
|
+
});
|
|
6633
|
+
pushCampaignReadinessCheck(checks, "rate-limit-block", rateLimited.blocked.some((block) => block.reason === "rate-limit"), {
|
|
6634
|
+
blocked: rateLimited.blocked
|
|
6635
|
+
});
|
|
6636
|
+
pushCampaignReadinessCheck(checks, "retry-backoff-block", retryInitial.attempted === 1 && retryBackoff.blocked.some((block) => block.reason === "retry-backoff"), {
|
|
6637
|
+
blocked: retryBackoff.blocked
|
|
6638
|
+
});
|
|
6639
|
+
pushCampaignReadinessCheck(checks, "retry-to-max-attempts", retryAllowed.attempted === 1 && finalRetry.recipients[0]?.status === "failed", {
|
|
6640
|
+
attempted: retryAllowed.attempted,
|
|
6641
|
+
recipientStatus: finalRetry.recipients[0]?.status
|
|
6642
|
+
});
|
|
6643
|
+
return {
|
|
6644
|
+
campaigns: {
|
|
6645
|
+
retry: finalRetry,
|
|
6646
|
+
scheduled: finalScheduled
|
|
6647
|
+
},
|
|
6648
|
+
checks,
|
|
6649
|
+
generatedAt: Date.now(),
|
|
6650
|
+
import: imported.import,
|
|
6651
|
+
ok: checks.every((check) => check.status === "pass"),
|
|
6652
|
+
proof: "voice-campaign-readiness",
|
|
6653
|
+
ticks: {
|
|
6654
|
+
allowed,
|
|
6655
|
+
quietHours,
|
|
6656
|
+
rateLimited,
|
|
6657
|
+
retryAllowed,
|
|
6658
|
+
retryBackoff,
|
|
6659
|
+
retryInitial,
|
|
6660
|
+
windowBlocked
|
|
6661
|
+
}
|
|
6662
|
+
};
|
|
6663
|
+
};
|
|
6282
6664
|
var escapeHtml3 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
6283
6665
|
var renderVoiceCampaignsHTML = (records, options = {}) => {
|
|
6284
6666
|
const title = options.title ?? "Voice Campaigns";
|
|
@@ -6303,13 +6685,13 @@ var createVoiceCampaignRoutes = (options) => {
|
|
|
6303
6685
|
const app = new Elysia2({ name: options.name ?? "absolutejs-voice-campaigns" }).get(path, async () => ({
|
|
6304
6686
|
campaigns: await runtime.list(),
|
|
6305
6687
|
summary: await runtime.summarize()
|
|
6306
|
-
})).get(`${path}/observability`, async () => buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability)).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
|
|
6688
|
+
})).get(`${path}/observability`, async () => buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability)).get(`${path}/readiness-proof`, () => runVoiceCampaignReadinessProof()).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
|
|
6307
6689
|
await runtime.remove(params.campaignId);
|
|
6308
6690
|
return { ok: true };
|
|
6309
6691
|
}).post(`${path}/:campaignId/recipients`, async ({ params, request }) => {
|
|
6310
6692
|
const body = await readJsonBody(request);
|
|
6311
6693
|
return runtime.addRecipients(params.campaignId, body.recipients ?? []);
|
|
6312
|
-
}).post(`${path}/:campaignId/enqueue`, ({ params }) => runtime.enqueue(params.campaignId)).post(`${path}/:campaignId/pause`, ({ params }) => runtime.pause(params.campaignId)).post(`${path}/:campaignId/resume`, ({ params }) => runtime.resume(params.campaignId)).post(`${path}/:campaignId/cancel`, ({ params }) => runtime.cancel(params.campaignId)).post(`${path}/:campaignId/tick`, ({ params }) => runtime.tick(params.campaignId)).post(`${path}/:campaignId/attempts/:attemptId/result`, async ({
|
|
6694
|
+
}).post(`${path}/:campaignId/recipients/import`, async ({ params, request }) => runtime.importRecipients(params.campaignId, await readJsonBody(request))).post(`${path}/:campaignId/enqueue`, ({ params }) => runtime.enqueue(params.campaignId)).post(`${path}/:campaignId/pause`, ({ params }) => runtime.pause(params.campaignId)).post(`${path}/:campaignId/resume`, ({ params }) => runtime.resume(params.campaignId)).post(`${path}/:campaignId/cancel`, ({ params }) => runtime.cancel(params.campaignId)).post(`${path}/:campaignId/tick`, ({ params }) => runtime.tick(params.campaignId)).post(`${path}/:campaignId/attempts/:attemptId/result`, async ({
|
|
6313
6695
|
params,
|
|
6314
6696
|
request
|
|
6315
6697
|
}) => runtime.completeAttempt(params.campaignId, params.attemptId, await readJsonBody(request)));
|
|
@@ -15189,6 +15571,308 @@ var createVoiceLiveLatencyRoutes = (options) => {
|
|
|
15189
15571
|
}
|
|
15190
15572
|
return routes;
|
|
15191
15573
|
};
|
|
15574
|
+
// src/latencySlo.ts
|
|
15575
|
+
var DEFAULT_WARN_AFTER_MS2 = 1800;
|
|
15576
|
+
var DEFAULT_FAIL_AFTER_MS2 = 3200;
|
|
15577
|
+
var STAGE_LABELS = {
|
|
15578
|
+
assistant_text_to_tts_send: "Assistant text to TTS send",
|
|
15579
|
+
barge_in_stop: "Barge-in stop latency",
|
|
15580
|
+
commit_to_assistant_text: "Commit to assistant text",
|
|
15581
|
+
final_to_commit: "Final transcript to commit",
|
|
15582
|
+
live_latency: "Browser live latency",
|
|
15583
|
+
provider_llm: "LLM provider latency",
|
|
15584
|
+
provider_stt: "STT provider latency",
|
|
15585
|
+
provider_tts: "TTS provider latency",
|
|
15586
|
+
speech_to_commit: "Speech detected to commit",
|
|
15587
|
+
tts_send_duration: "TTS send duration",
|
|
15588
|
+
tts_to_first_audio: "TTS to first audio"
|
|
15589
|
+
};
|
|
15590
|
+
var TRACE_TYPES = [
|
|
15591
|
+
"assistant.run",
|
|
15592
|
+
"client.barge_in",
|
|
15593
|
+
"client.live_latency",
|
|
15594
|
+
"turn.transcript",
|
|
15595
|
+
"turn_latency.stage"
|
|
15596
|
+
];
|
|
15597
|
+
var getNumber6 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
15598
|
+
var getString11 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
15599
|
+
var percentile2 = (values, percentileValue) => {
|
|
15600
|
+
if (values.length === 0) {
|
|
15601
|
+
return;
|
|
15602
|
+
}
|
|
15603
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
15604
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentileValue / 100 * sorted.length) - 1));
|
|
15605
|
+
return Math.round(sorted[index] ?? 0);
|
|
15606
|
+
};
|
|
15607
|
+
var average = (values) => values.length === 0 ? undefined : Math.round(values.reduce((total, value) => total + value, 0) / values.length);
|
|
15608
|
+
var resolveBudget = (stage, options) => ({
|
|
15609
|
+
failAfterMs: options.budgets?.[stage]?.failAfterMs ?? options.failAfterMs ?? DEFAULT_FAIL_AFTER_MS2,
|
|
15610
|
+
warnAfterMs: options.budgets?.[stage]?.warnAfterMs ?? options.warnAfterMs ?? DEFAULT_WARN_AFTER_MS2
|
|
15611
|
+
});
|
|
15612
|
+
var statusForLatency = (latencyMs, budget) => latencyMs > budget.failAfterMs ? "fail" : budget.warnAfterMs !== undefined && latencyMs > budget.warnAfterMs ? "warn" : "pass";
|
|
15613
|
+
var stageMeasurement = (input) => {
|
|
15614
|
+
if (input.latencyMs === undefined) {
|
|
15615
|
+
return;
|
|
15616
|
+
}
|
|
15617
|
+
return {
|
|
15618
|
+
at: input.at,
|
|
15619
|
+
latencyMs: Math.max(0, Math.round(input.latencyMs)),
|
|
15620
|
+
label: STAGE_LABELS[input.stage],
|
|
15621
|
+
provider: input.provider,
|
|
15622
|
+
sessionId: input.sessionId,
|
|
15623
|
+
stage: input.stage,
|
|
15624
|
+
status: statusForLatency(Math.max(0, input.latencyMs), input.budget),
|
|
15625
|
+
turnId: input.turnId
|
|
15626
|
+
};
|
|
15627
|
+
};
|
|
15628
|
+
var providerStageForEvent = (event) => {
|
|
15629
|
+
if (event.type === "assistant.run") {
|
|
15630
|
+
return "provider_llm";
|
|
15631
|
+
}
|
|
15632
|
+
if (event.type === "turn.transcript") {
|
|
15633
|
+
return "provider_stt";
|
|
15634
|
+
}
|
|
15635
|
+
const kind = getString11(event.payload.providerKind) ?? getString11(event.payload.kind) ?? getString11(event.payload.lane);
|
|
15636
|
+
if (kind === "llm" || kind === "model") {
|
|
15637
|
+
return "provider_llm";
|
|
15638
|
+
}
|
|
15639
|
+
if (kind === "stt" || kind === "transcription") {
|
|
15640
|
+
return "provider_stt";
|
|
15641
|
+
}
|
|
15642
|
+
if (kind === "tts" || kind === "speech") {
|
|
15643
|
+
return "provider_tts";
|
|
15644
|
+
}
|
|
15645
|
+
return;
|
|
15646
|
+
};
|
|
15647
|
+
var eventElapsedMs = (event) => getNumber6(event.payload.elapsedMs) ?? getNumber6(event.payload.latencyMs) ?? getNumber6(event.payload.durationMs);
|
|
15648
|
+
var collectTraceStageMeasurements = (events, options) => {
|
|
15649
|
+
const grouped = new Map;
|
|
15650
|
+
for (const event of events) {
|
|
15651
|
+
if (event.type !== "turn_latency.stage" || !event.turnId) {
|
|
15652
|
+
continue;
|
|
15653
|
+
}
|
|
15654
|
+
const stage = getString11(event.payload.stage);
|
|
15655
|
+
if (!stage) {
|
|
15656
|
+
continue;
|
|
15657
|
+
}
|
|
15658
|
+
const key = `${event.sessionId}:${event.turnId}`;
|
|
15659
|
+
const stages = grouped.get(key) ?? new Map;
|
|
15660
|
+
const previous = stages.get(stage);
|
|
15661
|
+
if (!previous || event.at < previous.at) {
|
|
15662
|
+
stages.set(stage, event);
|
|
15663
|
+
}
|
|
15664
|
+
grouped.set(key, stages);
|
|
15665
|
+
}
|
|
15666
|
+
const measurements = [];
|
|
15667
|
+
for (const [key, stages] of grouped) {
|
|
15668
|
+
const [sessionId, turnId] = key.split(":");
|
|
15669
|
+
if (!sessionId || !turnId) {
|
|
15670
|
+
continue;
|
|
15671
|
+
}
|
|
15672
|
+
const speechDetected = stages.get("speech_detected")?.at;
|
|
15673
|
+
const finalTranscript = stages.get("final_transcript")?.at;
|
|
15674
|
+
const turnCommitted = stages.get("turn_committed")?.at;
|
|
15675
|
+
const assistantTextStarted = stages.get("assistant_text_started")?.at;
|
|
15676
|
+
const ttsSendStarted = stages.get("tts_send_started")?.at;
|
|
15677
|
+
const ttsSendCompleted = stages.get("tts_send_completed")?.at;
|
|
15678
|
+
const assistantAudioReceived = stages.get("assistant_audio_received")?.at;
|
|
15679
|
+
const candidates = [
|
|
15680
|
+
{
|
|
15681
|
+
at: turnCommitted ?? 0,
|
|
15682
|
+
budget: resolveBudget("speech_to_commit", options),
|
|
15683
|
+
latencyMs: speechDetected === undefined || turnCommitted === undefined ? undefined : turnCommitted - speechDetected,
|
|
15684
|
+
required: true,
|
|
15685
|
+
sessionId,
|
|
15686
|
+
stage: "speech_to_commit",
|
|
15687
|
+
turnId
|
|
15688
|
+
},
|
|
15689
|
+
{
|
|
15690
|
+
at: turnCommitted ?? 0,
|
|
15691
|
+
budget: resolveBudget("final_to_commit", options),
|
|
15692
|
+
latencyMs: finalTranscript === undefined || turnCommitted === undefined ? undefined : turnCommitted - finalTranscript,
|
|
15693
|
+
required: true,
|
|
15694
|
+
sessionId,
|
|
15695
|
+
stage: "final_to_commit",
|
|
15696
|
+
turnId
|
|
15697
|
+
},
|
|
15698
|
+
{
|
|
15699
|
+
at: assistantTextStarted ?? 0,
|
|
15700
|
+
budget: resolveBudget("commit_to_assistant_text", options),
|
|
15701
|
+
latencyMs: turnCommitted === undefined || assistantTextStarted === undefined ? undefined : assistantTextStarted - turnCommitted,
|
|
15702
|
+
required: true,
|
|
15703
|
+
sessionId,
|
|
15704
|
+
stage: "commit_to_assistant_text",
|
|
15705
|
+
turnId
|
|
15706
|
+
},
|
|
15707
|
+
{
|
|
15708
|
+
at: ttsSendStarted ?? 0,
|
|
15709
|
+
budget: resolveBudget("assistant_text_to_tts_send", options),
|
|
15710
|
+
latencyMs: assistantTextStarted === undefined || ttsSendStarted === undefined ? undefined : ttsSendStarted - assistantTextStarted,
|
|
15711
|
+
required: true,
|
|
15712
|
+
sessionId,
|
|
15713
|
+
stage: "assistant_text_to_tts_send",
|
|
15714
|
+
turnId
|
|
15715
|
+
},
|
|
15716
|
+
{
|
|
15717
|
+
at: ttsSendCompleted ?? 0,
|
|
15718
|
+
budget: resolveBudget("tts_send_duration", options),
|
|
15719
|
+
latencyMs: ttsSendStarted === undefined || ttsSendCompleted === undefined ? undefined : ttsSendCompleted - ttsSendStarted,
|
|
15720
|
+
required: true,
|
|
15721
|
+
sessionId,
|
|
15722
|
+
stage: "tts_send_duration",
|
|
15723
|
+
turnId
|
|
15724
|
+
},
|
|
15725
|
+
{
|
|
15726
|
+
at: assistantAudioReceived ?? 0,
|
|
15727
|
+
budget: resolveBudget("tts_to_first_audio", options),
|
|
15728
|
+
latencyMs: ttsSendCompleted === undefined || assistantAudioReceived === undefined ? undefined : assistantAudioReceived - ttsSendCompleted,
|
|
15729
|
+
required: true,
|
|
15730
|
+
sessionId,
|
|
15731
|
+
stage: "tts_to_first_audio",
|
|
15732
|
+
turnId
|
|
15733
|
+
}
|
|
15734
|
+
];
|
|
15735
|
+
for (const candidate of candidates) {
|
|
15736
|
+
const measurement = stageMeasurement(candidate);
|
|
15737
|
+
if (measurement) {
|
|
15738
|
+
measurements.push(measurement);
|
|
15739
|
+
}
|
|
15740
|
+
}
|
|
15741
|
+
}
|
|
15742
|
+
return measurements;
|
|
15743
|
+
};
|
|
15744
|
+
var collectDirectMeasurements = (events, options) => {
|
|
15745
|
+
const measurements = [];
|
|
15746
|
+
for (const event of events) {
|
|
15747
|
+
if (event.type === "client.live_latency") {
|
|
15748
|
+
const stage = "live_latency";
|
|
15749
|
+
const measurement = stageMeasurement({
|
|
15750
|
+
at: event.at,
|
|
15751
|
+
budget: resolveBudget(stage, options),
|
|
15752
|
+
latencyMs: eventElapsedMs(event),
|
|
15753
|
+
sessionId: event.sessionId,
|
|
15754
|
+
stage,
|
|
15755
|
+
turnId: event.turnId
|
|
15756
|
+
});
|
|
15757
|
+
if (measurement) {
|
|
15758
|
+
measurements.push(measurement);
|
|
15759
|
+
}
|
|
15760
|
+
continue;
|
|
15761
|
+
}
|
|
15762
|
+
if (event.type === "client.barge_in") {
|
|
15763
|
+
const stage = "barge_in_stop";
|
|
15764
|
+
const measurement = stageMeasurement({
|
|
15765
|
+
at: event.at,
|
|
15766
|
+
budget: resolveBudget(stage, options),
|
|
15767
|
+
latencyMs: eventElapsedMs(event),
|
|
15768
|
+
sessionId: event.sessionId,
|
|
15769
|
+
stage,
|
|
15770
|
+
turnId: event.turnId
|
|
15771
|
+
});
|
|
15772
|
+
if (measurement) {
|
|
15773
|
+
measurements.push(measurement);
|
|
15774
|
+
}
|
|
15775
|
+
continue;
|
|
15776
|
+
}
|
|
15777
|
+
const providerStage = providerStageForEvent(event);
|
|
15778
|
+
if (providerStage) {
|
|
15779
|
+
const measurement = stageMeasurement({
|
|
15780
|
+
at: event.at,
|
|
15781
|
+
budget: resolveBudget(providerStage, options),
|
|
15782
|
+
latencyMs: eventElapsedMs(event),
|
|
15783
|
+
provider: getString11(event.payload.provider),
|
|
15784
|
+
sessionId: event.sessionId,
|
|
15785
|
+
stage: providerStage,
|
|
15786
|
+
turnId: event.turnId
|
|
15787
|
+
});
|
|
15788
|
+
if (measurement) {
|
|
15789
|
+
measurements.push(measurement);
|
|
15790
|
+
}
|
|
15791
|
+
}
|
|
15792
|
+
}
|
|
15793
|
+
return measurements;
|
|
15794
|
+
};
|
|
15795
|
+
var summarizeStage = (stage, measurements, options) => {
|
|
15796
|
+
const stageMeasurements = measurements.filter((measurement) => measurement.stage === stage);
|
|
15797
|
+
const latencies = stageMeasurements.map((measurement) => measurement.latencyMs);
|
|
15798
|
+
const failed = stageMeasurements.filter((measurement) => measurement.status === "fail").length;
|
|
15799
|
+
const warnings = stageMeasurements.filter((measurement) => measurement.status === "warn").length;
|
|
15800
|
+
return {
|
|
15801
|
+
averageMs: average(latencies),
|
|
15802
|
+
budget: resolveBudget(stage, options),
|
|
15803
|
+
failed,
|
|
15804
|
+
label: STAGE_LABELS[stage],
|
|
15805
|
+
maxMs: latencies.length > 0 ? Math.max(...latencies) : undefined,
|
|
15806
|
+
measurements: stageMeasurements,
|
|
15807
|
+
p50Ms: percentile2(latencies, 50),
|
|
15808
|
+
p95Ms: percentile2(latencies, 95),
|
|
15809
|
+
stage,
|
|
15810
|
+
status: stageMeasurements.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
15811
|
+
total: stageMeasurements.length,
|
|
15812
|
+
warnings
|
|
15813
|
+
};
|
|
15814
|
+
};
|
|
15815
|
+
var buildVoiceLatencySLOGate = async (options) => {
|
|
15816
|
+
const events = options.events ?? await options.store?.list({
|
|
15817
|
+
limit: options.limit ?? 1000,
|
|
15818
|
+
type: TRACE_TYPES
|
|
15819
|
+
}) ?? [];
|
|
15820
|
+
const measurements = [
|
|
15821
|
+
...collectTraceStageMeasurements(events, options),
|
|
15822
|
+
...collectDirectMeasurements(events, options)
|
|
15823
|
+
].sort((left, right) => right.at - left.at);
|
|
15824
|
+
const stageKeys = new Set([
|
|
15825
|
+
...Object.keys(options.budgets ?? {}),
|
|
15826
|
+
...measurements.map((measurement) => measurement.stage)
|
|
15827
|
+
]);
|
|
15828
|
+
const stages = [...stageKeys].map((stage) => summarizeStage(stage, measurements, options)).sort((left, right) => left.label.localeCompare(right.label));
|
|
15829
|
+
const failed = measurements.filter((measurement) => measurement.status === "fail").length;
|
|
15830
|
+
const warnings = measurements.filter((measurement) => measurement.status === "warn").length;
|
|
15831
|
+
return {
|
|
15832
|
+
checkedAt: Date.now(),
|
|
15833
|
+
failed,
|
|
15834
|
+
measurements,
|
|
15835
|
+
stages,
|
|
15836
|
+
status: measurements.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
15837
|
+
total: measurements.length,
|
|
15838
|
+
warnings
|
|
15839
|
+
};
|
|
15840
|
+
};
|
|
15841
|
+
var assertVoiceLatencySLOGate = async (options) => {
|
|
15842
|
+
const report = await buildVoiceLatencySLOGate(options);
|
|
15843
|
+
if (report.status === "fail") {
|
|
15844
|
+
const error = new Error(`Voice latency SLO gate failed with ${report.failed} failed measurement(s).`);
|
|
15845
|
+
error.report = report;
|
|
15846
|
+
throw error;
|
|
15847
|
+
}
|
|
15848
|
+
return report;
|
|
15849
|
+
};
|
|
15850
|
+
var renderVoiceLatencySLOMarkdown = (report, options = {}) => {
|
|
15851
|
+
const title = options.title ?? "Voice Latency SLO Gate";
|
|
15852
|
+
const rows = report.stages.map((stage) => `| ${stage.label} | ${stage.status} | ${stage.total} | ${stage.p50Ms ?? "n/a"} | ${stage.p95Ms ?? "n/a"} | ${stage.budget.warnAfterMs ?? "n/a"} | ${stage.budget.failAfterMs} |`).join(`
|
|
15853
|
+
`);
|
|
15854
|
+
const failures = report.measurements.filter((measurement) => measurement.status === "fail").map((measurement) => `- ${measurement.label}: ${measurement.latencyMs}ms in ${measurement.sessionId}${measurement.turnId ? `/${measurement.turnId}` : ""}${measurement.provider ? ` via ${measurement.provider}` : ""}`).join(`
|
|
15855
|
+
`);
|
|
15856
|
+
return `# ${title}
|
|
15857
|
+
|
|
15858
|
+
Status: ${report.status}
|
|
15859
|
+
|
|
15860
|
+
Total measurements: ${report.total}
|
|
15861
|
+
Warnings: ${report.warnings}
|
|
15862
|
+
Failures: ${report.failed}
|
|
15863
|
+
|
|
15864
|
+
| Stage | Status | Samples | p50 ms | p95 ms | Warn ms | Fail ms |
|
|
15865
|
+
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
|
15866
|
+
${rows || "| No latency measurements | empty | 0 | n/a | n/a | n/a | n/a |"}
|
|
15867
|
+
|
|
15868
|
+
${failures ? `## Failures
|
|
15869
|
+
|
|
15870
|
+
${failures}
|
|
15871
|
+
` : `## Failures
|
|
15872
|
+
|
|
15873
|
+
None.
|
|
15874
|
+
`}`;
|
|
15875
|
+
};
|
|
15192
15876
|
// src/turnQuality.ts
|
|
15193
15877
|
import { Elysia as Elysia24 } from "elysia";
|
|
15194
15878
|
var DEFAULT_CONFIDENCE_WARN_THRESHOLD = 0.72;
|
|
@@ -20237,8 +20921,8 @@ var createVoiceProviderCapabilityRoutes = (options) => {
|
|
|
20237
20921
|
// src/resilienceRoutes.ts
|
|
20238
20922
|
import { Elysia as Elysia33 } from "elysia";
|
|
20239
20923
|
var escapeHtml34 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
20240
|
-
var
|
|
20241
|
-
var
|
|
20924
|
+
var getString12 = (value) => typeof value === "string" ? value : undefined;
|
|
20925
|
+
var getNumber7 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
20242
20926
|
var getBoolean2 = (value) => value === true;
|
|
20243
20927
|
var isProviderStatus2 = (value) => value === "error" || value === "fallback" || value === "success";
|
|
20244
20928
|
var listVoiceRoutingEvents = (events) => {
|
|
@@ -20247,27 +20931,27 @@ var listVoiceRoutingEvents = (events) => {
|
|
|
20247
20931
|
if (event.type !== "session.error") {
|
|
20248
20932
|
continue;
|
|
20249
20933
|
}
|
|
20250
|
-
const provider =
|
|
20934
|
+
const provider = getString12(event.payload.provider);
|
|
20251
20935
|
const providerStatus = isProviderStatus2(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
|
|
20252
20936
|
if (!provider || !providerStatus) {
|
|
20253
20937
|
continue;
|
|
20254
20938
|
}
|
|
20255
|
-
const kind =
|
|
20939
|
+
const kind = getString12(event.payload.kind);
|
|
20256
20940
|
routingEvents.push({
|
|
20257
20941
|
at: event.at,
|
|
20258
|
-
attempt:
|
|
20259
|
-
elapsedMs:
|
|
20260
|
-
error:
|
|
20261
|
-
fallbackProvider:
|
|
20942
|
+
attempt: getNumber7(event.payload.attempt),
|
|
20943
|
+
elapsedMs: getNumber7(event.payload.elapsedMs),
|
|
20944
|
+
error: getString12(event.payload.error),
|
|
20945
|
+
fallbackProvider: getString12(event.payload.fallbackProvider),
|
|
20262
20946
|
kind: kind === "stt" || kind === "tts" ? kind : "llm",
|
|
20263
|
-
latencyBudgetMs:
|
|
20264
|
-
operation:
|
|
20947
|
+
latencyBudgetMs: getNumber7(event.payload.latencyBudgetMs),
|
|
20948
|
+
operation: getString12(event.payload.operation),
|
|
20265
20949
|
provider,
|
|
20266
|
-
routing:
|
|
20267
|
-
selectedProvider:
|
|
20950
|
+
routing: getString12(event.payload.routing),
|
|
20951
|
+
selectedProvider: getString12(event.payload.selectedProvider),
|
|
20268
20952
|
sessionId: event.sessionId,
|
|
20269
20953
|
status: providerStatus,
|
|
20270
|
-
suppressionRemainingMs:
|
|
20954
|
+
suppressionRemainingMs: getNumber7(event.payload.suppressionRemainingMs),
|
|
20271
20955
|
timedOut: getBoolean2(event.payload.timedOut),
|
|
20272
20956
|
turnId: event.turnId
|
|
20273
20957
|
});
|
|
@@ -20764,6 +21448,7 @@ var readinessGateCodes = {
|
|
|
20764
21448
|
"Audit evidence": "voice.readiness.audit_evidence",
|
|
20765
21449
|
"Audit sink delivery": "voice.readiness.audit_sink_delivery",
|
|
20766
21450
|
"Barge-in interruption proof": "voice.readiness.barge_in_interruption",
|
|
21451
|
+
"Campaign readiness proof": "voice.readiness.campaign_readiness",
|
|
20767
21452
|
"Carrier readiness": "voice.readiness.carrier_readiness",
|
|
20768
21453
|
"Delivery runtime": "voice.readiness.delivery_runtime",
|
|
20769
21454
|
"Handoff delivery": "voice.readiness.handoff_delivery",
|
|
@@ -20877,6 +21562,12 @@ var resolveBargeInReports = async (options, input) => {
|
|
|
20877
21562
|
}
|
|
20878
21563
|
return typeof options.bargeInReports === "function" ? await options.bargeInReports(input) : options.bargeInReports;
|
|
20879
21564
|
};
|
|
21565
|
+
var resolveCampaignReadiness = async (options, input) => {
|
|
21566
|
+
if (options.campaignReadiness === false || options.campaignReadiness === undefined) {
|
|
21567
|
+
return;
|
|
21568
|
+
}
|
|
21569
|
+
return typeof options.campaignReadiness === "function" ? await options.campaignReadiness(input) : options.campaignReadiness;
|
|
21570
|
+
};
|
|
20880
21571
|
var resolveProofSources = async (options, input) => {
|
|
20881
21572
|
if (options.proofSources === false || options.proofSources === undefined) {
|
|
20882
21573
|
return;
|
|
@@ -21097,8 +21788,8 @@ var summarizeLiveLatency = (events, options) => {
|
|
|
21097
21788
|
warnings
|
|
21098
21789
|
};
|
|
21099
21790
|
};
|
|
21100
|
-
var
|
|
21101
|
-
var
|
|
21791
|
+
var getString13 = (value) => typeof value === "string" ? value : undefined;
|
|
21792
|
+
var getNumber8 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
21102
21793
|
var voiceOperationsRecordHref = (base, sessionId) => {
|
|
21103
21794
|
const encoded = encodeURIComponent(sessionId);
|
|
21104
21795
|
if (base.includes(":sessionId")) {
|
|
@@ -21109,7 +21800,7 @@ var voiceOperationsRecordHref = (base, sessionId) => {
|
|
|
21109
21800
|
var buildOperationsRecordLinks = (input) => {
|
|
21110
21801
|
const failedSessionSet = new Set(input.failedSessionIds);
|
|
21111
21802
|
const providerErrors = input.events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")).map((event) => ({
|
|
21112
|
-
detail:
|
|
21803
|
+
detail: getString13(event.payload.error),
|
|
21113
21804
|
href: voiceOperationsRecordHref(input.base, event.sessionId),
|
|
21114
21805
|
label: "Open provider error operations record",
|
|
21115
21806
|
sessionId: event.sessionId,
|
|
@@ -21117,7 +21808,7 @@ var buildOperationsRecordLinks = (input) => {
|
|
|
21117
21808
|
}));
|
|
21118
21809
|
const failingLatency = input.events.filter((event) => event.type === "client.live_latency").map((event) => ({
|
|
21119
21810
|
event,
|
|
21120
|
-
latencyMs:
|
|
21811
|
+
latencyMs: getNumber8(event.payload.latencyMs) ?? getNumber8(event.payload.elapsedMs)
|
|
21121
21812
|
})).filter((entry) => entry.latencyMs !== undefined && entry.latencyMs > input.liveLatencyWarnAfterMs).map(({ event, latencyMs }) => ({
|
|
21122
21813
|
detail: `${latencyMs}ms live latency`,
|
|
21123
21814
|
href: voiceOperationsRecordHref(input.base, event.sessionId),
|
|
@@ -21163,6 +21854,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
21163
21854
|
phoneAgentSmokes,
|
|
21164
21855
|
reconnectContracts,
|
|
21165
21856
|
bargeInReports,
|
|
21857
|
+
campaignReadiness,
|
|
21166
21858
|
proofSources
|
|
21167
21859
|
] = await Promise.all([
|
|
21168
21860
|
evaluateVoiceQuality({ events }),
|
|
@@ -21195,6 +21887,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
21195
21887
|
resolvePhoneAgentSmokes(options, { query, request }),
|
|
21196
21888
|
resolveReconnectContracts(options, { query, request }),
|
|
21197
21889
|
resolveBargeInReports(options, { query, request }),
|
|
21890
|
+
resolveCampaignReadiness(options, { query, request }),
|
|
21198
21891
|
resolveProofSources(options, { query, request })
|
|
21199
21892
|
]);
|
|
21200
21893
|
const deliveryRuntime = summarizeDeliveryRuntime(deliveryRuntimeSummary);
|
|
@@ -21381,6 +22074,12 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
21381
22074
|
total: bargeInReports.reduce((total, report) => total + report.total, 0),
|
|
21382
22075
|
warnings: bargeInReports.filter((report) => report.status === "warn").length
|
|
21383
22076
|
} : undefined;
|
|
22077
|
+
const campaignReadinessSummary = campaignReadiness ? {
|
|
22078
|
+
failed: campaignReadiness.checks.filter((check) => check.status !== "pass").length,
|
|
22079
|
+
passed: campaignReadiness.checks.filter((check) => check.status === "pass").length,
|
|
22080
|
+
status: campaignReadiness.ok ? "pass" : "fail",
|
|
22081
|
+
total: campaignReadiness.checks.length
|
|
22082
|
+
} : undefined;
|
|
21384
22083
|
if (agentSquadContractSummary) {
|
|
21385
22084
|
checks.push({
|
|
21386
22085
|
detail: agentSquadContractSummary.status === "pass" ? `${agentSquadContractSummary.passed} agent squad contract(s) are passing.` : agentSquadContractSummary.total === 0 ? "No agent squad contracts are configured." : `${agentSquadContractSummary.failed} agent squad contract(s) failed.`,
|
|
@@ -21502,6 +22201,24 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
21502
22201
|
]
|
|
21503
22202
|
});
|
|
21504
22203
|
}
|
|
22204
|
+
if (campaignReadinessSummary) {
|
|
22205
|
+
const failedChecks = campaignReadiness?.checks.filter((check) => check.status !== "pass").map((check) => check.name);
|
|
22206
|
+
checks.push({
|
|
22207
|
+
detail: campaignReadinessSummary.status === "pass" ? `${campaignReadinessSummary.passed} campaign readiness check(s) are passing without live dialing.` : failedChecks && failedChecks.length > 0 ? `Campaign readiness failed: ${failedChecks.join(", ")}.` : "Campaign readiness proof failed.",
|
|
22208
|
+
href: options.links?.campaignReadiness ?? "/api/voice/campaigns/readiness-proof",
|
|
22209
|
+
label: "Campaign readiness proof",
|
|
22210
|
+
proofSource: proofSource("campaignReadiness", "campaigns"),
|
|
22211
|
+
status: campaignReadinessSummary.status,
|
|
22212
|
+
value: `${campaignReadinessSummary.passed}/${campaignReadinessSummary.total}`,
|
|
22213
|
+
actions: campaignReadinessSummary.status === "pass" ? [] : [
|
|
22214
|
+
{
|
|
22215
|
+
description: "Open campaign readiness proof and inspect import, scheduling, rate-limit, and retry checks.",
|
|
22216
|
+
href: options.links?.campaignReadiness ?? "/api/voice/campaigns/readiness-proof",
|
|
22217
|
+
label: "Open campaign proof"
|
|
22218
|
+
}
|
|
22219
|
+
]
|
|
22220
|
+
});
|
|
22221
|
+
}
|
|
21505
22222
|
if (audit) {
|
|
21506
22223
|
const missingLabels = audit.missing.map((requirement) => requirement.label ?? requirement.type);
|
|
21507
22224
|
checks.push({
|
|
@@ -21611,6 +22328,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
21611
22328
|
audit: "/audit",
|
|
21612
22329
|
auditDeliveries: "/audit",
|
|
21613
22330
|
bargeIn: "/barge-in",
|
|
22331
|
+
campaignReadiness: "/api/voice/campaigns/readiness-proof",
|
|
21614
22332
|
carriers: "/carriers",
|
|
21615
22333
|
deliveryRuntime: "/delivery-runtime",
|
|
21616
22334
|
handoffs: "/handoffs",
|
|
@@ -21637,6 +22355,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
21637
22355
|
audit,
|
|
21638
22356
|
auditDeliveries,
|
|
21639
22357
|
bargeIn: bargeInSummary,
|
|
22358
|
+
campaignReadiness: campaignReadinessSummary,
|
|
21640
22359
|
carriers: carrierSummary,
|
|
21641
22360
|
deliveryRuntime,
|
|
21642
22361
|
handoffs: {
|
|
@@ -21762,6 +22481,7 @@ var profileSurfaceLabels = {
|
|
|
21762
22481
|
},
|
|
21763
22482
|
"phone-agent": {
|
|
21764
22483
|
auditDeliveries: "audit delivery queue configured",
|
|
22484
|
+
campaignReadiness: "campaign readiness proof configured",
|
|
21765
22485
|
carriers: "carrier readiness configured",
|
|
21766
22486
|
deliveryRuntime: "delivery runtime configured",
|
|
21767
22487
|
phoneAgentSmokes: "phone-agent smoke proof configured",
|
|
@@ -21785,6 +22505,7 @@ var profileRequiredKeys = {
|
|
|
21785
22505
|
"phone-agent": [
|
|
21786
22506
|
"carriers",
|
|
21787
22507
|
"phoneAgentSmokes",
|
|
22508
|
+
"campaignReadiness",
|
|
21788
22509
|
"providerRoutingContracts",
|
|
21789
22510
|
"auditDeliveries",
|
|
21790
22511
|
"traceDeliveries",
|
|
@@ -21797,6 +22518,7 @@ var configuredProfileKeys = (options) => {
|
|
|
21797
22518
|
"audit",
|
|
21798
22519
|
"auditDeliveries",
|
|
21799
22520
|
"bargeInReports",
|
|
22521
|
+
"campaignReadiness",
|
|
21800
22522
|
"carriers",
|
|
21801
22523
|
"deliveryRuntime",
|
|
21802
22524
|
"opsActionHistory",
|
|
@@ -21893,6 +22615,12 @@ var profileExplanation = (profile, options, links) => {
|
|
|
21893
22615
|
key: "phoneAgentSmokes",
|
|
21894
22616
|
label: "Phone agent smoke"
|
|
21895
22617
|
},
|
|
22618
|
+
{
|
|
22619
|
+
configured: isConfigured(options.campaignReadiness),
|
|
22620
|
+
href: links.campaignReadiness,
|
|
22621
|
+
key: "campaignReadiness",
|
|
22622
|
+
label: "Campaign readiness proof"
|
|
22623
|
+
},
|
|
21896
22624
|
{
|
|
21897
22625
|
configured: true,
|
|
21898
22626
|
href: links.handoffs,
|
|
@@ -21988,6 +22716,7 @@ var createVoiceReadinessProfile = (profile, options = {}) => {
|
|
|
21988
22716
|
if (profile === "phone-agent") {
|
|
21989
22717
|
const links2 = mergeLinks({
|
|
21990
22718
|
auditDeliveries: "/audit/deliveries",
|
|
22719
|
+
campaignReadiness: "/api/voice/campaigns/readiness-proof",
|
|
21991
22720
|
carriers: "/carriers",
|
|
21992
22721
|
deliveryRuntime: "/delivery-runtime",
|
|
21993
22722
|
handoffs: "/handoffs",
|
|
@@ -21999,6 +22728,7 @@ var createVoiceReadinessProfile = (profile, options = {}) => {
|
|
|
21999
22728
|
}, options.links);
|
|
22000
22729
|
return withDefined({
|
|
22001
22730
|
auditDeliveries: options.auditDeliveries,
|
|
22731
|
+
campaignReadiness: options.campaignReadiness,
|
|
22002
22732
|
carriers: options.carriers,
|
|
22003
22733
|
deliveryRuntime: options.deliveryRuntime,
|
|
22004
22734
|
gate: options.gate,
|
|
@@ -22551,11 +23281,11 @@ import { Elysia as Elysia38 } from "elysia";
|
|
|
22551
23281
|
// src/traceTimeline.ts
|
|
22552
23282
|
import { Elysia as Elysia37 } from "elysia";
|
|
22553
23283
|
var escapeHtml38 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
22554
|
-
var
|
|
22555
|
-
var
|
|
23284
|
+
var getString14 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
23285
|
+
var getNumber9 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
22556
23286
|
var firstString3 = (payload, keys) => {
|
|
22557
23287
|
for (const key of keys) {
|
|
22558
|
-
const value =
|
|
23288
|
+
const value = getString14(payload[key]);
|
|
22559
23289
|
if (value) {
|
|
22560
23290
|
return value;
|
|
22561
23291
|
}
|
|
@@ -22564,7 +23294,7 @@ var firstString3 = (payload, keys) => {
|
|
|
22564
23294
|
};
|
|
22565
23295
|
var firstNumber3 = (payload, keys) => {
|
|
22566
23296
|
for (const key of keys) {
|
|
22567
|
-
const value =
|
|
23297
|
+
const value = getNumber9(payload[key]);
|
|
22568
23298
|
if (value !== undefined) {
|
|
22569
23299
|
return value;
|
|
22570
23300
|
}
|
|
@@ -22584,7 +23314,7 @@ var eventStatus = (event) => firstString3(event.payload, [
|
|
|
22584
23314
|
"type",
|
|
22585
23315
|
"reason"
|
|
22586
23316
|
]);
|
|
22587
|
-
var
|
|
23317
|
+
var eventElapsedMs2 = (event) => firstNumber3(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
|
|
22588
23318
|
var timelineLabel = (event) => {
|
|
22589
23319
|
switch (event.type) {
|
|
22590
23320
|
case "call.lifecycle":
|
|
@@ -22592,15 +23322,15 @@ var timelineLabel = (event) => {
|
|
|
22592
23322
|
case "turn.transcript":
|
|
22593
23323
|
return event.payload.isFinal === true ? "Final transcript" : "Partial transcript";
|
|
22594
23324
|
case "turn.committed":
|
|
22595
|
-
return `Committed turn${
|
|
23325
|
+
return `Committed turn${getString14(event.payload.reason) ? ` (${getString14(event.payload.reason)})` : ""}`;
|
|
22596
23326
|
case "turn.assistant":
|
|
22597
23327
|
return "Assistant reply";
|
|
22598
23328
|
case "agent.model":
|
|
22599
23329
|
return `Model call${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
|
|
22600
23330
|
case "agent.tool":
|
|
22601
|
-
return `Tool ${
|
|
23331
|
+
return `Tool ${getString14(event.payload.toolName) ?? "call"}`;
|
|
22602
23332
|
case "agent.handoff":
|
|
22603
|
-
return `Agent handoff${
|
|
23333
|
+
return `Agent handoff${getString14(event.payload.targetAgentId) ? ` to ${getString14(event.payload.targetAgentId)}` : ""}`;
|
|
22604
23334
|
case "assistant.run":
|
|
22605
23335
|
return `Assistant run${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
|
|
22606
23336
|
case "assistant.guardrail":
|
|
@@ -22608,13 +23338,13 @@ var timelineLabel = (event) => {
|
|
|
22608
23338
|
case "call.handoff":
|
|
22609
23339
|
return `Call handoff ${eventStatus(event) ?? ""}`.trim();
|
|
22610
23340
|
case "client.live_latency":
|
|
22611
|
-
return `Live latency${
|
|
23341
|
+
return `Live latency${eventElapsedMs2(event) !== undefined ? ` ${eventElapsedMs2(event)}ms` : ""}`;
|
|
22612
23342
|
case "session.error":
|
|
22613
|
-
return `Error${
|
|
23343
|
+
return `Error${getString14(event.payload.error) ? `: ${getString14(event.payload.error)}` : ""}`;
|
|
22614
23344
|
case "turn.cost":
|
|
22615
23345
|
return "Cost telemetry";
|
|
22616
23346
|
case "turn_latency.stage":
|
|
22617
|
-
return `Latency ${
|
|
23347
|
+
return `Latency ${getString14(event.payload.stage) ?? "stage"}`;
|
|
22618
23348
|
case "workflow.contract":
|
|
22619
23349
|
return `Workflow contract ${eventStatus(event) ?? ""}`.trim();
|
|
22620
23350
|
default:
|
|
@@ -22646,7 +23376,7 @@ var summarizeProviders = (events) => {
|
|
|
22646
23376
|
}
|
|
22647
23377
|
const entry = getEntry(provider);
|
|
22648
23378
|
const status = eventStatus(event);
|
|
22649
|
-
const elapsedMs =
|
|
23379
|
+
const elapsedMs = eventElapsedMs2(event);
|
|
22650
23380
|
entry.eventCount += 1;
|
|
22651
23381
|
if (elapsedMs !== undefined) {
|
|
22652
23382
|
entry.elapsed.push(elapsedMs);
|
|
@@ -22692,7 +23422,7 @@ var summarizeVoiceTraceTimeline = (events, options = {}) => {
|
|
|
22692
23422
|
evaluation,
|
|
22693
23423
|
events: sorted.map((event) => ({
|
|
22694
23424
|
at: event.at,
|
|
22695
|
-
elapsedMs:
|
|
23425
|
+
elapsedMs: eventElapsedMs2(event),
|
|
22696
23426
|
id: event.id,
|
|
22697
23427
|
label: timelineLabel(event),
|
|
22698
23428
|
offsetMs: Math.max(0, event.at - startedAt),
|
|
@@ -22807,26 +23537,32 @@ var createVoiceTraceTimelineRoutes = (options) => {
|
|
|
22807
23537
|
};
|
|
22808
23538
|
|
|
22809
23539
|
// src/operationsRecord.ts
|
|
22810
|
-
var
|
|
22811
|
-
var
|
|
23540
|
+
var getString15 = (value) => typeof value === "string" ? value : undefined;
|
|
23541
|
+
var getNumber10 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
22812
23542
|
var countOutcome = (events, outcome) => events.filter((event) => event.outcome === outcome).length;
|
|
23543
|
+
var matchesSessionScopedId = (id, sessionId) => id === sessionId || id.startsWith(`${sessionId}:`);
|
|
23544
|
+
var hasPayloadValue = (payload, key, values) => {
|
|
23545
|
+
const value = payload[key];
|
|
23546
|
+
return typeof value === "string" && values.has(value);
|
|
23547
|
+
};
|
|
23548
|
+
var countIntegrationDeliveryStatus = (events, status) => events.filter((event) => event.deliveryStatus === status).length;
|
|
22813
23549
|
var toHandoff = (event) => ({
|
|
22814
23550
|
at: event.at,
|
|
22815
|
-
fromAgentId:
|
|
23551
|
+
fromAgentId: getString15(event.payload.fromAgentId),
|
|
22816
23552
|
metadata: event.payload.metadata && typeof event.payload.metadata === "object" && !Array.isArray(event.payload.metadata) ? event.payload.metadata : undefined,
|
|
22817
|
-
reason:
|
|
22818
|
-
status:
|
|
22819
|
-
summary:
|
|
22820
|
-
targetAgentId:
|
|
23553
|
+
reason: getString15(event.payload.reason),
|
|
23554
|
+
status: getString15(event.payload.status),
|
|
23555
|
+
summary: getString15(event.payload.summary),
|
|
23556
|
+
targetAgentId: getString15(event.payload.targetAgentId),
|
|
22821
23557
|
turnId: event.turnId
|
|
22822
23558
|
});
|
|
22823
23559
|
var toTool = (event) => ({
|
|
22824
23560
|
at: event.at,
|
|
22825
|
-
elapsedMs:
|
|
22826
|
-
error:
|
|
22827
|
-
status:
|
|
22828
|
-
toolCallId:
|
|
22829
|
-
toolName:
|
|
23561
|
+
elapsedMs: getNumber10(event.payload.elapsedMs),
|
|
23562
|
+
error: getString15(event.payload.error),
|
|
23563
|
+
status: getString15(event.payload.status),
|
|
23564
|
+
toolCallId: getString15(event.payload.toolCallId),
|
|
23565
|
+
toolName: getString15(event.payload.toolName),
|
|
22830
23566
|
turnId: event.turnId
|
|
22831
23567
|
});
|
|
22832
23568
|
var resolveOutcome4 = (events) => {
|
|
@@ -22858,6 +23594,12 @@ var buildVoiceOperationsRecord = async (options) => {
|
|
|
22858
23594
|
});
|
|
22859
23595
|
const rawAuditEvents = options.audit ? filterVoiceAuditEvents(await options.audit.list({ sessionId: options.sessionId })) : undefined;
|
|
22860
23596
|
const auditEvents = options.redact && rawAuditEvents ? redactVoiceAuditEvents(rawAuditEvents, options.redact) : rawAuditEvents;
|
|
23597
|
+
const reviews = options.reviews ? (await options.reviews.list()).filter((review) => matchesSessionScopedId(review.id, options.sessionId)) : undefined;
|
|
23598
|
+
const reviewIds = new Set(reviews?.map((review) => review.id) ?? []);
|
|
23599
|
+
const tasks = options.tasks ? (await options.tasks.list()).filter((task) => matchesSessionScopedId(task.id, options.sessionId) || typeof task.reviewId === "string" && reviewIds.has(task.reviewId)) : undefined;
|
|
23600
|
+
const taskIds = new Set(tasks?.map((task) => task.id) ?? []);
|
|
23601
|
+
const integrationEvents = options.integrationEvents ? (await options.integrationEvents.list()).filter((event) => hasPayloadValue(event.payload, "sessionId", new Set([options.sessionId])) || hasPayloadValue(event.payload, "reviewId", reviewIds) || hasPayloadValue(event.payload, "taskId", taskIds)) : undefined;
|
|
23602
|
+
const sinkDeliveries = integrationEvents?.reduce((total, event) => total + Object.keys(event.sinkDeliveries ?? {}).length, 0) ?? 0;
|
|
22861
23603
|
return {
|
|
22862
23604
|
audit: auditEvents ? {
|
|
22863
23605
|
error: countOutcome(auditEvents, "error"),
|
|
@@ -22868,12 +23610,34 @@ var buildVoiceOperationsRecord = async (options) => {
|
|
|
22868
23610
|
} : undefined,
|
|
22869
23611
|
checkedAt: Date.now(),
|
|
22870
23612
|
handoffs: traceEvents.filter((event) => event.type === "agent.handoff").map(toHandoff),
|
|
23613
|
+
integrationEvents: integrationEvents ? {
|
|
23614
|
+
delivered: countIntegrationDeliveryStatus(integrationEvents, "delivered"),
|
|
23615
|
+
events: integrationEvents,
|
|
23616
|
+
failed: countIntegrationDeliveryStatus(integrationEvents, "failed"),
|
|
23617
|
+
pending: countIntegrationDeliveryStatus(integrationEvents, "pending"),
|
|
23618
|
+
sinkDeliveries,
|
|
23619
|
+
skipped: countIntegrationDeliveryStatus(integrationEvents, "skipped"),
|
|
23620
|
+
total: integrationEvents.length
|
|
23621
|
+
} : undefined,
|
|
22871
23622
|
outcome: resolveOutcome4(traceEvents),
|
|
22872
23623
|
providers: timelineSession?.providers ?? [],
|
|
22873
23624
|
replay,
|
|
23625
|
+
reviews: reviews ? {
|
|
23626
|
+
failed: reviews.filter((review) => !review.summary.pass).length,
|
|
23627
|
+
reviews,
|
|
23628
|
+
total: reviews.length
|
|
23629
|
+
} : undefined,
|
|
22874
23630
|
sessionId: options.sessionId,
|
|
22875
23631
|
status: timelineSession?.status ?? "healthy",
|
|
22876
23632
|
summary: timelineSession?.summary ?? replay.summary,
|
|
23633
|
+
tasks: tasks ? {
|
|
23634
|
+
done: tasks.filter((task) => task.status === "done").length,
|
|
23635
|
+
inProgress: tasks.filter((task) => task.status === "in-progress").length,
|
|
23636
|
+
open: tasks.filter((task) => task.status === "open").length,
|
|
23637
|
+
overdue: tasks.filter((task) => typeof task.dueAt === "number" && task.status !== "done" && task.dueAt <= Date.now()).length,
|
|
23638
|
+
tasks,
|
|
23639
|
+
total: tasks.length
|
|
23640
|
+
} : undefined,
|
|
22877
23641
|
timeline: timelineSession?.events ?? [],
|
|
22878
23642
|
tools: traceEvents.filter((event) => event.type === "agent.tool").map(toTool),
|
|
22879
23643
|
traceEvents
|
|
@@ -22885,18 +23649,24 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
|
22885
23649
|
const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml39(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs4(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
|
|
22886
23650
|
const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${escapeHtml39(handoff.fromAgentId ?? "unknown")}</strong> to <strong>${escapeHtml39(handoff.targetAgentId ?? "unknown")}</strong> <span>${escapeHtml39(handoff.status ?? "")}</span><p>${escapeHtml39(handoff.summary ?? handoff.reason ?? "")}</p></li>`).join("") : "<li>No agent handoffs recorded.</li>";
|
|
22887
23651
|
const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${escapeHtml39(tool.toolName ?? "tool")}</strong> <span>${escapeHtml39(tool.status ?? "")}</span> ${formatMs4(tool.elapsedMs)} ${tool.error ? `<p>${escapeHtml39(tool.error)}</p>` : ""}</li>`).join("") : "<li>No tool calls recorded.</li>";
|
|
23652
|
+
const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml39(review.title)}</strong> <span>${escapeHtml39(review.summary.outcome ?? "")}</span><p>${escapeHtml39(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
|
|
23653
|
+
const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${escapeHtml39(task.title)}</strong> <span>${escapeHtml39(task.status)}</span><p>${escapeHtml39(task.recommendedAction)}</p></li>`).join("") : "<li>No ops tasks recorded.</li>";
|
|
23654
|
+
const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${escapeHtml39(event.type)}</strong> <span>${escapeHtml39(event.deliveryStatus ?? "local")}</span><p>${escapeHtml39(event.deliveryError ?? event.deliveredTo ?? "")}</p></li>`).join("") : "<li>No integration events recorded.</li>";
|
|
22888
23655
|
const snippet = escapeHtml39(`app.use(
|
|
22889
23656
|
createVoiceOperationsRecordRoutes({
|
|
22890
23657
|
audit: auditStore,
|
|
23658
|
+
integrationEvents: opsEvents,
|
|
22891
23659
|
htmlPath: '/voice-ops/:sessionId',
|
|
22892
23660
|
path: '/api/voice-ops/:sessionId',
|
|
22893
23661
|
redact: {
|
|
22894
23662
|
keys: ['authorization', 'apiKey', 'token']
|
|
22895
23663
|
},
|
|
22896
|
-
|
|
23664
|
+
reviews: callReviews,
|
|
23665
|
+
store: traceStore,
|
|
23666
|
+
tasks: opsTasks
|
|
22897
23667
|
})
|
|
22898
23668
|
);`);
|
|
22899
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml39(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted{color:#a9b4bd}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}</style></head><body><main><p class="eyebrow">Portable production proof</p><h1>${escapeHtml39(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml39(record.status)}">${escapeHtml39(record.status)}</p><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs4(record.summary.callDurationMs)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, audit, latency, and
|
|
23669
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml39(options.title ?? "Voice Operations Record")}</title><style>body{background:#101417;color:#f9f4e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.eyebrow{color:#fbbf24;font-size:.8rem;font-weight:900;letter-spacing:.14em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.8rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #475569;border-radius:999px;display:inline-flex;padding:8px 12px}.healthy{color:#86efac}.warning{color:#fbbf24}.failed,.error{color:#fca5a5}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));margin:20px 0}.card,.primitive{background:#182025;border:1px solid #2d3a43;border-radius:20px;padding:16px}.card span,.muted{color:#a9b4bd}.card strong{display:block;font-size:2rem}section{margin-top:28px}article{display:grid;gap:8px}ul{display:grid;gap:10px;list-style:none;padding:0}li{background:#182025;border:1px solid #2d3a43;border-radius:16px;padding:14px}pre{background:#080d10;border:1px solid #2d3a43;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}</style></head><body><main><p class="eyebrow">Portable production proof</p><h1>${escapeHtml39(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml39(record.status)}">${escapeHtml39(record.status)}</p><section class="grid"><div class="card"><span>Events</span><strong>${String(record.summary.eventCount)}</strong></div><div class="card"><span>Turns</span><strong>${String(record.summary.turnCount)}</strong></div><div class="card"><span>Errors</span><strong>${String(record.summary.errorCount)}</strong></div><div class="card"><span>Duration</span><strong>${formatMs4(record.summary.callDurationMs)}</strong></div><div class="card"><span>Audit</span><strong>${String(record.audit?.total ?? 0)}</strong></div><div class="card"><span>Reviews</span><strong>${String(record.reviews?.total ?? 0)}</strong></div><div class="card"><span>Tasks</span><strong>${String(record.tasks?.total ?? 0)}</strong></div><div class="card"><span>Integrations</span><strong>${String(record.integrationEvents?.total ?? 0)}</strong></div></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceOperationsRecordRoutes(...)</code> gives every call one debuggable object</h2><p class="muted">Use this as the support/debug payload across traces, provider routing, tools, handoffs, audit, latency, replay, reviews, tasks, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Providers</h2><div class="grid">${providers}</div></section><section><h2>Handoffs</h2><ul>${handoffs}</ul></section><section><h2>Tools</h2><ul>${tools}</ul></section><section><h2>Reviews</h2><ul>${reviews}</ul></section><section><h2>Tasks</h2><ul>${tasks}</ul></section><section><h2>Integration Events</h2><ul>${integrationEvents}</ul></section></main></body></html>`;
|
|
22900
23670
|
};
|
|
22901
23671
|
var createVoiceOperationsRecordRoutes = (options) => {
|
|
22902
23672
|
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
@@ -22908,9 +23678,12 @@ var createVoiceOperationsRecordRoutes = (options) => {
|
|
|
22908
23678
|
audit: options.audit,
|
|
22909
23679
|
evaluation: options.evaluation,
|
|
22910
23680
|
events: options.events,
|
|
23681
|
+
integrationEvents: options.integrationEvents,
|
|
22911
23682
|
redact: options.redact,
|
|
23683
|
+
reviews: options.reviews,
|
|
22912
23684
|
sessionId,
|
|
22913
|
-
store: options.store
|
|
23685
|
+
store: options.store,
|
|
23686
|
+
tasks: options.tasks
|
|
22914
23687
|
});
|
|
22915
23688
|
const getSessionId = (params) => params.sessionId ?? "";
|
|
22916
23689
|
routes.get(path, async ({ params }) => Response.json(await buildRecord(getSessionId(params))));
|
|
@@ -23778,8 +24551,8 @@ var createVoiceTTSProviderRouter = (options) => {
|
|
|
23778
24551
|
// src/traceDeliveryRoutes.ts
|
|
23779
24552
|
import { Elysia as Elysia41 } from "elysia";
|
|
23780
24553
|
var escapeHtml41 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
23781
|
-
var
|
|
23782
|
-
var
|
|
24554
|
+
var getString16 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
24555
|
+
var getNumber11 = (value) => {
|
|
23783
24556
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
23784
24557
|
return value;
|
|
23785
24558
|
}
|
|
@@ -23790,13 +24563,13 @@ var getNumber10 = (value) => {
|
|
|
23790
24563
|
return;
|
|
23791
24564
|
};
|
|
23792
24565
|
var parseStatus2 = (value) => {
|
|
23793
|
-
const text =
|
|
24566
|
+
const text = getString16(value);
|
|
23794
24567
|
return text === "pending" || text === "delivered" || text === "failed" || text === "skipped" || text === "all" ? text : undefined;
|
|
23795
24568
|
};
|
|
23796
24569
|
var resolveVoiceTraceDeliveryFilter = (query = {}, base = {}) => ({
|
|
23797
24570
|
...base,
|
|
23798
|
-
limit:
|
|
23799
|
-
q:
|
|
24571
|
+
limit: getNumber11(query.limit) ?? base.limit,
|
|
24572
|
+
q: getString16(query.q) ?? base.q,
|
|
23800
24573
|
status: parseStatus2(query.status) ?? base.status
|
|
23801
24574
|
});
|
|
23802
24575
|
var deliverySearchText2 = (delivery) => [
|
|
@@ -25535,6 +26308,7 @@ export {
|
|
|
25535
26308
|
runVoiceProviderRoutingContract,
|
|
25536
26309
|
runVoicePhoneAgentProductionSmokeContract,
|
|
25537
26310
|
runVoiceOutcomeContractSuite,
|
|
26311
|
+
runVoiceCampaignReadinessProof,
|
|
25538
26312
|
runVoiceCampaignProof,
|
|
25539
26313
|
runVoiceCampaignDialerProof,
|
|
25540
26314
|
runVoiceAgentSquadContract,
|
|
@@ -25584,6 +26358,7 @@ export {
|
|
|
25584
26358
|
renderVoiceOpsActionHistoryHTML,
|
|
25585
26359
|
renderVoiceOperationsRecordHTML,
|
|
25586
26360
|
renderVoiceLiveLatencyHTML,
|
|
26361
|
+
renderVoiceLatencySLOMarkdown,
|
|
25587
26362
|
renderVoiceHandoffHealthHTML,
|
|
25588
26363
|
renderVoiceEvalHTML,
|
|
25589
26364
|
renderVoiceEvalBaselineHTML,
|
|
@@ -25624,6 +26399,7 @@ export {
|
|
|
25624
26399
|
listVoiceRoutingEvents,
|
|
25625
26400
|
listVoiceOpsTasks,
|
|
25626
26401
|
isVoiceOpsTaskOverdue,
|
|
26402
|
+
importVoiceCampaignRecipients,
|
|
25627
26403
|
heartbeatVoiceOpsTask,
|
|
25628
26404
|
hasVoiceOpsTaskSLABreach,
|
|
25629
26405
|
getVoiceLiveOpsControlStatus,
|
|
@@ -25907,6 +26683,7 @@ export {
|
|
|
25907
26683
|
buildVoiceOpsActionHistoryReport,
|
|
25908
26684
|
buildVoiceOperationsRecord,
|
|
25909
26685
|
buildVoiceLiveOpsControlState,
|
|
26686
|
+
buildVoiceLatencySLOGate,
|
|
25910
26687
|
buildVoiceIncidentBundle,
|
|
25911
26688
|
buildVoiceDiagnosticsMarkdown,
|
|
25912
26689
|
buildVoiceDemoReadyReport,
|
|
@@ -25919,6 +26696,7 @@ export {
|
|
|
25919
26696
|
buildVoiceAuditDeliveryReport,
|
|
25920
26697
|
assignVoiceOpsTask,
|
|
25921
26698
|
assertVoiceProviderRoutingContract,
|
|
26699
|
+
assertVoiceLatencySLOGate,
|
|
25922
26700
|
assertVoiceAgentSquadContract,
|
|
25923
26701
|
applyVoiceTelephonyOutcome,
|
|
25924
26702
|
applyVoiceOpsTaskPolicy,
|