@absolutejs/voice 0.0.22-beta.356 → 0.0.22-beta.357

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.js CHANGED
@@ -4994,6 +4994,12 @@ var createVoiceSession = (options) => {
4994
4994
  if (options.scenarioId && session.scenarioId !== options.scenarioId) {
4995
4995
  session.scenarioId = options.scenarioId;
4996
4996
  }
4997
+ if (options.sessionMetadata) {
4998
+ session.metadata = {
4999
+ ...session.metadata && typeof session.metadata === "object" ? session.metadata : {},
5000
+ ...options.sessionMetadata
5001
+ };
5002
+ }
4997
5003
  ensureCommittedTurnGuard(session);
4998
5004
  let shouldFireOnSession = !existingSession;
4999
5005
  if (existingSession?.scenarioId && options.scenarioId && existingSession.scenarioId !== options.scenarioId) {
@@ -5200,6 +5206,319 @@ var createVoiceSession = (options) => {
5200
5206
  return api;
5201
5207
  };
5202
5208
 
5209
+ // src/audit.ts
5210
+ var includes = (filter, value) => {
5211
+ if (!filter) {
5212
+ return true;
5213
+ }
5214
+ if (!value) {
5215
+ return false;
5216
+ }
5217
+ return Array.isArray(filter) ? filter.includes(value) : filter === value;
5218
+ };
5219
+ var createVoiceAuditEvent = (event) => ({
5220
+ ...event,
5221
+ at: event.at ?? Date.now(),
5222
+ id: event.id ?? crypto.randomUUID()
5223
+ });
5224
+ var filterVoiceAuditEvents = (events, filter = {}) => {
5225
+ const sorted = events.filter((event) => {
5226
+ if (!includes(filter.type, event.type)) {
5227
+ return false;
5228
+ }
5229
+ if (!includes(filter.outcome, event.outcome)) {
5230
+ return false;
5231
+ }
5232
+ if (filter.actorId && event.actor?.id !== filter.actorId) {
5233
+ return false;
5234
+ }
5235
+ if (filter.resourceId && event.resource?.id !== filter.resourceId) {
5236
+ return false;
5237
+ }
5238
+ if (filter.resourceType && event.resource?.type !== filter.resourceType) {
5239
+ return false;
5240
+ }
5241
+ if (filter.sessionId && event.sessionId !== filter.sessionId) {
5242
+ return false;
5243
+ }
5244
+ if (filter.traceId && event.traceId !== filter.traceId) {
5245
+ return false;
5246
+ }
5247
+ if (typeof filter.after === "number" && event.at <= filter.after) {
5248
+ return false;
5249
+ }
5250
+ if (typeof filter.afterOrAt === "number" && event.at < filter.afterOrAt) {
5251
+ return false;
5252
+ }
5253
+ if (typeof filter.before === "number" && event.at >= filter.before) {
5254
+ return false;
5255
+ }
5256
+ if (typeof filter.beforeOrAt === "number" && event.at > filter.beforeOrAt) {
5257
+ return false;
5258
+ }
5259
+ return true;
5260
+ }).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
5261
+ return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
5262
+ };
5263
+ var createVoiceMemoryAuditEventStore = () => {
5264
+ const events = new Map;
5265
+ return {
5266
+ append: (event) => {
5267
+ const stored = createVoiceAuditEvent(event);
5268
+ events.set(stored.id, stored);
5269
+ return stored;
5270
+ },
5271
+ get: (id) => events.get(id),
5272
+ list: (filter) => filterVoiceAuditEvents([...events.values()], filter)
5273
+ };
5274
+ };
5275
+ var recordVoiceAuditEvent = (store, event) => store.append(createVoiceAuditEvent(event));
5276
+ var recordVoiceProviderAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
5277
+ action: `${input.kind}.provider.call`,
5278
+ actor: input.actor,
5279
+ metadata: input.metadata,
5280
+ outcome: input.outcome,
5281
+ payload: {
5282
+ cost: input.cost,
5283
+ elapsedMs: input.elapsedMs,
5284
+ error: input.error,
5285
+ kind: input.kind,
5286
+ model: input.model,
5287
+ provider: input.provider
5288
+ },
5289
+ resource: {
5290
+ id: input.provider,
5291
+ type: "provider"
5292
+ },
5293
+ sessionId: input.sessionId,
5294
+ traceId: input.traceId,
5295
+ type: "provider.call"
5296
+ });
5297
+ var recordVoiceToolAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
5298
+ action: "tool.call",
5299
+ actor: input.actor,
5300
+ metadata: input.metadata,
5301
+ outcome: input.outcome,
5302
+ payload: {
5303
+ elapsedMs: input.elapsedMs,
5304
+ error: input.error,
5305
+ toolCallId: input.toolCallId,
5306
+ toolName: input.toolName
5307
+ },
5308
+ resource: {
5309
+ id: input.toolName,
5310
+ type: "tool"
5311
+ },
5312
+ sessionId: input.sessionId,
5313
+ traceId: input.traceId,
5314
+ type: "tool.call"
5315
+ });
5316
+ var recordVoiceHandoffAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
5317
+ action: "handoff",
5318
+ actor: input.actor,
5319
+ metadata: input.metadata,
5320
+ outcome: input.outcome,
5321
+ payload: {
5322
+ fromAgentId: input.fromAgentId,
5323
+ reason: input.reason,
5324
+ target: input.target,
5325
+ toAgentId: input.toAgentId
5326
+ },
5327
+ resource: {
5328
+ id: input.toAgentId ?? input.target,
5329
+ type: "handoff"
5330
+ },
5331
+ sessionId: input.sessionId,
5332
+ traceId: input.traceId,
5333
+ type: "handoff"
5334
+ });
5335
+ var recordVoiceRetentionAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
5336
+ action: input.dryRun ? "retention.plan" : "retention.apply",
5337
+ actor: input.actor ?? {
5338
+ id: "voice-retention",
5339
+ kind: "system"
5340
+ },
5341
+ metadata: input.metadata,
5342
+ outcome: "success",
5343
+ payload: {
5344
+ deletedCount: input.report.deletedCount,
5345
+ dryRun: input.dryRun,
5346
+ scopes: input.report.scopes
5347
+ },
5348
+ resource: {
5349
+ type: "retention-policy"
5350
+ },
5351
+ type: "retention.policy"
5352
+ });
5353
+ var recordVoiceOperatorAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
5354
+ action: input.action,
5355
+ actor: input.actor,
5356
+ metadata: input.metadata,
5357
+ outcome: input.outcome ?? "success",
5358
+ payload: input.payload,
5359
+ resource: input.resource,
5360
+ sessionId: input.sessionId,
5361
+ traceId: input.traceId,
5362
+ type: "operator.action"
5363
+ });
5364
+ var createVoiceAuditLogger = (store) => ({
5365
+ handoff: (input) => recordVoiceHandoffAuditEvent({ ...input, store }),
5366
+ operatorAction: (input) => recordVoiceOperatorAuditEvent({ ...input, store }),
5367
+ providerCall: (input) => recordVoiceProviderAuditEvent({ ...input, store }),
5368
+ record: (event) => recordVoiceAuditEvent(store, event),
5369
+ retention: (input) => recordVoiceRetentionAuditEvent({ ...input, store }),
5370
+ toolCall: (input) => recordVoiceToolAuditEvent({ ...input, store })
5371
+ });
5372
+
5373
+ // src/profileSwitchRecommendation.ts
5374
+ var readDefaults = (input) => ("defaults" in input) ? input.defaults : input;
5375
+ var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
5376
+ var exceeds = (observed, budget) => isNumber(observed) && isNumber(budget) && observed > budget;
5377
+ var scoreProfile = (profile, observed) => {
5378
+ const evidence = profile.evidence;
5379
+ const live = evidence.liveP95Ms ?? observed.liveP95Ms ?? 0;
5380
+ const provider = evidence.providerP95Ms ?? observed.providerP95Ms ?? 0;
5381
+ const turn = evidence.turnP95Ms ?? observed.turnP95Ms ?? 0;
5382
+ const statusPenalty = profile.status === "pass" ? 0 : profile.status === "warn" ? 1e4 : 25000;
5383
+ const noisyBonus = (observed.fallbackUsed || (observed.turnWarnings ?? 0) > 0) && /noisy|phone/i.test(`${profile.profileId} ${profile.label ?? ""}`) ? -1000 : 0;
5384
+ return live + provider + turn + statusPenalty + noisyBonus;
5385
+ };
5386
+ var clampConfidence = (value) => Math.max(0, Math.min(0.99, Number(value.toFixed(2))));
5387
+ var estimateSwitchConfidence = (recommendation) => {
5388
+ if (!recommendation.ok || recommendation.status !== "switch") {
5389
+ return recommendation.status === "stay" && recommendation.ok ? 0.99 : 0;
5390
+ }
5391
+ const observed = recommendation.observed;
5392
+ const currentStatus = recommendation.currentProfile?.status;
5393
+ const recommendedStatus = recommendation.recommendedProfile?.status;
5394
+ let confidence = 0.58;
5395
+ if (currentStatus && currentStatus !== "pass") {
5396
+ confidence += 0.12;
5397
+ }
5398
+ if (recommendedStatus === "pass") {
5399
+ confidence += 0.1;
5400
+ }
5401
+ if (observed.fallbackUsed) {
5402
+ confidence += 0.08;
5403
+ }
5404
+ if ((observed.turnWarnings ?? 0) > 0) {
5405
+ confidence += 0.08;
5406
+ }
5407
+ if (recommendation.reasons.some((reason) => /budget|strongest measured fit/i.test(reason))) {
5408
+ confidence += 0.08;
5409
+ }
5410
+ return clampConfidence(confidence);
5411
+ };
5412
+ var recommendVoiceProfileSwitch = (options) => {
5413
+ const defaults = readDefaults(options.defaults);
5414
+ const observed = options.observed ?? {};
5415
+ const currentProfileId = observed.currentProfileId ?? options.defaultProfileId ?? defaults.profiles[0]?.profileId;
5416
+ const currentProfile = defaults.profiles.find((profile) => profile.profileId === currentProfileId);
5417
+ const candidates = defaults.profiles.filter((profile) => profile.status !== "fail");
5418
+ const recommended = candidates.slice().sort((left, right) => scoreProfile(left, observed) - scoreProfile(right, observed))[0];
5419
+ const issues = [
5420
+ ...defaults.profiles.length === 0 ? ["No measured profile defaults are available."] : [],
5421
+ ...!currentProfile && currentProfileId ? [`Current profile ${currentProfileId} is not present in measured defaults.`] : [],
5422
+ ...!recommended ? ["No non-failing measured profile can be recommended."] : []
5423
+ ];
5424
+ const currentOverBudget = currentProfile ? [
5425
+ exceeds(observed.liveP95Ms, currentProfile.latencyBudgets.maxLiveP95Ms) ? "live p95 exceeds this profile budget" : undefined,
5426
+ exceeds(observed.providerP95Ms, currentProfile.latencyBudgets.maxProviderP95Ms) ? "provider p95 exceeds this profile budget" : undefined,
5427
+ exceeds(observed.turnP95Ms, currentProfile.latencyBudgets.maxTurnP95Ms) ? "turn p95 exceeds this profile budget" : undefined
5428
+ ].filter((reason) => Boolean(reason)) : [];
5429
+ const minImprovementMs = options.minImprovementMs ?? 250;
5430
+ const currentScore = currentProfile ? scoreProfile(currentProfile, observed) : Number.POSITIVE_INFINITY;
5431
+ const recommendedScore = recommended ? scoreProfile(recommended, observed) : Number.POSITIVE_INFINITY;
5432
+ const shouldSwitch = Boolean(recommended) && recommended?.profileId !== currentProfile?.profileId && (!currentProfile || currentProfile.status !== "pass" || currentOverBudget.length > 0 || currentScore - recommendedScore >= minImprovementMs);
5433
+ const reasons = [
5434
+ ...currentOverBudget,
5435
+ ...currentProfile?.status && currentProfile.status !== "pass" ? [`current profile is ${currentProfile.status}`] : [],
5436
+ ...observed.fallbackUsed ? ["current session used provider fallback"] : [],
5437
+ ...(observed.turnWarnings ?? 0) > 0 ? [`${observed.turnWarnings} turn quality warning(s) observed`] : [],
5438
+ ...shouldSwitch && recommended ? [
5439
+ `${recommended.label ?? recommended.profileId} has the strongest measured fit for these signals`
5440
+ ] : []
5441
+ ];
5442
+ return {
5443
+ currentProfile: currentProfile ? {
5444
+ label: currentProfile.label,
5445
+ profileId: currentProfile.profileId,
5446
+ status: currentProfile.status
5447
+ } : undefined,
5448
+ generatedAt: new Date().toISOString(),
5449
+ issues,
5450
+ nextMove: issues.length > 0 ? "Collect fresh real-call profile evidence before switching automatically." : shouldSwitch && recommended ? `Switch to ${recommended.label ?? recommended.profileId} for this session profile.` : "Keep the current measured profile unless new session evidence drifts.",
5451
+ ok: issues.length === 0,
5452
+ observed,
5453
+ reasons: reasons.length > 0 ? reasons : ["current profile matches measured defaults and observed budgets"],
5454
+ recommendedProfile: recommended ? {
5455
+ evidence: recommended.evidence,
5456
+ label: recommended.label,
5457
+ latencyBudgets: recommended.latencyBudgets,
5458
+ profileId: recommended.profileId,
5459
+ providerRoutes: recommended.providerRoutes,
5460
+ status: recommended.status
5461
+ } : undefined,
5462
+ status: issues.length > 0 ? "warn" : shouldSwitch ? "switch" : "stay"
5463
+ };
5464
+ };
5465
+ var applyVoiceProfileSwitchGuard = async (options) => {
5466
+ const mode = options.mode ?? "recommend";
5467
+ const minConfidence = options.minConfidence ?? 0.75;
5468
+ const recommendation = recommendVoiceProfileSwitch(options);
5469
+ const confidence = estimateSwitchConfidence(recommendation);
5470
+ const previousProfileId = recommendation.currentProfile?.profileId;
5471
+ const recommendedProfileId = recommendation.recommendedProfile?.profileId;
5472
+ const canSwitch = recommendation.status === "switch" && recommendation.ok && Boolean(recommendedProfileId) && confidence >= minConfidence;
5473
+ const action = recommendation.status === "stay" ? "stay" : canSwitch ? mode === "auto" ? "switch" : "recommend" : "blocked";
5474
+ const selectedProfileId = action === "switch" ? recommendedProfileId : previousProfileId ?? recommendedProfileId;
5475
+ const reason = action === "switch" ? `Auto-switched from ${previousProfileId ?? "unknown"} to ${recommendedProfileId}.` : action === "recommend" ? `Recommended ${recommendedProfileId} but left selection unchanged because mode is recommend.` : action === "blocked" ? `Blocked profile switch because confidence ${confidence} is below ${minConfidence} or evidence is incomplete.` : "Kept current profile because measured evidence does not require a switch.";
5476
+ const decision = {
5477
+ action,
5478
+ autoApplied: action === "switch",
5479
+ confidence,
5480
+ minConfidence,
5481
+ mode,
5482
+ previousProfileId,
5483
+ reason,
5484
+ recommendation,
5485
+ recommendedProfileId,
5486
+ selectedProfileId
5487
+ };
5488
+ if (options.audit) {
5489
+ const auditEvent = await options.audit.append(createVoiceAuditEvent({
5490
+ action: `profile.switch.${action}`,
5491
+ actor: options.actor ?? {
5492
+ id: "absolutejs-voice-profile-switch-guard",
5493
+ kind: "system",
5494
+ name: "AbsoluteJS Voice Profile Switch Guard"
5495
+ },
5496
+ metadata: options.metadata,
5497
+ outcome: action === "blocked" ? "skipped" : "success",
5498
+ payload: {
5499
+ autoApplied: decision.autoApplied,
5500
+ confidence,
5501
+ minConfidence,
5502
+ mode,
5503
+ previousProfileId,
5504
+ reasons: recommendation.reasons,
5505
+ recommendedProfileId,
5506
+ selectedProfileId,
5507
+ status: recommendation.status
5508
+ },
5509
+ resource: {
5510
+ id: selectedProfileId,
5511
+ type: "voice-profile"
5512
+ },
5513
+ sessionId: options.sessionId,
5514
+ traceId: options.traceId,
5515
+ type: "profile.switch"
5516
+ }));
5517
+ decision.auditEvent = auditEvent;
5518
+ }
5519
+ return decision;
5520
+ };
5521
+
5203
5522
  // src/plugin.ts
