@absolutejs/voice 0.0.22-beta.329 → 0.0.22-beta.330
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +366 -0
- package/dist/testing/index.js +2551 -39
- package/dist/testing/telephony.d.ts +25 -0
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -8990,12 +8990,2425 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
|
8990
8990
|
reports
|
|
8991
8991
|
});
|
|
8992
8992
|
};
|
|
8993
|
+
// src/operationsRecord.ts
|
|
8994
|
+
import { Elysia as Elysia4 } from "elysia";
|
|
8995
|
+
|
|
8996
|
+
// src/audit.ts
|
|
8997
|
+
var includes = (filter, value) => {
|
|
8998
|
+
if (!filter) {
|
|
8999
|
+
return true;
|
|
9000
|
+
}
|
|
9001
|
+
if (!value) {
|
|
9002
|
+
return false;
|
|
9003
|
+
}
|
|
9004
|
+
return Array.isArray(filter) ? filter.includes(value) : filter === value;
|
|
9005
|
+
};
|
|
9006
|
+
var createVoiceAuditEvent = (event) => ({
|
|
9007
|
+
...event,
|
|
9008
|
+
at: event.at ?? Date.now(),
|
|
9009
|
+
id: event.id ?? crypto.randomUUID()
|
|
9010
|
+
});
|
|
9011
|
+
var filterVoiceAuditEvents = (events, filter = {}) => {
|
|
9012
|
+
const sorted = events.filter((event) => {
|
|
9013
|
+
if (!includes(filter.type, event.type)) {
|
|
9014
|
+
return false;
|
|
9015
|
+
}
|
|
9016
|
+
if (!includes(filter.outcome, event.outcome)) {
|
|
9017
|
+
return false;
|
|
9018
|
+
}
|
|
9019
|
+
if (filter.actorId && event.actor?.id !== filter.actorId) {
|
|
9020
|
+
return false;
|
|
9021
|
+
}
|
|
9022
|
+
if (filter.resourceId && event.resource?.id !== filter.resourceId) {
|
|
9023
|
+
return false;
|
|
9024
|
+
}
|
|
9025
|
+
if (filter.resourceType && event.resource?.type !== filter.resourceType) {
|
|
9026
|
+
return false;
|
|
9027
|
+
}
|
|
9028
|
+
if (filter.sessionId && event.sessionId !== filter.sessionId) {
|
|
9029
|
+
return false;
|
|
9030
|
+
}
|
|
9031
|
+
if (filter.traceId && event.traceId !== filter.traceId) {
|
|
9032
|
+
return false;
|
|
9033
|
+
}
|
|
9034
|
+
if (typeof filter.after === "number" && event.at <= filter.after) {
|
|
9035
|
+
return false;
|
|
9036
|
+
}
|
|
9037
|
+
if (typeof filter.afterOrAt === "number" && event.at < filter.afterOrAt) {
|
|
9038
|
+
return false;
|
|
9039
|
+
}
|
|
9040
|
+
if (typeof filter.before === "number" && event.at >= filter.before) {
|
|
9041
|
+
return false;
|
|
9042
|
+
}
|
|
9043
|
+
if (typeof filter.beforeOrAt === "number" && event.at > filter.beforeOrAt) {
|
|
9044
|
+
return false;
|
|
9045
|
+
}
|
|
9046
|
+
return true;
|
|
9047
|
+
}).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
9048
|
+
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
9049
|
+
};
|
|
9050
|
+
var createVoiceMemoryAuditEventStore = () => {
|
|
9051
|
+
const events = new Map;
|
|
9052
|
+
return {
|
|
9053
|
+
append: (event) => {
|
|
9054
|
+
const stored = createVoiceAuditEvent(event);
|
|
9055
|
+
events.set(stored.id, stored);
|
|
9056
|
+
return stored;
|
|
9057
|
+
},
|
|
9058
|
+
get: (id) => events.get(id),
|
|
9059
|
+
list: (filter) => filterVoiceAuditEvents([...events.values()], filter)
|
|
9060
|
+
};
|
|
9061
|
+
};
|
|
9062
|
+
var recordVoiceAuditEvent = (store, event) => store.append(createVoiceAuditEvent(event));
|
|
9063
|
+
var recordVoiceProviderAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9064
|
+
action: `${input.kind}.provider.call`,
|
|
9065
|
+
actor: input.actor,
|
|
9066
|
+
metadata: input.metadata,
|
|
9067
|
+
outcome: input.outcome,
|
|
9068
|
+
payload: {
|
|
9069
|
+
cost: input.cost,
|
|
9070
|
+
elapsedMs: input.elapsedMs,
|
|
9071
|
+
error: input.error,
|
|
9072
|
+
kind: input.kind,
|
|
9073
|
+
model: input.model,
|
|
9074
|
+
provider: input.provider
|
|
9075
|
+
},
|
|
9076
|
+
resource: {
|
|
9077
|
+
id: input.provider,
|
|
9078
|
+
type: "provider"
|
|
9079
|
+
},
|
|
9080
|
+
sessionId: input.sessionId,
|
|
9081
|
+
traceId: input.traceId,
|
|
9082
|
+
type: "provider.call"
|
|
9083
|
+
});
|
|
9084
|
+
var recordVoiceToolAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9085
|
+
action: "tool.call",
|
|
9086
|
+
actor: input.actor,
|
|
9087
|
+
metadata: input.metadata,
|
|
9088
|
+
outcome: input.outcome,
|
|
9089
|
+
payload: {
|
|
9090
|
+
elapsedMs: input.elapsedMs,
|
|
9091
|
+
error: input.error,
|
|
9092
|
+
toolCallId: input.toolCallId,
|
|
9093
|
+
toolName: input.toolName
|
|
9094
|
+
},
|
|
9095
|
+
resource: {
|
|
9096
|
+
id: input.toolName,
|
|
9097
|
+
type: "tool"
|
|
9098
|
+
},
|
|
9099
|
+
sessionId: input.sessionId,
|
|
9100
|
+
traceId: input.traceId,
|
|
9101
|
+
type: "tool.call"
|
|
9102
|
+
});
|
|
9103
|
+
var recordVoiceHandoffAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9104
|
+
action: "handoff",
|
|
9105
|
+
actor: input.actor,
|
|
9106
|
+
metadata: input.metadata,
|
|
9107
|
+
outcome: input.outcome,
|
|
9108
|
+
payload: {
|
|
9109
|
+
fromAgentId: input.fromAgentId,
|
|
9110
|
+
reason: input.reason,
|
|
9111
|
+
target: input.target,
|
|
9112
|
+
toAgentId: input.toAgentId
|
|
9113
|
+
},
|
|
9114
|
+
resource: {
|
|
9115
|
+
id: input.toAgentId ?? input.target,
|
|
9116
|
+
type: "handoff"
|
|
9117
|
+
},
|
|
9118
|
+
sessionId: input.sessionId,
|
|
9119
|
+
traceId: input.traceId,
|
|
9120
|
+
type: "handoff"
|
|
9121
|
+
});
|
|
9122
|
+
var recordVoiceRetentionAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9123
|
+
action: input.dryRun ? "retention.plan" : "retention.apply",
|
|
9124
|
+
actor: input.actor ?? {
|
|
9125
|
+
id: "voice-retention",
|
|
9126
|
+
kind: "system"
|
|
9127
|
+
},
|
|
9128
|
+
metadata: input.metadata,
|
|
9129
|
+
outcome: "success",
|
|
9130
|
+
payload: {
|
|
9131
|
+
deletedCount: input.report.deletedCount,
|
|
9132
|
+
dryRun: input.dryRun,
|
|
9133
|
+
scopes: input.report.scopes
|
|
9134
|
+
},
|
|
9135
|
+
resource: {
|
|
9136
|
+
type: "retention-policy"
|
|
9137
|
+
},
|
|
9138
|
+
type: "retention.policy"
|
|
9139
|
+
});
|
|
9140
|
+
var recordVoiceOperatorAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
9141
|
+
action: input.action,
|
|
9142
|
+
actor: input.actor,
|
|
9143
|
+
metadata: input.metadata,
|
|
9144
|
+
outcome: input.outcome ?? "success",
|
|
9145
|
+
payload: input.payload,
|
|
9146
|
+
resource: input.resource,
|
|
9147
|
+
sessionId: input.sessionId,
|
|
9148
|
+
traceId: input.traceId,
|
|
9149
|
+
type: "operator.action"
|
|
9150
|
+
});
|
|
9151
|
+
var createVoiceAuditLogger = (store) => ({
|
|
9152
|
+
handoff: (input) => recordVoiceHandoffAuditEvent({ ...input, store }),
|
|
9153
|
+
operatorAction: (input) => recordVoiceOperatorAuditEvent({ ...input, store }),
|
|
9154
|
+
providerCall: (input) => recordVoiceProviderAuditEvent({ ...input, store }),
|
|
9155
|
+
record: (event) => recordVoiceAuditEvent(store, event),
|
|
9156
|
+
retention: (input) => recordVoiceRetentionAuditEvent({ ...input, store }),
|
|
9157
|
+
toolCall: (input) => recordVoiceToolAuditEvent({ ...input, store })
|
|
9158
|
+
});
|
|
9159
|
+
|
|
9160
|
+
// src/trace.ts
|
|
9161
|
+
var createVoiceTraceEventId = (event) => [
|
|
9162
|
+
event.sessionId,
|
|
9163
|
+
event.turnId ?? "session",
|
|
9164
|
+
event.type,
|
|
9165
|
+
String(event.at ?? Date.now()),
|
|
9166
|
+
crypto.randomUUID()
|
|
9167
|
+
].map(encodeURIComponent).join(":");
|
|
9168
|
+
var createVoiceTraceEvent = (event) => ({
|
|
9169
|
+
...event,
|
|
9170
|
+
at: event.at,
|
|
9171
|
+
id: event.id ?? createVoiceTraceEventId({
|
|
9172
|
+
at: event.at,
|
|
9173
|
+
sessionId: event.sessionId,
|
|
9174
|
+
turnId: event.turnId,
|
|
9175
|
+
type: event.type
|
|
9176
|
+
})
|
|
9177
|
+
});
|
|
9178
|
+
var createVoiceTraceSinkDeliveryId = (events) => {
|
|
9179
|
+
const firstEvent = events[0];
|
|
9180
|
+
return [
|
|
9181
|
+
firstEvent?.sessionId ?? "trace",
|
|
9182
|
+
firstEvent?.traceId ?? "sink",
|
|
9183
|
+
String(firstEvent?.at ?? Date.now()),
|
|
9184
|
+
crypto.randomUUID()
|
|
9185
|
+
].map(encodeURIComponent).join(":");
|
|
9186
|
+
};
|
|
9187
|
+
var createVoiceTraceSinkDeliveryRecord = (input) => {
|
|
9188
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
9189
|
+
return {
|
|
9190
|
+
createdAt,
|
|
9191
|
+
deliveredAt: input.deliveredAt,
|
|
9192
|
+
deliveryAttempts: input.deliveryAttempts,
|
|
9193
|
+
deliveryError: input.deliveryError,
|
|
9194
|
+
deliveryStatus: input.deliveryStatus ?? "pending",
|
|
9195
|
+
events: input.events,
|
|
9196
|
+
id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
|
|
9197
|
+
sinkDeliveries: input.sinkDeliveries,
|
|
9198
|
+
updatedAt: input.updatedAt ?? createdAt
|
|
9199
|
+
};
|
|
9200
|
+
};
|
|
9201
|
+
var matchesTraceFilter = (event, filter) => {
|
|
9202
|
+
if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
|
|
9203
|
+
return false;
|
|
9204
|
+
}
|
|
9205
|
+
if (filter.turnId !== undefined && event.turnId !== filter.turnId) {
|
|
9206
|
+
return false;
|
|
9207
|
+
}
|
|
9208
|
+
if (filter.scenarioId !== undefined && event.scenarioId !== filter.scenarioId) {
|
|
9209
|
+
return false;
|
|
9210
|
+
}
|
|
9211
|
+
if (filter.traceId !== undefined && event.traceId !== filter.traceId) {
|
|
9212
|
+
return false;
|
|
9213
|
+
}
|
|
9214
|
+
if (filter.type !== undefined) {
|
|
9215
|
+
const types = Array.isArray(filter.type) ? filter.type : [filter.type];
|
|
9216
|
+
if (!types.includes(event.type)) {
|
|
9217
|
+
return false;
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
9220
|
+
return true;
|
|
9221
|
+
};
|
|
9222
|
+
var filterVoiceTraceEvents = (events, filter = {}) => {
|
|
9223
|
+
const sorted = events.filter((event) => matchesTraceFilter(event, filter)).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
9224
|
+
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
9225
|
+
};
|
|
9226
|
+
var isPruneTimeMatch = (event, options) => {
|
|
9227
|
+
if (typeof options.before === "number" && event.at >= options.before) {
|
|
9228
|
+
return false;
|
|
9229
|
+
}
|
|
9230
|
+
if (typeof options.beforeOrAt === "number" && event.at > options.beforeOrAt) {
|
|
9231
|
+
return false;
|
|
9232
|
+
}
|
|
9233
|
+
return true;
|
|
9234
|
+
};
|
|
9235
|
+
var selectVoiceTraceEventsForPrune = (events, options = {}) => {
|
|
9236
|
+
let candidates = filterVoiceTraceEvents(events, options.filter).filter((event) => isPruneTimeMatch(event, options));
|
|
9237
|
+
if (typeof options.keepNewest === "number" && options.keepNewest >= 0) {
|
|
9238
|
+
const newestIds = new Set([...candidates].sort((left, right) => right.at - left.at || right.id.localeCompare(left.id)).slice(0, options.keepNewest).map((event) => event.id));
|
|
9239
|
+
candidates = candidates.filter((event) => !newestIds.has(event.id));
|
|
9240
|
+
}
|
|
9241
|
+
return typeof options.limit === "number" && options.limit >= 0 ? candidates.slice(0, options.limit) : candidates;
|
|
9242
|
+
};
|
|
9243
|
+
var pruneVoiceTraceEvents = async (options) => {
|
|
9244
|
+
const events = await options.store.list(options.filter);
|
|
9245
|
+
const deleted = selectVoiceTraceEventsForPrune(events, options);
|
|
9246
|
+
if (!options.dryRun) {
|
|
9247
|
+
await Promise.all(deleted.map((event) => options.store.remove(event.id)));
|
|
9248
|
+
}
|
|
9249
|
+
return {
|
|
9250
|
+
deleted,
|
|
9251
|
+
deletedCount: deleted.length,
|
|
9252
|
+
dryRun: Boolean(options.dryRun),
|
|
9253
|
+
scannedCount: events.length
|
|
9254
|
+
};
|
|
9255
|
+
};
|
|
9256
|
+
var sleep2 = async (delayMs) => {
|
|
9257
|
+
if (delayMs <= 0) {
|
|
9258
|
+
return;
|
|
9259
|
+
}
|
|
9260
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
9261
|
+
};
|
|
9262
|
+
var toHex2 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9263
|
+
var signVoiceTraceSinkBody = async (input) => {
|
|
9264
|
+
const encoder = new TextEncoder;
|
|
9265
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
9266
|
+
hash: "SHA-256",
|
|
9267
|
+
name: "HMAC"
|
|
9268
|
+
}, false, ["sign"]);
|
|
9269
|
+
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
9270
|
+
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
9271
|
+
return `sha256=${toHex2(new Uint8Array(signature))}`;
|
|
9272
|
+
};
|
|
9273
|
+
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
9274
|
+
if (input.response) {
|
|
9275
|
+
const statusText = input.response.statusText?.trim();
|
|
9276
|
+
return `Attempt ${input.attempt} failed with trace sink response ${input.response.status}${statusText ? ` ${statusText}` : ""}.`;
|
|
9277
|
+
}
|
|
9278
|
+
if (input.error instanceof Error) {
|
|
9279
|
+
return `Attempt ${input.attempt} failed: ${input.error.message}`;
|
|
9280
|
+
}
|
|
9281
|
+
return `Attempt ${input.attempt} failed: ${String(input.error)}`;
|
|
9282
|
+
};
|
|
9283
|
+
var normalizeVoiceTraceS3KeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/trace-deliveries";
|
|
9284
|
+
var createVoiceTraceS3ObjectKey = (prefix, events) => {
|
|
9285
|
+
const firstEvent = events[0];
|
|
9286
|
+
const safeSessionId = encodeURIComponent(firstEvent?.sessionId ?? "trace");
|
|
9287
|
+
const safeEventId = encodeURIComponent(firstEvent?.id ?? crypto.randomUUID());
|
|
9288
|
+
return `${prefix}/${safeSessionId}/${Date.now()}-${safeEventId}.json`;
|
|
9289
|
+
};
|
|
9290
|
+
var resolveVoiceS3DeliveredTo = (options, key) => {
|
|
9291
|
+
const bucket = options.bucket;
|
|
9292
|
+
return bucket ? `s3://${bucket}/${key}` : `s3://${key}`;
|
|
9293
|
+
};
|
|
9294
|
+
var aggregateVoiceTraceSinkDeliveryStatus = (deliveries) => {
|
|
9295
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
9296
|
+
if (statuses.length === 0 || statuses.every((status) => status === "skipped")) {
|
|
9297
|
+
return "skipped";
|
|
9298
|
+
}
|
|
9299
|
+
if (statuses.some((status) => status === "failed")) {
|
|
9300
|
+
return "failed";
|
|
9301
|
+
}
|
|
9302
|
+
return "delivered";
|
|
9303
|
+
};
|
|
9304
|
+
var createVoiceTraceHTTPSink = (options) => ({
|
|
9305
|
+
deliver: async ({ events }) => {
|
|
9306
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
9307
|
+
if (typeof fetchImpl !== "function") {
|
|
9308
|
+
return {
|
|
9309
|
+
attempts: 0,
|
|
9310
|
+
deliveredTo: options.url,
|
|
9311
|
+
error: "Trace sink delivery failed: fetch is not available in this runtime.",
|
|
9312
|
+
eventCount: events.length,
|
|
9313
|
+
status: "failed"
|
|
9314
|
+
};
|
|
9315
|
+
}
|
|
9316
|
+
const maxRetries = Math.max(0, options.retries ?? 0);
|
|
9317
|
+
const backoffMs = Math.max(0, options.backoffMs ?? 250);
|
|
9318
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? 1e4);
|
|
9319
|
+
const payload = options.body ? await options.body({ events }) : {
|
|
9320
|
+
eventCount: events.length,
|
|
9321
|
+
events,
|
|
9322
|
+
source: "absolutejs-voice"
|
|
9323
|
+
};
|
|
9324
|
+
const body = JSON.stringify(payload);
|
|
9325
|
+
let lastError = "Trace sink delivery failed.";
|
|
9326
|
+
for (let attempt = 1;attempt <= maxRetries + 1; attempt += 1) {
|
|
9327
|
+
let controller;
|
|
9328
|
+
let timeout;
|
|
9329
|
+
try {
|
|
9330
|
+
const headers = {
|
|
9331
|
+
"content-type": "application/json",
|
|
9332
|
+
...options.headers
|
|
9333
|
+
};
|
|
9334
|
+
if (options.signingSecret) {
|
|
9335
|
+
const timestamp = String(Date.now());
|
|
9336
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
9337
|
+
headers["x-absolutejs-signature"] = await signVoiceTraceSinkBody({
|
|
9338
|
+
body,
|
|
9339
|
+
secret: options.signingSecret,
|
|
9340
|
+
timestamp
|
|
9341
|
+
});
|
|
9342
|
+
}
|
|
9343
|
+
controller = timeoutMs > 0 ? new AbortController : undefined;
|
|
9344
|
+
if (controller && timeoutMs > 0) {
|
|
9345
|
+
timeout = setTimeout(() => controller?.abort(), timeoutMs);
|
|
9346
|
+
}
|
|
9347
|
+
const response = await fetchImpl(options.url, {
|
|
9348
|
+
body,
|
|
9349
|
+
headers,
|
|
9350
|
+
method: options.method ?? "POST",
|
|
9351
|
+
signal: controller?.signal
|
|
9352
|
+
});
|
|
9353
|
+
if (response.ok) {
|
|
9354
|
+
let responseBody;
|
|
9355
|
+
try {
|
|
9356
|
+
responseBody = await response.clone().json();
|
|
9357
|
+
} catch {
|
|
9358
|
+
responseBody = undefined;
|
|
9359
|
+
}
|
|
9360
|
+
return {
|
|
9361
|
+
attempts: attempt,
|
|
9362
|
+
deliveredAt: Date.now(),
|
|
9363
|
+
deliveredTo: options.url,
|
|
9364
|
+
eventCount: events.length,
|
|
9365
|
+
responseBody,
|
|
9366
|
+
status: "delivered"
|
|
9367
|
+
};
|
|
9368
|
+
}
|
|
9369
|
+
lastError = createVoiceTraceSinkDeliveryError({
|
|
9370
|
+
attempt,
|
|
9371
|
+
response
|
|
9372
|
+
});
|
|
9373
|
+
} catch (error) {
|
|
9374
|
+
lastError = createVoiceTraceSinkDeliveryError({
|
|
9375
|
+
attempt,
|
|
9376
|
+
error
|
|
9377
|
+
});
|
|
9378
|
+
} finally {
|
|
9379
|
+
if (timeout) {
|
|
9380
|
+
clearTimeout(timeout);
|
|
9381
|
+
}
|
|
9382
|
+
}
|
|
9383
|
+
if (attempt <= maxRetries) {
|
|
9384
|
+
await sleep2(backoffMs * attempt);
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9387
|
+
return {
|
|
9388
|
+
attempts: maxRetries + 1,
|
|
9389
|
+
deliveredTo: options.url,
|
|
9390
|
+
error: lastError,
|
|
9391
|
+
eventCount: events.length,
|
|
9392
|
+
status: "failed"
|
|
9393
|
+
};
|
|
9394
|
+
},
|
|
9395
|
+
eventTypes: options.eventTypes,
|
|
9396
|
+
id: options.id,
|
|
9397
|
+
kind: options.kind ?? "http"
|
|
9398
|
+
});
|
|
9399
|
+
var createVoiceTraceS3Sink = (options) => {
|
|
9400
|
+
const client = options.client ?? new Bun.S3Client(options);
|
|
9401
|
+
const keyPrefix = normalizeVoiceTraceS3KeyPrefix(options.keyPrefix);
|
|
9402
|
+
return {
|
|
9403
|
+
deliver: async ({ events }) => {
|
|
9404
|
+
const key = createVoiceTraceS3ObjectKey(keyPrefix, events);
|
|
9405
|
+
const payload = options.body ? await options.body({ events, key }) : {
|
|
9406
|
+
eventCount: events.length,
|
|
9407
|
+
events,
|
|
9408
|
+
key,
|
|
9409
|
+
source: "absolutejs-voice"
|
|
9410
|
+
};
|
|
9411
|
+
try {
|
|
9412
|
+
const file = client.file(key, options);
|
|
9413
|
+
await file.write(JSON.stringify(payload), {
|
|
9414
|
+
type: options.contentType ?? "application/json"
|
|
9415
|
+
});
|
|
9416
|
+
return {
|
|
9417
|
+
attempts: 1,
|
|
9418
|
+
deliveredAt: Date.now(),
|
|
9419
|
+
deliveredTo: resolveVoiceS3DeliveredTo(options, key),
|
|
9420
|
+
eventCount: events.length,
|
|
9421
|
+
responseBody: { key },
|
|
9422
|
+
status: "delivered"
|
|
9423
|
+
};
|
|
9424
|
+
} catch (error) {
|
|
9425
|
+
return {
|
|
9426
|
+
attempts: 1,
|
|
9427
|
+
deliveredTo: resolveVoiceS3DeliveredTo(options, key),
|
|
9428
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9429
|
+
eventCount: events.length,
|
|
9430
|
+
status: "failed"
|
|
9431
|
+
};
|
|
9432
|
+
}
|
|
9433
|
+
},
|
|
9434
|
+
eventTypes: options.eventTypes,
|
|
9435
|
+
id: options.id,
|
|
9436
|
+
kind: options.kind ?? "s3"
|
|
9437
|
+
};
|
|
9438
|
+
};
|
|
9439
|
+
var deliverVoiceTraceEventsToSinks = async (input) => {
|
|
9440
|
+
const events = input.redact ? redactVoiceTraceEvents(input.events, input.redact) : input.events;
|
|
9441
|
+
const sinkDeliveries = {};
|
|
9442
|
+
for (const sink of input.sinks) {
|
|
9443
|
+
const sinkEvents = sink.eventTypes?.length ? events.filter((event) => sink.eventTypes?.includes(event.type)) : events;
|
|
9444
|
+
if (sinkEvents.length === 0) {
|
|
9445
|
+
sinkDeliveries[sink.id] = {
|
|
9446
|
+
attempts: 0,
|
|
9447
|
+
eventCount: 0,
|
|
9448
|
+
status: "skipped"
|
|
9449
|
+
};
|
|
9450
|
+
continue;
|
|
9451
|
+
}
|
|
9452
|
+
try {
|
|
9453
|
+
sinkDeliveries[sink.id] = await sink.deliver({
|
|
9454
|
+
events: sinkEvents
|
|
9455
|
+
});
|
|
9456
|
+
} catch (error) {
|
|
9457
|
+
sinkDeliveries[sink.id] = {
|
|
9458
|
+
attempts: 1,
|
|
9459
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9460
|
+
eventCount: sinkEvents.length,
|
|
9461
|
+
status: "failed"
|
|
9462
|
+
};
|
|
9463
|
+
}
|
|
9464
|
+
}
|
|
9465
|
+
return {
|
|
9466
|
+
deliveredAt: Date.now(),
|
|
9467
|
+
eventCount: events.length,
|
|
9468
|
+
sinkDeliveries,
|
|
9469
|
+
status: aggregateVoiceTraceSinkDeliveryStatus(sinkDeliveries)
|
|
9470
|
+
};
|
|
9471
|
+
};
|
|
9472
|
+
var createVoiceTraceSinkStore = (options) => {
|
|
9473
|
+
const deliver = async (event) => {
|
|
9474
|
+
const result = await deliverVoiceTraceEventsToSinks({
|
|
9475
|
+
events: [event],
|
|
9476
|
+
redact: options.redact,
|
|
9477
|
+
sinks: options.sinks
|
|
9478
|
+
});
|
|
9479
|
+
await options.onDelivery?.(result);
|
|
9480
|
+
};
|
|
9481
|
+
return {
|
|
9482
|
+
append: async (event) => {
|
|
9483
|
+
const stored = await options.store.append(event);
|
|
9484
|
+
if (options.deliveryQueue) {
|
|
9485
|
+
const delivery2 = createVoiceTraceSinkDeliveryRecord({
|
|
9486
|
+
events: [stored]
|
|
9487
|
+
});
|
|
9488
|
+
await options.deliveryQueue.set(delivery2.id, delivery2);
|
|
9489
|
+
return stored;
|
|
9490
|
+
}
|
|
9491
|
+
const delivery = deliver(stored);
|
|
9492
|
+
if (options.awaitDelivery) {
|
|
9493
|
+
await delivery;
|
|
9494
|
+
} else {
|
|
9495
|
+
delivery.catch((error) => {
|
|
9496
|
+
options.onError?.(error);
|
|
9497
|
+
});
|
|
9498
|
+
}
|
|
9499
|
+
return stored;
|
|
9500
|
+
},
|
|
9501
|
+
get: (id) => options.store.get(id),
|
|
9502
|
+
list: (filter) => options.store.list(filter),
|
|
9503
|
+
remove: (id) => options.store.remove(id)
|
|
9504
|
+
};
|
|
9505
|
+
};
|
|
9506
|
+
var createVoiceMemoryTraceSinkDeliveryStore = () => {
|
|
9507
|
+
const deliveries = new Map;
|
|
9508
|
+
return {
|
|
9509
|
+
get: async (id) => deliveries.get(id),
|
|
9510
|
+
list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
|
|
9511
|
+
remove: async (id) => {
|
|
9512
|
+
deliveries.delete(id);
|
|
9513
|
+
},
|
|
9514
|
+
set: async (id, delivery) => {
|
|
9515
|
+
deliveries.set(id, delivery);
|
|
9516
|
+
}
|
|
9517
|
+
};
|
|
9518
|
+
};
|
|
9519
|
+
var createVoiceMemoryTraceEventStore = () => {
|
|
9520
|
+
const events = new Map;
|
|
9521
|
+
const append = async (event) => {
|
|
9522
|
+
const stored = createVoiceTraceEvent(event);
|
|
9523
|
+
events.set(stored.id, stored);
|
|
9524
|
+
return stored;
|
|
9525
|
+
};
|
|
9526
|
+
const get = async (id) => events.get(id);
|
|
9527
|
+
const list = async (filter) => filterVoiceTraceEvents([...events.values()], filter);
|
|
9528
|
+
const remove = async (id) => {
|
|
9529
|
+
events.delete(id);
|
|
9530
|
+
};
|
|
9531
|
+
return { append, get, list, remove };
|
|
9532
|
+
};
|
|
9533
|
+
var exportVoiceTrace = async (input) => {
|
|
9534
|
+
const events = await input.store.list(input.filter);
|
|
9535
|
+
return {
|
|
9536
|
+
exportedAt: Date.now(),
|
|
9537
|
+
events: input.redact ? redactVoiceTraceEvents(events, input.redact) : events,
|
|
9538
|
+
filter: input.filter,
|
|
9539
|
+
redacted: Boolean(input.redact)
|
|
9540
|
+
};
|
|
9541
|
+
};
|
|
9542
|
+
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
9543
|
+
var escapeHtml2 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
9544
|
+
var formatTraceValue = (value) => {
|
|
9545
|
+
if (value === undefined || value === null) {
|
|
9546
|
+
return "";
|
|
9547
|
+
}
|
|
9548
|
+
if (typeof value === "string") {
|
|
9549
|
+
return value;
|
|
9550
|
+
}
|
|
9551
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
9552
|
+
return String(value);
|
|
9553
|
+
}
|
|
9554
|
+
try {
|
|
9555
|
+
return JSON.stringify(value);
|
|
9556
|
+
} catch {
|
|
9557
|
+
return String(value);
|
|
9558
|
+
}
|
|
9559
|
+
};
|
|
9560
|
+
var DEFAULT_REDACTION_KEYS = [
|
|
9561
|
+
"apiKey",
|
|
9562
|
+
"authorization",
|
|
9563
|
+
"creditCard",
|
|
9564
|
+
"email",
|
|
9565
|
+
"externalId",
|
|
9566
|
+
"password",
|
|
9567
|
+
"phone",
|
|
9568
|
+
"secret",
|
|
9569
|
+
"ssn",
|
|
9570
|
+
"token"
|
|
9571
|
+
];
|
|
9572
|
+
var DEFAULT_REDACTION_TEXT_KEYS = [
|
|
9573
|
+
"assistantText",
|
|
9574
|
+
"content",
|
|
9575
|
+
"error",
|
|
9576
|
+
"reason",
|
|
9577
|
+
"summary",
|
|
9578
|
+
"text"
|
|
9579
|
+
];
|
|
9580
|
+
var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
9581
|
+
var PHONE_PATTERN = /(?<!\d)(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}(?!\d)/g;
|
|
9582
|
+
var normalizeRedactionKey = (key) => key.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
9583
|
+
var resolveVoiceTraceRedactionOptions = (options = {}) => ({
|
|
9584
|
+
keys: typeof options === "boolean" ? DEFAULT_REDACTION_KEYS : options.keys ?? DEFAULT_REDACTION_KEYS,
|
|
9585
|
+
redactEmails: typeof options === "boolean" ? true : options.redactEmails ?? true,
|
|
9586
|
+
redactPhoneNumbers: typeof options === "boolean" ? true : options.redactPhoneNumbers ?? true,
|
|
9587
|
+
redactText: typeof options === "boolean" ? true : options.redactText ?? true,
|
|
9588
|
+
replacement: typeof options === "boolean" ? "[redacted]" : options.replacement ?? "[redacted]",
|
|
9589
|
+
textKeys: typeof options === "boolean" ? DEFAULT_REDACTION_TEXT_KEYS : options.textKeys ?? DEFAULT_REDACTION_TEXT_KEYS
|
|
9590
|
+
});
|
|
9591
|
+
var resolveReplacement = (input) => typeof input.options.replacement === "function" ? input.options.replacement({
|
|
9592
|
+
key: input.key,
|
|
9593
|
+
path: input.path,
|
|
9594
|
+
value: input.value
|
|
9595
|
+
}) : input.options.replacement;
|
|
9596
|
+
var redactVoiceTraceText = (value, options = {}, input = {}) => {
|
|
9597
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
9598
|
+
let redacted = value;
|
|
9599
|
+
const replacement = resolveReplacement({
|
|
9600
|
+
key: input.key,
|
|
9601
|
+
options: resolved,
|
|
9602
|
+
path: input.path ?? [],
|
|
9603
|
+
value
|
|
9604
|
+
});
|
|
9605
|
+
if (resolved.redactEmails) {
|
|
9606
|
+
redacted = redacted.replace(EMAIL_PATTERN, replacement);
|
|
9607
|
+
}
|
|
9608
|
+
if (resolved.redactPhoneNumbers) {
|
|
9609
|
+
redacted = redacted.replace(PHONE_PATTERN, replacement);
|
|
9610
|
+
}
|
|
9611
|
+
return redacted;
|
|
9612
|
+
};
|
|
9613
|
+
var redactTraceValue = (value, options, path) => {
|
|
9614
|
+
const key = path.at(-1);
|
|
9615
|
+
const normalizedKey = key ? normalizeRedactionKey(key) : undefined;
|
|
9616
|
+
const sensitiveKeys = new Set(options.keys.map(normalizeRedactionKey));
|
|
9617
|
+
const textKeys = new Set(options.textKeys.map(normalizeRedactionKey));
|
|
9618
|
+
if (normalizedKey && sensitiveKeys.has(normalizedKey) && (value === null || ["boolean", "number", "string", "undefined"].includes(typeof value))) {
|
|
9619
|
+
return resolveReplacement({
|
|
9620
|
+
key,
|
|
9621
|
+
options,
|
|
9622
|
+
path,
|
|
9623
|
+
value: String(value ?? "")
|
|
9624
|
+
});
|
|
9625
|
+
}
|
|
9626
|
+
if (typeof value === "string") {
|
|
9627
|
+
const shouldRedactText = options.redactText && (!normalizedKey || textKeys.has(normalizedKey) || path.length === 0);
|
|
9628
|
+
return shouldRedactText ? redactVoiceTraceText(value, options, {
|
|
9629
|
+
key,
|
|
9630
|
+
path
|
|
9631
|
+
}) : value;
|
|
9632
|
+
}
|
|
9633
|
+
if (Array.isArray(value)) {
|
|
9634
|
+
return value.map((item, index) => redactTraceValue(item, options, [...path, String(index)]));
|
|
9635
|
+
}
|
|
9636
|
+
if (typeof value === "object" && value) {
|
|
9637
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
9638
|
+
entryKey,
|
|
9639
|
+
redactTraceValue(entryValue, options, [...path, entryKey])
|
|
9640
|
+
]));
|
|
9641
|
+
}
|
|
9642
|
+
return value;
|
|
9643
|
+
};
|
|
9644
|
+
var redactVoiceTraceEvent = (event, options = {}) => {
|
|
9645
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
9646
|
+
return {
|
|
9647
|
+
...event,
|
|
9648
|
+
metadata: redactTraceValue(event.metadata, resolved, ["metadata"]),
|
|
9649
|
+
payload: redactTraceValue(event.payload, resolved, ["payload"])
|
|
9650
|
+
};
|
|
9651
|
+
};
|
|
9652
|
+
var redactVoiceTraceEvents = (events, options = {}) => events.map((event) => redactVoiceTraceEvent(event, options));
|
|
9653
|
+
var summarizeVoiceTrace = (events) => {
|
|
9654
|
+
const sorted = filterVoiceTraceEvents(events);
|
|
9655
|
+
const firstEvent = sorted[0];
|
|
9656
|
+
const lastEvent = sorted.at(-1);
|
|
9657
|
+
const lifecycleEvents = sorted.filter((event) => event.type === "call.lifecycle");
|
|
9658
|
+
const startEvent = lifecycleEvents.find((event) => event.payload.type === "start");
|
|
9659
|
+
const endEvent = lifecycleEvents.toReversed().find((event) => event.payload.type === "end");
|
|
9660
|
+
const costEvents = sorted.filter((event) => event.type === "turn.cost");
|
|
9661
|
+
const toolEvents = sorted.filter((event) => event.type === "agent.tool");
|
|
9662
|
+
const startedAt = startEvent?.at ?? firstEvent?.at;
|
|
9663
|
+
const endedAt = endEvent?.at ?? lastEvent?.at;
|
|
9664
|
+
const failed = sorted.some((event) => event.type === "session.error") || endEvent?.payload.disposition === "failed";
|
|
9665
|
+
return {
|
|
9666
|
+
assistantReplyCount: sorted.filter((event) => event.type === "turn.assistant").length,
|
|
9667
|
+
callDurationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined,
|
|
9668
|
+
cost: {
|
|
9669
|
+
estimatedRelativeCostUnits: costEvents.reduce((total, event) => total + toNumber(event.payload.estimatedRelativeCostUnits), 0),
|
|
9670
|
+
totalBillableAudioMs: costEvents.reduce((total, event) => total + toNumber(event.payload.totalBillableAudioMs), 0)
|
|
9671
|
+
},
|
|
9672
|
+
endedAt,
|
|
9673
|
+
errorCount: sorted.filter((event) => event.type === "session.error").length,
|
|
9674
|
+
eventCount: sorted.length,
|
|
9675
|
+
failed,
|
|
9676
|
+
handoffCount: sorted.filter((event) => event.type === "agent.handoff").length,
|
|
9677
|
+
modelCallCount: sorted.filter((event) => event.type === "agent.model").length,
|
|
9678
|
+
sessionId: firstEvent?.sessionId,
|
|
9679
|
+
startedAt,
|
|
9680
|
+
toolCallCount: toolEvents.length,
|
|
9681
|
+
toolErrorCount: toolEvents.filter((event) => event.payload.status === "error").length,
|
|
9682
|
+
traceId: firstEvent?.traceId,
|
|
9683
|
+
transcriptCount: sorted.filter((event) => event.type === "turn.transcript").length,
|
|
9684
|
+
turnCount: sorted.filter((event) => event.type === "turn.committed").length
|
|
9685
|
+
};
|
|
9686
|
+
};
|
|
9687
|
+
var evaluateVoiceTrace = (events, options = {}) => {
|
|
9688
|
+
const summary = summarizeVoiceTrace(events);
|
|
9689
|
+
const issues = [];
|
|
9690
|
+
const maxHandoffs = options.maxHandoffs ?? 3;
|
|
9691
|
+
const maxToolErrors = options.maxToolErrors ?? 0;
|
|
9692
|
+
const maxModelCallsPerTurn = options.maxModelCallsPerTurn ?? 6;
|
|
9693
|
+
const turnCountForRatio = Math.max(1, summary.turnCount);
|
|
9694
|
+
if (options.requireCompletedCall !== false && !summary.endedAt) {
|
|
9695
|
+
issues.push({
|
|
9696
|
+
code: "call-not-ended",
|
|
9697
|
+
message: "Trace does not include a call end lifecycle event.",
|
|
9698
|
+
severity: "warning"
|
|
9699
|
+
});
|
|
9700
|
+
}
|
|
9701
|
+
if (summary.failed) {
|
|
9702
|
+
issues.push({
|
|
9703
|
+
code: "session-error",
|
|
9704
|
+
message: "Trace contains a session error or failed call disposition.",
|
|
9705
|
+
severity: "error"
|
|
9706
|
+
});
|
|
9707
|
+
}
|
|
9708
|
+
if (options.requireTranscript !== false && summary.transcriptCount === 0) {
|
|
9709
|
+
issues.push({
|
|
9710
|
+
code: "missing-transcript",
|
|
9711
|
+
message: "Trace does not include any transcript events.",
|
|
9712
|
+
severity: "error"
|
|
9713
|
+
});
|
|
9714
|
+
}
|
|
9715
|
+
if (options.requireTurn !== false && summary.turnCount === 0) {
|
|
9716
|
+
issues.push({
|
|
9717
|
+
code: "missing-turn",
|
|
9718
|
+
message: "Trace does not include any committed turns.",
|
|
9719
|
+
severity: "error"
|
|
9720
|
+
});
|
|
9721
|
+
}
|
|
9722
|
+
if (options.requireAssistantReply !== false && summary.turnCount > 0 && summary.assistantReplyCount === 0) {
|
|
9723
|
+
issues.push({
|
|
9724
|
+
code: "missing-assistant-reply",
|
|
9725
|
+
message: "Trace has committed turns but no assistant replies.",
|
|
9726
|
+
severity: "warning"
|
|
9727
|
+
});
|
|
9728
|
+
}
|
|
9729
|
+
if (summary.toolErrorCount > maxToolErrors) {
|
|
9730
|
+
issues.push({
|
|
9731
|
+
code: "tool-errors",
|
|
9732
|
+
message: `Trace has ${summary.toolErrorCount} tool error(s), above the allowed ${maxToolErrors}.`,
|
|
9733
|
+
severity: "error"
|
|
9734
|
+
});
|
|
9735
|
+
}
|
|
9736
|
+
if (summary.handoffCount > maxHandoffs) {
|
|
9737
|
+
issues.push({
|
|
9738
|
+
code: "too-many-handoffs",
|
|
9739
|
+
message: `Trace has ${summary.handoffCount} handoff(s), above the allowed ${maxHandoffs}.`,
|
|
9740
|
+
severity: "warning"
|
|
9741
|
+
});
|
|
9742
|
+
}
|
|
9743
|
+
if (summary.modelCallCount / turnCountForRatio > maxModelCallsPerTurn) {
|
|
9744
|
+
issues.push({
|
|
9745
|
+
code: "too-many-model-calls",
|
|
9746
|
+
message: `Trace averages more than ${maxModelCallsPerTurn} model calls per committed turn.`,
|
|
9747
|
+
severity: "warning"
|
|
9748
|
+
});
|
|
9749
|
+
}
|
|
9750
|
+
return {
|
|
9751
|
+
issues,
|
|
9752
|
+
pass: !issues.some((issue) => issue.severity === "error"),
|
|
9753
|
+
summary
|
|
9754
|
+
};
|
|
9755
|
+
};
|
|
9756
|
+
var renderTraceEventMarkdown = (event, startedAt) => {
|
|
9757
|
+
const offset = startedAt === undefined ? `${event.at}` : `+${Math.max(0, event.at - startedAt)}ms`;
|
|
9758
|
+
const label = `- ${offset} [${event.type}]`;
|
|
9759
|
+
switch (event.type) {
|
|
9760
|
+
case "turn.transcript":
|
|
9761
|
+
return `${label} ${event.payload.isFinal ? "final" : "partial"} "${formatTraceValue(event.payload.text)}"`;
|
|
9762
|
+
case "turn.committed":
|
|
9763
|
+
return `${label} committed "${formatTraceValue(event.payload.text)}"`;
|
|
9764
|
+
case "turn.assistant":
|
|
9765
|
+
return event.payload.text ? `${label} assistant "${formatTraceValue(event.payload.text)}"` : `${label} ${formatTraceValue(event.payload.status)}`;
|
|
9766
|
+
case "agent.tool":
|
|
9767
|
+
return `${label} ${formatTraceValue(event.payload.toolName)} ${formatTraceValue(event.payload.status)}`;
|
|
9768
|
+
case "agent.context":
|
|
9769
|
+
return `${label} ${formatTraceValue(event.payload.fromAgentId)} -> ${formatTraceValue(event.payload.targetAgentId)} ${formatTraceValue(event.payload.status)}`;
|
|
9770
|
+
case "agent.handoff":
|
|
9771
|
+
return `${label} ${formatTraceValue(event.payload.fromAgentId)} -> ${formatTraceValue(event.payload.targetAgentId)}`;
|
|
9772
|
+
case "session.error":
|
|
9773
|
+
return `${label} ${formatTraceValue(event.payload.error)}`;
|
|
9774
|
+
case "call.lifecycle":
|
|
9775
|
+
return `${label} ${formatTraceValue(event.payload.type)} ${formatTraceValue(event.payload.disposition)}`.trim();
|
|
9776
|
+
default:
|
|
9777
|
+
return `${label} ${formatTraceValue(event.payload)}`;
|
|
9778
|
+
}
|
|
9779
|
+
};
|
|
9780
|
+
var renderVoiceTraceMarkdown = (events, options = {}) => {
|
|
9781
|
+
const sorted = filterVoiceTraceEvents(options.redact ? redactVoiceTraceEvents(events, options.redact) : events);
|
|
9782
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
9783
|
+
const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
|
|
9784
|
+
const lines = [
|
|
9785
|
+
`# ${options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim()}`,
|
|
9786
|
+
"",
|
|
9787
|
+
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
9788
|
+
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
9789
|
+
`Events: ${summary.eventCount}`,
|
|
9790
|
+
`Turns: ${summary.turnCount}`,
|
|
9791
|
+
`Transcripts: ${summary.transcriptCount}`,
|
|
9792
|
+
`Assistant replies: ${summary.assistantReplyCount}`,
|
|
9793
|
+
`Model calls: ${summary.modelCallCount}`,
|
|
9794
|
+
`Tool calls: ${summary.toolCallCount}`,
|
|
9795
|
+
`Handoffs: ${summary.handoffCount}`,
|
|
9796
|
+
`Errors: ${summary.errorCount}`,
|
|
9797
|
+
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
9798
|
+
""
|
|
9799
|
+
];
|
|
9800
|
+
if (evaluation.issues.length > 0) {
|
|
9801
|
+
lines.push("## Issues", "");
|
|
9802
|
+
for (const issue of evaluation.issues) {
|
|
9803
|
+
lines.push(`- [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
9804
|
+
}
|
|
9805
|
+
lines.push("");
|
|
9806
|
+
}
|
|
9807
|
+
lines.push("## Timeline", "");
|
|
9808
|
+
for (const event of sorted) {
|
|
9809
|
+
lines.push(renderTraceEventMarkdown(event, summary.startedAt));
|
|
9810
|
+
}
|
|
9811
|
+
return lines.join(`
|
|
9812
|
+
`);
|
|
9813
|
+
};
|
|
9814
|
+
var renderVoiceTraceHTML = (events, options = {}) => {
|
|
9815
|
+
const markdown = renderVoiceTraceMarkdown(events, options);
|
|
9816
|
+
const renderEvents = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
|
|
9817
|
+
const summary = summarizeVoiceTrace(renderEvents);
|
|
9818
|
+
const evaluation = evaluateVoiceTrace(renderEvents, options.evaluation);
|
|
9819
|
+
const eventRows = filterVoiceTraceEvents(renderEvents).map((event) => {
|
|
9820
|
+
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
9821
|
+
return [
|
|
9822
|
+
"<tr>",
|
|
9823
|
+
`<td>${escapeHtml2(String(offset))}</td>`,
|
|
9824
|
+
`<td>${escapeHtml2(event.type)}</td>`,
|
|
9825
|
+
`<td>${escapeHtml2(event.turnId ?? "")}</td>`,
|
|
9826
|
+
`<td><code>${escapeHtml2(JSON.stringify(event.payload))}</code></td>`,
|
|
9827
|
+
"</tr>"
|
|
9828
|
+
].join("");
|
|
9829
|
+
}).join(`
|
|
9830
|
+
`);
|
|
9831
|
+
return [
|
|
9832
|
+
"<!doctype html>",
|
|
9833
|
+
'<html lang="en">',
|
|
9834
|
+
"<head>",
|
|
9835
|
+
'<meta charset="utf-8" />',
|
|
9836
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
9837
|
+
`<title>${escapeHtml2(options.title ?? "Voice Trace")}</title>`,
|
|
9838
|
+
"<style>",
|
|
9839
|
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
9840
|
+
"main{max-width:1100px;margin:auto}",
|
|
9841
|
+
".summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.75rem;margin:1rem 0}",
|
|
9842
|
+
".card{background:white;border:1px solid #ded9cc;border-radius:12px;padding:1rem}",
|
|
9843
|
+
".pass{color:#126b3a}.fail{color:#9d2222}",
|
|
9844
|
+
"table{border-collapse:collapse;width:100%;background:white;border:1px solid #ded9cc}",
|
|
9845
|
+
"th,td{border-bottom:1px solid #eee8dc;padding:.65rem;text-align:left;vertical-align:top}",
|
|
9846
|
+
"code{white-space:pre-wrap;word-break:break-word}",
|
|
9847
|
+
"pre{background:#181713;color:#f8f7f2;padding:1rem;border-radius:12px;overflow:auto}",
|
|
9848
|
+
"</style>",
|
|
9849
|
+
"</head>",
|
|
9850
|
+
"<body><main>",
|
|
9851
|
+
`<h1>${escapeHtml2(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
9852
|
+
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
9853
|
+
'<section class="summary">',
|
|
9854
|
+
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
9855
|
+
`<div class="card"><strong>Turns</strong><br>${summary.turnCount}</div>`,
|
|
9856
|
+
`<div class="card"><strong>Transcripts</strong><br>${summary.transcriptCount}</div>`,
|
|
9857
|
+
`<div class="card"><strong>Tool errors</strong><br>${summary.toolErrorCount}</div>`,
|
|
9858
|
+
`<div class="card"><strong>Cost units</strong><br>${summary.cost.estimatedRelativeCostUnits}</div>`,
|
|
9859
|
+
"</section>",
|
|
9860
|
+
"<h2>Timeline</h2>",
|
|
9861
|
+
"<table><thead><tr><th>Offset ms</th><th>Type</th><th>Turn</th><th>Payload</th></tr></thead><tbody>",
|
|
9862
|
+
eventRows,
|
|
9863
|
+
"</tbody></table>",
|
|
9864
|
+
"<h2>Markdown Export</h2>",
|
|
9865
|
+
`<pre>${escapeHtml2(markdown)}</pre>`,
|
|
9866
|
+
"</main></body></html>"
|
|
9867
|
+
].join(`
|
|
9868
|
+
`);
|
|
9869
|
+
};
|
|
9870
|
+
var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
9871
|
+
evaluation: evaluateVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events, options.evaluation),
|
|
9872
|
+
html: renderVoiceTraceHTML(events, options),
|
|
9873
|
+
markdown: renderVoiceTraceMarkdown(events, options),
|
|
9874
|
+
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
9875
|
+
});
|
|
9876
|
+
|
|
9877
|
+
// src/auditRoutes.ts
|
|
9878
|
+
import { Elysia } from "elysia";
|
|
9879
|
+
var escapeHtml3 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
9880
|
+
var getString = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
9881
|
+
var getNumber = (value) => {
|
|
9882
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
9883
|
+
return value;
|
|
9884
|
+
}
|
|
9885
|
+
if (typeof value === "string" && value.trim()) {
|
|
9886
|
+
const parsed = Number(value);
|
|
9887
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
9888
|
+
}
|
|
9889
|
+
return;
|
|
9890
|
+
};
|
|
9891
|
+
var parseType = (value) => {
|
|
9892
|
+
const text = getString(value);
|
|
9893
|
+
return text && [
|
|
9894
|
+
"handoff",
|
|
9895
|
+
"operator.action",
|
|
9896
|
+
"provider.call",
|
|
9897
|
+
"retention.policy",
|
|
9898
|
+
"tool.call"
|
|
9899
|
+
].includes(text) ? text : undefined;
|
|
9900
|
+
};
|
|
9901
|
+
var parseOutcome = (value) => {
|
|
9902
|
+
const text = getString(value);
|
|
9903
|
+
return text && ["error", "skipped", "success"].includes(text) ? text : undefined;
|
|
9904
|
+
};
|
|
9905
|
+
var parseBoolean = (value, fallback = false) => {
|
|
9906
|
+
const text = getString(value)?.toLowerCase();
|
|
9907
|
+
if (["1", "true", "yes"].includes(text ?? "")) {
|
|
9908
|
+
return true;
|
|
9909
|
+
}
|
|
9910
|
+
if (["0", "false", "no"].includes(text ?? "")) {
|
|
9911
|
+
return false;
|
|
9912
|
+
}
|
|
9913
|
+
return fallback;
|
|
9914
|
+
};
|
|
9915
|
+
var parseExportFormat = (value) => {
|
|
9916
|
+
const text = getString(value)?.toLowerCase();
|
|
9917
|
+
return text === "markdown" || text === "md" ? "markdown" : text === "html" ? "html" : "json";
|
|
9918
|
+
};
|
|
9919
|
+
var increment = (counts, key) => {
|
|
9920
|
+
if (!key) {
|
|
9921
|
+
return;
|
|
9922
|
+
}
|
|
9923
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
9924
|
+
};
|
|
9925
|
+
var sortedCounts = (counts) => [...counts.entries()].sort(([leftKey, leftCount], [rightKey, rightCount]) => rightCount - leftCount || leftKey.localeCompare(rightKey));
|
|
9926
|
+
var resolveVoiceAuditTrailFilter = (query = {}, base = {}) => ({
|
|
9927
|
+
...base,
|
|
9928
|
+
actorId: getString(query.actorId) ?? base.actorId,
|
|
9929
|
+
after: getNumber(query.after) ?? base.after,
|
|
9930
|
+
afterOrAt: getNumber(query.afterOrAt) ?? base.afterOrAt,
|
|
9931
|
+
before: getNumber(query.before) ?? base.before,
|
|
9932
|
+
beforeOrAt: getNumber(query.beforeOrAt) ?? base.beforeOrAt,
|
|
9933
|
+
limit: getNumber(query.limit) ?? base.limit,
|
|
9934
|
+
outcome: parseOutcome(query.outcome) ?? base.outcome,
|
|
9935
|
+
resourceId: getString(query.resourceId) ?? base.resourceId,
|
|
9936
|
+
resourceType: getString(query.resourceType) ?? base.resourceType,
|
|
9937
|
+
sessionId: getString(query.sessionId) ?? base.sessionId,
|
|
9938
|
+
traceId: getString(query.traceId) ?? base.traceId,
|
|
9939
|
+
type: parseType(query.type) ?? base.type
|
|
9940
|
+
});
|
|
9941
|
+
var summarizeVoiceAuditTrail = (events) => {
|
|
9942
|
+
const byActor = new Map;
|
|
9943
|
+
const byOutcome = new Map;
|
|
9944
|
+
const byResourceType = new Map;
|
|
9945
|
+
const byType = new Map;
|
|
9946
|
+
for (const event of events) {
|
|
9947
|
+
increment(byActor, event.actor?.id);
|
|
9948
|
+
increment(byOutcome, event.outcome);
|
|
9949
|
+
increment(byResourceType, event.resource?.type);
|
|
9950
|
+
increment(byType, event.type);
|
|
9951
|
+
}
|
|
9952
|
+
return {
|
|
9953
|
+
byActor: sortedCounts(byActor),
|
|
9954
|
+
byOutcome: sortedCounts(byOutcome),
|
|
9955
|
+
byResourceType: sortedCounts(byResourceType),
|
|
9956
|
+
byType: sortedCounts(byType),
|
|
9957
|
+
errors: events.filter((event) => event.outcome === "error").length,
|
|
9958
|
+
latestAt: events.at(-1)?.at,
|
|
9959
|
+
total: events.length
|
|
9960
|
+
};
|
|
9961
|
+
};
|
|
9962
|
+
var buildVoiceAuditTrailReport = async (options) => {
|
|
9963
|
+
const filter = {
|
|
9964
|
+
...options.filter,
|
|
9965
|
+
limit: options.filter?.limit ?? options.limit
|
|
9966
|
+
};
|
|
9967
|
+
const events = await options.store.list(filter);
|
|
9968
|
+
return {
|
|
9969
|
+
checkedAt: Date.now(),
|
|
9970
|
+
events,
|
|
9971
|
+
filter,
|
|
9972
|
+
summary: summarizeVoiceAuditTrail(events)
|
|
9973
|
+
};
|
|
9974
|
+
};
|
|
9975
|
+
var renderVoiceAuditTrailHTML = (report, options = {}) => {
|
|
9976
|
+
const title = options.title ?? "AbsoluteJS Voice Audit Trail";
|
|
9977
|
+
const chips = report.summary.byType.map(([type, count]) => `<span>${escapeHtml3(type)} <strong>${count}</strong></span>`).join("");
|
|
9978
|
+
const rows = report.events.map((event) => {
|
|
9979
|
+
const actor = event.actor ? `${event.actor.kind}:${event.actor.id}` : "unknown";
|
|
9980
|
+
const resource = event.resource ? `${event.resource.type}${event.resource.id ? `:${event.resource.id}` : ""}` : "";
|
|
9981
|
+
const payload = event.payload ? JSON.stringify(event.payload, null, 2) : "";
|
|
9982
|
+
return `<article class="event ${escapeHtml3(event.outcome ?? "unknown")}"><div><span>${escapeHtml3(event.type)}</span><h2>${escapeHtml3(event.action)}</h2><p>${escapeHtml3(new Date(event.at).toLocaleString())}</p><p>Actor: ${escapeHtml3(actor)}${resource ? ` \xB7 Resource: ${escapeHtml3(resource)}` : ""}</p>${event.sessionId ? `<p>Session: ${escapeHtml3(event.sessionId)}</p>` : ""}</div><strong>${escapeHtml3(event.outcome ?? "recorded")}</strong>${payload ? `<pre>${escapeHtml3(payload)}</pre>` : ""}</article>`;
|
|
9983
|
+
}).join("");
|
|
9984
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml3(title)}</title><style>body{background:#11140f;color:#f7f1df;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero{background:linear-gradient(135deg,rgba(34,197,94,.18),rgba(245,158,11,.12));border:1px solid #2c3327;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#facc15;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,4.8rem);line-height:.92;margin:.2rem 0 1rem}.chips{display:flex;flex-wrap:wrap;gap:10px}.chips span{border:1px solid #46513b;border-radius:999px;padding:8px 12px}.events{display:grid;gap:14px}.event{background:#181d15;border:1px solid #2c3327;border-radius:22px;display:grid;gap:16px;grid-template-columns:1fr auto;padding:18px}.event.error{border-color:rgba(239,68,68,.75)}.event.skipped{border-color:rgba(245,158,11,.7)}.event.success{border-color:rgba(34,197,94,.55)}.event span{color:#facc15;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.event h2{margin:.2rem 0}.event p{color:#c8ccb8;margin:.2rem 0}.event strong{text-transform:uppercase}pre{background:#0c0f0a;border-radius:14px;grid-column:1/-1;overflow:auto;padding:14px;white-space:pre-wrap}@media(max-width:760px){main{padding:20px}.event{grid-template-columns:1fr}}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted evidence</p><h1>${escapeHtml3(title)}</h1><p>${report.summary.total} event(s), ${report.summary.errors} error(s). Latest ${report.summary.latestAt ? escapeHtml3(new Date(report.summary.latestAt).toLocaleString()) : "never"}.</p><div class="chips">${chips}</div></section><section class="events">${rows || "<p>No audit events match this filter.</p>"}</section></main></body></html>`;
|
|
9985
|
+
};
|
|
9986
|
+
var createVoiceAuditTrailRoutes = (options) => {
|
|
9987
|
+
const path = options.path ?? "/api/voice-audit";
|
|
9988
|
+
const htmlPath = options.htmlPath ?? "/audit";
|
|
9989
|
+
const exportPath = options.exportPath ?? `${path}/export`;
|
|
9990
|
+
const exportHtmlPath = options.exportHtmlPath ?? (htmlPath === false ? false : `${htmlPath}/export`);
|
|
9991
|
+
const routes = new Elysia({
|
|
9992
|
+
name: options.name ?? "absolutejs-voice-audit-trail"
|
|
9993
|
+
});
|
|
9994
|
+
routes.get(path, async ({ query }) => buildVoiceAuditTrailReport({
|
|
9995
|
+
filter: resolveVoiceAuditTrailFilter(query, options.filter),
|
|
9996
|
+
limit: options.limit,
|
|
9997
|
+
store: options.store
|
|
9998
|
+
}));
|
|
9999
|
+
if (htmlPath !== false) {
|
|
10000
|
+
routes.get(htmlPath, async ({ query }) => {
|
|
10001
|
+
const report = await buildVoiceAuditTrailReport({
|
|
10002
|
+
filter: resolveVoiceAuditTrailFilter(query, options.filter),
|
|
10003
|
+
limit: options.limit,
|
|
10004
|
+
store: options.store
|
|
10005
|
+
});
|
|
10006
|
+
const body = await (options.render ?? ((value) => renderVoiceAuditTrailHTML(value, { title: options.title })))(report);
|
|
10007
|
+
return new Response(body, {
|
|
10008
|
+
headers: {
|
|
10009
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10010
|
+
...options.headers
|
|
10011
|
+
}
|
|
10012
|
+
});
|
|
10013
|
+
});
|
|
10014
|
+
}
|
|
10015
|
+
if (exportPath !== false) {
|
|
10016
|
+
routes.get(exportPath, async ({ query }) => {
|
|
10017
|
+
const filter = resolveVoiceAuditTrailFilter(query, options.filter);
|
|
10018
|
+
const redact = parseBoolean(query.redact, true);
|
|
10019
|
+
const exported = await exportVoiceAuditTrail({
|
|
10020
|
+
filter: {
|
|
10021
|
+
...filter,
|
|
10022
|
+
limit: filter.limit ?? options.limit
|
|
10023
|
+
},
|
|
10024
|
+
redact,
|
|
10025
|
+
store: options.store
|
|
10026
|
+
});
|
|
10027
|
+
const format = parseExportFormat(query.format);
|
|
10028
|
+
if (format === "markdown") {
|
|
10029
|
+
return new Response(renderVoiceAuditMarkdown(exported.events, {
|
|
10030
|
+
title: options.title
|
|
10031
|
+
}), {
|
|
10032
|
+
headers: {
|
|
10033
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
10034
|
+
...options.headers
|
|
10035
|
+
}
|
|
10036
|
+
});
|
|
10037
|
+
}
|
|
10038
|
+
if (format === "html") {
|
|
10039
|
+
return new Response(renderVoiceAuditHTML(exported.events, {
|
|
10040
|
+
title: options.title
|
|
10041
|
+
}), {
|
|
10042
|
+
headers: {
|
|
10043
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10044
|
+
...options.headers
|
|
10045
|
+
}
|
|
10046
|
+
});
|
|
10047
|
+
}
|
|
10048
|
+
return exported;
|
|
10049
|
+
});
|
|
10050
|
+
}
|
|
10051
|
+
if (exportHtmlPath !== false) {
|
|
10052
|
+
routes.get(exportHtmlPath, async ({ query }) => {
|
|
10053
|
+
const filter = resolveVoiceAuditTrailFilter(query, options.filter);
|
|
10054
|
+
const exported = await exportVoiceAuditTrail({
|
|
10055
|
+
filter: {
|
|
10056
|
+
...filter,
|
|
10057
|
+
limit: filter.limit ?? options.limit
|
|
10058
|
+
},
|
|
10059
|
+
redact: parseBoolean(query.redact, true),
|
|
10060
|
+
store: options.store
|
|
10061
|
+
});
|
|
10062
|
+
return new Response(renderVoiceAuditHTML(exported.events, {
|
|
10063
|
+
title: options.title
|
|
10064
|
+
}), {
|
|
10065
|
+
headers: {
|
|
10066
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10067
|
+
...options.headers
|
|
10068
|
+
}
|
|
10069
|
+
});
|
|
10070
|
+
});
|
|
10071
|
+
}
|
|
10072
|
+
return routes;
|
|
10073
|
+
};
|
|
10074
|
+
|
|
10075
|
+
// src/auditExport.ts
|
|
10076
|
+
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10077
|
+
var normalizeRedactionKey2 = (key) => key.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
10078
|
+
var resolveReplacement2 = (input) => typeof input.options.replacement === "function" ? input.options.replacement({
|
|
10079
|
+
key: input.key,
|
|
10080
|
+
path: input.path,
|
|
10081
|
+
value: input.value
|
|
10082
|
+
}) : input.options.replacement;
|
|
10083
|
+
var redactAuditValue = (value, config, options, path) => {
|
|
10084
|
+
const key = path.at(-1);
|
|
10085
|
+
const normalizedKey = key ? normalizeRedactionKey2(key) : undefined;
|
|
10086
|
+
const sensitiveKeys = new Set(options.keys.map(normalizeRedactionKey2));
|
|
10087
|
+
const textKeys = new Set(options.textKeys.map(normalizeRedactionKey2));
|
|
10088
|
+
if (normalizedKey && sensitiveKeys.has(normalizedKey) && (value === null || ["boolean", "number", "string", "undefined"].includes(typeof value))) {
|
|
10089
|
+
return resolveReplacement2({
|
|
10090
|
+
key,
|
|
10091
|
+
options,
|
|
10092
|
+
path,
|
|
10093
|
+
value: String(value ?? "")
|
|
10094
|
+
});
|
|
10095
|
+
}
|
|
10096
|
+
if (typeof value === "string") {
|
|
10097
|
+
const shouldRedactText = options.redactText && (!normalizedKey || textKeys.has(normalizedKey) || path.length === 0 || path[0] === "payload" || path[0] === "metadata");
|
|
10098
|
+
return shouldRedactText ? redactVoiceTraceText(value, config, {
|
|
10099
|
+
key,
|
|
10100
|
+
path
|
|
10101
|
+
}) : value;
|
|
10102
|
+
}
|
|
10103
|
+
if (Array.isArray(value)) {
|
|
10104
|
+
return value.map((item, index) => redactAuditValue(item, config, options, [...path, String(index)]));
|
|
10105
|
+
}
|
|
10106
|
+
if (typeof value === "object" && value) {
|
|
10107
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
10108
|
+
entryKey,
|
|
10109
|
+
redactAuditValue(entryValue, config, options, [...path, entryKey])
|
|
10110
|
+
]));
|
|
10111
|
+
}
|
|
10112
|
+
return value;
|
|
10113
|
+
};
|
|
10114
|
+
var redactVoiceAuditEvent = (event, options = {}) => {
|
|
10115
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
10116
|
+
return {
|
|
10117
|
+
...event,
|
|
10118
|
+
actor: redactAuditValue(event.actor, options, resolved, [
|
|
10119
|
+
"actor"
|
|
10120
|
+
]),
|
|
10121
|
+
metadata: redactAuditValue(event.metadata, options, resolved, [
|
|
10122
|
+
"metadata"
|
|
10123
|
+
]),
|
|
10124
|
+
payload: redactAuditValue(event.payload, options, resolved, [
|
|
10125
|
+
"payload"
|
|
10126
|
+
]),
|
|
10127
|
+
resource: redactAuditValue(event.resource, options, resolved, [
|
|
10128
|
+
"resource"
|
|
10129
|
+
])
|
|
10130
|
+
};
|
|
10131
|
+
};
|
|
10132
|
+
var redactVoiceAuditEvents = (events, options = {}) => events.map((event) => redactVoiceAuditEvent(event, options));
|
|
10133
|
+
var exportVoiceAuditTrail = async (input) => {
|
|
10134
|
+
const events = await input.store.list(input.filter);
|
|
10135
|
+
const exportedEvents = input.redact ? redactVoiceAuditEvents(events, input.redact) : events;
|
|
10136
|
+
return {
|
|
10137
|
+
events: exportedEvents,
|
|
10138
|
+
exportedAt: Date.now(),
|
|
10139
|
+
filter: input.filter,
|
|
10140
|
+
redacted: Boolean(input.redact),
|
|
10141
|
+
summary: summarizeVoiceAuditTrail(exportedEvents)
|
|
10142
|
+
};
|
|
10143
|
+
};
|
|
10144
|
+
var formatAuditValue = (value) => {
|
|
10145
|
+
if (value === undefined || value === null || value === "") {
|
|
10146
|
+
return "";
|
|
10147
|
+
}
|
|
10148
|
+
if (typeof value === "string") {
|
|
10149
|
+
return value;
|
|
10150
|
+
}
|
|
10151
|
+
try {
|
|
10152
|
+
return JSON.stringify(value);
|
|
10153
|
+
} catch {
|
|
10154
|
+
return String(value);
|
|
10155
|
+
}
|
|
10156
|
+
};
|
|
10157
|
+
var renderAuditEventMarkdown = (event) => {
|
|
10158
|
+
const actor = event.actor ? `${event.actor.kind}:${event.actor.id}` : "unknown";
|
|
10159
|
+
const resource = event.resource ? `${event.resource.type}${event.resource.id ? `:${event.resource.id}` : ""}` : "";
|
|
10160
|
+
const detail = [
|
|
10161
|
+
`actor=${actor}`,
|
|
10162
|
+
resource ? `resource=${resource}` : undefined,
|
|
10163
|
+
event.sessionId ? `session=${event.sessionId}` : undefined,
|
|
10164
|
+
event.traceId ? `trace=${event.traceId}` : undefined
|
|
10165
|
+
].filter(Boolean).join(" ");
|
|
10166
|
+
return `- ${new Date(event.at).toISOString()} [${event.type}] ${event.action} ${event.outcome ?? "recorded"} ${detail} ${formatAuditValue(event.payload)}`.trim();
|
|
10167
|
+
};
|
|
10168
|
+
var renderVoiceAuditMarkdown = (events, options = {}) => {
|
|
10169
|
+
const renderEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10170
|
+
const summary = summarizeVoiceAuditTrail(renderEvents);
|
|
10171
|
+
const lines = [
|
|
10172
|
+
`# ${options.title ?? "Voice Audit Trail"}`,
|
|
10173
|
+
"",
|
|
10174
|
+
`Events: ${summary.total}`,
|
|
10175
|
+
`Errors: ${summary.errors}`,
|
|
10176
|
+
`Latest: ${summary.latestAt ? new Date(summary.latestAt).toISOString() : "never"}`,
|
|
10177
|
+
"",
|
|
10178
|
+
"## Event Types",
|
|
10179
|
+
"",
|
|
10180
|
+
...summary.byType.map(([type, count]) => `- ${type}: ${count}`),
|
|
10181
|
+
"",
|
|
10182
|
+
"## Events",
|
|
10183
|
+
"",
|
|
10184
|
+
...renderEvents.map(renderAuditEventMarkdown)
|
|
10185
|
+
];
|
|
10186
|
+
return lines.join(`
|
|
10187
|
+
`);
|
|
10188
|
+
};
|
|
10189
|
+
var renderVoiceAuditHTML = (events, options = {}) => {
|
|
10190
|
+
const title = options.title ?? "Voice Audit Trail";
|
|
10191
|
+
const markdown = renderVoiceAuditMarkdown(events, options);
|
|
10192
|
+
const renderEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10193
|
+
const summary = summarizeVoiceAuditTrail(renderEvents);
|
|
10194
|
+
const rows = renderEvents.map((event) => `<tr><td>${escapeHtml4(new Date(event.at).toISOString())}</td><td>${escapeHtml4(event.type)}</td><td>${escapeHtml4(event.action)}</td><td>${escapeHtml4(event.outcome ?? "")}</td><td><code>${escapeHtml4(JSON.stringify(event.payload ?? {}))}</code></td></tr>`).join("");
|
|
10195
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml4(title)}</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}main{max-width:1100px;margin:auto}.summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.75rem;margin:1rem 0}.card{background:white;border:1px solid #ded9cc;border-radius:12px;padding:1rem}table{border-collapse:collapse;width:100%;background:white;border:1px solid #ded9cc}th,td{border-bottom:1px solid #eee8dc;padding:.65rem;text-align:left;vertical-align:top}code{white-space:pre-wrap;word-break:break-word}pre{background:#181713;color:#f8f7f2;padding:1rem;border-radius:12px;overflow:auto}</style></head><body><main><h1>${escapeHtml4(title)}</h1><section class="summary"><div class="card"><strong>Events</strong><br>${summary.total}</div><div class="card"><strong>Errors</strong><br>${summary.errors}</div><div class="card"><strong>Latest</strong><br>${summary.latestAt ? escapeHtml4(new Date(summary.latestAt).toLocaleString()) : "never"}</div></section><table><thead><tr><th>At</th><th>Type</th><th>Action</th><th>Outcome</th><th>Payload</th></tr></thead><tbody>${rows}</tbody></table><h2>Markdown Export</h2><pre>${escapeHtml4(markdown)}</pre></main></body></html>`;
|
|
10196
|
+
};
|
|
10197
|
+
var buildVoiceAuditExport = (events, options = {}) => {
|
|
10198
|
+
const exportEvents = options.redact ? redactVoiceAuditEvents(events, options.redact) : events;
|
|
10199
|
+
return {
|
|
10200
|
+
events: exportEvents,
|
|
10201
|
+
html: renderVoiceAuditHTML(events, options),
|
|
10202
|
+
markdown: renderVoiceAuditMarkdown(events, options),
|
|
10203
|
+
summary: summarizeVoiceAuditTrail(exportEvents)
|
|
10204
|
+
};
|
|
10205
|
+
};
|
|
10206
|
+
|
|
10207
|
+
// src/sessionReplay.ts
|
|
10208
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
10209
|
+
var getString2 = (value) => typeof value === "string" ? value : undefined;
|
|
10210
|
+
var escapeHtml5 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10211
|
+
var increment2 = (record, key) => {
|
|
10212
|
+
record[key] = (record[key] ?? 0) + 1;
|
|
10213
|
+
};
|
|
10214
|
+
var resolveSessionHref = (value, session) => {
|
|
10215
|
+
if (value === false) {
|
|
10216
|
+
return;
|
|
10217
|
+
}
|
|
10218
|
+
if (typeof value === "function") {
|
|
10219
|
+
return value(session);
|
|
10220
|
+
}
|
|
10221
|
+
if (typeof value === "string") {
|
|
10222
|
+
const encoded = encodeURIComponent(session.sessionId);
|
|
10223
|
+
return value.includes(":sessionId") ? value.replace(":sessionId", encoded) : `${value.replace(/\/$/, "")}/${encoded}`;
|
|
10224
|
+
}
|
|
10225
|
+
return;
|
|
10226
|
+
};
|
|
10227
|
+
var isProviderErrorEvent = (event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string");
|
|
10228
|
+
var isRecoveredProviderFallbackEvent = (event) => event.type === "session.error" && (event.payload.providerStatus === "fallback" || event.payload.status === "fallback") && event.payload.recovered === true;
|
|
10229
|
+
var turnRecoveryKey = (event) => event.turnId ?? `session:${event.sessionId}`;
|
|
10230
|
+
var summarizeVoiceProviderFallbackRecovery = (events) => {
|
|
10231
|
+
const sorted = filterVoiceTraceEvents(events);
|
|
10232
|
+
const recoveredEvents = sorted.filter(isRecoveredProviderFallbackEvent);
|
|
10233
|
+
const recoveredKeys = new Set(recoveredEvents.map((event) => turnRecoveryKey(event)));
|
|
10234
|
+
const recoveredSessions = new Set(recoveredEvents.map((event) => event.sessionId));
|
|
10235
|
+
const unresolvedErrorEvents = sorted.filter((event) => isProviderErrorEvent(event) && !recoveredKeys.has(turnRecoveryKey(event)));
|
|
10236
|
+
const unresolvedSessions = new Set(unresolvedErrorEvents.map((event) => event.sessionId));
|
|
10237
|
+
return {
|
|
10238
|
+
recovered: recoveredEvents.length,
|
|
10239
|
+
recoveredSessions: recoveredSessions.size,
|
|
10240
|
+
recoveredTurns: recoveredKeys.size,
|
|
10241
|
+
status: unresolvedErrorEvents.length > 0 ? "fail" : "pass",
|
|
10242
|
+
total: recoveredEvents.length + unresolvedErrorEvents.length,
|
|
10243
|
+
unresolvedErrors: unresolvedErrorEvents.length,
|
|
10244
|
+
unresolvedSessions: unresolvedSessions.size
|
|
10245
|
+
};
|
|
10246
|
+
};
|
|
10247
|
+
var buildReplayTurns = (events) => {
|
|
10248
|
+
const turns = new Map;
|
|
10249
|
+
const getTurn = (turnId) => {
|
|
10250
|
+
const existing = turns.get(turnId);
|
|
10251
|
+
if (existing) {
|
|
10252
|
+
return existing;
|
|
10253
|
+
}
|
|
10254
|
+
const turn = {
|
|
10255
|
+
assistantReplies: [],
|
|
10256
|
+
errors: [],
|
|
10257
|
+
id: turnId,
|
|
10258
|
+
modelCalls: [],
|
|
10259
|
+
tools: [],
|
|
10260
|
+
transcripts: []
|
|
10261
|
+
};
|
|
10262
|
+
turns.set(turnId, turn);
|
|
10263
|
+
return turn;
|
|
10264
|
+
};
|
|
10265
|
+
for (const event of events) {
|
|
10266
|
+
const turnId = event.turnId ?? "session";
|
|
10267
|
+
const turn = getTurn(turnId);
|
|
10268
|
+
switch (event.type) {
|
|
10269
|
+
case "turn.transcript":
|
|
10270
|
+
turn.transcripts.push({
|
|
10271
|
+
isFinal: event.payload.isFinal === true,
|
|
10272
|
+
text: getString2(event.payload.text)
|
|
10273
|
+
});
|
|
10274
|
+
break;
|
|
10275
|
+
case "turn.committed":
|
|
10276
|
+
turn.committedText = getString2(event.payload.text);
|
|
10277
|
+
break;
|
|
10278
|
+
case "turn.assistant": {
|
|
10279
|
+
const text = getString2(event.payload.text);
|
|
10280
|
+
if (text) {
|
|
10281
|
+
turn.assistantReplies.push(text);
|
|
10282
|
+
}
|
|
10283
|
+
break;
|
|
10284
|
+
}
|
|
10285
|
+
case "agent.model":
|
|
10286
|
+
case "assistant.run":
|
|
10287
|
+
turn.modelCalls.push(event.payload);
|
|
10288
|
+
break;
|
|
10289
|
+
case "agent.tool":
|
|
10290
|
+
turn.tools.push(event.payload);
|
|
10291
|
+
break;
|
|
10292
|
+
case "session.error":
|
|
10293
|
+
turn.errors.push(event.payload);
|
|
10294
|
+
break;
|
|
10295
|
+
}
|
|
10296
|
+
}
|
|
10297
|
+
return [...turns.values()];
|
|
10298
|
+
};
|
|
10299
|
+
var summarizeVoiceSessionReplay = async (options) => {
|
|
10300
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
10301
|
+
const events = filterVoiceTraceEvents(sourceEvents, {
|
|
10302
|
+
sessionId: options.sessionId
|
|
10303
|
+
});
|
|
10304
|
+
const replay = buildVoiceTraceReplay(events, {
|
|
10305
|
+
evaluation: options.evaluation,
|
|
10306
|
+
redact: options.redact,
|
|
10307
|
+
title: options.title ?? `Voice Session ${options.sessionId}`
|
|
10308
|
+
});
|
|
10309
|
+
const startedAt = replay.summary.startedAt;
|
|
10310
|
+
return {
|
|
10311
|
+
evaluation: replay.evaluation,
|
|
10312
|
+
events,
|
|
10313
|
+
html: replay.html,
|
|
10314
|
+
markdown: replay.markdown,
|
|
10315
|
+
sessionId: options.sessionId,
|
|
10316
|
+
summary: replay.summary,
|
|
10317
|
+
timeline: events.map((event) => ({
|
|
10318
|
+
at: event.at,
|
|
10319
|
+
offsetMs: startedAt === undefined ? undefined : Math.max(0, event.at - startedAt),
|
|
10320
|
+
payload: event.payload,
|
|
10321
|
+
turnId: event.turnId,
|
|
10322
|
+
type: event.type
|
|
10323
|
+
})),
|
|
10324
|
+
turns: buildReplayTurns(events)
|
|
10325
|
+
};
|
|
10326
|
+
};
|
|
10327
|
+
var summarizeVoiceSessions = async (options = {}) => {
|
|
10328
|
+
const events = options.events ?? await options.store?.list() ?? [];
|
|
10329
|
+
const grouped = new Map;
|
|
10330
|
+
for (const event of events) {
|
|
10331
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
10332
|
+
}
|
|
10333
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
10334
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
10335
|
+
const summary = buildVoiceTraceReplay(sorted, {
|
|
10336
|
+
evaluation: {
|
|
10337
|
+
requireAssistantReply: false,
|
|
10338
|
+
requireCompletedCall: false,
|
|
10339
|
+
requireTranscript: false,
|
|
10340
|
+
requireTurn: false
|
|
10341
|
+
}
|
|
10342
|
+
}).summary;
|
|
10343
|
+
const providerErrors = {};
|
|
10344
|
+
const providers = new Set;
|
|
10345
|
+
let latestOutcome;
|
|
10346
|
+
let errorCount = 0;
|
|
10347
|
+
const recoveredTurns = new Set(sorted.filter(isRecoveredProviderFallbackEvent).map((event) => turnRecoveryKey(event)));
|
|
10348
|
+
for (const event of sorted) {
|
|
10349
|
+
const provider = getString2(event.payload.provider);
|
|
10350
|
+
if (provider) {
|
|
10351
|
+
providers.add(provider);
|
|
10352
|
+
}
|
|
10353
|
+
if (isProviderErrorEvent(event) && !recoveredTurns.has(turnRecoveryKey(event))) {
|
|
10354
|
+
errorCount += 1;
|
|
10355
|
+
increment2(providerErrors, provider ?? "unknown");
|
|
10356
|
+
}
|
|
10357
|
+
const outcome = getString2(event.payload.outcome);
|
|
10358
|
+
if (outcome) {
|
|
10359
|
+
latestOutcome = outcome;
|
|
10360
|
+
}
|
|
10361
|
+
}
|
|
10362
|
+
const item = {
|
|
10363
|
+
endedAt: summary.endedAt,
|
|
10364
|
+
errorCount,
|
|
10365
|
+
eventCount: summary.eventCount,
|
|
10366
|
+
latestOutcome,
|
|
10367
|
+
providerErrors,
|
|
10368
|
+
providers: [...providers].sort(),
|
|
10369
|
+
sessionId,
|
|
10370
|
+
startedAt: summary.startedAt,
|
|
10371
|
+
status: errorCount > 0 ? "failed" : "healthy",
|
|
10372
|
+
transcriptCount: summary.transcriptCount,
|
|
10373
|
+
turnCount: summary.turnCount
|
|
10374
|
+
};
|
|
10375
|
+
const replayHref = options.replayHref === false ? "" : typeof options.replayHref === "function" ? options.replayHref(item) : `${options.replayHref ?? "/api/voice-sessions"}/${encodeURIComponent(sessionId)}/replay/htmx`;
|
|
10376
|
+
return {
|
|
10377
|
+
...item,
|
|
10378
|
+
operationsRecordHref: resolveSessionHref(options.operationsRecordHref, item),
|
|
10379
|
+
replayHref
|
|
10380
|
+
};
|
|
10381
|
+
});
|
|
10382
|
+
const search = options.q?.trim().toLowerCase();
|
|
10383
|
+
return sessions.filter((session) => {
|
|
10384
|
+
if (options.status && options.status !== "all" && session.status !== options.status) {
|
|
10385
|
+
return false;
|
|
10386
|
+
}
|
|
10387
|
+
if (options.provider && !session.providers.includes(options.provider)) {
|
|
10388
|
+
return false;
|
|
10389
|
+
}
|
|
10390
|
+
if (!search) {
|
|
10391
|
+
return true;
|
|
10392
|
+
}
|
|
10393
|
+
return [
|
|
10394
|
+
session.sessionId,
|
|
10395
|
+
session.latestOutcome,
|
|
10396
|
+
session.status,
|
|
10397
|
+
...session.providers
|
|
10398
|
+
].some((value) => value?.toLowerCase().includes(search));
|
|
10399
|
+
}).sort((left, right) => (right.endedAt ?? right.startedAt ?? 0) - (left.endedAt ?? left.startedAt ?? 0)).slice(0, options.limit ?? 50);
|
|
10400
|
+
};
|
|
10401
|
+
var renderVoiceSessionsHTML = (sessions) => sessions.length === 0 ? '<p class="voice-sessions-empty">No voice sessions found.</p>' : [
|
|
10402
|
+
'<div class="voice-sessions-list">',
|
|
10403
|
+
...sessions.map((session) => [
|
|
10404
|
+
`<article class="voice-session-card ${escapeHtml5(session.status)}">`,
|
|
10405
|
+
'<div class="voice-session-card-header">',
|
|
10406
|
+
`<strong>${escapeHtml5(session.sessionId)}</strong>`,
|
|
10407
|
+
`<span>${escapeHtml5(session.status)}</span>`,
|
|
10408
|
+
"</div>",
|
|
10409
|
+
"<dl>",
|
|
10410
|
+
`<div><dt>Events</dt><dd>${String(session.eventCount)}</dd></div>`,
|
|
10411
|
+
`<div><dt>Turns</dt><dd>${String(session.turnCount)}</dd></div>`,
|
|
10412
|
+
`<div><dt>Transcripts</dt><dd>${String(session.transcriptCount)}</dd></div>`,
|
|
10413
|
+
`<div><dt>Errors</dt><dd>${String(session.errorCount)}</dd></div>`,
|
|
10414
|
+
"</dl>",
|
|
10415
|
+
session.latestOutcome ? `<p>Outcome: ${escapeHtml5(session.latestOutcome)}</p>` : "",
|
|
10416
|
+
session.providers.length ? `<p>Providers: ${session.providers.map(escapeHtml5).join(", ")}</p>` : "",
|
|
10417
|
+
session.replayHref ? `<p>${session.operationsRecordHref ? `<a href="${escapeHtml5(session.operationsRecordHref)}">Open operations record</a> \xB7 ` : ""}<a href="${escapeHtml5(session.replayHref)}">Open replay</a></p>` : "",
|
|
10418
|
+
"</article>"
|
|
10419
|
+
].join("")),
|
|
10420
|
+
"</div>"
|
|
10421
|
+
].join("");
|
|
10422
|
+
var createVoiceSessionsJSONHandler = (options = {}) => async ({ query }) => summarizeVoiceSessions({
|
|
10423
|
+
...options,
|
|
10424
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10425
|
+
provider: query?.provider ?? options.provider,
|
|
10426
|
+
q: query?.q ?? options.q,
|
|
10427
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
10428
|
+
});
|
|
10429
|
+
var createVoiceSessionsHTMLHandler = (options = {}) => async ({ query }) => {
|
|
10430
|
+
const sessions = await summarizeVoiceSessions({
|
|
10431
|
+
...options,
|
|
10432
|
+
limit: typeof query?.limit === "string" ? Number(query.limit) : options.limit,
|
|
10433
|
+
provider: query?.provider ?? options.provider,
|
|
10434
|
+
q: query?.q ?? options.q,
|
|
10435
|
+
status: query?.status === "failed" || query?.status === "healthy" || query?.status === "all" ? query.status : options.status
|
|
10436
|
+
});
|
|
10437
|
+
const body = await (options.render?.(sessions) ?? renderVoiceSessionsHTML(sessions));
|
|
10438
|
+
return new Response(body, {
|
|
10439
|
+
headers: {
|
|
10440
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10441
|
+
...options.headers
|
|
10442
|
+
}
|
|
10443
|
+
});
|
|
10444
|
+
};
|
|
10445
|
+
var createVoiceSessionListRoutes = (options = {}) => {
|
|
10446
|
+
const path = options.path ?? "/api/voice-sessions";
|
|
10447
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10448
|
+
const routes = new Elysia2({
|
|
10449
|
+
name: options.name ?? "absolutejs-voice-session-list"
|
|
10450
|
+
}).get(path, createVoiceSessionsJSONHandler(options));
|
|
10451
|
+
if (htmlPath) {
|
|
10452
|
+
routes.get(htmlPath, createVoiceSessionsHTMLHandler(options));
|
|
10453
|
+
}
|
|
10454
|
+
return routes;
|
|
10455
|
+
};
|
|
10456
|
+
var createVoiceSessionReplayJSONHandler = (options) => async ({ params }) => summarizeVoiceSessionReplay({
|
|
10457
|
+
...options,
|
|
10458
|
+
sessionId: params.sessionId ?? ""
|
|
10459
|
+
});
|
|
10460
|
+
var createVoiceSessionReplayHTMLHandler = (options) => async ({ params }) => {
|
|
10461
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
10462
|
+
...options,
|
|
10463
|
+
sessionId: params.sessionId ?? ""
|
|
10464
|
+
});
|
|
10465
|
+
const body = await (options.render?.(replay) ?? replay.html);
|
|
10466
|
+
return new Response(body, {
|
|
10467
|
+
headers: {
|
|
10468
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10469
|
+
...options.headers
|
|
10470
|
+
}
|
|
10471
|
+
});
|
|
10472
|
+
};
|
|
10473
|
+
var createVoiceSessionReplayRoutes = (options) => {
|
|
10474
|
+
const path = options.path ?? "/api/voice-sessions/:sessionId/replay";
|
|
10475
|
+
const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
|
|
10476
|
+
const routes = new Elysia2({
|
|
10477
|
+
name: options.name ?? "absolutejs-voice-session-replay"
|
|
10478
|
+
}).get(path, createVoiceSessionReplayJSONHandler(options));
|
|
10479
|
+
if (htmlPath) {
|
|
10480
|
+
routes.get(htmlPath, createVoiceSessionReplayHTMLHandler(options));
|
|
10481
|
+
}
|
|
10482
|
+
return routes;
|
|
10483
|
+
};
|
|
10484
|
+
|
|
10485
|
+
// src/traceTimeline.ts
|
|
10486
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
10487
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10488
|
+
var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
10489
|
+
var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
10490
|
+
var firstString2 = (payload, keys) => {
|
|
10491
|
+
for (const key of keys) {
|
|
10492
|
+
const value = getString3(payload[key]);
|
|
10493
|
+
if (value) {
|
|
10494
|
+
return value;
|
|
10495
|
+
}
|
|
10496
|
+
}
|
|
10497
|
+
return;
|
|
10498
|
+
};
|
|
10499
|
+
var firstNumber2 = (payload, keys) => {
|
|
10500
|
+
for (const key of keys) {
|
|
10501
|
+
const value = getNumber2(payload[key]);
|
|
10502
|
+
if (value !== undefined) {
|
|
10503
|
+
return value;
|
|
10504
|
+
}
|
|
10505
|
+
}
|
|
10506
|
+
return;
|
|
10507
|
+
};
|
|
10508
|
+
var eventProvider = (event) => firstString2(event.payload, [
|
|
10509
|
+
"provider",
|
|
10510
|
+
"selectedProvider",
|
|
10511
|
+
"fallbackProvider",
|
|
10512
|
+
"variantId"
|
|
10513
|
+
]);
|
|
10514
|
+
var eventStatus = (event) => firstString2(event.payload, [
|
|
10515
|
+
"providerStatus",
|
|
10516
|
+
"status",
|
|
10517
|
+
"disposition",
|
|
10518
|
+
"type",
|
|
10519
|
+
"reason"
|
|
10520
|
+
]);
|
|
10521
|
+
var eventElapsedMs = (event) => firstNumber2(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
|
|
10522
|
+
var resolveSessionHref2 = (value, sessionId) => {
|
|
10523
|
+
if (value === false) {
|
|
10524
|
+
return;
|
|
10525
|
+
}
|
|
10526
|
+
if (typeof value === "function") {
|
|
10527
|
+
return value(sessionId);
|
|
10528
|
+
}
|
|
10529
|
+
if (typeof value === "string") {
|
|
10530
|
+
const encoded = encodeURIComponent(sessionId);
|
|
10531
|
+
return value.includes(":sessionId") ? value.replace(":sessionId", encoded) : `${value.replace(/\/$/, "")}/${encoded}`;
|
|
10532
|
+
}
|
|
10533
|
+
return;
|
|
10534
|
+
};
|
|
10535
|
+
var timelineLabel = (event) => {
|
|
10536
|
+
switch (event.type) {
|
|
10537
|
+
case "call.lifecycle":
|
|
10538
|
+
return `Call ${eventStatus(event) ?? "lifecycle"}`;
|
|
10539
|
+
case "turn.transcript":
|
|
10540
|
+
return event.payload.isFinal === true ? "Final transcript" : "Partial transcript";
|
|
10541
|
+
case "turn.committed":
|
|
10542
|
+
return `Committed turn${getString3(event.payload.reason) ? ` (${getString3(event.payload.reason)})` : ""}`;
|
|
10543
|
+
case "turn.assistant":
|
|
10544
|
+
return "Assistant reply";
|
|
10545
|
+
case "agent.model":
|
|
10546
|
+
return `Model call${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
|
|
10547
|
+
case "agent.tool":
|
|
10548
|
+
return `Tool ${getString3(event.payload.toolName) ?? "call"}`;
|
|
10549
|
+
case "agent.handoff":
|
|
10550
|
+
return `Agent handoff${getString3(event.payload.targetAgentId) ? ` to ${getString3(event.payload.targetAgentId)}` : ""}`;
|
|
10551
|
+
case "assistant.run":
|
|
10552
|
+
return `Assistant run${eventProvider(event) ? ` via ${eventProvider(event)}` : ""}`;
|
|
10553
|
+
case "assistant.guardrail":
|
|
10554
|
+
return `Guardrail ${eventStatus(event) ?? "check"}`;
|
|
10555
|
+
case "call.handoff":
|
|
10556
|
+
return `Call handoff ${eventStatus(event) ?? ""}`.trim();
|
|
10557
|
+
case "client.live_latency":
|
|
10558
|
+
return `Live latency${eventElapsedMs(event) !== undefined ? ` ${eventElapsedMs(event)}ms` : ""}`;
|
|
10559
|
+
case "session.error":
|
|
10560
|
+
return `Error${getString3(event.payload.error) ? `: ${getString3(event.payload.error)}` : ""}`;
|
|
10561
|
+
case "turn.cost":
|
|
10562
|
+
return "Cost telemetry";
|
|
10563
|
+
case "turn_latency.stage":
|
|
10564
|
+
return `Latency ${getString3(event.payload.stage) ?? "stage"}`;
|
|
10565
|
+
case "workflow.contract":
|
|
10566
|
+
return `Workflow contract ${eventStatus(event) ?? ""}`.trim();
|
|
10567
|
+
default:
|
|
10568
|
+
return event.type;
|
|
10569
|
+
}
|
|
10570
|
+
};
|
|
10571
|
+
var summarizeProviders = (events) => {
|
|
10572
|
+
const entries = new Map;
|
|
10573
|
+
const getEntry = (provider) => {
|
|
10574
|
+
const existing = entries.get(provider);
|
|
10575
|
+
if (existing) {
|
|
10576
|
+
return existing;
|
|
10577
|
+
}
|
|
10578
|
+
const entry = {
|
|
10579
|
+
elapsed: [],
|
|
10580
|
+
errorCount: 0,
|
|
10581
|
+
eventCount: 0,
|
|
10582
|
+
fallbackCount: 0,
|
|
10583
|
+
successCount: 0,
|
|
10584
|
+
timeoutCount: 0
|
|
10585
|
+
};
|
|
10586
|
+
entries.set(provider, entry);
|
|
10587
|
+
return entry;
|
|
10588
|
+
};
|
|
10589
|
+
for (const event of events) {
|
|
10590
|
+
const provider = eventProvider(event);
|
|
10591
|
+
if (!provider) {
|
|
10592
|
+
continue;
|
|
10593
|
+
}
|
|
10594
|
+
const entry = getEntry(provider);
|
|
10595
|
+
const status = eventStatus(event);
|
|
10596
|
+
const elapsedMs = eventElapsedMs(event);
|
|
10597
|
+
entry.eventCount += 1;
|
|
10598
|
+
if (elapsedMs !== undefined) {
|
|
10599
|
+
entry.elapsed.push(elapsedMs);
|
|
10600
|
+
}
|
|
10601
|
+
if (status === "success") {
|
|
10602
|
+
entry.successCount += 1;
|
|
10603
|
+
}
|
|
10604
|
+
if (status === "fallback") {
|
|
10605
|
+
entry.fallbackCount += 1;
|
|
10606
|
+
}
|
|
10607
|
+
if (status === "error") {
|
|
10608
|
+
entry.errorCount += 1;
|
|
10609
|
+
}
|
|
10610
|
+
if (status === "timeout") {
|
|
10611
|
+
entry.timeoutCount += 1;
|
|
10612
|
+
}
|
|
10613
|
+
}
|
|
10614
|
+
return [...entries.entries()].map(([provider, entry]) => ({
|
|
10615
|
+
averageElapsedMs: entry.elapsed.length > 0 ? Math.round(entry.elapsed.reduce((total, value) => total + value, 0) / entry.elapsed.length) : undefined,
|
|
10616
|
+
errorCount: entry.errorCount,
|
|
10617
|
+
eventCount: entry.eventCount,
|
|
10618
|
+
fallbackCount: entry.fallbackCount,
|
|
10619
|
+
maxElapsedMs: entry.elapsed.length > 0 ? Math.max(...entry.elapsed) : undefined,
|
|
10620
|
+
provider,
|
|
10621
|
+
successCount: entry.successCount,
|
|
10622
|
+
timeoutCount: entry.timeoutCount
|
|
10623
|
+
})).sort((left, right) => right.eventCount - left.eventCount);
|
|
10624
|
+
};
|
|
10625
|
+
var summarizeVoiceTraceTimeline = (events, options = {}) => {
|
|
10626
|
+
const source = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
|
|
10627
|
+
const grouped = new Map;
|
|
10628
|
+
for (const event of filterVoiceTraceEvents(source)) {
|
|
10629
|
+
grouped.set(event.sessionId, [...grouped.get(event.sessionId) ?? [], event]);
|
|
10630
|
+
}
|
|
10631
|
+
const sessions = [...grouped.entries()].map(([sessionId, sessionEvents]) => {
|
|
10632
|
+
const sorted = filterVoiceTraceEvents(sessionEvents);
|
|
10633
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
10634
|
+
const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
|
|
10635
|
+
const startedAt = summary.startedAt ?? sorted[0]?.at ?? 0;
|
|
10636
|
+
const status = summary.failed ? "failed" : evaluation.issues.length > 0 ? "warning" : "healthy";
|
|
10637
|
+
return {
|
|
10638
|
+
endedAt: summary.endedAt,
|
|
10639
|
+
evaluation,
|
|
10640
|
+
events: sorted.map((event) => ({
|
|
10641
|
+
at: event.at,
|
|
10642
|
+
elapsedMs: eventElapsedMs(event),
|
|
10643
|
+
id: event.id,
|
|
10644
|
+
label: timelineLabel(event),
|
|
10645
|
+
offsetMs: Math.max(0, event.at - startedAt),
|
|
10646
|
+
payload: event.payload,
|
|
10647
|
+
provider: eventProvider(event),
|
|
10648
|
+
status: eventStatus(event),
|
|
10649
|
+
turnId: event.turnId,
|
|
10650
|
+
type: event.type
|
|
10651
|
+
})),
|
|
10652
|
+
lastEventAt: sorted.at(-1)?.at,
|
|
10653
|
+
operationsRecordHref: resolveSessionHref2(options.operationsRecordHref, sessionId),
|
|
10654
|
+
providers: summarizeProviders(sorted),
|
|
10655
|
+
sessionId,
|
|
10656
|
+
startedAt: summary.startedAt,
|
|
10657
|
+
status,
|
|
10658
|
+
summary
|
|
10659
|
+
};
|
|
10660
|
+
}).sort((left, right) => (right.lastEventAt ?? 0) - (left.lastEventAt ?? 0)).slice(0, options.limit ?? 50);
|
|
10661
|
+
return {
|
|
10662
|
+
checkedAt: Date.now(),
|
|
10663
|
+
failed: sessions.filter((session) => session.status === "failed").length,
|
|
10664
|
+
sessions,
|
|
10665
|
+
total: sessions.length,
|
|
10666
|
+
warnings: sessions.filter((session) => session.status === "warning").length
|
|
10667
|
+
};
|
|
10668
|
+
};
|
|
10669
|
+
var formatMs = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
10670
|
+
var renderProviderCards = (session) => session.providers.length === 0 ? '<p class="muted">No provider events recorded for this session.</p>' : `<div class="providers">${session.providers.map((provider) => `<article><strong>${escapeHtml6(provider.provider)}</strong><dl><div><dt>Events</dt><dd>${String(provider.eventCount)}</dd></div><div><dt>Avg</dt><dd>${formatMs(provider.averageElapsedMs)}</dd></div><div><dt>Max</dt><dd>${formatMs(provider.maxElapsedMs)}</dd></div><div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div><div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div><div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div></dl></article>`).join("")}</div>`;
|
|
10671
|
+
var renderVoiceTraceTimelineSessionHTML = (session, options = {}) => {
|
|
10672
|
+
const events = session.events.map((event) => `<tr class="${escapeHtml6(event.status ?? "")}"><td>+${String(event.offsetMs)}ms</td><td>${escapeHtml6(event.type)}</td><td>${escapeHtml6(event.label)}</td><td>${escapeHtml6(event.provider ?? "")}</td><td>${escapeHtml6(event.status ?? "")}</td><td>${formatMs(event.elapsedMs)}</td></tr>`).join("");
|
|
10673
|
+
const issues = session.evaluation.issues.length ? session.evaluation.issues.map((issue) => `<li class="${escapeHtml6(issue.severity)}">${escapeHtml6(issue.code)}: ${escapeHtml6(issue.message)}</li>`).join("") : "<li>none</li>";
|
|
10674
|
+
const supportLinks = session.operationsRecordHref ? `<p><a href="${escapeHtml6(session.operationsRecordHref)}">Open operations record</a></p>` : "";
|
|
10675
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml6(options.title ?? "Voice Trace Timeline")}</title><style>${timelineCSS}</style></head><body><main><a href="/traces">Back to traces</a><header><p class="eyebrow">Call timeline</p><h1>${escapeHtml6(session.sessionId)}</h1><p class="status ${escapeHtml6(session.status)}">${escapeHtml6(session.status)}</p>${supportLinks}</header><section class="metrics"><article><span>Events</span><strong>${String(session.summary.eventCount)}</strong></article><article><span>Turns</span><strong>${String(session.summary.turnCount)}</strong></article><article><span>Errors</span><strong>${String(session.summary.errorCount)}</strong></article><article><span>Duration</span><strong>${formatMs(session.summary.callDurationMs)}</strong></article></section><section><h2>Providers</h2>${renderProviderCards(session)}</section><section><h2>Issues</h2><ul>${issues}</ul></section><section><h2>Timeline</h2><table><thead><tr><th>Offset</th><th>Type</th><th>Event</th><th>Provider</th><th>Status</th><th>Latency</th></tr></thead><tbody>${events}</tbody></table></section></main></body></html>`;
|
|
10676
|
+
};
|
|
10677
|
+
var renderSessionRows = (report) => report.sessions.length === 0 ? '<tr><td colspan="7">No trace events recorded yet.</td></tr>' : report.sessions.map((session) => `<tr class="${escapeHtml6(session.status)}"><td>${session.operationsRecordHref ? `<a href="${escapeHtml6(session.operationsRecordHref)}">${escapeHtml6(session.sessionId)}</a>` : `<a href="/traces/${encodeURIComponent(session.sessionId)}">${escapeHtml6(session.sessionId)}</a>`}</td><td>${escapeHtml6(session.status)}</td><td>${String(session.summary.eventCount)}</td><td>${String(session.summary.turnCount)}</td><td>${String(session.summary.errorCount)}</td><td>${formatMs(session.summary.callDurationMs)}</td><td>${session.providers.map((provider) => escapeHtml6(provider.provider)).join(", ")}</td></tr>`).join("");
|
|
10678
|
+
var timelineCSS = "body{background:#0f1318;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}a{color:#fbbf24}.eyebrow{color:#fbbf24;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,4.5rem);line-height:.92;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}.metrics,.providers{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article,.providers article{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span,dt,.muted{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:12px 0 0}dd{font-weight:800;margin:4px 0 0}table{background:#181f27;border-collapse:collapse;border-radius:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #2b3642;padding:12px;text-align:left}section{margin-top:28px}@media(max-width:760px){main{padding:20px}table{font-size:.9rem}}";
|
|
10679
|
+
var renderVoiceTraceTimelineHTML = (report, options = {}) => {
|
|
10680
|
+
const snippet = escapeHtml6(`const traceStore = createVoiceTraceSinkStore({
|
|
10681
|
+
store: runtimeStorage.traces,
|
|
10682
|
+
sinks: [
|
|
10683
|
+
createVoiceTraceHTTPSink({
|
|
10684
|
+
endpoint: process.env.VOICE_TRACE_WEBHOOK_URL
|
|
10685
|
+
})
|
|
10686
|
+
]
|
|
10687
|
+
});
|
|
10688
|
+
|
|
10689
|
+
app.use(
|
|
10690
|
+
createVoiceTraceTimelineRoutes({
|
|
10691
|
+
htmlPath: '/traces',
|
|
10692
|
+
path: '/api/voice-traces',
|
|
10693
|
+
redact: {
|
|
10694
|
+
keys: ['authorization', 'apiKey', 'token']
|
|
10695
|
+
},
|
|
10696
|
+
store: traceStore
|
|
10697
|
+
})
|
|
10698
|
+
);
|
|
10699
|
+
|
|
10700
|
+
app.use(
|
|
10701
|
+
createVoiceProductionReadinessRoutes({
|
|
10702
|
+
store: traceStore,
|
|
10703
|
+
traceDeliveries: runtimeStorage.traceDeliveries
|
|
10704
|
+
})
|
|
10705
|
+
);`);
|
|
10706
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml6(options.title ?? "Voice Trace Timelines")}</title><style>${timelineCSS}.primitive{background:#181f27;border:1px solid #334155;border-radius:20px;margin:20px 0;padding:18px}.primitive p{line-height:1.55}.primitive pre{background:#0b1118;border:1px solid #2b3642;border-radius:16px;color:#dbeafe;overflow:auto;padding:14px}.primitive code{color:#bfdbfe}</style></head><body><main><header><p class="eyebrow">Self-hosted voice debugging</p><h1>${escapeHtml6(options.title ?? "Voice Trace Timelines")}</h1><p class="muted">Per-call event timelines with provider latency, fallback, timeout, handoff, and error context.</p></header><section class="metrics"><article><span>Sessions</span><strong>${String(report.total)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article><article><span>Warnings</span><strong>${String(report.warnings)}</strong></article></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceTraceTimelineRoutes(...)</code> makes traces the proof backbone</h2><p class="muted">Mount trace timelines from the same trace store used by readiness, simulations, provider recovery, delivery sinks, and phone-agent smoke proof.</p><pre><code>${snippet}</code></pre></section><table><thead><tr><th>Session</th><th>Status</th><th>Events</th><th>Turns</th><th>Errors</th><th>Duration</th><th>Providers</th></tr></thead><tbody>${renderSessionRows(report)}</tbody></table></main></body></html>`;
|
|
10707
|
+
};
|
|
10708
|
+
var createVoiceTraceTimelineRoutes = (options) => {
|
|
10709
|
+
const path = options.path ?? "/api/voice-traces";
|
|
10710
|
+
const htmlPath = options.htmlPath ?? "/traces";
|
|
10711
|
+
const title = options.title ?? "AbsoluteJS Voice Trace Timelines";
|
|
10712
|
+
const routes = new Elysia3({
|
|
10713
|
+
name: options.name ?? "absolutejs-voice-trace-timelines"
|
|
10714
|
+
});
|
|
10715
|
+
const buildReport = async () => summarizeVoiceTraceTimeline(await options.store.list(), {
|
|
10716
|
+
evaluation: options.evaluation,
|
|
10717
|
+
limit: options.limit,
|
|
10718
|
+
operationsRecordHref: options.operationsRecordHref,
|
|
10719
|
+
redact: options.redact
|
|
10720
|
+
});
|
|
10721
|
+
const findSession = async (sessionId) => {
|
|
10722
|
+
const report = summarizeVoiceTraceTimeline(await options.store.list({ sessionId }), {
|
|
10723
|
+
evaluation: options.evaluation,
|
|
10724
|
+
limit: 1,
|
|
10725
|
+
operationsRecordHref: options.operationsRecordHref,
|
|
10726
|
+
redact: options.redact
|
|
10727
|
+
});
|
|
10728
|
+
return report.sessions[0];
|
|
10729
|
+
};
|
|
10730
|
+
routes.get(path, async () => Response.json(await buildReport()));
|
|
10731
|
+
routes.get(`${path}/:sessionId`, async ({ params }) => {
|
|
10732
|
+
const session = await findSession(params.sessionId);
|
|
10733
|
+
return session ? Response.json(session) : Response.json({ error: "Voice trace session not found." }, { status: 404 });
|
|
10734
|
+
});
|
|
10735
|
+
routes.get(htmlPath, async () => {
|
|
10736
|
+
const report = await buildReport();
|
|
10737
|
+
const body = await (options.render ?? ((input) => renderVoiceTraceTimelineHTML(input, { title })))(report);
|
|
10738
|
+
return new Response(body, {
|
|
10739
|
+
headers: {
|
|
10740
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10741
|
+
...options.headers
|
|
10742
|
+
}
|
|
10743
|
+
});
|
|
10744
|
+
});
|
|
10745
|
+
routes.get(`${htmlPath}/:sessionId`, async ({ params }) => {
|
|
10746
|
+
const session = await findSession(params.sessionId);
|
|
10747
|
+
if (!session) {
|
|
10748
|
+
return Response.json({ error: "Voice trace session not found." }, { status: 404 });
|
|
10749
|
+
}
|
|
10750
|
+
const body = await (options.renderSession ?? ((input) => renderVoiceTraceTimelineSessionHTML(input, { title })))(session);
|
|
10751
|
+
return new Response(body, {
|
|
10752
|
+
headers: {
|
|
10753
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
10754
|
+
...options.headers
|
|
10755
|
+
}
|
|
10756
|
+
});
|
|
10757
|
+
});
|
|
10758
|
+
return routes;
|
|
10759
|
+
};
|
|
10760
|
+
|
|
10761
|
+
// src/operationsRecord.ts
|
|
10762
|
+
var getString4 = (value) => typeof value === "string" ? value : undefined;
|
|
10763
|
+
var getNumber3 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
10764
|
+
var getBoolean = (value) => typeof value === "boolean" ? value : undefined;
|
|
10765
|
+
var getRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
10766
|
+
var countOutcome = (events, outcome) => events.filter((event) => event.outcome === outcome).length;
|
|
10767
|
+
var matchesSessionScopedId = (id, sessionId) => id === sessionId || id.startsWith(`${sessionId}:`);
|
|
10768
|
+
var hasPayloadValue = (payload, key, values) => {
|
|
10769
|
+
const value = payload[key];
|
|
10770
|
+
return typeof value === "string" && values.has(value);
|
|
10771
|
+
};
|
|
10772
|
+
var countIntegrationDeliveryStatus = (events, status) => events.filter((event) => event.deliveryStatus === status).length;
|
|
10773
|
+
var uniqueSorted = (values) => [
|
|
10774
|
+
...new Set(values.filter((value) => typeof value === "string"))
|
|
10775
|
+
].sort();
|
|
10776
|
+
var pushMissingValuesIssue = (input) => {
|
|
10777
|
+
const missing = (input.expected ?? []).filter((value) => !input.actual.includes(value));
|
|
10778
|
+
if (missing.length > 0) {
|
|
10779
|
+
input.issues.push(`Missing ${input.prefix ?? "guardrail"} ${input.label}: ${missing.join(", ")}`);
|
|
10780
|
+
}
|
|
10781
|
+
};
|
|
10782
|
+
var resolveRoutePath = (path, sessionId) => path.replace(":sessionId", encodeURIComponent(sessionId));
|
|
10783
|
+
var toHandoff = (event) => ({
|
|
10784
|
+
at: event.at,
|
|
10785
|
+
fromAgentId: getString4(event.payload.fromAgentId),
|
|
10786
|
+
metadata: event.payload.metadata && typeof event.payload.metadata === "object" && !Array.isArray(event.payload.metadata) ? event.payload.metadata : undefined,
|
|
10787
|
+
reason: getString4(event.payload.reason),
|
|
10788
|
+
status: getString4(event.payload.status),
|
|
10789
|
+
summary: getString4(event.payload.summary),
|
|
10790
|
+
targetAgentId: getString4(event.payload.targetAgentId),
|
|
10791
|
+
turnId: event.turnId
|
|
10792
|
+
});
|
|
10793
|
+
var toTool = (event) => ({
|
|
10794
|
+
at: event.at,
|
|
10795
|
+
elapsedMs: getNumber3(event.payload.elapsedMs),
|
|
10796
|
+
error: getString4(event.payload.error),
|
|
10797
|
+
status: getString4(event.payload.status),
|
|
10798
|
+
toolCallId: getString4(event.payload.toolCallId),
|
|
10799
|
+
toolName: getString4(event.payload.toolName),
|
|
10800
|
+
turnId: event.turnId
|
|
10801
|
+
});
|
|
10802
|
+
var toGuardrailFinding = (value) => {
|
|
10803
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10804
|
+
return;
|
|
10805
|
+
}
|
|
10806
|
+
const record = value;
|
|
10807
|
+
return {
|
|
10808
|
+
action: getString4(record.action),
|
|
10809
|
+
label: getString4(record.label),
|
|
10810
|
+
ruleId: getString4(record.ruleId)
|
|
10811
|
+
};
|
|
10812
|
+
};
|
|
10813
|
+
var toGuardrailDecision = (event) => ({
|
|
10814
|
+
allowed: getBoolean(event.payload.allowed),
|
|
10815
|
+
at: event.at,
|
|
10816
|
+
findings: Array.isArray(event.payload.findings) ? event.payload.findings.map(toGuardrailFinding).filter((finding) => finding !== undefined) : [],
|
|
10817
|
+
metadata: event.metadata && typeof event.metadata === "object" && !Array.isArray(event.metadata) ? event.metadata : undefined,
|
|
10818
|
+
proof: getString4(event.metadata?.proof),
|
|
10819
|
+
stage: getString4(event.payload.stage),
|
|
10820
|
+
status: getString4(event.payload.status),
|
|
10821
|
+
toolName: getString4(event.payload.toolName),
|
|
10822
|
+
turnId: event.turnId
|
|
10823
|
+
});
|
|
10824
|
+
var decodeBase64Bytes = (value) => {
|
|
10825
|
+
if (typeof value !== "string") {
|
|
10826
|
+
return 0;
|
|
10827
|
+
}
|
|
10828
|
+
try {
|
|
10829
|
+
return Buffer.from(value, "base64").byteLength;
|
|
10830
|
+
} catch {
|
|
10831
|
+
return 0;
|
|
10832
|
+
}
|
|
10833
|
+
};
|
|
10834
|
+
var toTelephonyMediaEvent = (event) => {
|
|
10835
|
+
if (event.type !== "client.telephony_media") {
|
|
10836
|
+
return;
|
|
10837
|
+
}
|
|
10838
|
+
const envelope = getRecord(event.payload.envelope);
|
|
10839
|
+
const start = getRecord(envelope?.start);
|
|
10840
|
+
const media = getRecord(envelope?.media);
|
|
10841
|
+
const stop = getRecord(envelope?.stop);
|
|
10842
|
+
const error = getRecord(envelope?.error);
|
|
10843
|
+
const eventName = getString4(event.payload.event) ?? getString4(envelope?.event) ?? getString4(event.payload.kind) ?? "unknown";
|
|
10844
|
+
const streamId = getString4(event.payload.streamId) ?? getString4(envelope?.streamSid) ?? getString4(start?.streamSid);
|
|
10845
|
+
const callSid = getString4(event.payload.callSid) ?? getString4(start?.callSid) ?? getString4(stop?.callSid);
|
|
10846
|
+
const sequenceNumber = getString4(media?.sequenceNumber) ?? getString4(envelope?.sequenceNumber);
|
|
10847
|
+
const direction = getString4(media?.track) ?? getString4(media?.direction) ?? getString4(event.payload.direction);
|
|
10848
|
+
return {
|
|
10849
|
+
at: event.at,
|
|
10850
|
+
audioBytes: getNumber3(event.payload.audioBytes) ?? getNumber3(media?.audioBytes) ?? decodeBase64Bytes(media?.payload),
|
|
10851
|
+
callSid,
|
|
10852
|
+
carrier: getString4(event.payload.carrier),
|
|
10853
|
+
direction,
|
|
10854
|
+
event: eventName,
|
|
10855
|
+
sequenceNumber,
|
|
10856
|
+
streamId
|
|
10857
|
+
};
|
|
10858
|
+
};
|
|
10859
|
+
var summarizeTelephonyMedia = (events) => {
|
|
10860
|
+
const mediaEvents = events.map(toTelephonyMediaEvent).filter((event) => event !== undefined);
|
|
10861
|
+
const eventNames = mediaEvents.map((event) => event.event.toLowerCase());
|
|
10862
|
+
return {
|
|
10863
|
+
audioBytes: mediaEvents.reduce((total, event) => total + event.audioBytes, 0),
|
|
10864
|
+
carriers: uniqueSorted(mediaEvents.map((event) => event.carrier)),
|
|
10865
|
+
clears: eventNames.filter((event) => event === "clear").length,
|
|
10866
|
+
errors: eventNames.filter((event) => event === "error").length,
|
|
10867
|
+
events: mediaEvents,
|
|
10868
|
+
inbound: mediaEvents.filter((event) => event.direction === "inbound").length,
|
|
10869
|
+
marks: eventNames.filter((event) => event === "mark").length,
|
|
10870
|
+
media: eventNames.filter((event) => event === "media").length,
|
|
10871
|
+
outbound: mediaEvents.filter((event) => event.direction === "outbound").length,
|
|
10872
|
+
starts: eventNames.filter((event) => event === "start").length,
|
|
10873
|
+
stops: eventNames.filter((event) => event === "stop").length,
|
|
10874
|
+
streamIds: uniqueSorted(mediaEvents.map((event) => event.streamId)),
|
|
10875
|
+
total: mediaEvents.length
|
|
10876
|
+
};
|
|
10877
|
+
};
|
|
10878
|
+
var summarizeGuardrails = (events) => {
|
|
10879
|
+
const decisions = events.filter((event) => event.type === "assistant.guardrail").map(toGuardrailDecision);
|
|
10880
|
+
const isBlocked = (decision) => decision.allowed === false || decision.status === "fail";
|
|
10881
|
+
const isWarned = (decision) => decision.status === "warn" || decision.findings.some((finding) => finding.action === "warn");
|
|
10882
|
+
const stages = [
|
|
10883
|
+
...new Set(decisions.map((decision) => decision.stage).filter((stage) => typeof stage === "string"))
|
|
10884
|
+
].sort();
|
|
10885
|
+
return {
|
|
10886
|
+
blocked: decisions.filter(isBlocked).length,
|
|
10887
|
+
decisions,
|
|
10888
|
+
passed: decisions.filter((decision) => !isBlocked(decision) && !isWarned(decision)).length,
|
|
10889
|
+
stages,
|
|
10890
|
+
total: decisions.length,
|
|
10891
|
+
warned: decisions.filter(isWarned).length
|
|
10892
|
+
};
|
|
10893
|
+
};
|
|
10894
|
+
var toProviderDecision = (event) => {
|
|
10895
|
+
const provider = getString4(event.payload.provider) ?? getString4(event.payload.selectedProvider) ?? getString4(event.payload.fallbackProvider) ?? getString4(event.payload.variantId);
|
|
10896
|
+
const status = getString4(event.payload.providerStatus) ?? getString4(event.payload.status);
|
|
10897
|
+
const error = getString4(event.payload.error);
|
|
10898
|
+
const elapsedMs = getNumber3(event.payload.elapsedMs) ?? getNumber3(event.payload.latencyMs) ?? getNumber3(event.payload.durationMs);
|
|
10899
|
+
const hasProviderSignal = event.type === "provider.decision" || provider !== undefined || getString4(event.payload.selectedProvider) !== undefined || getString4(event.payload.fallbackProvider) !== undefined || getString4(event.payload.variantId) !== undefined;
|
|
10900
|
+
if (!hasProviderSignal) {
|
|
10901
|
+
return;
|
|
10902
|
+
}
|
|
10903
|
+
return {
|
|
10904
|
+
at: event.at,
|
|
10905
|
+
elapsedMs,
|
|
10906
|
+
error,
|
|
10907
|
+
fallbackProvider: getString4(event.payload.fallbackProvider),
|
|
10908
|
+
kind: getString4(event.payload.kind),
|
|
10909
|
+
provider,
|
|
10910
|
+
reason: getString4(event.payload.reason),
|
|
10911
|
+
selectedProvider: getString4(event.payload.selectedProvider),
|
|
10912
|
+
status,
|
|
10913
|
+
surface: getString4(event.payload.surface),
|
|
10914
|
+
type: event.type,
|
|
10915
|
+
turnId: event.turnId
|
|
10916
|
+
};
|
|
10917
|
+
};
|
|
10918
|
+
var summarizeProviderDecisions = (decisions) => {
|
|
10919
|
+
const providers = uniqueSorted(decisions.flatMap((decision) => [
|
|
10920
|
+
decision.provider,
|
|
10921
|
+
decision.selectedProvider,
|
|
10922
|
+
decision.fallbackProvider
|
|
10923
|
+
]));
|
|
10924
|
+
const surfaces = uniqueSorted(decisions.map((decision) => decision.surface));
|
|
10925
|
+
const degraded = decisions.filter((decision) => decision.status === "degraded").length;
|
|
10926
|
+
const errors = decisions.filter((decision) => decision.status === "error").length;
|
|
10927
|
+
const fallbacks = decisions.filter((decision) => decision.status === "fallback").length;
|
|
10928
|
+
const selected = decisions.filter((decision) => decision.status === "selected" || decision.status === "success").length;
|
|
10929
|
+
const recoveryStatus = errors > 0 ? "failed" : degraded > 0 ? "degraded" : fallbacks > 0 ? "recovered" : selected > 0 ? "selected" : "none";
|
|
10930
|
+
return {
|
|
10931
|
+
degraded,
|
|
10932
|
+
errors,
|
|
10933
|
+
fallbacks,
|
|
10934
|
+
providers,
|
|
10935
|
+
recoveryStatus,
|
|
10936
|
+
selected,
|
|
10937
|
+
surfaces,
|
|
10938
|
+
total: decisions.length
|
|
10939
|
+
};
|
|
10940
|
+
};
|
|
10941
|
+
var buildTranscript = (replay) => replay.turns.map((turn) => ({
|
|
10942
|
+
assistantReplies: turn.assistantReplies,
|
|
10943
|
+
committedText: turn.committedText,
|
|
10944
|
+
errors: turn.errors.map((error) => getString4(error.error) ?? JSON.stringify(error)).filter((error) => typeof error === "string"),
|
|
10945
|
+
id: turn.id,
|
|
10946
|
+
transcripts: turn.transcripts.map((transcript) => transcript.text).filter((text) => typeof text === "string" && text.length > 0)
|
|
10947
|
+
})).filter((turn) => turn.committedText || turn.assistantReplies.length > 0 || turn.transcripts.length > 0 || turn.errors.length > 0);
|
|
10948
|
+
var resolveOutcome = (events) => {
|
|
10949
|
+
const agentResults = events.filter((event) => event.type === "agent.result");
|
|
10950
|
+
return {
|
|
10951
|
+
assistantReplies: events.filter((event) => event.type === "turn.assistant").length,
|
|
10952
|
+
complete: agentResults.some((event) => event.payload.complete === true),
|
|
10953
|
+
escalated: agentResults.some((event) => event.payload.escalated === true),
|
|
10954
|
+
noAnswer: agentResults.some((event) => event.payload.noAnswer === true),
|
|
10955
|
+
transferred: agentResults.some((event) => event.payload.transferred === true),
|
|
10956
|
+
voicemail: agentResults.some((event) => event.payload.voicemail === true)
|
|
10957
|
+
};
|
|
10958
|
+
};
|
|
10959
|
+
var buildVoiceOperationsRecord = async (options) => {
|
|
10960
|
+
const sourceEvents = options.events ?? await options.store?.list({ sessionId: options.sessionId }) ?? [];
|
|
10961
|
+
const rawTraceEvents = filterVoiceTraceEvents(sourceEvents, {
|
|
10962
|
+
sessionId: options.sessionId
|
|
10963
|
+
});
|
|
10964
|
+
const traceEvents = options.redact ? redactVoiceTraceEvents(rawTraceEvents, options.redact) : rawTraceEvents;
|
|
10965
|
+
const timelineReport = summarizeVoiceTraceTimeline(traceEvents, {
|
|
10966
|
+
evaluation: options.evaluation,
|
|
10967
|
+
limit: 1
|
|
10968
|
+
});
|
|
10969
|
+
const timelineSession = timelineReport.sessions[0];
|
|
10970
|
+
const replay = await summarizeVoiceSessionReplay({
|
|
10971
|
+
evaluation: options.evaluation,
|
|
10972
|
+
events: traceEvents,
|
|
10973
|
+
sessionId: options.sessionId
|
|
10974
|
+
});
|
|
10975
|
+
const rawAuditEvents = options.audit ? filterVoiceAuditEvents(await options.audit.list({ sessionId: options.sessionId })) : undefined;
|
|
10976
|
+
const auditEvents = options.redact && rawAuditEvents ? redactVoiceAuditEvents(rawAuditEvents, options.redact) : rawAuditEvents;
|
|
10977
|
+
const reviews = options.reviews ? (await options.reviews.list()).filter((review) => matchesSessionScopedId(review.id, options.sessionId)) : undefined;
|
|
10978
|
+
const reviewIds = new Set(reviews?.map((review) => review.id) ?? []);
|
|
10979
|
+
const tasks = options.tasks ? (await options.tasks.list()).filter((task) => matchesSessionScopedId(task.id, options.sessionId) || typeof task.reviewId === "string" && reviewIds.has(task.reviewId)) : undefined;
|
|
10980
|
+
const taskIds = new Set(tasks?.map((task) => task.id) ?? []);
|
|
10981
|
+
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;
|
|
10982
|
+
const sinkDeliveries = integrationEvents?.reduce((total, event) => total + Object.keys(event.sinkDeliveries ?? {}).length, 0) ?? 0;
|
|
10983
|
+
const providerDecisions = traceEvents.map(toProviderDecision).filter((decision) => decision !== undefined);
|
|
10984
|
+
return {
|
|
10985
|
+
audit: auditEvents ? {
|
|
10986
|
+
error: countOutcome(auditEvents, "error"),
|
|
10987
|
+
events: auditEvents,
|
|
10988
|
+
skipped: countOutcome(auditEvents, "skipped"),
|
|
10989
|
+
success: countOutcome(auditEvents, "success"),
|
|
10990
|
+
total: auditEvents.length
|
|
10991
|
+
} : undefined,
|
|
10992
|
+
checkedAt: Date.now(),
|
|
10993
|
+
guardrails: summarizeGuardrails(traceEvents),
|
|
10994
|
+
handoffs: traceEvents.filter((event) => event.type === "agent.handoff").map(toHandoff),
|
|
10995
|
+
integrationEvents: integrationEvents ? {
|
|
10996
|
+
delivered: countIntegrationDeliveryStatus(integrationEvents, "delivered"),
|
|
10997
|
+
events: integrationEvents,
|
|
10998
|
+
failed: countIntegrationDeliveryStatus(integrationEvents, "failed"),
|
|
10999
|
+
pending: countIntegrationDeliveryStatus(integrationEvents, "pending"),
|
|
11000
|
+
sinkDeliveries,
|
|
11001
|
+
skipped: countIntegrationDeliveryStatus(integrationEvents, "skipped"),
|
|
11002
|
+
total: integrationEvents.length
|
|
11003
|
+
} : undefined,
|
|
11004
|
+
outcome: resolveOutcome(traceEvents),
|
|
11005
|
+
providerDecisions,
|
|
11006
|
+
providerDecisionSummary: summarizeProviderDecisions(providerDecisions),
|
|
11007
|
+
providers: timelineSession?.providers ?? [],
|
|
11008
|
+
replay,
|
|
11009
|
+
reviews: reviews ? {
|
|
11010
|
+
failed: reviews.filter((review) => !review.summary.pass).length,
|
|
11011
|
+
reviews,
|
|
11012
|
+
total: reviews.length
|
|
11013
|
+
} : undefined,
|
|
11014
|
+
sessionId: options.sessionId,
|
|
11015
|
+
status: timelineSession?.status ?? "healthy",
|
|
11016
|
+
summary: timelineSession?.summary ?? replay.summary,
|
|
11017
|
+
tasks: tasks ? {
|
|
11018
|
+
done: tasks.filter((task) => task.status === "done").length,
|
|
11019
|
+
inProgress: tasks.filter((task) => task.status === "in-progress").length,
|
|
11020
|
+
open: tasks.filter((task) => task.status === "open").length,
|
|
11021
|
+
overdue: tasks.filter((task) => typeof task.dueAt === "number" && task.status !== "done" && task.dueAt <= Date.now()).length,
|
|
11022
|
+
tasks,
|
|
11023
|
+
total: tasks.length
|
|
11024
|
+
} : undefined,
|
|
11025
|
+
telephonyMedia: summarizeTelephonyMedia(traceEvents),
|
|
11026
|
+
timeline: timelineSession?.events ?? [],
|
|
11027
|
+
tools: traceEvents.filter((event) => event.type === "agent.tool").map(toTool),
|
|
11028
|
+
traceEvents,
|
|
11029
|
+
transcript: buildTranscript(replay)
|
|
11030
|
+
};
|
|
11031
|
+
};
|
|
11032
|
+
var evaluateVoiceOperationsRecordGuardrails = (record, input = {}) => {
|
|
11033
|
+
const issues = [];
|
|
11034
|
+
const decisions = record.guardrails.decisions;
|
|
11035
|
+
const proofs = uniqueSorted(decisions.map((decision) => decision.proof));
|
|
11036
|
+
const ruleIds = uniqueSorted(decisions.flatMap((decision) => decision.findings.map((finding) => finding.ruleId)));
|
|
11037
|
+
const stages = uniqueSorted(decisions.map((decision) => decision.stage));
|
|
11038
|
+
const statuses = uniqueSorted(decisions.map((decision) => decision.status));
|
|
11039
|
+
const toolNames = uniqueSorted(decisions.map((decision) => decision.toolName));
|
|
11040
|
+
const minDecisions = input.minDecisions ?? 1;
|
|
11041
|
+
if (record.guardrails.total < minDecisions) {
|
|
11042
|
+
issues.push(`Expected at least ${String(minDecisions)} guardrail decisions, found ${String(record.guardrails.total)}.`);
|
|
11043
|
+
}
|
|
11044
|
+
if (input.minBlocked !== undefined && record.guardrails.blocked < input.minBlocked) {
|
|
11045
|
+
issues.push(`Expected at least ${String(input.minBlocked)} blocked guardrail decisions, found ${String(record.guardrails.blocked)}.`);
|
|
11046
|
+
}
|
|
11047
|
+
if (input.minWarned !== undefined && record.guardrails.warned < input.minWarned) {
|
|
11048
|
+
issues.push(`Expected at least ${String(input.minWarned)} warned guardrail decisions, found ${String(record.guardrails.warned)}.`);
|
|
11049
|
+
}
|
|
11050
|
+
if (input.minPassed !== undefined && record.guardrails.passed < input.minPassed) {
|
|
11051
|
+
issues.push(`Expected at least ${String(input.minPassed)} passed guardrail decisions, found ${String(record.guardrails.passed)}.`);
|
|
11052
|
+
}
|
|
11053
|
+
pushMissingValuesIssue({
|
|
11054
|
+
actual: proofs,
|
|
11055
|
+
expected: input.proofs,
|
|
11056
|
+
issues,
|
|
11057
|
+
label: "proofs"
|
|
11058
|
+
});
|
|
11059
|
+
pushMissingValuesIssue({
|
|
11060
|
+
actual: ruleIds,
|
|
11061
|
+
expected: input.ruleIds,
|
|
11062
|
+
issues,
|
|
11063
|
+
label: "rule IDs"
|
|
11064
|
+
});
|
|
11065
|
+
pushMissingValuesIssue({
|
|
11066
|
+
actual: stages,
|
|
11067
|
+
expected: input.stages,
|
|
11068
|
+
issues,
|
|
11069
|
+
label: "stages"
|
|
11070
|
+
});
|
|
11071
|
+
pushMissingValuesIssue({
|
|
11072
|
+
actual: statuses,
|
|
11073
|
+
expected: input.statuses,
|
|
11074
|
+
issues,
|
|
11075
|
+
label: "statuses"
|
|
11076
|
+
});
|
|
11077
|
+
pushMissingValuesIssue({
|
|
11078
|
+
actual: toolNames,
|
|
11079
|
+
expected: input.toolNames,
|
|
11080
|
+
issues,
|
|
11081
|
+
label: "tool names"
|
|
11082
|
+
});
|
|
11083
|
+
return {
|
|
11084
|
+
blocked: record.guardrails.blocked,
|
|
11085
|
+
decisions: record.guardrails.total,
|
|
11086
|
+
issues,
|
|
11087
|
+
ok: issues.length === 0,
|
|
11088
|
+
passed: record.guardrails.passed,
|
|
11089
|
+
proofs,
|
|
11090
|
+
ruleIds,
|
|
11091
|
+
stages,
|
|
11092
|
+
statuses,
|
|
11093
|
+
toolNames,
|
|
11094
|
+
warned: record.guardrails.warned
|
|
11095
|
+
};
|
|
11096
|
+
};
|
|
11097
|
+
var assertVoiceOperationsRecordGuardrails = (record, input = {}) => {
|
|
11098
|
+
const report = evaluateVoiceOperationsRecordGuardrails(record, input);
|
|
11099
|
+
if (!report.ok) {
|
|
11100
|
+
throw new Error(`Voice operations record guardrail assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
|
|
11101
|
+
}
|
|
11102
|
+
return report;
|
|
11103
|
+
};
|
|
11104
|
+
var evaluateVoiceOperationsRecordProviderRecovery = (record, input = {}) => {
|
|
11105
|
+
const issues = [];
|
|
11106
|
+
const summary = record.providerDecisionSummary;
|
|
11107
|
+
const decisions = record.providerDecisions;
|
|
11108
|
+
const providers = uniqueSorted(decisions.flatMap((decision) => [
|
|
11109
|
+
decision.provider,
|
|
11110
|
+
decision.selectedProvider,
|
|
11111
|
+
decision.fallbackProvider
|
|
11112
|
+
]));
|
|
11113
|
+
const selectedProviders = uniqueSorted(decisions.map((decision) => decision.selectedProvider));
|
|
11114
|
+
const fallbackProviders = uniqueSorted(decisions.map((decision) => decision.fallbackProvider));
|
|
11115
|
+
const statuses = uniqueSorted(decisions.map((decision) => decision.status));
|
|
11116
|
+
const surfaces = uniqueSorted(decisions.map((decision) => decision.surface));
|
|
11117
|
+
if (input.recoveryStatus && summary.recoveryStatus !== input.recoveryStatus) {
|
|
11118
|
+
issues.push(`Expected provider recovery status ${input.recoveryStatus}, got ${summary.recoveryStatus}.`);
|
|
11119
|
+
}
|
|
11120
|
+
if (input.minTotal !== undefined && summary.total < input.minTotal) {
|
|
11121
|
+
issues.push(`Expected at least ${String(input.minTotal)} provider decision(s), found ${String(summary.total)}.`);
|
|
11122
|
+
}
|
|
11123
|
+
if (input.minSelected !== undefined && summary.selected < input.minSelected) {
|
|
11124
|
+
issues.push(`Expected at least ${String(input.minSelected)} selected provider decision(s), found ${String(summary.selected)}.`);
|
|
11125
|
+
}
|
|
11126
|
+
if (input.minFallbacks !== undefined && summary.fallbacks < input.minFallbacks) {
|
|
11127
|
+
issues.push(`Expected at least ${String(input.minFallbacks)} provider fallback decision(s), found ${String(summary.fallbacks)}.`);
|
|
11128
|
+
}
|
|
11129
|
+
if (input.minDegraded !== undefined && summary.degraded < input.minDegraded) {
|
|
11130
|
+
issues.push(`Expected at least ${String(input.minDegraded)} degraded provider decision(s), found ${String(summary.degraded)}.`);
|
|
11131
|
+
}
|
|
11132
|
+
if (input.minErrors !== undefined && summary.errors < input.minErrors) {
|
|
11133
|
+
issues.push(`Expected at least ${String(input.minErrors)} provider error decision(s), found ${String(summary.errors)}.`);
|
|
11134
|
+
}
|
|
11135
|
+
pushMissingValuesIssue({
|
|
11136
|
+
actual: providers,
|
|
11137
|
+
expected: input.requiredProviders,
|
|
11138
|
+
issues,
|
|
11139
|
+
label: "providers",
|
|
11140
|
+
prefix: "provider recovery"
|
|
11141
|
+
});
|
|
11142
|
+
pushMissingValuesIssue({
|
|
11143
|
+
actual: selectedProviders,
|
|
11144
|
+
expected: input.requiredSelectedProviders,
|
|
11145
|
+
issues,
|
|
11146
|
+
label: "selected providers",
|
|
11147
|
+
prefix: "provider recovery"
|
|
11148
|
+
});
|
|
11149
|
+
pushMissingValuesIssue({
|
|
11150
|
+
actual: fallbackProviders,
|
|
11151
|
+
expected: input.requiredFallbackProviders,
|
|
11152
|
+
issues,
|
|
11153
|
+
label: "fallback providers",
|
|
11154
|
+
prefix: "provider recovery"
|
|
11155
|
+
});
|
|
11156
|
+
pushMissingValuesIssue({
|
|
11157
|
+
actual: statuses,
|
|
11158
|
+
expected: input.requiredStatuses,
|
|
11159
|
+
issues,
|
|
11160
|
+
label: "statuses",
|
|
11161
|
+
prefix: "provider recovery"
|
|
11162
|
+
});
|
|
11163
|
+
pushMissingValuesIssue({
|
|
11164
|
+
actual: surfaces,
|
|
11165
|
+
expected: input.requiredSurfaces,
|
|
11166
|
+
issues,
|
|
11167
|
+
label: "surfaces",
|
|
11168
|
+
prefix: "provider recovery"
|
|
11169
|
+
});
|
|
11170
|
+
for (const phrase of input.requiredReasonIncludes ?? []) {
|
|
11171
|
+
if (!decisions.some((decision) => decision.reason?.includes(phrase))) {
|
|
11172
|
+
issues.push(`Missing provider recovery reason containing: ${phrase}.`);
|
|
11173
|
+
}
|
|
11174
|
+
}
|
|
11175
|
+
return {
|
|
11176
|
+
degraded: summary.degraded,
|
|
11177
|
+
errors: summary.errors,
|
|
11178
|
+
fallbacks: summary.fallbacks,
|
|
11179
|
+
issues,
|
|
11180
|
+
ok: issues.length === 0,
|
|
11181
|
+
providers,
|
|
11182
|
+
recoveryStatus: summary.recoveryStatus,
|
|
11183
|
+
selected: summary.selected,
|
|
11184
|
+
selectedProviders,
|
|
11185
|
+
statuses,
|
|
11186
|
+
surfaces,
|
|
11187
|
+
total: summary.total
|
|
11188
|
+
};
|
|
11189
|
+
};
|
|
11190
|
+
var assertVoiceOperationsRecordProviderRecovery = (record, input = {}) => {
|
|
11191
|
+
const report = evaluateVoiceOperationsRecordProviderRecovery(record, input);
|
|
11192
|
+
if (!report.ok) {
|
|
11193
|
+
throw new Error(`Voice operations record provider recovery assertion failed for ${record.sessionId}: ${report.issues.join(" ")}`);
|
|
11194
|
+
}
|
|
11195
|
+
return report;
|
|
11196
|
+
};
|
|
11197
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11198
|
+
var formatMs2 = (value) => value === undefined ? "n/a" : `${String(value)}ms`;
|
|
11199
|
+
var outcomeLabels = (outcome) => [
|
|
11200
|
+
outcome.complete ? "complete" : undefined,
|
|
11201
|
+
outcome.escalated ? "escalated" : undefined,
|
|
11202
|
+
outcome.transferred ? "transferred" : undefined,
|
|
11203
|
+
outcome.voicemail ? "voicemail" : undefined,
|
|
11204
|
+
outcome.noAnswer ? "no-answer" : undefined
|
|
11205
|
+
].filter((label) => label !== undefined);
|
|
11206
|
+
var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
|
|
11207
|
+
const outcomes = outcomeLabels(record.outcome);
|
|
11208
|
+
const topErrors = record.traceEvents.filter((event) => event.type === "session.error").map((event) => getString4(event.payload.error)).filter((error) => typeof error === "string").slice(0, 3);
|
|
11209
|
+
const openTasks = record.tasks?.tasks.filter((task) => task.status !== "done").map((task) => task.title).slice(0, 3) ?? [];
|
|
11210
|
+
const providerDecisions = record.providerDecisions.filter((decision) => decision.provider || decision.selectedProvider || decision.fallbackProvider || decision.reason).slice(0, 5);
|
|
11211
|
+
const providerDecisionLines = providerDecisions.length ? providerDecisions.map((decision) => {
|
|
11212
|
+
const provider = decision.provider ?? decision.selectedProvider ?? decision.fallbackProvider ?? "provider";
|
|
11213
|
+
const parts = [
|
|
11214
|
+
decision.surface ? `surface=${decision.surface}` : undefined,
|
|
11215
|
+
decision.status ? `status=${decision.status}` : undefined,
|
|
11216
|
+
decision.selectedProvider ? `selected=${decision.selectedProvider}` : undefined,
|
|
11217
|
+
decision.fallbackProvider ? `fallback=${decision.fallbackProvider}` : undefined,
|
|
11218
|
+
decision.reason ? `reason=${decision.reason}` : undefined
|
|
11219
|
+
].filter((part) => typeof part === "string");
|
|
11220
|
+
return `- ${provider}: ${parts.join("; ") || "decision recorded"}`;
|
|
11221
|
+
}) : ["- none recorded"];
|
|
11222
|
+
const providerDecisionSummary = record.providerDecisionSummary;
|
|
11223
|
+
const providerRecoveryLine = [
|
|
11224
|
+
`status=${providerDecisionSummary.recoveryStatus}`,
|
|
11225
|
+
`selected=${String(providerDecisionSummary.selected)}`,
|
|
11226
|
+
`fallbacks=${String(providerDecisionSummary.fallbacks)}`,
|
|
11227
|
+
`degraded=${String(providerDecisionSummary.degraded)}`,
|
|
11228
|
+
`errors=${String(providerDecisionSummary.errors)}`
|
|
11229
|
+
].join("; ");
|
|
11230
|
+
const telephonyMediaLine = [
|
|
11231
|
+
`events=${String(record.telephonyMedia.total)}`,
|
|
11232
|
+
`starts=${String(record.telephonyMedia.starts)}`,
|
|
11233
|
+
`media=${String(record.telephonyMedia.media)}`,
|
|
11234
|
+
`inbound=${String(record.telephonyMedia.inbound)}`,
|
|
11235
|
+
`outbound=${String(record.telephonyMedia.outbound)}`,
|
|
11236
|
+
`marks=${String(record.telephonyMedia.marks)}`,
|
|
11237
|
+
`clears=${String(record.telephonyMedia.clears)}`,
|
|
11238
|
+
`stops=${String(record.telephonyMedia.stops)}`,
|
|
11239
|
+
`errors=${String(record.telephonyMedia.errors)}`,
|
|
11240
|
+
`audioBytes=${String(record.telephonyMedia.audioBytes)}`,
|
|
11241
|
+
`carriers=${record.telephonyMedia.carriers.join(", ") || "none"}`,
|
|
11242
|
+
`streams=${record.telephonyMedia.streamIds.join(", ") || "none"}`
|
|
11243
|
+
].join("; ");
|
|
11244
|
+
const telephonyMediaLines = record.telephonyMedia.events.length ? record.telephonyMedia.events.slice(0, 12).map((event) => {
|
|
11245
|
+
const parts = [
|
|
11246
|
+
event.carrier ? `carrier=${event.carrier}` : undefined,
|
|
11247
|
+
event.streamId ? `stream=${event.streamId}` : undefined,
|
|
11248
|
+
event.callSid ? `call=${event.callSid}` : undefined,
|
|
11249
|
+
event.direction ? `direction=${event.direction}` : undefined,
|
|
11250
|
+
event.sequenceNumber ? `seq=${event.sequenceNumber}` : undefined,
|
|
11251
|
+
`audioBytes=${String(event.audioBytes)}`
|
|
11252
|
+
].filter((part) => typeof part === "string");
|
|
11253
|
+
return `- ${event.event}: ${parts.join("; ")}`;
|
|
11254
|
+
}) : ["- none recorded"];
|
|
11255
|
+
return [
|
|
11256
|
+
`# Voice incident handoff: ${record.sessionId}`,
|
|
11257
|
+
"",
|
|
11258
|
+
`- Status: ${record.status}`,
|
|
11259
|
+
`- Duration: ${formatMs2(record.summary.callDurationMs)}`,
|
|
11260
|
+
`- Turns: ${String(record.summary.turnCount)}`,
|
|
11261
|
+
`- Errors: ${String(record.summary.errorCount)}`,
|
|
11262
|
+
`- Outcome: ${outcomes.join(", ") || "unknown"}`,
|
|
11263
|
+
`- Providers: ${record.providers.map((provider) => provider.provider).join(", ") || "none recorded"}`,
|
|
11264
|
+
`- Open tasks: ${openTasks.join("; ") || "none"}`,
|
|
11265
|
+
`- Top errors: ${topErrors.join("; ") || "none"}`,
|
|
11266
|
+
`- Guardrails: ${String(record.guardrails.blocked)} blocked / ${String(record.guardrails.warned)} warned / ${String(record.guardrails.total)} decisions`,
|
|
11267
|
+
`- Provider recovery: ${providerRecoveryLine}`,
|
|
11268
|
+
`- Telephony media: ${telephonyMediaLine}`,
|
|
11269
|
+
"",
|
|
11270
|
+
"## Provider decisions",
|
|
11271
|
+
"",
|
|
11272
|
+
...providerDecisionLines,
|
|
11273
|
+
"",
|
|
11274
|
+
"## Telephony media",
|
|
11275
|
+
"",
|
|
11276
|
+
...telephonyMediaLines,
|
|
11277
|
+
"",
|
|
11278
|
+
renderVoiceOperationsRecordGuardrailMarkdown(record),
|
|
11279
|
+
"",
|
|
11280
|
+
"## Next checks",
|
|
11281
|
+
"- Review provider decisions and fallback status.",
|
|
11282
|
+
"- Review transcript and assistant replies.",
|
|
11283
|
+
"- Review handoffs, tools, audit, tasks, and integration delivery."
|
|
11284
|
+
].join(`
|
|
11285
|
+
`);
|
|
11286
|
+
};
|
|
11287
|
+
var renderVoiceOperationsRecordGuardrailMarkdown = (record) => {
|
|
11288
|
+
if (record.guardrails.total === 0) {
|
|
11289
|
+
return [
|
|
11290
|
+
"## Guardrail evidence",
|
|
11291
|
+
"",
|
|
11292
|
+
"- No assistant.guardrail events were recorded for this session."
|
|
11293
|
+
].join(`
|
|
11294
|
+
`);
|
|
11295
|
+
}
|
|
11296
|
+
return [
|
|
11297
|
+
"## Guardrail evidence",
|
|
11298
|
+
"",
|
|
11299
|
+
...record.guardrails.decisions.map((decision) => {
|
|
11300
|
+
const findings = decision.findings.map((finding) => [finding.action, finding.ruleId, finding.label].filter((value) => typeof value === "string").join(":")).filter(Boolean).join(", ");
|
|
11301
|
+
return `- assistant.guardrail ${decision.stage ?? "unknown"}: ${decision.status ?? "unknown"}; allowed=${String(decision.allowed ?? "unknown")}; proof=${decision.proof ?? "runtime"}; findings=${findings || "none"}`;
|
|
11302
|
+
})
|
|
11303
|
+
].join(`
|
|
11304
|
+
`);
|
|
11305
|
+
};
|
|
11306
|
+
var renderVoiceOperationsRecordHTML = (record, options = {}) => {
|
|
11307
|
+
const providers = record.providers.length ? record.providers.map((provider) => `<article><strong>${escapeHtml7(provider.provider)}</strong><span>${String(provider.eventCount)} events</span><span>${formatMs2(provider.averageElapsedMs)} avg</span><span>${String(provider.errorCount)} errors</span></article>`).join("") : '<p class="muted">No provider events recorded.</p>';
|
|
11308
|
+
const transcript = record.transcript.length ? record.transcript.map((turn) => `<li><strong>${escapeHtml7(turn.id)}</strong>${turn.committedText ? `<p><span class="label">Caller</span>${escapeHtml7(turn.committedText)}</p>` : ""}${turn.assistantReplies.map((reply) => `<p><span class="label">Assistant</span>${escapeHtml7(reply)}</p>`).join("")}${turn.errors.map((error) => `<p class="error"><span class="label">Error</span>${escapeHtml7(error)}</p>`).join("")}</li>`).join("") : "<li>No transcript turns recorded.</li>";
|
|
11309
|
+
const providerDecisions = record.providerDecisions.length ? record.providerDecisions.map((decision) => `<li><strong>${escapeHtml7(decision.provider ?? decision.selectedProvider ?? decision.fallbackProvider ?? "provider")}</strong> <span>${escapeHtml7(decision.status ?? decision.type)}</span> ${formatMs2(decision.elapsedMs)}${decision.surface ? `<p><span class="label">Surface</span>${escapeHtml7(decision.surface)}</p>` : ""}${decision.kind ? `<p><span class="label">Kind</span>${escapeHtml7(decision.kind)}</p>` : ""}${decision.selectedProvider ? `<p>Selected: ${escapeHtml7(decision.selectedProvider)}</p>` : ""}${decision.fallbackProvider ? `<p>Fallback: ${escapeHtml7(decision.fallbackProvider)}</p>` : ""}${decision.error ? `<p class="error">${escapeHtml7(decision.error)}</p>` : ""}${decision.reason ? `<p>${escapeHtml7(decision.reason)}</p>` : ""}</li>`).join("") : "<li>No provider decisions recorded.</li>";
|
|
11310
|
+
const providerDecisionSummary = record.providerDecisionSummary;
|
|
11311
|
+
const handoffs = record.handoffs.length ? record.handoffs.map((handoff) => `<li><strong>${escapeHtml7(handoff.fromAgentId ?? "unknown")}</strong> to <strong>${escapeHtml7(handoff.targetAgentId ?? "unknown")}</strong> <span>${escapeHtml7(handoff.status ?? "")}</span><p>${escapeHtml7(handoff.summary ?? handoff.reason ?? "")}</p></li>`).join("") : "<li>No agent handoffs recorded.</li>";
|
|
11312
|
+
const tools = record.tools.length ? record.tools.map((tool) => `<li><strong>${escapeHtml7(tool.toolName ?? "tool")}</strong> <span>${escapeHtml7(tool.status ?? "")}</span> ${formatMs2(tool.elapsedMs)} ${tool.error ? `<p>${escapeHtml7(tool.error)}</p>` : ""}</li>`).join("") : "<li>No tool calls recorded.</li>";
|
|
11313
|
+
const reviews = record.reviews?.reviews.length ? record.reviews.reviews.map((review) => `<li><strong>${escapeHtml7(review.title)}</strong> <span>${escapeHtml7(review.summary.outcome ?? "")}</span><p>${escapeHtml7(review.postCall?.summary ?? review.transcript.actual)}</p></li>`).join("") : "<li>No call reviews recorded.</li>";
|
|
11314
|
+
const tasks = record.tasks?.tasks.length ? record.tasks.tasks.map((task) => `<li><strong>${escapeHtml7(task.title)}</strong> <span>${escapeHtml7(task.status)}</span><p>${escapeHtml7(task.recommendedAction)}</p></li>`).join("") : "<li>No ops tasks recorded.</li>";
|
|
11315
|
+
const integrationEvents = record.integrationEvents?.events.length ? record.integrationEvents.events.map((event) => `<li><strong>${escapeHtml7(event.type)}</strong> <span>${escapeHtml7(event.deliveryStatus ?? "local")}</span><p>${escapeHtml7(event.deliveryError ?? event.deliveredTo ?? "")}</p></li>`).join("") : "<li>No integration events recorded.</li>";
|
|
11316
|
+
const guardrails = record.guardrails.total ? record.guardrails.decisions.map((decision) => {
|
|
11317
|
+
const findings = decision.findings.map((finding) => finding.label ?? finding.ruleId ?? finding.action).filter((value) => typeof value === "string").join(", ") || "none";
|
|
11318
|
+
return `<li><strong>assistant.guardrail ${escapeHtml7(decision.stage ?? "unknown")}</strong> <span>${escapeHtml7(decision.status ?? "")}</span><p>Allowed: ${escapeHtml7(String(decision.allowed ?? "unknown"))} \xB7 Proof: ${escapeHtml7(decision.proof ?? "runtime")}${decision.turnId ? ` \xB7 Turn: ${escapeHtml7(decision.turnId)}` : ""}</p><p>${escapeHtml7(findings)}</p></li>`;
|
|
11319
|
+
}).join("") : "<li>No assistant.guardrail events recorded.</li>";
|
|
11320
|
+
const telephonyMedia = record.telephonyMedia.events.length ? record.telephonyMedia.events.slice(0, 50).map((event) => {
|
|
11321
|
+
const details = [
|
|
11322
|
+
event.carrier ? `Carrier: ${event.carrier}` : undefined,
|
|
11323
|
+
event.streamId ? `Stream: ${event.streamId}` : undefined,
|
|
11324
|
+
event.callSid ? `Call: ${event.callSid}` : undefined,
|
|
11325
|
+
event.direction ? `Direction: ${event.direction}` : undefined,
|
|
11326
|
+
event.sequenceNumber ? `Seq: ${event.sequenceNumber}` : undefined,
|
|
11327
|
+
`Audio bytes: ${String(event.audioBytes)}`
|
|
11328
|
+
].filter((detail) => typeof detail === "string");
|
|
11329
|
+
return `<li><strong>${escapeHtml7(event.event)}</strong> <span>${escapeHtml7(new Date(event.at).toLocaleString())}</span><p>${escapeHtml7(details.join(" \xB7 "))}</p></li>`;
|
|
11330
|
+
}).join("") : "<li>No telephony media trace events recorded.</li>";
|
|
11331
|
+
const snippet = escapeHtml7(`app.use(
|
|
11332
|
+
createVoiceOperationsRecordRoutes({
|
|
11333
|
+
audit: auditStore,
|
|
11334
|
+
integrationEvents: opsEvents,
|
|
11335
|
+
htmlPath: '/voice-ops/:sessionId',
|
|
11336
|
+
path: '/api/voice-ops/:sessionId',
|
|
11337
|
+
redact: {
|
|
11338
|
+
keys: ['authorization', 'apiKey', 'token']
|
|
11339
|
+
},
|
|
11340
|
+
reviews: callReviews,
|
|
11341
|
+
store: traceStore,
|
|
11342
|
+
tasks: opsTasks
|
|
11343
|
+
})
|
|
11344
|
+
);`);
|
|
11345
|
+
const incidentMarkdown = escapeHtml7(renderVoiceOperationsRecordIncidentMarkdown(record));
|
|
11346
|
+
const incidentLink = options.incidentHref ? `<a href="${escapeHtml7(options.incidentHref)}">Download incident.md</a>` : "";
|
|
11347
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml7(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,.label{color:#a9b4bd}.label{display:block;font-size:.72rem;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.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}.hero-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:16px}.hero-actions a{background:#fbbf24;border-radius:999px;color:#111827;font-weight:900;padding:10px 14px;text-decoration:none}.two-column{display:grid;gap:18px;grid-template-columns:minmax(0,1.15fr) minmax(280px,.85fr)}@media(max-width:860px){main{padding:20px}.two-column{grid-template-columns:1fr}}</style></head><body><main><p class="eyebrow">Call log replacement</p><h1>${escapeHtml7(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml7(record.status)}">${escapeHtml7(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</a><a href="#telephony-media">Telephony media</a><a href="#guardrails">Guardrails</a><a href="#incident-handoff">Incident handoff</a>${incidentLink}</div><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>${formatMs2(record.summary.callDurationMs)}</strong></div><div class="card"><span>Provider recovery</span><strong>${escapeHtml7(providerDecisionSummary.recoveryStatus)}</strong><span>${String(providerDecisionSummary.fallbacks)} fallback / ${String(providerDecisionSummary.degraded)} degraded / ${String(providerDecisionSummary.errors)} errors</span></div><div class="card"><span>Telephony media</span><strong>${String(record.telephonyMedia.media)}</strong><span>${String(record.telephonyMedia.inbound)} inbound / ${String(record.telephonyMedia.outbound)} outbound / ${String(record.telephonyMedia.clears)} clears</span></div><div class="card"><span>Guardrails</span><strong>${String(record.guardrails.blocked)}</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="two-column"><div><h2 id="transcript">Transcript</h2><ul>${transcript}</ul></div><div><h2 id="provider-decisions">Provider Decisions</h2><ul>${providerDecisions}</ul></div></section><section id="telephony-media"><h2>Telephony Media</h2><p class="muted">Live <code>client.telephony_media</code> stream lifecycle evidence attached to this session. Carriers: ${escapeHtml7(record.telephonyMedia.carriers.join(", ") || "none")}. Streams: ${escapeHtml7(record.telephonyMedia.streamIds.join(", ") || "none")}. Inbound: ${String(record.telephonyMedia.inbound)}. Outbound: ${String(record.telephonyMedia.outbound)}. Marks: ${String(record.telephonyMedia.marks)}. Clears: ${String(record.telephonyMedia.clears)}.</p><ul>${telephonyMedia}</ul></section><section id="guardrails"><h2>Guardrail Evidence</h2><p class="muted">Live <code>assistant.guardrail</code> decisions attached to this session.</p><ul>${guardrails}</ul></section><section id="incident-handoff"><h2>Copyable Incident Handoff</h2><p class="muted">Paste this into Slack, Linear, Zendesk, or an incident review. ${incidentLink}</p><pre><code>${incidentMarkdown}</code></pre></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, guardrails, audit, latency, replay, reviews, tasks, media streams, and webhook delivery.</p><pre><code>${snippet}</code></pre></section><section><h2>Provider Summary</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>`;
|
|
11348
|
+
};
|
|
11349
|
+
var createVoiceOperationsRecordRoutes = (options) => {
|
|
11350
|
+
const path = options.path ?? "/api/voice-operations/:sessionId";
|
|
11351
|
+
const htmlPath = options.htmlPath === undefined ? "/voice-operations/:sessionId" : options.htmlPath;
|
|
11352
|
+
const incidentPath = options.incidentPath === undefined ? `${path}/incident.md` : options.incidentPath;
|
|
11353
|
+
const incidentHtmlPath = options.incidentHtmlPath === undefined && htmlPath ? `${htmlPath}/incident.md` : options.incidentHtmlPath;
|
|
11354
|
+
const routes = new Elysia4({
|
|
11355
|
+
name: options.name ?? "absolutejs-voice-operations-record"
|
|
11356
|
+
});
|
|
11357
|
+
const buildRecord = (sessionId) => buildVoiceOperationsRecord({
|
|
11358
|
+
audit: options.audit,
|
|
11359
|
+
evaluation: options.evaluation,
|
|
11360
|
+
events: options.events,
|
|
11361
|
+
integrationEvents: options.integrationEvents,
|
|
11362
|
+
redact: options.redact,
|
|
11363
|
+
reviews: options.reviews,
|
|
11364
|
+
sessionId,
|
|
11365
|
+
store: options.store,
|
|
11366
|
+
tasks: options.tasks
|
|
11367
|
+
});
|
|
11368
|
+
const getSessionId = (params) => params.sessionId ?? "";
|
|
11369
|
+
routes.get(path, async ({ params }) => Response.json(await buildRecord(getSessionId(params))));
|
|
11370
|
+
const incidentHandler = async ({
|
|
11371
|
+
params
|
|
11372
|
+
}) => {
|
|
11373
|
+
const record = await buildRecord(getSessionId(params));
|
|
11374
|
+
const body = await (options.renderIncidentMarkdown ?? renderVoiceOperationsRecordIncidentMarkdown)(record);
|
|
11375
|
+
return new Response(body, {
|
|
11376
|
+
headers: {
|
|
11377
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
11378
|
+
...options.headers
|
|
11379
|
+
}
|
|
11380
|
+
});
|
|
11381
|
+
};
|
|
11382
|
+
if (incidentPath) {
|
|
11383
|
+
routes.get(incidentPath, incidentHandler);
|
|
11384
|
+
}
|
|
11385
|
+
if (htmlPath) {
|
|
11386
|
+
routes.get(htmlPath, async ({ params }) => {
|
|
11387
|
+
const record = await buildRecord(getSessionId(params));
|
|
11388
|
+
const body = await (options.render ?? ((input) => renderVoiceOperationsRecordHTML(input, {
|
|
11389
|
+
incidentHref: incidentHtmlPath ? resolveRoutePath(incidentHtmlPath, input.sessionId) : undefined,
|
|
11390
|
+
title: options.title
|
|
11391
|
+
})))(record);
|
|
11392
|
+
return new Response(body, {
|
|
11393
|
+
headers: {
|
|
11394
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
11395
|
+
...options.headers
|
|
11396
|
+
}
|
|
11397
|
+
});
|
|
11398
|
+
});
|
|
11399
|
+
}
|
|
11400
|
+
if (incidentHtmlPath && incidentHtmlPath !== incidentPath) {
|
|
11401
|
+
routes.get(incidentHtmlPath, incidentHandler);
|
|
11402
|
+
}
|
|
11403
|
+
return routes;
|
|
11404
|
+
};
|
|
11405
|
+
|
|
8993
11406
|
// src/telephony/twilio.ts
|
|
8994
11407
|
import { Buffer as Buffer3 } from "buffer";
|
|
8995
|
-
import { Elysia as
|
|
11408
|
+
import { Elysia as Elysia6 } from "elysia";
|
|
8996
11409
|
|
|
8997
11410
|
// src/telephonyOutcome.ts
|
|
8998
|
-
import { Elysia } from "elysia";
|
|
11411
|
+
import { Elysia as Elysia5 } from "elysia";
|
|
8999
11412
|
var DEFAULT_COMPLETED_STATUSES = [
|
|
9000
11413
|
"answered",
|
|
9001
11414
|
"completed",
|
|
@@ -9036,7 +11449,7 @@ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
|
|
|
9036
11449
|
];
|
|
9037
11450
|
var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
|
|
9038
11451
|
var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9039
|
-
var
|
|
11452
|
+
var uniqueSorted2 = (values) => Array.from(new Set(values)).sort();
|
|
9040
11453
|
var findMissing = (values, required) => {
|
|
9041
11454
|
if (!required?.length) {
|
|
9042
11455
|
return [];
|
|
@@ -9069,22 +11482,22 @@ var evaluateVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
|
9069
11482
|
const issues = [];
|
|
9070
11483
|
const decisions = input.decisions ?? [];
|
|
9071
11484
|
const verificationAttempts = input.verificationAttempts ?? [];
|
|
9072
|
-
const actions =
|
|
9073
|
-
const dispositions =
|
|
9074
|
-
const providers =
|
|
9075
|
-
const sources =
|
|
11485
|
+
const actions = uniqueSorted2(decisions.map((decision) => decision.decision?.action ?? decision.action).filter(isTelephonyOutcomeAction));
|
|
11486
|
+
const dispositions = uniqueSorted2(decisions.map((decision) => decision.decision?.disposition ?? decision.disposition).filter(isCallDisposition));
|
|
11487
|
+
const providers = uniqueSorted2(decisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
|
|
11488
|
+
const sources = uniqueSorted2(decisions.map((decision) => decision.decision?.source ?? decision.source).filter((source) => typeof source === "string"));
|
|
9076
11489
|
const applied = decisions.filter((decision) => decision.applied === true).length;
|
|
9077
11490
|
const duplicateDecisions = decisions.filter((decision) => decision.duplicate === true);
|
|
9078
|
-
const duplicateProviders =
|
|
11491
|
+
const duplicateProviders = uniqueSorted2(duplicateDecisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
|
|
9079
11492
|
const duplicateIdempotencyKeys = new Set(duplicateDecisions.map((decision) => decision.idempotencyKey).filter((key) => typeof key === "string" && key.length > 0)).size;
|
|
9080
11493
|
const duplicateCampaignOutcomesApplied = duplicateDecisions.filter((decision) => isRecord(decision.campaignOutcome) && decision.campaignOutcome.applied === true).length;
|
|
9081
|
-
const duplicateOutcomeReasons =
|
|
11494
|
+
const duplicateOutcomeReasons = uniqueSorted2(duplicateDecisions.map((decision) => isRecord(decision.campaignOutcome) ? decision.campaignOutcome.reason : undefined).filter((reason) => typeof reason === "string"));
|
|
9082
11495
|
const routeResults = decisions.filter((decision) => isRecord(decision.routeResult)).length;
|
|
9083
11496
|
const missingSessionIds = decisions.filter((decision) => !decision.sessionId).length;
|
|
9084
11497
|
const rejectedVerificationAttempts = verificationAttempts.filter((attempt) => attempt.rejected === true || attempt.status === 401 || attempt.verification?.ok === false && attempt.verification.reason === "invalid-signature");
|
|
9085
|
-
const rejectedVerificationProviders =
|
|
11498
|
+
const rejectedVerificationProviders = uniqueSorted2(rejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
|
|
9086
11499
|
const replayRejectedVerificationAttempts = rejectedVerificationAttempts.filter((attempt) => attempt.replayRejected === true);
|
|
9087
|
-
const replayRejectedVerificationProviders =
|
|
11500
|
+
const replayRejectedVerificationProviders = uniqueSorted2(replayRejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
|
|
9088
11501
|
const rejectedVerificationSideEffects = rejectedVerificationAttempts.reduce((total, attempt) => total + Math.max(0, attempt.sideEffects ?? 0), 0);
|
|
9089
11502
|
if (input.minDecisions !== undefined && decisions.length < input.minDecisions) {
|
|
9090
11503
|
issues.push(`Expected at least ${String(input.minDecisions)} telephony webhook decision(s), found ${String(decisions.length)}.`);
|
|
@@ -9166,7 +11579,7 @@ var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
|
9166
11579
|
return assertion;
|
|
9167
11580
|
};
|
|
9168
11581
|
var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
|
|
9169
|
-
var
|
|
11582
|
+
var firstString3 = (source, keys) => {
|
|
9170
11583
|
for (const key of keys) {
|
|
9171
11584
|
const value = source[key];
|
|
9172
11585
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -9177,7 +11590,7 @@ var firstString2 = (source, keys) => {
|
|
|
9177
11590
|
}
|
|
9178
11591
|
}
|
|
9179
11592
|
};
|
|
9180
|
-
var
|
|
11593
|
+
var firstNumber3 = (source, keys) => {
|
|
9181
11594
|
for (const key of keys) {
|
|
9182
11595
|
const value = source[key];
|
|
9183
11596
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -9538,8 +11951,8 @@ var verifyVoiceTelephonyWebhook = async (input) => {
|
|
|
9538
11951
|
var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
|
|
9539
11952
|
var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
9540
11953
|
const payload = flattenPayload(input.body);
|
|
9541
|
-
const provider =
|
|
9542
|
-
const status =
|
|
11954
|
+
const provider = firstString3(payload, ["provider", "Provider"]) ?? input.provider;
|
|
11955
|
+
const status = firstString3(payload, [
|
|
9543
11956
|
"CallStatus",
|
|
9544
11957
|
"call_status",
|
|
9545
11958
|
"callStatus",
|
|
@@ -9549,7 +11962,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
9549
11962
|
"event_type",
|
|
9550
11963
|
"type"
|
|
9551
11964
|
]);
|
|
9552
|
-
const durationMs =
|
|
11965
|
+
const durationMs = firstNumber3(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber3(payload, [
|
|
9553
11966
|
"CallDuration",
|
|
9554
11967
|
"call_duration",
|
|
9555
11968
|
"callDuration",
|
|
@@ -9557,16 +11970,16 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
9557
11970
|
"dial_call_duration",
|
|
9558
11971
|
"duration"
|
|
9559
11972
|
]));
|
|
9560
|
-
const sipCode =
|
|
11973
|
+
const sipCode = firstNumber3(payload, [
|
|
9561
11974
|
"SipResponseCode",
|
|
9562
11975
|
"sip_response_code",
|
|
9563
11976
|
"sipCode",
|
|
9564
11977
|
"sip_code",
|
|
9565
11978
|
"hangupCauseCode"
|
|
9566
11979
|
]);
|
|
9567
|
-
const from =
|
|
9568
|
-
const to =
|
|
9569
|
-
const target =
|
|
11980
|
+
const from = firstString3(payload, ["From", "from", "caller_id", "callerId"]);
|
|
11981
|
+
const to = firstString3(payload, ["To", "to", "called_number", "calledNumber"]);
|
|
11982
|
+
const target = firstString3(payload, [
|
|
9570
11983
|
"transferTarget",
|
|
9571
11984
|
"TransferTarget",
|
|
9572
11985
|
"target",
|
|
@@ -9574,7 +11987,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
9574
11987
|
"department"
|
|
9575
11988
|
]);
|
|
9576
11989
|
return {
|
|
9577
|
-
answeredBy:
|
|
11990
|
+
answeredBy: firstString3(payload, [
|
|
9578
11991
|
"AnsweredBy",
|
|
9579
11992
|
"answered_by",
|
|
9580
11993
|
"answeredBy",
|
|
@@ -9588,7 +12001,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
9588
12001
|
...payload
|
|
9589
12002
|
},
|
|
9590
12003
|
provider,
|
|
9591
|
-
reason:
|
|
12004
|
+
reason: firstString3(payload, [
|
|
9592
12005
|
"Reason",
|
|
9593
12006
|
"reason",
|
|
9594
12007
|
"HangupCause",
|
|
@@ -9604,7 +12017,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
9604
12017
|
var defaultSessionId = (input) => {
|
|
9605
12018
|
const payload = flattenPayload(input.body);
|
|
9606
12019
|
const metadataSessionId = input.event.metadata?.sessionId;
|
|
9607
|
-
return
|
|
12020
|
+
return firstString3(input.query, ["sessionId", "session_id"]) ?? firstString3(payload, [
|
|
9608
12021
|
"sessionId",
|
|
9609
12022
|
"session_id",
|
|
9610
12023
|
"SessionId",
|
|
@@ -9619,7 +12032,7 @@ var defaultSessionId = (input) => {
|
|
|
9619
12032
|
};
|
|
9620
12033
|
var defaultIdempotencyKey = (input) => {
|
|
9621
12034
|
const payload = flattenPayload(input.body);
|
|
9622
|
-
const eventId =
|
|
12035
|
+
const eventId = firstString3(payload, [
|
|
9623
12036
|
"id",
|
|
9624
12037
|
"event_id",
|
|
9625
12038
|
"eventId",
|
|
@@ -9756,7 +12169,7 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
|
|
|
9756
12169
|
var createVoiceTelephonyWebhookRoutes = (options = {}) => {
|
|
9757
12170
|
const path = options.path ?? "/api/voice/telephony/webhook";
|
|
9758
12171
|
const handler = createVoiceTelephonyWebhookHandler(options);
|
|
9759
|
-
return new
|
|
12172
|
+
return new Elysia5({
|
|
9760
12173
|
name: options.name ?? "absolutejs-voice-telephony-webhooks"
|
|
9761
12174
|
}).post(path, async ({ query, request }) => {
|
|
9762
12175
|
try {
|
|
@@ -9807,7 +12220,7 @@ var resolveTwilioStreamParameters = async (parameters, input) => {
|
|
|
9807
12220
|
return parameters;
|
|
9808
12221
|
};
|
|
9809
12222
|
var joinUrlPath = (origin, path) => `${origin.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
9810
|
-
var
|
|
12223
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
9811
12224
|
var getWebhookVerificationUrl = (webhook, input) => {
|
|
9812
12225
|
if (!webhook?.verificationUrl) {
|
|
9813
12226
|
return;
|
|
@@ -9850,23 +12263,23 @@ var buildTwilioVoiceSetupStatus = async (options, input) => {
|
|
|
9850
12263
|
};
|
|
9851
12264
|
var renderTwilioVoiceSetupHTML = (status, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
9852
12265
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio setup</p>
|
|
9853
|
-
<h1>${
|
|
12266
|
+
<h1>${escapeHtml8(title)}</h1>
|
|
9854
12267
|
<p><strong>Status:</strong> ${status.ready ? "Ready" : "Needs attention"}</p>
|
|
9855
12268
|
<section>
|
|
9856
12269
|
<h2>URLs</h2>
|
|
9857
12270
|
<ul>
|
|
9858
|
-
<li><strong>TwiML:</strong> <code>${
|
|
9859
|
-
<li><strong>Media stream:</strong> <code>${
|
|
9860
|
-
<li><strong>Status webhook:</strong> <code>${
|
|
12271
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml8(status.urls.twiml)}</code></li>
|
|
12272
|
+
<li><strong>Media stream:</strong> <code>${escapeHtml8(status.urls.stream)}</code></li>
|
|
12273
|
+
<li><strong>Status webhook:</strong> <code>${escapeHtml8(status.urls.webhook)}</code></li>
|
|
9861
12274
|
</ul>
|
|
9862
12275
|
</section>
|
|
9863
12276
|
<section>
|
|
9864
12277
|
<h2>Signing</h2>
|
|
9865
12278
|
<p>Mode: <code>${status.signing.mode}</code></p>
|
|
9866
|
-
${status.signing.verificationUrl ? `<p>Verification URL: <code>${
|
|
12279
|
+
${status.signing.verificationUrl ? `<p>Verification URL: <code>${escapeHtml8(status.signing.verificationUrl)}</code></p>` : ""}
|
|
9867
12280
|
</section>
|
|
9868
|
-
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${
|
|
9869
|
-
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${
|
|
12281
|
+
${status.missing.length ? `<section><h2>Missing env</h2><ul>${status.missing.map((name) => `<li><code>${escapeHtml8(name)}</code></li>`).join("")}</ul></section>` : ""}
|
|
12282
|
+
${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml8(warning)}</li>`).join("")}</ul></section>` : ""}
|
|
9870
12283
|
</main>`;
|
|
9871
12284
|
var extractTwilioStreamUrl = (twiml) => twiml.match(/<Stream\b[^>]*\surl="([^"]+)"/i)?.[1]?.replaceAll("&", "&");
|
|
9872
12285
|
var createSmokeCheck = (name, status, message, details) => ({
|
|
@@ -9877,20 +12290,20 @@ var createSmokeCheck = (name, status, message, details) => ({
|
|
|
9877
12290
|
});
|
|
9878
12291
|
var renderTwilioVoiceSmokeHTML = (report, title) => `<main style="font-family: ui-sans-serif, system-ui; max-width: 860px; margin: 40px auto; padding: 0 20px;">
|
|
9879
12292
|
<p style="letter-spacing: .12em; text-transform: uppercase; color: #52606d;">Twilio smoke test</p>
|
|
9880
|
-
<h1>${
|
|
12293
|
+
<h1>${escapeHtml8(title)}</h1>
|
|
9881
12294
|
<p><strong>Status:</strong> ${report.pass ? "Pass" : "Fail"}</p>
|
|
9882
12295
|
<section>
|
|
9883
12296
|
<h2>Checks</h2>
|
|
9884
12297
|
<ul>
|
|
9885
|
-
${report.checks.map((check) => `<li><strong>${
|
|
12298
|
+
${report.checks.map((check) => `<li><strong>${escapeHtml8(check.name)}</strong>: ${escapeHtml8(check.status)}${check.message ? ` - ${escapeHtml8(check.message)}` : ""}</li>`).join("")}
|
|
9886
12299
|
</ul>
|
|
9887
12300
|
</section>
|
|
9888
12301
|
<section>
|
|
9889
12302
|
<h2>Observed URLs</h2>
|
|
9890
12303
|
<ul>
|
|
9891
|
-
<li><strong>TwiML:</strong> <code>${
|
|
9892
|
-
<li><strong>Stream:</strong> <code>${
|
|
9893
|
-
<li><strong>Webhook:</strong> <code>${
|
|
12304
|
+
<li><strong>TwiML:</strong> <code>${escapeHtml8(report.setup.urls.twiml)}</code></li>
|
|
12305
|
+
<li><strong>Stream:</strong> <code>${escapeHtml8(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
|
|
12306
|
+
<li><strong>Webhook:</strong> <code>${escapeHtml8(report.setup.urls.webhook)}</code></li>
|
|
9894
12307
|
</ul>
|
|
9895
12308
|
</section>
|
|
9896
12309
|
</main>`;
|
|
@@ -10436,7 +12849,7 @@ var createTwilioVoiceRoutes = (options) => {
|
|
|
10436
12849
|
const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/twilio/smoke";
|
|
10437
12850
|
const bridges = new WeakMap;
|
|
10438
12851
|
const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
|
|
10439
|
-
const app = new
|
|
12852
|
+
const app = new Elysia6({
|
|
10440
12853
|
name: options.name ?? "absolutejs-voice-twilio"
|
|
10441
12854
|
}).get(twimlPath, async ({ query, request }) => {
|
|
10442
12855
|
const streamUrl = await resolveTwilioStreamUrl(options, {
|
|
@@ -10714,6 +13127,104 @@ var createFakeTTSAdapter = (chunkCount, chunkDelayMs) => ({
|
|
|
10714
13127
|
};
|
|
10715
13128
|
}
|
|
10716
13129
|
});
|
|
13130
|
+
var defaultOperationsRecordHref = ({
|
|
13131
|
+
sessionId
|
|
13132
|
+
}) => `/voice-operations/${encodeURIComponent(sessionId)}`;
|
|
13133
|
+
var resolveOperationsRecordHref = (href, input) => typeof href === "function" ? href(input) : href === undefined ? defaultOperationsRecordHref(input) : href;
|
|
13134
|
+
var runVoiceTelephonyMediaOperationsSmoke = async (options = {}) => {
|
|
13135
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
13136
|
+
const sessionId = options.sessionId ?? `telephony-media-ops-${Date.now()}`;
|
|
13137
|
+
const streamSid = options.streamSid ?? "telephony-media-ops-stream";
|
|
13138
|
+
const callSid = options.callSid ?? "telephony-media-ops-call";
|
|
13139
|
+
const scenarioId = options.scenarioId ?? "telephony-media-operations-smoke";
|
|
13140
|
+
const trace = options.store ?? createVoiceMemoryTraceEventStore();
|
|
13141
|
+
const sentEvents = [];
|
|
13142
|
+
const receivedAudio = [];
|
|
13143
|
+
const bridge = createTwilioMediaStreamBridge({
|
|
13144
|
+
close: () => {},
|
|
13145
|
+
send: (data) => {
|
|
13146
|
+
sentEvents.push(JSON.parse(data));
|
|
13147
|
+
}
|
|
13148
|
+
}, {
|
|
13149
|
+
context: {},
|
|
13150
|
+
onComplete: async () => {},
|
|
13151
|
+
onTurn: async () => ({
|
|
13152
|
+
assistantText: "Confirmed. Two way carrier media is visible."
|
|
13153
|
+
}),
|
|
13154
|
+
session: createVoiceMemoryStore(),
|
|
13155
|
+
stt: createFakeSTTAdapter(receivedAudio, 0),
|
|
13156
|
+
trace,
|
|
13157
|
+
tts: createFakeTTSAdapter(1, 0),
|
|
13158
|
+
turnDetection: {
|
|
13159
|
+
transcriptStabilityMs: 0
|
|
13160
|
+
}
|
|
13161
|
+
});
|
|
13162
|
+
const payload = encodeTwilioMulawBase64(new Int16Array([500, -500, 1500, -1500, 2500, -2500]));
|
|
13163
|
+
await bridge.handleMessage({
|
|
13164
|
+
event: "start",
|
|
13165
|
+
start: {
|
|
13166
|
+
callSid,
|
|
13167
|
+
customParameters: {
|
|
13168
|
+
scenarioId,
|
|
13169
|
+
sessionId
|
|
13170
|
+
},
|
|
13171
|
+
streamSid
|
|
13172
|
+
},
|
|
13173
|
+
streamSid
|
|
13174
|
+
});
|
|
13175
|
+
await bridge.handleMessage({
|
|
13176
|
+
event: "media",
|
|
13177
|
+
media: {
|
|
13178
|
+
payload,
|
|
13179
|
+
track: "inbound"
|
|
13180
|
+
},
|
|
13181
|
+
streamSid
|
|
13182
|
+
});
|
|
13183
|
+
await waitFor(() => sentEvents.some((message) => message.event === "media"), timeoutMs);
|
|
13184
|
+
await bridge.handleMessage({
|
|
13185
|
+
event: "media",
|
|
13186
|
+
media: {
|
|
13187
|
+
payload,
|
|
13188
|
+
track: "inbound"
|
|
13189
|
+
},
|
|
13190
|
+
streamSid
|
|
13191
|
+
});
|
|
13192
|
+
await waitFor(() => sentEvents.some((message) => message.event === "clear"), timeoutMs);
|
|
13193
|
+
await bridge.handleMessage({
|
|
13194
|
+
event: "stop",
|
|
13195
|
+
stop: {
|
|
13196
|
+
callSid
|
|
13197
|
+
},
|
|
13198
|
+
streamSid
|
|
13199
|
+
});
|
|
13200
|
+
await bridge.close("telephony-media-operations-smoke-complete");
|
|
13201
|
+
const operationsRecord = await buildVoiceOperationsRecord({
|
|
13202
|
+
sessionId,
|
|
13203
|
+
store: trace
|
|
13204
|
+
});
|
|
13205
|
+
const media = operationsRecord.telephonyMedia;
|
|
13206
|
+
const issues = [
|
|
13207
|
+
media.starts < 1 ? "Missing telephony media start event." : undefined,
|
|
13208
|
+
media.stops < 1 ? "Missing telephony media stop event." : undefined,
|
|
13209
|
+
media.inbound < 2 ? "Expected at least two inbound telephony media events." : undefined,
|
|
13210
|
+
media.outbound < 2 ? "Expected outbound assistant media/control evidence." : undefined,
|
|
13211
|
+
media.media < 3 ? "Expected inbound and outbound telephony media packet evidence." : undefined,
|
|
13212
|
+
media.clears < 1 ? "Missing outbound clear evidence." : undefined,
|
|
13213
|
+
media.audioBytes <= 0 ? "Missing telephony media audio bytes." : undefined,
|
|
13214
|
+
media.carriers.includes("twilio") ? undefined : "Missing Twilio carrier evidence.",
|
|
13215
|
+
media.streamIds.includes(streamSid) ? undefined : "Missing telephony media stream ID evidence."
|
|
13216
|
+
].filter((issue) => typeof issue === "string");
|
|
13217
|
+
return {
|
|
13218
|
+
issues,
|
|
13219
|
+
ok: issues.length === 0,
|
|
13220
|
+
operationsRecord,
|
|
13221
|
+
operationsRecordHref: resolveOperationsRecordHref(options.operationsRecordHref, { sessionId, streamSid }),
|
|
13222
|
+
sentEvents: sentEvents.map((message) => message.event).filter((event) => typeof event === "string"),
|
|
13223
|
+
sessionId,
|
|
13224
|
+
streamSid,
|
|
13225
|
+
telephonyMedia: media
|
|
13226
|
+
};
|
|
13227
|
+
};
|
|
10717
13228
|
var getDefaultVoiceTelephonyBenchmarkScenarios = () => DEFAULT_SCENARIOS2.map((scenario) => ({ ...scenario }));
|
|
10718
13229
|
var runVoiceTelephonyBenchmarkScenario = async (scenario, options = {}) => {
|
|
10719
13230
|
const timeoutMs = options.timeoutMs ?? 1000;
|
|
@@ -11035,6 +13546,7 @@ export {
|
|
|
11035
13546
|
summarizeSTTBenchmark,
|
|
11036
13547
|
scoreTranscriptAccuracy,
|
|
11037
13548
|
scoreCorrectedExpectedTerms,
|
|
13549
|
+
runVoiceTelephonyMediaOperationsSmoke,
|
|
11038
13550
|
runVoiceTelephonyBenchmarkScenario,
|
|
11039
13551
|
runVoiceTelephonyBenchmark,
|
|
11040
13552
|
runVoiceSessionBenchmarkSeries,
|