@absolutejs/voice 0.0.22-beta.327 → 0.0.22-beta.329

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -141,7 +141,7 @@ export type { VoiceProductionReadinessAction, VoiceProductionReadinessAuditOptio
141
141
  export type { VoiceMonitorDefinition, VoiceMonitorEvaluation, VoiceMonitorEvaluationInput, VoiceMonitorIssue, VoiceMonitorIssueStatus, VoiceMonitorIssueStore, VoiceMonitorNotifier, VoiceMonitorNotifierDeliveryInput, VoiceMonitorNotifierDeliveryOptions, VoiceMonitorNotifierDeliveryReceipt, VoiceMonitorNotifierDeliveryReceiptStore, VoiceMonitorNotifierDeliveryReport, VoiceMonitorNotifierDeliveryResult, VoiceMonitorRoutesOptions, VoiceMonitorRun, VoiceMonitorRunOptions, VoiceMonitorRunReport, VoiceMonitorRunner, VoiceMonitorRunnerOptions, VoiceMonitorRunnerRoutesOptions, VoiceMonitorRunnerTickResult, VoiceMonitorSeverity, VoiceMonitorStatus, VoiceMonitorWebhookNotifierOptions } from './voiceMonitoring';
142
142
  export type { VoiceReadinessProfileName, VoiceReadinessProfileOptions, VoiceReadinessProfileRecommendation, VoiceReadinessProfileRecommendationScore, VoiceReadinessProfileRoutesOptions } from './readinessProfiles';
143
143
  export type { VoiceProviderStackChoice, VoiceProviderStackCapabilities, VoiceProviderStackCapabilityGap, VoiceProviderStackCapabilityGapInput, VoiceProviderStackCapabilityGapReport, VoiceProviderContractCheck, VoiceProviderContractCheckStatus, VoiceProviderContractDefinition, VoiceProviderContractMatrixAssertionInput, VoiceProviderContractMatrixAssertionReport, VoiceProviderContractMatrixHandlerOptions, VoiceProviderContractMatrixHTMLHandlerOptions, VoiceProviderContractMatrixInput, VoiceProviderContractMatrixPresetOptions, VoiceProviderContractMatrixReport, VoiceProviderContractMatrixRoutesOptions, VoiceProviderContractMatrixRow, VoiceProviderStackAssertionInput, VoiceProviderStackAssertionReport, VoiceProviderStackInput, VoiceProviderStackKind, VoiceProviderStackRecommendation } from './providerStackRecommendations';
144
- export type { VoiceOperationsRecord, VoiceOperationsRecordAgentHandoff, VoiceOperationsRecordAuditSummary, VoiceOperationsRecordGuardrailAssertionInput, VoiceOperationsRecordGuardrailAssertionReport, VoiceOperationsRecordGuardrailDecision, VoiceOperationsRecordGuardrailFinding, VoiceOperationsRecordGuardrailSummary, VoiceOperationsRecordIntegrationEventSummary, VoiceOperationsRecordOptions, VoiceOperationsRecordOutcome, VoiceOperationsRecordProviderDecision, VoiceOperationsRecordProviderDecisionRecoveryStatus, VoiceOperationsRecordProviderDecisionSummary, VoiceOperationsRecordProviderRecoveryAssertionInput, VoiceOperationsRecordProviderRecoveryAssertionReport, VoiceOperationsRecordReviewSummary, VoiceOperationsRecordRoutesOptions, VoiceOperationsRecordStatus, VoiceOperationsRecordTaskSummary, VoiceOperationsRecordTranscriptTurn, VoiceOperationsRecordTool } from './operationsRecord';
144
+ export type { VoiceOperationsRecord, VoiceOperationsRecordAgentHandoff, VoiceOperationsRecordAuditSummary, VoiceOperationsRecordGuardrailAssertionInput, VoiceOperationsRecordGuardrailAssertionReport, VoiceOperationsRecordGuardrailDecision, VoiceOperationsRecordGuardrailFinding, VoiceOperationsRecordGuardrailSummary, VoiceOperationsRecordIntegrationEventSummary, VoiceOperationsRecordOptions, VoiceOperationsRecordOutcome, VoiceOperationsRecordProviderDecision, VoiceOperationsRecordProviderDecisionRecoveryStatus, VoiceOperationsRecordProviderDecisionSummary, VoiceOperationsRecordProviderRecoveryAssertionInput, VoiceOperationsRecordProviderRecoveryAssertionReport, VoiceOperationsRecordReviewSummary, VoiceOperationsRecordRoutesOptions, VoiceOperationsRecordStatus, VoiceOperationsRecordTaskSummary, VoiceOperationsRecordTelephonyMediaEvent, VoiceOperationsRecordTelephonyMediaSummary, VoiceOperationsRecordTranscriptTurn, VoiceOperationsRecordTool } from './operationsRecord';
145
145
  export type { VoiceObservabilityExportArtifact, VoiceObservabilityExportArtifactChecksum, VoiceObservabilityExportArtifactFreshness, VoiceObservabilityExportArtifactIndex, VoiceObservabilityExportArtifactIndexItem, VoiceObservabilityExportArtifactKind, VoiceObservabilityExportDeliveryAssertionInput, VoiceObservabilityExportDeliveryAssertionReport, VoiceObservabilityExportDeliverySummary, VoiceObservabilityExportDeliveryDestination, VoiceObservabilityExportDeliveryDestinationResult, VoiceObservabilityExportDeliveryHistory, VoiceObservabilityExportDeliveryOptions, VoiceObservabilityExportDeliveryReceipt, VoiceObservabilityExportDeliveryReceiptStore, VoiceObservabilityExportDeliveryReport, VoiceObservabilityExportEnvelope, VoiceObservabilityExportIssue, VoiceObservabilityExportIssueCode, VoiceObservabilityExportOptions, VoiceObservabilityExportIngestedRecordKind, VoiceObservabilityExportRedactionSummary, VoiceObservabilityExportRecordValidationOptions, VoiceObservabilityExportReplayIssue, VoiceObservabilityExportReplayIssueCode, VoiceObservabilityExportReplayAssertionInput, VoiceObservabilityExportReplayAssertionReport, VoiceObservabilityExportReplayRecords, VoiceObservabilityExportReplayReport, VoiceObservabilityExportReplayRoutesOptions, VoiceObservabilityExportReplaySource, VoiceObservabilityExportReport, VoiceObservabilityExportRoutesOptions, VoiceObservabilityExportSchema, VoiceObservabilityExportStatus, VoiceObservabilityExportValidationIssue, VoiceObservabilityExportValidationResult } from './observabilityExport';
146
146
  export type { VoiceOpsRecoveryFailedSession, VoiceOpsRecoveryInterventionSummary, VoiceOpsRecoveryIssue, VoiceOpsRecoveryIssueCode, VoiceOpsRecoveryLinks, VoiceOpsRecoveryProviderSummary, VoiceOpsRecoveryReport, VoiceOpsRecoveryReportOptions, VoiceOpsRecoveryRoutesOptions, VoiceOpsRecoveryStatus } from './opsRecovery';
147
147
  export type { StoredVoiceIncidentBundleArtifact, VoiceIncidentBundle, VoiceIncidentBundleArtifactOptions, VoiceIncidentBundleFormat, VoiceIncidentBundleOptions, VoiceIncidentBundleRetentionOptions, VoiceIncidentBundleRetentionReport, VoiceIncidentBundleRoutesOptions, VoiceIncidentBundleStore, VoiceIncidentBundleStoreFilter, VoiceIncidentBundleSummary } from './incidentBundle';
package/dist/index.js CHANGED
@@ -21722,12 +21722,35 @@ var createTwilioSocketAdapter = (socket, getState) => ({
21722
21722
  }
21723
21723
  if (message.type === "audio") {
21724
21724
  const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer4.from(message.chunkBase64, "base64")), message.format);
21725
+ const outboundMessage = {
21726
+ event: "media",
21727
+ media: {
21728
+ payload,
21729
+ track: "outbound"
21730
+ },
21731
+ streamSid: state.streamSid
21732
+ };
21725
21733
  state.hasOutboundAudioSinceLastInbound = true;
21726
21734
  state.reviewRecorder?.recordTwilioOutbound({
21727
21735
  bytes: payload.length,
21728
21736
  event: "media",
21729
21737
  track: "outbound"
21730
21738
  });
21739
+ await state.trace?.append({
21740
+ at: Date.now(),
21741
+ payload: {
21742
+ audioBytes: Buffer4.from(payload, "base64").byteLength,
21743
+ callSid: state.callSid ?? undefined,
21744
+ carrier: state.carrier,
21745
+ direction: "outbound",
21746
+ envelope: outboundMessage,
21747
+ event: "media",
21748
+ streamId: state.streamSid
21749
+ },
21750
+ scenarioId: state.scenarioId ?? undefined,
21751
+ sessionId: state.sessionId ?? state.streamSid,
21752
+ type: "client.telephony_media"
21753
+ });
21731
21754
  await Promise.resolve(socket.send(JSON.stringify({
21732
21755
  event: "media",
21733
21756
  media: {
@@ -21738,17 +21761,32 @@ var createTwilioSocketAdapter = (socket, getState) => ({
21738
21761
  return;
21739
21762
  }
21740
21763
  if (message.type === "assistant" && message.turnId) {
21741
- state.reviewRecorder?.recordTwilioOutbound({
21742
- event: "mark",
21743
- name: `assistant:${message.turnId}`
21744
- });
21745
- await Promise.resolve(socket.send(JSON.stringify({
21764
+ const outboundMessage = {
21746
21765
  event: "mark",
21747
21766
  mark: {
21748
21767
  name: `assistant:${message.turnId}`
21749
21768
  },
21750
21769
  streamSid: state.streamSid
21751
- })));
21770
+ };
21771
+ state.reviewRecorder?.recordTwilioOutbound({
21772
+ event: "mark",
21773
+ name: `assistant:${message.turnId}`
21774
+ });
21775
+ await state.trace?.append({
21776
+ at: Date.now(),
21777
+ payload: {
21778
+ callSid: state.callSid ?? undefined,
21779
+ carrier: state.carrier,
21780
+ direction: "outbound",
21781
+ envelope: outboundMessage,
21782
+ event: "mark",
21783
+ streamId: state.streamSid
21784
+ },
21785
+ scenarioId: state.scenarioId ?? undefined,
21786
+ sessionId: state.sessionId ?? state.streamSid,
21787
+ type: "client.telephony_media"
21788
+ });
21789
+ await Promise.resolve(socket.send(JSON.stringify(outboundMessage)));
21752
21790
  }
21753
21791
  }
21754
21792
  });
@@ -21771,6 +21809,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
21771
21809
  };
21772
21810
  const bridgeState = {
21773
21811
  callSid: null,
21812
+ carrier: options.telephonyMediaCarrier ?? "twilio",
21774
21813
  hasOutboundAudioSinceLastInbound: false,
21775
21814
  onVoiceMessage: options.onVoiceMessage,
21776
21815
  reviewRecorder: options.review ? createVoiceCallReviewRecorder({
@@ -21790,11 +21829,12 @@ var createTwilioMediaStreamBridge = (socket, options) => {
21790
21829
  }) : undefined,
21791
21830
  scenarioId: options.scenarioId ?? null,
21792
21831
  sessionId: options.sessionId ?? null,
21793
- streamSid: null
21832
+ streamSid: null,
21833
+ trace: options.trace
21794
21834
  };
21795
21835
  let sessionHandle = null;
21796
21836
  let reviewArtifactDelivered = false;
21797
- const telephonyMediaCarrier = options.telephonyMediaCarrier ?? "twilio";
21837
+ const telephonyMediaCarrier = bridgeState.carrier;
21798
21838
  const appendTelephonyMediaTrace = async (message, override) => {
21799
21839
  const trace = options.trace;
21800
21840
  const sessionId = override?.sessionId ?? bridgeState.sessionId ?? (message.event === "start" ? message.start.customParameters?.sessionId : undefined) ?? (message.event === "start" ? message.start.streamSid : "telephony-media");
@@ -21928,13 +21968,28 @@ var createTwilioMediaStreamBridge = (socket, options) => {
21928
21968
  track: message.media.track
21929
21969
  });
21930
21970
  if (options.clearOnInboundMedia !== false && bridgeState.hasOutboundAudioSinceLastInbound && bridgeState.streamSid) {
21971
+ const outboundMessage = {
21972
+ event: "clear",
21973
+ streamSid: bridgeState.streamSid
21974
+ };
21931
21975
  bridgeState.reviewRecorder?.recordTwilioOutbound({
21932
21976
  event: "clear"
21933
21977
  });
21934
- await Promise.resolve(socket.send(JSON.stringify({
21935
- event: "clear",
21936
- streamSid: bridgeState.streamSid
21937
- })));
21978
+ await options.trace?.append({
21979
+ at: Date.now(),
21980
+ payload: {
21981
+ callSid: bridgeState.callSid ?? undefined,
21982
+ carrier: telephonyMediaCarrier,
21983
+ direction: "outbound",
21984
+ envelope: outboundMessage,
21985
+ event: "clear",
21986
+ streamId: bridgeState.streamSid
21987
+ },
21988
+ scenarioId: bridgeState.scenarioId ?? undefined,
21989
+ sessionId: bridgeState.sessionId ?? bridgeState.streamSid,
21990
+ type: "client.telephony_media"
21991
+ });
21992
+ await Promise.resolve(socket.send(JSON.stringify(outboundMessage)));
21938
21993
  }
21939
21994
  bridgeState.hasOutboundAudioSinceLastInbound = false;
21940
21995
  await appendTelephonyMediaTrace(message, {
@@ -27765,6 +27820,7 @@ var createVoiceTraceTimelineRoutes = (options) => {
27765
27820
  var getString17 = (value) => typeof value === "string" ? value : undefined;
27766
27821
  var getNumber10 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
27767
27822
  var getBoolean3 = (value) => typeof value === "boolean" ? value : undefined;
27823
+ var getRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
27768
27824
  var countOutcome = (events, outcome) => events.filter((event) => event.outcome === outcome).length;
27769
27825
  var matchesSessionScopedId = (id, sessionId) => id === sessionId || id.startsWith(`${sessionId}:`);
27770
27826
  var hasPayloadValue = (payload, key, values) => {
@@ -27823,6 +27879,60 @@ var toGuardrailDecision = (event) => ({
27823
27879
  toolName: getString17(event.payload.toolName),
27824
27880
  turnId: event.turnId
27825
27881
  });
27882
+ var decodeBase64Bytes = (value) => {
27883
+ if (typeof value !== "string") {
27884
+ return 0;
27885
+ }
27886
+ try {
27887
+ return Buffer.from(value, "base64").byteLength;
27888
+ } catch {
27889
+ return 0;
27890
+ }
27891
+ };
27892
+ var toTelephonyMediaEvent = (event) => {
27893
+ if (event.type !== "client.telephony_media") {
27894
+ return;
27895
+ }
27896
+ const envelope = getRecord(event.payload.envelope);
27897
+ const start = getRecord(envelope?.start);
27898
+ const media = getRecord(envelope?.media);
27899
+ const stop = getRecord(envelope?.stop);
27900
+ const error = getRecord(envelope?.error);
27901
+ const eventName = getString17(event.payload.event) ?? getString17(envelope?.event) ?? getString17(event.payload.kind) ?? "unknown";
27902
+ const streamId = getString17(event.payload.streamId) ?? getString17(envelope?.streamSid) ?? getString17(start?.streamSid);
27903
+ const callSid = getString17(event.payload.callSid) ?? getString17(start?.callSid) ?? getString17(stop?.callSid);
27904
+ const sequenceNumber = getString17(media?.sequenceNumber) ?? getString17(envelope?.sequenceNumber);
27905
+ const direction = getString17(media?.track) ?? getString17(media?.direction) ?? getString17(event.payload.direction);
27906
+ return {
27907
+ at: event.at,
27908
+ audioBytes: getNumber10(event.payload.audioBytes) ?? getNumber10(media?.audioBytes) ?? decodeBase64Bytes(media?.payload),
27909
+ callSid,
27910
+ carrier: getString17(event.payload.carrier),
27911
+ direction,
27912
+ event: eventName,
27913
+ sequenceNumber,
27914
+ streamId
27915
+ };
27916
+ };
27917
+ var summarizeTelephonyMedia = (events) => {
27918
+ const mediaEvents = events.map(toTelephonyMediaEvent).filter((event) => event !== undefined);
27919
+ const eventNames = mediaEvents.map((event) => event.event.toLowerCase());
27920
+ return {
27921
+ audioBytes: mediaEvents.reduce((total, event) => total + event.audioBytes, 0),
27922
+ carriers: uniqueSorted8(mediaEvents.map((event) => event.carrier)),
27923
+ clears: eventNames.filter((event) => event === "clear").length,
27924
+ errors: eventNames.filter((event) => event === "error").length,
27925
+ events: mediaEvents,
27926
+ inbound: mediaEvents.filter((event) => event.direction === "inbound").length,
27927
+ marks: eventNames.filter((event) => event === "mark").length,
27928
+ media: eventNames.filter((event) => event === "media").length,
27929
+ outbound: mediaEvents.filter((event) => event.direction === "outbound").length,
27930
+ starts: eventNames.filter((event) => event === "start").length,
27931
+ stops: eventNames.filter((event) => event === "stop").length,
27932
+ streamIds: uniqueSorted8(mediaEvents.map((event) => event.streamId)),
27933
+ total: mediaEvents.length
27934
+ };
27935
+ };
27826
27936
  var summarizeGuardrails = (events) => {
27827
27937
  const decisions = events.filter((event) => event.type === "assistant.guardrail").map(toGuardrailDecision);
27828
27938
  const isBlocked = (decision) => decision.allowed === false || decision.status === "fail";
@@ -27970,6 +28080,7 @@ var buildVoiceOperationsRecord = async (options) => {
27970
28080
  tasks,
27971
28081
  total: tasks.length
27972
28082
  } : undefined,
28083
+ telephonyMedia: summarizeTelephonyMedia(traceEvents),
27973
28084
  timeline: timelineSession?.events ?? [],
27974
28085
  tools: traceEvents.filter((event) => event.type === "agent.tool").map(toTool),
27975
28086
  traceEvents,
@@ -28174,6 +28285,31 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
28174
28285
  `degraded=${String(providerDecisionSummary.degraded)}`,
28175
28286
  `errors=${String(providerDecisionSummary.errors)}`
28176
28287
  ].join("; ");
28288
+ const telephonyMediaLine = [
28289
+ `events=${String(record.telephonyMedia.total)}`,
28290
+ `starts=${String(record.telephonyMedia.starts)}`,
28291
+ `media=${String(record.telephonyMedia.media)}`,
28292
+ `inbound=${String(record.telephonyMedia.inbound)}`,
28293
+ `outbound=${String(record.telephonyMedia.outbound)}`,
28294
+ `marks=${String(record.telephonyMedia.marks)}`,
28295
+ `clears=${String(record.telephonyMedia.clears)}`,
28296
+ `stops=${String(record.telephonyMedia.stops)}`,
28297
+ `errors=${String(record.telephonyMedia.errors)}`,
28298
+ `audioBytes=${String(record.telephonyMedia.audioBytes)}`,
28299
+ `carriers=${record.telephonyMedia.carriers.join(", ") || "none"}`,
28300
+ `streams=${record.telephonyMedia.streamIds.join(", ") || "none"}`
28301
+ ].join("; ");
28302
+ const telephonyMediaLines = record.telephonyMedia.events.length ? record.telephonyMedia.events.slice(0, 12).map((event) => {
28303
+ const parts = [
28304
+ event.carrier ? `carrier=${event.carrier}` : undefined,
28305
+ event.streamId ? `stream=${event.streamId}` : undefined,
28306
+ event.callSid ? `call=${event.callSid}` : undefined,
28307
+ event.direction ? `direction=${event.direction}` : undefined,
28308
+ event.sequenceNumber ? `seq=${event.sequenceNumber}` : undefined,
28309
+ `audioBytes=${String(event.audioBytes)}`
28310
+ ].filter((part) => typeof part === "string");
28311
+ return `- ${event.event}: ${parts.join("; ")}`;
28312
+ }) : ["- none recorded"];
28177
28313
  return [
28178
28314
  `# Voice incident handoff: ${record.sessionId}`,
28179
28315
  "",
@@ -28187,11 +28323,16 @@ var renderVoiceOperationsRecordIncidentMarkdown = (record) => {
28187
28323
  `- Top errors: ${topErrors.join("; ") || "none"}`,
28188
28324
  `- Guardrails: ${String(record.guardrails.blocked)} blocked / ${String(record.guardrails.warned)} warned / ${String(record.guardrails.total)} decisions`,
28189
28325
  `- Provider recovery: ${providerRecoveryLine}`,
28326
+ `- Telephony media: ${telephonyMediaLine}`,
28190
28327
  "",
28191
28328
  "## Provider decisions",
28192
28329
  "",
28193
28330
  ...providerDecisionLines,
28194
28331
  "",
28332
+ "## Telephony media",
28333
+ "",
28334
+ ...telephonyMediaLines,
28335
+ "",
28195
28336
  renderVoiceOperationsRecordGuardrailMarkdown(record),
28196
28337
  "",
28197
28338
  "## Next checks",
@@ -28234,6 +28375,17 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
28234
28375
  const findings = decision.findings.map((finding) => finding.label ?? finding.ruleId ?? finding.action).filter((value) => typeof value === "string").join(", ") || "none";
28235
28376
  return `<li><strong>assistant.guardrail ${escapeHtml48(decision.stage ?? "unknown")}</strong> <span>${escapeHtml48(decision.status ?? "")}</span><p>Allowed: ${escapeHtml48(String(decision.allowed ?? "unknown"))} \xB7 Proof: ${escapeHtml48(decision.proof ?? "runtime")}${decision.turnId ? ` \xB7 Turn: ${escapeHtml48(decision.turnId)}` : ""}</p><p>${escapeHtml48(findings)}</p></li>`;
28236
28377
  }).join("") : "<li>No assistant.guardrail events recorded.</li>";
28378
+ const telephonyMedia = record.telephonyMedia.events.length ? record.telephonyMedia.events.slice(0, 50).map((event) => {
28379
+ const details = [
28380
+ event.carrier ? `Carrier: ${event.carrier}` : undefined,
28381
+ event.streamId ? `Stream: ${event.streamId}` : undefined,
28382
+ event.callSid ? `Call: ${event.callSid}` : undefined,
28383
+ event.direction ? `Direction: ${event.direction}` : undefined,
28384
+ event.sequenceNumber ? `Seq: ${event.sequenceNumber}` : undefined,
28385
+ `Audio bytes: ${String(event.audioBytes)}`
28386
+ ].filter((detail) => typeof detail === "string");
28387
+ return `<li><strong>${escapeHtml48(event.event)}</strong> <span>${escapeHtml48(new Date(event.at).toLocaleString())}</span><p>${escapeHtml48(details.join(" \xB7 "))}</p></li>`;
28388
+ }).join("") : "<li>No telephony media trace events recorded.</li>";
28237
28389
  const snippet = escapeHtml48(`app.use(
28238
28390
  createVoiceOperationsRecordRoutes({
28239
28391
  audit: auditStore,
@@ -28250,7 +28402,7 @@ var renderVoiceOperationsRecordHTML = (record, options = {}) => {
28250
28402
  );`);
28251
28403
  const incidentMarkdown = escapeHtml48(renderVoiceOperationsRecordIncidentMarkdown(record));
28252
28404
  const incidentLink = options.incidentHref ? `<a href="${escapeHtml48(options.incidentHref)}">Download incident.md</a>` : "";
28253
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml48(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>${escapeHtml48(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml48(record.status)}">${escapeHtml48(record.status)}</p><div class="hero-actions"><a href="#transcript">Transcript</a><a href="#provider-decisions">Provider decisions</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>${formatMs5(record.summary.callDurationMs)}</strong></div><div class="card"><span>Provider recovery</span><strong>${escapeHtml48(providerDecisionSummary.recoveryStatus)}</strong><span>${String(providerDecisionSummary.fallbacks)} fallback / ${String(providerDecisionSummary.degraded)} degraded / ${String(providerDecisionSummary.errors)} errors</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="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, 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>`;
28405
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml48(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>${escapeHtml48(options.title ?? "Voice Operations Record")}</h1><p class="status ${escapeHtml48(record.status)}">${escapeHtml48(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>${formatMs5(record.summary.callDurationMs)}</strong></div><div class="card"><span>Provider recovery</span><strong>${escapeHtml48(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: ${escapeHtml48(record.telephonyMedia.carriers.join(", ") || "none")}. Streams: ${escapeHtml48(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>`;
28254
28406
  };
28255
28407
  var createVoiceOperationsRecordRoutes = (options) => {
28256
28408
  const path = options.path ?? "/api/voice-operations/:sessionId";
@@ -28323,7 +28475,7 @@ var assertVoiceObservabilityExportSchema = (input) => {
28323
28475
  };
28324
28476
  var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
28325
28477
  var isStatus2 = (value) => value === "fail" || value === "pass" || value === "warn";
28326
- var getRecord = (value, key) => isRecord2(value) && isRecord2(value[key]) ? value[key] : undefined;
28478
+ var getRecord2 = (value, key) => isRecord2(value) && isRecord2(value[key]) ? value[key] : undefined;
28327
28479
  var getRecordArray = (value, key) => isRecord2(value) && Array.isArray(value[key]) ? value[key] : undefined;
28328
28480
  var inferVoiceObservabilityExportRecordKind = (record) => {
28329
28481
  if (isRecord2(record.manifest) && isRecord2(record.artifactIndex)) {
@@ -28347,7 +28499,7 @@ var pushValidationIssue = (issues, issue) => {
28347
28499
  issues.push(issue);
28348
28500
  };
28349
28501
  var requireRecordSchema = (issues, record, path) => {
28350
- const schema = getRecord(record, "schema");
28502
+ const schema = getRecord2(record, "schema");
28351
28503
  if (schema?.id !== voiceObservabilityExportSchemaId || schema?.version !== voiceObservabilityExportSchemaVersion) {
28352
28504
  pushValidationIssue(issues, {
28353
28505
  code: "voice.observability.export.unsupported_schema",
@@ -118,6 +118,31 @@ export type VoiceOperationsRecordGuardrailSummary = {
118
118
  total: number;
119
119
  warned: number;
120
120
  };
121
+ export type VoiceOperationsRecordTelephonyMediaEvent = {
122
+ at: number;
123
+ audioBytes: number;
124
+ callSid?: string;
125
+ carrier?: string;
126
+ direction?: string;
127
+ event: string;
128
+ sequenceNumber?: string;
129
+ streamId?: string;
130
+ };
131
+ export type VoiceOperationsRecordTelephonyMediaSummary = {
132
+ audioBytes: number;
133
+ carriers: string[];
134
+ clears: number;
135
+ errors: number;
136
+ events: VoiceOperationsRecordTelephonyMediaEvent[];
137
+ inbound: number;
138
+ marks: number;
139
+ media: number;
140
+ outbound: number;
141
+ starts: number;
142
+ stops: number;
143
+ streamIds: string[];
144
+ total: number;
145
+ };
121
146
  export type VoiceOperationsRecordGuardrailAssertionInput = {
122
147
  minBlocked?: number;
123
148
  minDecisions?: number;
@@ -186,6 +211,7 @@ export type VoiceOperationsRecord = {
186
211
  status: VoiceOperationsRecordStatus;
187
212
  summary: VoiceTraceSummary;
188
213
  tasks?: VoiceOperationsRecordTaskSummary;
214
+ telephonyMedia: VoiceOperationsRecordTelephonyMediaSummary;
189
215
  timeline: VoiceTraceTimelineEvent[];
190
216
  tools: VoiceOperationsRecordTool[];
191
217
  traceEvents: StoredVoiceTraceEvent[];
@@ -10130,12 +10130,35 @@ var createTwilioSocketAdapter = (socket, getState) => ({
10130
10130
  }
10131
10131
  if (message.type === "audio") {
10132
10132
  const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
10133
+ const outboundMessage = {
10134
+ event: "media",
10135
+ media: {
10136
+ payload,
10137
+ track: "outbound"
10138
+ },
10139
+ streamSid: state.streamSid
10140
+ };
10133
10141
  state.hasOutboundAudioSinceLastInbound = true;
10134
10142
  state.reviewRecorder?.recordTwilioOutbound({
10135
10143
  bytes: payload.length,
10136
10144
  event: "media",
10137
10145
  track: "outbound"
10138
10146
  });
10147
+ await state.trace?.append({
10148
+ at: Date.now(),
10149
+ payload: {
10150
+ audioBytes: Buffer3.from(payload, "base64").byteLength,
10151
+ callSid: state.callSid ?? undefined,
10152
+ carrier: state.carrier,
10153
+ direction: "outbound",
10154
+ envelope: outboundMessage,
10155
+ event: "media",
10156
+ streamId: state.streamSid
10157
+ },
10158
+ scenarioId: state.scenarioId ?? undefined,
10159
+ sessionId: state.sessionId ?? state.streamSid,
10160
+ type: "client.telephony_media"
10161
+ });
10139
10162
  await Promise.resolve(socket.send(JSON.stringify({
10140
10163
  event: "media",
10141
10164
  media: {
@@ -10146,17 +10169,32 @@ var createTwilioSocketAdapter = (socket, getState) => ({
10146
10169
  return;
10147
10170
  }
10148
10171
  if (message.type === "assistant" && message.turnId) {
10149
- state.reviewRecorder?.recordTwilioOutbound({
10150
- event: "mark",
10151
- name: `assistant:${message.turnId}`
10152
- });
10153
- await Promise.resolve(socket.send(JSON.stringify({
10172
+ const outboundMessage = {
10154
10173
  event: "mark",
10155
10174
  mark: {
10156
10175
  name: `assistant:${message.turnId}`
10157
10176
  },
10158
10177
  streamSid: state.streamSid
10159
- })));
10178
+ };
10179
+ state.reviewRecorder?.recordTwilioOutbound({
10180
+ event: "mark",
10181
+ name: `assistant:${message.turnId}`
10182
+ });
10183
+ await state.trace?.append({
10184
+ at: Date.now(),
10185
+ payload: {
10186
+ callSid: state.callSid ?? undefined,
10187
+ carrier: state.carrier,
10188
+ direction: "outbound",
10189
+ envelope: outboundMessage,
10190
+ event: "mark",
10191
+ streamId: state.streamSid
10192
+ },
10193
+ scenarioId: state.scenarioId ?? undefined,
10194
+ sessionId: state.sessionId ?? state.streamSid,
10195
+ type: "client.telephony_media"
10196
+ });
10197
+ await Promise.resolve(socket.send(JSON.stringify(outboundMessage)));
10160
10198
  }
10161
10199
  }
10162
10200
  });
@@ -10179,6 +10217,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
10179
10217
  };
10180
10218
  const bridgeState = {
10181
10219
  callSid: null,
10220
+ carrier: options.telephonyMediaCarrier ?? "twilio",
10182
10221
  hasOutboundAudioSinceLastInbound: false,
10183
10222
  onVoiceMessage: options.onVoiceMessage,
10184
10223
  reviewRecorder: options.review ? createVoiceCallReviewRecorder({
@@ -10198,11 +10237,12 @@ var createTwilioMediaStreamBridge = (socket, options) => {
10198
10237
  }) : undefined,
10199
10238
  scenarioId: options.scenarioId ?? null,
10200
10239
  sessionId: options.sessionId ?? null,
10201
- streamSid: null
10240
+ streamSid: null,
10241
+ trace: options.trace
10202
10242
  };
10203
10243
  let sessionHandle = null;
10204
10244
  let reviewArtifactDelivered = false;
10205
- const telephonyMediaCarrier = options.telephonyMediaCarrier ?? "twilio";
10245
+ const telephonyMediaCarrier = bridgeState.carrier;
10206
10246
  const appendTelephonyMediaTrace = async (message, override) => {
10207
10247
  const trace = options.trace;
10208
10248
  const sessionId = override?.sessionId ?? bridgeState.sessionId ?? (message.event === "start" ? message.start.customParameters?.sessionId : undefined) ?? (message.event === "start" ? message.start.streamSid : "telephony-media");
@@ -10336,13 +10376,28 @@ var createTwilioMediaStreamBridge = (socket, options) => {
10336
10376
  track: message.media.track
10337
10377
  });
10338
10378
  if (options.clearOnInboundMedia !== false && bridgeState.hasOutboundAudioSinceLastInbound && bridgeState.streamSid) {
10379
+ const outboundMessage = {
10380
+ event: "clear",
10381
+ streamSid: bridgeState.streamSid
10382
+ };
10339
10383
  bridgeState.reviewRecorder?.recordTwilioOutbound({
10340
10384
  event: "clear"
10341
10385
  });
10342
- await Promise.resolve(socket.send(JSON.stringify({
10343
- event: "clear",
10344
- streamSid: bridgeState.streamSid
10345
- })));
10386
+ await options.trace?.append({
10387
+ at: Date.now(),
10388
+ payload: {
10389
+ callSid: bridgeState.callSid ?? undefined,
10390
+ carrier: telephonyMediaCarrier,
10391
+ direction: "outbound",
10392
+ envelope: outboundMessage,
10393
+ event: "clear",
10394
+ streamId: bridgeState.streamSid
10395
+ },
10396
+ scenarioId: bridgeState.scenarioId ?? undefined,
10397
+ sessionId: bridgeState.sessionId ?? bridgeState.streamSid,
10398
+ type: "client.telephony_media"
10399
+ });
10400
+ await Promise.resolve(socket.send(JSON.stringify(outboundMessage)));
10346
10401
  }
10347
10402
  bridgeState.hasOutboundAudioSinceLastInbound = false;
10348
10403
  await appendTelephonyMediaTrace(message, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.327",
3
+ "version": "0.0.22-beta.329",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",