5204
5523
  var resolveQueryScenario = (query) => {
5205
5524
  if (typeof query?.scenarioId === "string" && query.scenarioId.trim()) {
@@ -5322,6 +5641,7 @@ var resolveSessionId = (runtime, ws) => {
5322
5641
  runtime.socketSessions.set(ws, resolved);
5323
5642
  return resolved;
5324
5643
  };
5644
+ var resolveMaybeFunction = async (value, input) => typeof value === "function" ? await value(input) : value;
5325
5645
  var toAudioChunk = (raw) => {
5326
5646
  if (raw instanceof ArrayBuffer) {
5327
5647
  return raw;
@@ -5395,6 +5715,69 @@ var resolveLexicon = async (config, input) => {
5395
5715
  }
5396
5716
  return normalizeLexicon(config.lexicon);
5397
5717
  };
5718
+ var resolveProfileSwitchGuard = async (config, runtime, input) => {
5719
+ const guard = config.profileSwitchGuard;
5720
+ if (!guard || runtime.profileSwitchGuardedSessions.has(input.sessionId)) {
5721
+ return;
5722
+ }
5723
+ runtime.profileSwitchGuardedSessions.add(input.sessionId);
5724
+ const resolverInput = input;
5725
+ const defaults = await resolveMaybeFunction(guard.defaults, resolverInput);
5726
+ if (!defaults) {
5727
+ throw new Error("voice profileSwitchGuard requires measured profile defaults.");
5728
+ }
5729
+ const observed = await resolveMaybeFunction(guard.observed, resolverInput);
5730
+ const currentProfileId = await resolveMaybeFunction(guard.currentProfileId, resolverInput);
5731
+ const metadata = await resolveMaybeFunction(guard.metadata, resolverInput);
5732
+ const minConfidence = await resolveMaybeFunction(guard.minConfidence, resolverInput);
5733
+ const mode = await resolveMaybeFunction(guard.mode, resolverInput);
5734
+ const decision = await applyVoiceProfileSwitchGuard({
5735
+ actor: guard.actor,
5736
+ audit: guard.audit,
5737
+ defaultProfileId: guard.defaultProfileId,
5738
+ defaults,
5739
+ metadata,
5740
+ minConfidence,
5741
+ mode,
5742
+ observed: {
5743
+ ...observed,
5744
+ currentProfileId: observed?.currentProfileId ?? currentProfileId
5745
+ },
5746
+ sessionId: input.sessionId
5747
+ });
5748
+ await guard.onDecision?.({
5749
+ context: input.context,
5750
+ decision,
5751
+ scenarioId: input.scenarioId,
5752
+ sessionId: input.sessionId
5753
+ });
5754
+ const trace = guard.trace === false ? undefined : guard.trace ?? config.trace;
5755
+ if (trace) {
5756
+ await trace.append({
5757
+ at: Date.now(),
5758
+ metadata: {
5759
+ ...metadata,
5760
+ source: "profile-switch-guard"
5761
+ },
5762
+ payload: {
5763
+ action: decision.action,
5764
+ autoApplied: decision.autoApplied,
5765
+ confidence: decision.confidence,
5766
+ minConfidence: decision.minConfidence,
5767
+ mode: decision.mode,
5768
+ previousProfileId: decision.previousProfileId,
5769
+ reason: decision.reason,
5770
+ recommendedProfileId: decision.recommendedProfileId,
5771
+ selectedProfileId: decision.selectedProfileId,
5772
+ status: decision.recommendation.status
5773
+ },
5774
+ scenarioId: input.scenarioId,
5775
+ sessionId: input.sessionId,
5776
+ type: "provider.decision"
5777
+ });
5778
+ }
5779
+ return decision;
5780
+ };
5398
5781
  var voice = (config) => {
5399
5782
  if (!config.stt && !config.realtime) {
5400
5783
  throw new Error("voice requires either an stt or realtime adapter.");
@@ -5402,6 +5785,7 @@ var voice = (config) => {
5402
5785
  const runtime = {
5403
5786
  activeSessions: new Map,
5404
5787
  logger: resolveLogger(config.logger),
5788
+ profileSwitchGuardedSessions: new Set,
5405
5789
  socketSessions: new WeakMap
5406
5790
  };
5407
5791
  const onTurn = normalizeOnTurn(config.onTurn);
@@ -5413,6 +5797,11 @@ var voice = (config) => {
5413
5797
  const htmxTargets = resolveVoiceHTMXTargets(htmxOptions?.targets);
5414
5798
  const createManagedSession = async (ws, sessionId, scenarioId) => {
5415
5799
  const context = ws.data;
5800
+ const profileSwitchDecision = await resolveProfileSwitchGuard(config, runtime, {
5801
+ context,
5802
+ scenarioId,
5803
+ sessionId
5804
+ });
5416
5805
  const phraseHints = await resolvePhraseHints(config, {
5417
5806
  context,
5418
5807
  scenarioId,
@@ -5470,6 +5859,9 @@ var voice = (config) => {
5470
5859
  onTurn,
5471
5860
  onVoicemail: config.onVoicemail
5472
5861
  },
5862
+ sessionMetadata: profileSwitchDecision && config.profileSwitchGuard?.sessionMetadataKey !== false ? {
5863
+ [config.profileSwitchGuard?.sessionMetadataKey ?? "profileSwitchGuard"]: profileSwitchDecision
5864
+ } : undefined,
5473
5865
  scenarioId,
5474
5866
  socket: createSocketAdapter(ws),
5475
5867
  store: config.session,
@@ -7186,182 +7578,18 @@ var evaluateVoiceCampaignDialerProofEvidence = (report, input = {}) => {
7186
7578
  issues,
7187
7579
  mode: report.mode,
7188
7580
  ok: issues.length === 0,
7189
- providers,
7190
- successfulOutcomes,
7191
- totalProviders: report.providers.length
7192
- };
7193
- };
7194
- var assertVoiceCampaignDialerProofEvidence = (report, input = {}) => {
7195
- const assertion = evaluateVoiceCampaignDialerProofEvidence(report, input);
7196
- if (!assertion.ok) {
7197
- throw new Error(`Voice campaign dialer proof evidence assertion failed: ${assertion.issues.join(" ")}`);
7198
- }
7199
- return assertion;
7200
- };
7201
- // src/audit.ts
7202
- var includes = (filter, value) => {
7203
- if (!filter) {
7204
- return true;
7205
- }
7206
- if (!value) {
7207
- return false;
7208
- }
7209
- return Array.isArray(filter) ? filter.includes(value) : filter === value;
7210
- };
7211
- var createVoiceAuditEvent = (event) => ({
7212
- ...event,
7213
- at: event.at ?? Date.now(),
7214
- id: event.id ?? crypto.randomUUID()
7215
- });
7216
- var filterVoiceAuditEvents = (events, filter = {}) => {
7217
- const sorted = events.filter((event) => {
7218
- if (!includes(filter.type, event.type)) {
7219
- return false;
7220
- }
7221
- if (!includes(filter.outcome, event.outcome)) {
7222
- return false;
7223
- }
7224
- if (filter.actorId && event.actor?.id !== filter.actorId) {
7225
- return false;
7226
- }
7227
- if (filter.resourceId && event.resource?.id !== filter.resourceId) {
7228
- return false;
7229
- }
7230
- if (filter.resourceType && event.resource?.type !== filter.resourceType) {
7231
- return false;
7232
- }
7233
- if (filter.sessionId && event.sessionId !== filter.sessionId) {
7234
- return false;
7235
- }
7236
- if (filter.traceId && event.traceId !== filter.traceId) {
7237
- return false;
7238
- }
7239
- if (typeof filter.after === "number" && event.at <= filter.after) {
7240
- return false;
7241
- }
7242
- if (typeof filter.afterOrAt === "number" && event.at < filter.afterOrAt) {
7243
- return false;
7244
- }
7245
- if (typeof filter.before === "number" && event.at >= filter.before) {
7246
- return false;
7247
- }
7248
- if (typeof filter.beforeOrAt === "number" && event.at > filter.beforeOrAt) {
7249
- return false;
7250
- }
7251
- return true;
7252
- }).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
7253
- return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
7254
- };
7255
- var createVoiceMemoryAuditEventStore = () => {
7256
- const events = new Map;
7257
- return {
7258
- append: (event) => {
7259
- const stored = createVoiceAuditEvent(event);
7260
- events.set(stored.id, stored);
7261
- return stored;
7262
- },
7263
- get: (id) => events.get(id),
7264
- list: (filter) => filterVoiceAuditEvents([...events.values()], filter)
7581
+ providers,
7582
+ successfulOutcomes,
7583
+ totalProviders: report.providers.length
7265
7584
  };
7266
7585
  };
7267
- var recordVoiceAuditEvent = (store, event) => store.append(createVoiceAuditEvent(event));
7268
- var recordVoiceProviderAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
7269
- action: `${input.kind}.provider.call`,
7270
- actor: input.actor,
7271
- metadata: input.metadata,
7272
- outcome: input.outcome,
7273
- payload: {
7274
- cost: input.cost,
7275
- elapsedMs: input.elapsedMs,
7276
- error: input.error,
7277
- kind: input.kind,
7278
- model: input.model,
7279
- provider: input.provider
7280
- },
7281
- resource: {
7282
- id: input.provider,
7283
- type: "provider"
7284
- },
7285
- sessionId: input.sessionId,
7286
- traceId: input.traceId,
7287
- type: "provider.call"
7288
- });
7289
- var recordVoiceToolAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
7290
- action: "tool.call",
7291
- actor: input.actor,
7292
- metadata: input.metadata,
7293
- outcome: input.outcome,
7294
- payload: {
7295
- elapsedMs: input.elapsedMs,
7296
- error: input.error,
7297
- toolCallId: input.toolCallId,
7298
- toolName: input.toolName
7299
- },
7300
- resource: {
7301
- id: input.toolName,
7302
- type: "tool"
7303
- },
7304
- sessionId: input.sessionId,
7305
- traceId: input.traceId,
7306
- type: "tool.call"
7307
- });
7308
- var recordVoiceHandoffAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
7309
- action: "handoff",
7310
- actor: input.actor,
7311
- metadata: input.metadata,
7312
- outcome: input.outcome,
7313
- payload: {
7314
- fromAgentId: input.fromAgentId,
7315
- reason: input.reason,
7316
- target: input.target,
7317
- toAgentId: input.toAgentId
7318
- },
7319
- resource: {
7320
- id: input.toAgentId ?? input.target,
7321
- type: "handoff"
7322
- },
7323
- sessionId: input.sessionId,
7324
- traceId: input.traceId,
7325
- type: "handoff"
7326
- });
7327
- var recordVoiceRetentionAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
7328
- action: input.dryRun ? "retention.plan" : "retention.apply",
7329
- actor: input.actor ?? {
7330
- id: "voice-retention",
7331
- kind: "system"
7332
- },
7333
- metadata: input.metadata,
7334
- outcome: "success",
7335
- payload: {
7336
- deletedCount: input.report.deletedCount,
7337
- dryRun: input.dryRun,
7338
- scopes: input.report.scopes
7339
- },
7340
- resource: {
7341
- type: "retention-policy"
7342
- },
7343
- type: "retention.policy"
7344
- });
7345
- var recordVoiceOperatorAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
7346
- action: input.action,
7347
- actor: input.actor,
7348
- metadata: input.metadata,
7349
- outcome: input.outcome ?? "success",
7350
- payload: input.payload,
7351
- resource: input.resource,
7352
- sessionId: input.sessionId,
7353
- traceId: input.traceId,
7354
- type: "operator.action"
7355
- });
7356
- var createVoiceAuditLogger = (store) => ({
7357
- handoff: (input) => recordVoiceHandoffAuditEvent({ ...input, store }),
7358
- operatorAction: (input) => recordVoiceOperatorAuditEvent({ ...input, store }),
7359
- providerCall: (input) => recordVoiceProviderAuditEvent({ ...input, store }),
7360
- record: (event) => recordVoiceAuditEvent(store, event),
7361
- retention: (input) => recordVoiceRetentionAuditEvent({ ...input, store }),
7362
- toolCall: (input) => recordVoiceToolAuditEvent({ ...input, store })
7363
- });
7364
-
7586
+ var assertVoiceCampaignDialerProofEvidence = (report, input = {}) => {
7587
+ const assertion = evaluateVoiceCampaignDialerProofEvidence(report, input);
7588
+ if (!assertion.ok) {
7589
+ throw new Error(`Voice campaign dialer proof evidence assertion failed: ${assertion.issues.join(" ")}`);
7590
+ }
7591
+ return assertion;
7592
+ };
7365
7593
  // src/agent.ts
