@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.
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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 Elysia2 } from "elysia";
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 uniqueSorted = (values) => Array.from(new Set(values)).sort();
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 = uniqueSorted(decisions.map((decision) => decision.decision?.action ?? decision.action).filter(isTelephonyOutcomeAction));
9073
- const dispositions = uniqueSorted(decisions.map((decision) => decision.decision?.disposition ?? decision.disposition).filter(isCallDisposition));
9074
- const providers = uniqueSorted(decisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
9075
- const sources = uniqueSorted(decisions.map((decision) => decision.decision?.source ?? decision.source).filter((source) => typeof source === "string"));
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 = uniqueSorted(duplicateDecisions.map((decision) => decision.provider ?? decision.event?.provider).filter(isTelephonyWebhookProvider));
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 = uniqueSorted(duplicateDecisions.map((decision) => isRecord(decision.campaignOutcome) ? decision.campaignOutcome.reason : undefined).filter((reason) => typeof reason === "string"));
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 = uniqueSorted(rejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
11498
+ const rejectedVerificationProviders = uniqueSorted2(rejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
9086
11499
  const replayRejectedVerificationAttempts = rejectedVerificationAttempts.filter((attempt) => attempt.replayRejected === true);
9087
- const replayRejectedVerificationProviders = uniqueSorted(replayRejectedVerificationAttempts.map((attempt) => attempt.provider).filter(isTelephonyWebhookProvider));
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 firstString2 = (source, keys) => {
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 firstNumber2 = (source, keys) => {
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 = firstString2(payload, ["provider", "Provider"]) ?? input.provider;
9542
- const status = firstString2(payload, [
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 = firstNumber2(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber2(payload, [
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 = firstNumber2(payload, [
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 = firstString2(payload, ["From", "from", "caller_id", "callerId"]);
9568
- const to = firstString2(payload, ["To", "to", "called_number", "calledNumber"]);
9569
- const target = firstString2(payload, [
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: firstString2(payload, [
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: firstString2(payload, [
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 firstString2(input.query, ["sessionId", "session_id"]) ?? firstString2(payload, [
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 = firstString2(payload, [
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 Elysia({
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 escapeHtml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
12223
+ var escapeHtml8 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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>${escapeHtml2(title)}</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>${escapeHtml2(status.urls.twiml)}</code></li>
9859
- <li><strong>Media stream:</strong> <code>${escapeHtml2(status.urls.stream)}</code></li>
9860
- <li><strong>Status webhook:</strong> <code>${escapeHtml2(status.urls.webhook)}</code></li>
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>${escapeHtml2(status.signing.verificationUrl)}</code></p>` : ""}
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>${escapeHtml2(name)}</code></li>`).join("")}</ul></section>` : ""}
9869
- ${status.warnings.length ? `<section><h2>Warnings</h2><ul>${status.warnings.map((warning) => `<li>${escapeHtml2(warning)}</li>`).join("")}</ul></section>` : ""}
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("&amp;", "&");
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>${escapeHtml2(title)}</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>${escapeHtml2(check.name)}</strong>: ${escapeHtml2(check.status)}${check.message ? ` - ${escapeHtml2(check.message)}` : ""}</li>`).join("")}
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>${escapeHtml2(report.setup.urls.twiml)}</code></li>
9892
- <li><strong>Stream:</strong> <code>${escapeHtml2(report.twiml?.streamUrl ?? report.setup.urls.stream)}</code></li>
9893
- <li><strong>Webhook:</strong> <code>${escapeHtml2(report.setup.urls.webhook)}</code></li>
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 Elysia2({
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,