7366
7594
  var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
7367
7595
  var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
@@ -15694,154 +15922,6 @@ var formatVoiceProofTrendAge = (ageMs) => {
15694
15922
  const days = Math.floor(hours / 24);
15695
15923
  return `${days}d ${hours % 24}h`;
15696
15924
  };
15697
- // src/profileSwitchRecommendation.ts
15698
- var readDefaults = (input) => ("defaults" in input) ? input.defaults : input;
15699
- var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
15700
- var exceeds = (observed, budget) => isNumber(observed) && isNumber(budget) && observed > budget;
15701
- var scoreProfile = (profile, observed) => {
15702
- const evidence = profile.evidence;
15703
- const live = evidence.liveP95Ms ?? observed.liveP95Ms ?? 0;
15704
- const provider = evidence.providerP95Ms ?? observed.providerP95Ms ?? 0;
15705
- const turn = evidence.turnP95Ms ?? observed.turnP95Ms ?? 0;
15706
- const statusPenalty = profile.status === "pass" ? 0 : profile.status === "warn" ? 1e4 : 25000;
15707
- const noisyBonus = (observed.fallbackUsed || (observed.turnWarnings ?? 0) > 0) && /noisy|phone/i.test(`${profile.profileId} ${profile.label ?? ""}`) ? -1000 : 0;
15708
- return live + provider + turn + statusPenalty + noisyBonus;
15709
- };
15710
- var clampConfidence = (value) => Math.max(0, Math.min(0.99, Number(value.toFixed(2))));
15711
- var estimateSwitchConfidence = (recommendation) => {
15712
- if (!recommendation.ok || recommendation.status !== "switch") {
15713
- return recommendation.status === "stay" && recommendation.ok ? 0.99 : 0;
15714
- }
15715
- const observed = recommendation.observed;
15716
- const currentStatus = recommendation.currentProfile?.status;
15717
- const recommendedStatus = recommendation.recommendedProfile?.status;
15718
- let confidence = 0.58;
15719
- if (currentStatus && currentStatus !== "pass") {
15720
- confidence += 0.12;
15721
- }
15722
- if (recommendedStatus === "pass") {
15723
- confidence += 0.1;
15724
- }
15725
- if (observed.fallbackUsed) {
15726
- confidence += 0.08;
15727
- }
15728
- if ((observed.turnWarnings ?? 0) > 0) {
15729
- confidence += 0.08;
15730
- }
15731
- if (recommendation.reasons.some((reason) => /budget|strongest measured fit/i.test(reason))) {
15732
- confidence += 0.08;
15733
- }
15734
- return clampConfidence(confidence);
15735
- };
15736
- var recommendVoiceProfileSwitch = (options) => {
15737
- const defaults = readDefaults(options.defaults);
15738
- const observed = options.observed ?? {};
15739
- const currentProfileId = observed.currentProfileId ?? options.defaultProfileId ?? defaults.profiles[0]?.profileId;
15740
- const currentProfile = defaults.profiles.find((profile) => profile.profileId === currentProfileId);
15741
- const candidates = defaults.profiles.filter((profile) => profile.status !== "fail");
15742
- const recommended = candidates.slice().sort((left, right) => scoreProfile(left, observed) - scoreProfile(right, observed))[0];
15743
- const issues = [
15744
- ...defaults.profiles.length === 0 ? ["No measured profile defaults are available."] : [],
15745
- ...!currentProfile && currentProfileId ? [`Current profile ${currentProfileId} is not present in measured defaults.`] : [],
15746
- ...!recommended ? ["No non-failing measured profile can be recommended."] : []
15747
- ];
15748
- const currentOverBudget = currentProfile ? [
15749
- exceeds(observed.liveP95Ms, currentProfile.latencyBudgets.maxLiveP95Ms) ? "live p95 exceeds this profile budget" : undefined,
15750
- exceeds(observed.providerP95Ms, currentProfile.latencyBudgets.maxProviderP95Ms) ? "provider p95 exceeds this profile budget" : undefined,
15751
- exceeds(observed.turnP95Ms, currentProfile.latencyBudgets.maxTurnP95Ms) ? "turn p95 exceeds this profile budget" : undefined
15752
- ].filter((reason) => Boolean(reason)) : [];
15753
- const minImprovementMs = options.minImprovementMs ?? 250;
15754
- const currentScore = currentProfile ? scoreProfile(currentProfile, observed) : Number.POSITIVE_INFINITY;
15755
- const recommendedScore = recommended ? scoreProfile(recommended, observed) : Number.POSITIVE_INFINITY;
15756
- const shouldSwitch = Boolean(recommended) && recommended?.profileId !== currentProfile?.profileId && (!currentProfile || currentProfile.status !== "pass" || currentOverBudget.length > 0 || currentScore - recommendedScore >= minImprovementMs);
15757
- const reasons = [
15758
- ...currentOverBudget,
15759
- ...currentProfile?.status && currentProfile.status !== "pass" ? [`current profile is ${currentProfile.status}`] : [],
15760
- ...observed.fallbackUsed ? ["current session used provider fallback"] : [],
15761
- ...(observed.turnWarnings ?? 0) > 0 ? [`${observed.turnWarnings} turn quality warning(s) observed`] : [],
15762
- ...shouldSwitch && recommended ? [
15763
- `${recommended.label ?? recommended.profileId} has the strongest measured fit for these signals`
15764
- ] : []
15765
- ];
15766
- return {
15767
- currentProfile: currentProfile ? {
15768
- label: currentProfile.label,
15769
- profileId: currentProfile.profileId,
15770
- status: currentProfile.status
15771
- } : undefined,
15772
- generatedAt: new Date().toISOString(),
15773
- issues,
15774
- nextMove: issues.length > 0 ? "Collect fresh real-call profile evidence before switching automatically." : shouldSwitch && recommended ? `Switch to ${recommended.label ?? recommended.profileId} for this session profile.` : "Keep the current measured profile unless new session evidence drifts.",
15775
- ok: issues.length === 0,
15776
- observed,
15777
- reasons: reasons.length > 0 ? reasons : ["current profile matches measured defaults and observed budgets"],
15778
- recommendedProfile: recommended ? {
15779
- evidence: recommended.evidence,
15780
- label: recommended.label,
15781
- latencyBudgets: recommended.latencyBudgets,
15782
- profileId: recommended.profileId,
15783
- providerRoutes: recommended.providerRoutes,
15784
- status: recommended.status
15785
- } : undefined,
15786
- status: issues.length > 0 ? "warn" : shouldSwitch ? "switch" : "stay"
15787
- };
15788
- };
15789
- var applyVoiceProfileSwitchGuard = async (options) => {
15790
- const mode = options.mode ?? "recommend";
15791
- const minConfidence = options.minConfidence ?? 0.75;
15792
- const recommendation = recommendVoiceProfileSwitch(options);
15793
- const confidence = estimateSwitchConfidence(recommendation);
15794
- const previousProfileId = recommendation.currentProfile?.profileId;
15795
- const recommendedProfileId = recommendation.recommendedProfile?.profileId;
15796
- const canSwitch = recommendation.status === "switch" && recommendation.ok && Boolean(recommendedProfileId) && confidence >= minConfidence;
15797
- const action = recommendation.status === "stay" ? "stay" : canSwitch ? mode === "auto" ? "switch" : "recommend" : "blocked";
15798
- const selectedProfileId = action === "switch" ? recommendedProfileId : previousProfileId ?? recommendedProfileId;
15799
- const reason = action === "switch" ? `Auto-switched from ${previousProfileId ?? "unknown"} to ${recommendedProfileId}.` : action === "recommend" ? `Recommended ${recommendedProfileId} but left selection unchanged because mode is recommend.` : action === "blocked" ? `Blocked profile switch because confidence ${confidence} is below ${minConfidence} or evidence is incomplete.` : "Kept current profile because measured evidence does not require a switch.";
15800
- const decision = {
15801
- action,
15802
- autoApplied: action === "switch",
15803
- confidence,
15804
- minConfidence,
15805
- mode,
15806
- previousProfileId,
15807
- reason,
15808
- recommendation,
15809
- recommendedProfileId,
15810
- selectedProfileId
15811
- };
15812
- if (options.audit) {
15813
- const auditEvent = await options.audit.append(createVoiceAuditEvent({
15814
- action: `profile.switch.${action}`,
15815
- actor: options.actor ?? {
15816
- id: "absolutejs-voice-profile-switch-guard",
15817
- kind: "system",
15818
- name: "AbsoluteJS Voice Profile Switch Guard"
15819
- },
15820
- metadata: options.metadata,
15821
- outcome: action === "blocked" ? "skipped" : "success",
15822
- payload: {
15823
- autoApplied: decision.autoApplied,
15824
- confidence,
15825
- minConfidence,
15826
- mode,
15827
- previousProfileId,
15828
- reasons: recommendation.reasons,
15829
- recommendedProfileId,
15830
- selectedProfileId,
15831
- status: recommendation.status
15832
- },
15833
- resource: {
15834
- id: selectedProfileId,
15835
- type: "voice-profile"
15836
- },
15837
- sessionId: options.sessionId,
15838
- traceId: options.traceId,
15839
- type: "profile.switch"
15840
- }));
15841
- decision.auditEvent = auditEvent;
15842
- }
15843
- return decision;
15844
- };
15845
15925
  // src/providerDecisionTraces.ts
15846
15926
  import { Elysia as Elysia23 } from "elysia";
15847
15927
 
@@ -7563,6 +7563,12 @@ var createVoiceSession = (options) => {
7563
7563
  if (options.scenarioId && session.scenarioId !== options.scenarioId) {
7564
7564
  session.scenarioId = options.scenarioId;
7565
7565
  }
7566
+ if (options.sessionMetadata) {
7567
+ session.metadata = {
7568
+ ...session.metadata && typeof session.metadata === "object" ? session.metadata : {},
7569
+ ...options.sessionMetadata
7570
+ };
7571
+ }
7566
7572
  ensureCommittedTurnGuard(session);
7567
7573
  let shouldFireOnSession = !existingSession;
7568
7574
  if (existingSession?.scenarioId && options.scenarioId && existingSession.scenarioId !== options.scenarioId) {
package/dist/types.d.ts CHANGED
@@ -5,6 +5,9 @@ import type { VoiceIntegrationSink } from './opsSinks';
5
5
  import type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewStore } from './testing/review';
6
6
  import type { VoiceTraceEventStore } from './trace';
7
7
  import type { VoiceLiveOpsControlState } from './liveOps';
8
+ import type { VoiceAuditActor, VoiceAuditEventStore } from './audit';
9
+ import type { VoiceProfileSwitchGuardDecision, VoiceProfileSwitchGuardMode, VoiceProfileSwitchObservedSignals } from './profileSwitchRecommendation';
10
+ import type { VoiceRealCallProfileDefaultsReport, VoiceRealCallProfileHistoryReport } from './proofTrends';
8
11
  export type AudioFormat = {
9
12
  container: 'raw';
10
13
  encoding: 'alaw' | 'mulaw' | 'pcm_s16le';
@@ -614,6 +617,30 @@ export type VoiceRuntimeOpsConfig<TContext = unknown, TSession extends VoiceSess
614
617
  export type VoiceLiveOpsRuntimeConfig = {
615
618
  getControl: (sessionId: string) => Promise<VoiceLiveOpsControlState | null | undefined> | VoiceLiveOpsControlState | null | undefined;
616
619
  };
620
+ export type VoiceProfileSwitchGuardResolverInput<TContext = unknown> = {
621
+ context: TContext;
622
+ scenarioId?: string;
623
+ sessionId: string;
624
+ };
625
+ export type VoicePluginProfileSwitchGuardConfig<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
626
+ actor?: VoiceAuditActor;
627
+ audit?: VoiceAuditEventStore;
628
+ currentProfileId?: string | ((input: VoiceProfileSwitchGuardResolverInput<TContext>) => Promise<string | undefined> | string | undefined);
629
+ defaultProfileId?: string;
630
+ defaults: VoiceRealCallProfileDefaultsReport | VoiceRealCallProfileHistoryReport | ((input: VoiceProfileSwitchGuardResolverInput<TContext>) => Promise<VoiceRealCallProfileDefaultsReport | VoiceRealCallProfileHistoryReport> | VoiceRealCallProfileDefaultsReport | VoiceRealCallProfileHistoryReport);
631
+ metadata?: Record<string, unknown> | ((input: VoiceProfileSwitchGuardResolverInput<TContext>) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined);
632
+ minConfidence?: number | ((input: VoiceProfileSwitchGuardResolverInput<TContext>) => Promise<number | undefined> | number | undefined);
633
+ mode?: VoiceProfileSwitchGuardMode | ((input: VoiceProfileSwitchGuardResolverInput<TContext>) => Promise<VoiceProfileSwitchGuardMode | undefined> | VoiceProfileSwitchGuardMode | undefined);
634
+ observed?: VoiceProfileSwitchObservedSignals | ((input: VoiceProfileSwitchGuardResolverInput<TContext>) => Promise<VoiceProfileSwitchObservedSignals | undefined> | VoiceProfileSwitchObservedSignals | undefined);
635
+ onDecision?: (input: {
636
+ context: TContext;
637
+ decision: VoiceProfileSwitchGuardDecision;
638
+ scenarioId?: string;
639
+ sessionId: string;
640
+ }) => Promise<void> | void;
641
+ sessionMetadataKey?: string | false;
642
+ trace?: false | VoiceTraceEventStore;
643
+ };
617
644
  export type VoiceNormalizedRouteConfig<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Omit<VoiceRouteConfig<TContext, TSession, TResult>, 'onTurn'> & {
618
645
  onTurn: VoiceOnTurnObjectHandler<TContext, TSession, TResult>;
619
646
  };
@@ -649,6 +676,7 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
649
676
  handoff?: VoiceHandoffConfig<TContext, TSession, TResult>;
650
677
  ops?: VoiceRuntimeOpsConfig<TContext, TSession, TResult>;
651
678
  liveOps?: VoiceLiveOpsRuntimeConfig;
679
+ profileSwitchGuard?: VoicePluginProfileSwitchGuardConfig<TContext, TSession, TResult>;
652
680
  trace?: VoiceTraceEventStore;
653
681
  } & VoiceRouteConfig<TContext, TSession, TResult>;
654
682
  export type CreateVoiceSessionOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
@@ -667,6 +695,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
667
695
  trace?: VoiceTraceEventStore;
668
696
  reconnect: Required<VoiceReconnectConfig>;
669
697
  phraseHints?: VoicePhraseHint[];
698
+ sessionMetadata?: Record<string, unknown>;
670
699
  scenarioId?: string;
671
700
  sttLifecycle: VoiceSTTLifecycle;
672
701
  turnDetection: VoiceResolvedTurnDetectionConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.356",
3
+ "version": "0.0.22-beta.357",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",