@absolutejs/voice 0.0.22-beta.180 → 0.0.22-beta.182

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/agent.d.ts CHANGED
@@ -61,10 +61,30 @@ export type VoiceAgentModelOutput<TResult = unknown> = VoiceRouteResult<TResult>
61
61
  export type VoiceAgentModel<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
62
62
  generate: (input: VoiceAgentModelInput<TContext, TSession>) => Promise<VoiceAgentModelOutput<TResult>> | VoiceAgentModelOutput<TResult>;
63
63
  };
64
+ export type VoiceAgentSquadHandoffStatus = 'allowed' | 'blocked' | 'max-exceeded' | 'unknown-target';
65
+ export type VoiceAgentSquadStateHandoff = {
66
+ at: number;
67
+ fromAgentId: string;
68
+ metadata?: Record<string, unknown>;
69
+ originalTargetAgentId?: string;
70
+ reason?: string;
71
+ status: VoiceAgentSquadHandoffStatus;
72
+ summary?: string;
73
+ targetAgentId: string;
74
+ turnId: string;
75
+ };
76
+ export type VoiceAgentSquadState = {
77
+ agentId: string;
78
+ handoffCount: number;
79
+ handoffs: VoiceAgentSquadStateHandoff[];
80
+ lastHandoff?: VoiceAgentSquadStateHandoff;
81
+ previousAgentId?: string;
82
+ };
64
83
  export type VoiceAgentRunResult<TResult = unknown> = VoiceRouteResult<TResult> & {
65
84
  agentId: string;
66
85
  handoff?: VoiceAgentModelOutput<TResult>['handoff'];
67
86
  messages: VoiceAgentMessage[];
87
+ squad?: VoiceAgentSquadState;
68
88
  toolResults: VoiceAgentToolResult[];
69
89
  };
70
90
  export type VoiceAgentSquadHandoffPolicyResult<TResult = unknown> = {
@@ -120,8 +140,10 @@ export type VoiceAgentSquadOptions<TContext = unknown, TSession extends VoiceSes
120
140
  onHandoff?: (input: {
121
141
  context: TContext;
122
142
  fromAgentId: string;
143
+ metadata?: Record<string, unknown>;
123
144
  reason?: string;
124
145
  session: TSession;
146
+ summary?: string;
125
147
  targetAgentId: string;
126
148
  turn: VoiceTurnRecord;
127
149
  }) => Promise<void> | void;
@@ -1,10 +1,15 @@
1
- import type { VoiceAgent, VoiceAgentRunResult } from './agent';
1
+ import type { VoiceAgent, VoiceAgentRunResult, VoiceAgentSquadHandoffStatus } from './agent';
2
2
  import type { VoiceTraceEventStore } from './trace';
3
3
  import type { VoiceRouteResult, VoiceSessionHandle, VoiceSessionRecord } from './types';
4
4
  export type VoiceAgentSquadContractOutcome = 'assistant' | 'complete' | 'escalate' | 'no-answer' | 'transfer' | 'voicemail';
5
5
  export type VoiceAgentSquadHandoffExpectation = {
6
6
  fromAgentId?: string;
7
- status?: 'allowed' | 'blocked' | 'max-exceeded' | 'unknown-target';
7
+ metadata?: Record<string, unknown>;
8
+ reason?: string;
9
+ reasonIncludes?: string[];
10
+ status?: VoiceAgentSquadHandoffStatus;
11
+ summary?: string;
12
+ summaryIncludes?: string[];
8
13
  targetAgentId?: string;
9
14
  };
10
15
  export type VoiceAgentSquadTurnExpectation<TResult = unknown> = {
package/dist/index.d.ts CHANGED
@@ -110,7 +110,7 @@ export type { VoiceProviderStackChoice, VoiceProviderStackCapabilities, VoicePro
110
110
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
111
111
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingKindSummary, VoiceRoutingDecisionSummary, VoiceRoutingDecisionSummaryOptions, VoiceRoutingEvent, VoiceRoutingEventKind, VoiceRoutingSessionSummary, VoiceRoutingSessionSummaryOptions } from './resilienceRoutes';
112
112
  export type { VoiceIOProviderRouterEvent, VoiceIOProviderRouterOptions, VoiceIOProviderRouterPolicy, VoiceIOProviderRouterPolicyConfig, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
113
- export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadHandoffPolicyResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
113
+ export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadHandoffPolicyResult, VoiceAgentSquadHandoffStatus, VoiceAgentSquadOptions, VoiceAgentSquadState, VoiceAgentSquadStateHandoff, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
114
114
  export type { VoiceAgentSquadContractDefinition, VoiceAgentSquadContractIssue, VoiceAgentSquadContractOutcome, VoiceAgentSquadContractReport, VoiceAgentSquadContractRunOptions, VoiceAgentSquadContractTurn, VoiceAgentSquadContractTurnReport, VoiceAgentSquadHandoffExpectation, VoiceAgentSquadTurnExpectation } from './agentSquadContract';
115
115
  export type { VoiceToolRetryDelay, VoiceToolRuntime, VoiceToolRuntimeExecuteInput, VoiceToolRuntimeOptions, VoiceToolRuntimeResult } from './toolRuntime';
116
116
  export type { VoiceToolContractCase, VoiceToolContractCaseReport, VoiceToolContractDefinition, VoiceToolContractExpectation, VoiceToolContractHandlerOptions, VoiceToolContractHTMLHandlerOptions, VoiceToolContractIssue, VoiceToolContractReport, VoiceToolContractRoutesOptions, VoiceToolContractSuiteReport } from './toolContract';
package/dist/index.js CHANGED
@@ -6887,6 +6887,47 @@ var resolveVoiceAgentAuditLogger = (audit) => {
6887
6887
  return "append" in audit ? createVoiceAuditLogger(audit) : audit;
6888
6888
  };
6889
6889
  var toAuditOutcome = (status) => status === "allowed" ? "success" : status === "blocked" ? "skipped" : "error";
6890
+ var createVoiceAgentSquadState = (input) => {
6891
+ const lastHandoff = input.handoffs.at(-1);
6892
+ return {
6893
+ agentId: input.agentId,
6894
+ handoffCount: input.handoffs.length,
6895
+ handoffs: [...input.handoffs],
6896
+ lastHandoff,
6897
+ previousAgentId: lastHandoff?.status === "allowed" ? lastHandoff.fromAgentId : undefined
6898
+ };
6899
+ };
6900
+ var appendVoiceAgentSquadHandoff = async (input) => {
6901
+ const handoff = {
6902
+ at: Date.now(),
6903
+ fromAgentId: input.fromAgentId,
6904
+ metadata: input.metadata,
6905
+ originalTargetAgentId: input.originalTargetAgentId,
6906
+ reason: input.reason,
6907
+ status: input.status,
6908
+ summary: input.summary,
6909
+ targetAgentId: input.targetAgentId,
6910
+ turnId: input.turn.id
6911
+ };
6912
+ input.handoffs.push(handoff);
6913
+ await appendVoiceAgentTrace({
6914
+ agentId: input.agentId,
6915
+ event: {
6916
+ fromAgentId: handoff.fromAgentId,
6917
+ metadata: handoff.metadata,
6918
+ originalTargetAgentId: handoff.originalTargetAgentId,
6919
+ reason: handoff.reason,
6920
+ status: handoff.status,
6921
+ summary: handoff.summary,
6922
+ targetAgentId: handoff.targetAgentId
6923
+ },
6924
+ session: input.session,
6925
+ trace: input.trace,
6926
+ turn: input.turn,
6927
+ type: "agent.handoff"
6928
+ });
6929
+ return handoff;
6930
+ };
6890
6931
  var createVoiceAgentTool = (tool) => tool;
6891
6932
  var createVoiceAgent = (options) => {
6892
6933
  const toolMap = new Map(options.tools?.map((tool) => [tool.name, tool]) ?? []);
@@ -7187,6 +7228,7 @@ var createVoiceAgentSquad = (options) => {
7187
7228
  let agent = agents.get(agentId) ?? defaultAgent;
7188
7229
  const messages = input.messages ?? createHistoryMessages(input.session, input.turn);
7189
7230
  const toolResults = [];
7231
+ const handoffs = [];
7190
7232
  const maxHandoffs = Math.max(0, options.maxHandoffsPerTurn ?? 2);
7191
7233
  let result = await agent.run({
7192
7234
  ...input,
@@ -7207,26 +7249,26 @@ var createVoiceAgentSquad = (options) => {
7207
7249
  const targetAgentId = normalizeText3(policy?.targetAgentId) || originalTargetAgentId;
7208
7250
  const nextAgent = agents.get(targetAgentId);
7209
7251
  const handoffReason = policy?.summary ?? policy?.reason ?? result.handoff.reason;
7252
+ const handoffSummary = policy?.summary ?? result.handoff.reason ?? policy?.reason;
7210
7253
  const handoffMetadata = {
7211
7254
  ...result.handoff.metadata,
7212
7255
  ...policy?.metadata
7213
7256
  };
7214
7257
  const metadata = Object.keys(handoffMetadata).length > 0 ? handoffMetadata : undefined;
7215
7258
  if (policy?.allow === false) {
7216
- await appendVoiceAgentTrace({
7259
+ await appendVoiceAgentSquadHandoff({
7217
7260
  agentId: options.id,
7218
- event: {
7219
- fromAgentId: agent.id,
7220
- metadata,
7221
- originalTargetAgentId,
7222
- reason: handoffReason,
7223
- status: "blocked",
7224
- targetAgentId
7225
- },
7261
+ fromAgentId: agent.id,
7262
+ handoffs,
7263
+ metadata,
7264
+ originalTargetAgentId: originalTargetAgentId === targetAgentId ? undefined : originalTargetAgentId,
7265
+ reason: handoffReason,
7226
7266
  session: input.session,
7267
+ status: "blocked",
7268
+ summary: handoffSummary,
7269
+ targetAgentId,
7227
7270
  trace: options.trace,
7228
- turn: input.turn,
7229
- type: "agent.handoff"
7271
+ turn: input.turn
7230
7272
  });
7231
7273
  await audit?.handoff({
7232
7274
  actor: {
@@ -7247,24 +7289,27 @@ var createVoiceAgentSquad = (options) => {
7247
7289
  reason: handoffReason ?? `Blocked handoff to ${targetAgentId}`
7248
7290
  },
7249
7291
  handoff: undefined,
7292
+ squad: createVoiceAgentSquadState({
7293
+ agentId: agent.id,
7294
+ handoffs
7295
+ }),
7250
7296
  toolResults
7251
7297
  };
7252
7298
  }
7253
7299
  if (!nextAgent) {
7254
- await appendVoiceAgentTrace({
7300
+ await appendVoiceAgentSquadHandoff({
7255
7301
  agentId: options.id,
7256
- event: {
7257
- fromAgentId: agent.id,
7258
- metadata,
7259
- originalTargetAgentId,
7260
- reason: handoffReason,
7261
- status: "unknown-target",
7262
- targetAgentId
7263
- },
7302
+ fromAgentId: agent.id,
7303
+ handoffs,
7304
+ metadata,
7305
+ originalTargetAgentId: originalTargetAgentId === targetAgentId ? undefined : originalTargetAgentId,
7306
+ reason: handoffReason,
7264
7307
  session: input.session,
7308
+ status: "unknown-target",
7309
+ summary: handoffSummary,
7310
+ targetAgentId,
7265
7311
  trace: options.trace,
7266
- turn: input.turn,
7267
- type: "agent.handoff"
7312
+ turn: input.turn
7268
7313
  });
7269
7314
  await audit?.handoff({
7270
7315
  actor: {
@@ -7285,31 +7330,36 @@ var createVoiceAgentSquad = (options) => {
7285
7330
  reason: `Unknown handoff target: ${targetAgentId}`
7286
7331
  },
7287
7332
  handoff: undefined,
7333
+ squad: createVoiceAgentSquadState({
7334
+ agentId: agent.id,
7335
+ handoffs
7336
+ }),
7288
7337
  toolResults
7289
7338
  };
7290
7339
  }
7291
7340
  await options.onHandoff?.({
7292
7341
  context: input.context,
7293
7342
  fromAgentId: agent.id,
7343
+ metadata,
7294
7344
  reason: handoffReason,
7295
7345
  session: input.session,
7346
+ summary: handoffSummary,
7296
7347
  targetAgentId: nextAgent.id,
7297
7348
  turn: input.turn
7298
7349
  });
7299
- await appendVoiceAgentTrace({
7350
+ await appendVoiceAgentSquadHandoff({
7300
7351
  agentId: options.id,
7301
- event: {
7302
- fromAgentId: agent.id,
7303
- metadata,
7304
- originalTargetAgentId: originalTargetAgentId === nextAgent.id ? undefined : originalTargetAgentId,
7305
- reason: handoffReason,
7306
- status: "allowed",
7307
- targetAgentId: nextAgent.id
7308
- },
7352
+ fromAgentId: agent.id,
7353
+ handoffs,
7354
+ metadata,
7355
+ originalTargetAgentId: originalTargetAgentId === nextAgent.id ? undefined : originalTargetAgentId,
7356
+ reason: handoffReason,
7309
7357
  session: input.session,
7358
+ status: "allowed",
7359
+ summary: handoffSummary,
7360
+ targetAgentId: nextAgent.id,
7310
7361
  trace: options.trace,
7311
- turn: input.turn,
7312
- type: "agent.handoff"
7362
+ turn: input.turn
7313
7363
  });
7314
7364
  await audit?.handoff({
7315
7365
  actor: {
@@ -7324,7 +7374,7 @@ var createVoiceAgentSquad = (options) => {
7324
7374
  toAgentId: nextAgent.id
7325
7375
  });
7326
7376
  messages.push({
7327
- content: handoffReason ?? `Handoff to ${nextAgent.id}`,
7377
+ content: handoffSummary ?? handoffReason ?? `Handoff to ${nextAgent.id}`,
7328
7378
  metadata,
7329
7379
  name: nextAgent.id,
7330
7380
  role: "system"
@@ -7338,19 +7388,18 @@ var createVoiceAgentSquad = (options) => {
7338
7388
  toolResults.push(...result.toolResults);
7339
7389
  }
7340
7390
  if (result.handoff) {
7341
- await appendVoiceAgentTrace({
7391
+ await appendVoiceAgentSquadHandoff({
7342
7392
  agentId: options.id,
7343
- event: {
7344
- fromAgentId: agent.id,
7345
- metadata: result.handoff.metadata,
7346
- reason: result.handoff.reason,
7347
- status: "max-exceeded",
7348
- targetAgentId: result.handoff.targetAgentId
7349
- },
7393
+ fromAgentId: agent.id,
7394
+ handoffs,
7395
+ metadata: result.handoff.metadata,
7396
+ reason: result.handoff.reason,
7350
7397
  session: input.session,
7398
+ status: "max-exceeded",
7399
+ summary: result.handoff.reason,
7400
+ targetAgentId: result.handoff.targetAgentId,
7351
7401
  trace: options.trace,
7352
- turn: input.turn,
7353
- type: "agent.handoff"
7402
+ turn: input.turn
7354
7403
  });
7355
7404
  await audit?.handoff({
7356
7405
  actor: {
@@ -7371,12 +7420,20 @@ var createVoiceAgentSquad = (options) => {
7371
7420
  reason: `Max handoffs exceeded: ${maxHandoffs}`
7372
7421
  },
7373
7422
  handoff: undefined,
7423
+ squad: createVoiceAgentSquadState({
7424
+ agentId: agent.id,
7425
+ handoffs
7426
+ }),
7374
7427
  toolResults
7375
7428
  };
7376
7429
  }
7377
7430
  return {
7378
7431
  ...result,
7379
7432
  agentId,
7433
+ squad: createVoiceAgentSquadState({
7434
+ agentId,
7435
+ handoffs
7436
+ }),
7380
7437
  toolResults
7381
7438
  };
7382
7439
  };
@@ -10059,8 +10116,24 @@ var summarizeVoiceBargeIn = (events, options = {}) => {
10059
10116
  };
10060
10117
  var renderVoiceBargeInHTML = (report, options = {}) => {
10061
10118
  const title = options.title ?? "Voice Barge-In";
10119
+ const snippet = `const traceStore = createVoiceMemoryTraceEventStore();
10120
+
10121
+ app.use(
10122
+ createVoiceBargeInRoutes({
10123
+ htmlPath: '/barge-in',
10124
+ path: '/api/voice-barge-in',
10125
+ store: traceStore,
10126
+ thresholdMs: 250
10127
+ })
10128
+ );
10129
+
10130
+ // Browser/runtime side:
10131
+ const bargeInMonitor = createVoiceBargeInMonitor({
10132
+ path: '/api/voice-barge-in',
10133
+ sessionId
10134
+ });`;
10062
10135
  const sessions = report.sessions.length ? report.sessions.map((session) => `<tr><td>${escapeHtml10(session.sessionId)}</td><td>${String(session.total)}</td><td>${String(session.passed)}</td><td>${String(session.failed)}</td><td>${String(session.averageLatencyMs ?? 0)}ms</td></tr>`).join("") : '<tr><td colspan="5">No barge-in events yet.</td></tr>';
10063
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml10(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1100px;padding:32px}.eyebrow{color:#5eead4;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}.pass{color:#86efac}.warn{color:#fbbf24}.fail{color:#fca5a5}.empty{color:#cbd5e1}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}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}</style></head><body><main><p class="eyebrow">Interruption quality</p><h1>${escapeHtml10(title)}</h1><p class="status ${escapeHtml10(report.status)}">Status: ${escapeHtml10(report.status)}</p><section class="metrics"><article><span>Interruptions</span><strong>${String(report.total)}</strong></article><article><span>Avg latency</span><strong>${String(report.averageLatencyMs ?? 0)}ms</strong></article><article><span>Passed</span><strong>${String(report.passed)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article></section><table><thead><tr><th>Session</th><th>Total</th><th>Passed</th><th>Failed</th><th>Avg latency</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
10136
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml10(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1100px;padding:32px}.eyebrow{color:#5eead4;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}.pass{color:#86efac}.warn{color:#fbbf24}.fail{color:#fca5a5}.empty{color:#cbd5e1}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));margin:20px 0}.metrics article,.primitive{background:#181f27;border:1px solid #2b3642;border-radius:20px;padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem}.primitive{margin:0 0 20px}.primitive h2{margin:.2rem 0 .5rem}.primitive p{color:#cbd5e1}.primitive pre{background:#0a0d10;border:1px solid #2b3642;border-radius:16px;color:#d9fff7;overflow:auto;padding:16px}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}</style></head><body><main><p class="eyebrow">Interruption quality</p><h1>${escapeHtml10(title)}</h1><p class="status ${escapeHtml10(report.status)}">Status: ${escapeHtml10(report.status)}</p><section class="metrics"><article><span>Interruptions</span><strong>${String(report.total)}</strong></article><article><span>Avg latency</span><strong>${String(report.averageLatencyMs ?? 0)}ms</strong></article><article><span>Passed</span><strong>${String(report.passed)}</strong></article><article><span>Failed</span><strong>${String(report.failed)}</strong></article></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceBargeInRoutes(...)</code> proves interruption quality</h2><p>Use the shared trace store for browser interrupts, readiness gates, trace timelines, and production evidence instead of trusting a black-box hosted dashboard.</p><pre><code>${escapeHtml10(snippet)}</code></pre></section><table><thead><tr><th>Session</th><th>Total</th><th>Passed</th><th>Failed</th><th>Avg latency</th></tr></thead><tbody>${sessions}</tbody></table></main></body></html>`;
10064
10137
  };
10065
10138
  var createVoiceBargeInRoutes = (options) => {
10066
10139
  const path = options.path ?? "/api/voice-barge-in";
@@ -14417,9 +14490,16 @@ var getPayloadString2 = (event, key) => {
14417
14490
  const value = event.payload[key];
14418
14491
  return typeof value === "string" ? value : undefined;
14419
14492
  };
14493
+ var getPayloadRecord = (event, key) => {
14494
+ const value = event.payload[key];
14495
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
14496
+ };
14420
14497
  var toHandoffExpectation = (event) => ({
14421
14498
  fromAgentId: getPayloadString2(event, "fromAgentId"),
14499
+ metadata: getPayloadRecord(event, "metadata"),
14500
+ reason: getPayloadString2(event, "reason"),
14422
14501
  status: getPayloadString2(event, "status"),
14502
+ summary: getPayloadString2(event, "summary"),
14423
14503
  targetAgentId: getPayloadString2(event, "targetAgentId")
14424
14504
  });
14425
14505
  var createContractApi = (session) => ({
@@ -14449,6 +14529,28 @@ var appendIssue = (issues, issue, turnId) => {
14449
14529
  turnId: issue.turnId ?? turnId
14450
14530
  });
14451
14531
  };
14532
+ var assertIncludes = (input) => {
14533
+ const actual = normalizeIncludes(input.actual?.join(" ") ?? "");
14534
+ for (const expected of input.expected ?? []) {
14535
+ if (!actual.includes(normalizeIncludes(expected))) {
14536
+ appendIssue(input.issues, {
14537
+ code: input.code,
14538
+ message: `Expected handoff ${input.handoffIndex + 1} ${input.label} to include: ${expected}`
14539
+ }, input.turnId);
14540
+ }
14541
+ }
14542
+ };
14543
+ var assertMetadata = (input) => {
14544
+ for (const [key, expectedValue] of Object.entries(input.expected ?? {})) {
14545
+ const actualValue = input.actual?.[key];
14546
+ if (actualValue !== expectedValue) {
14547
+ appendIssue(input.issues, {
14548
+ code: "agent_squad.handoff_metadata_mismatch",
14549
+ message: `Expected handoff ${input.handoffIndex + 1} metadata ${key} ${String(expectedValue)}, saw ${String(actualValue ?? "none")}.`
14550
+ }, input.turnId);
14551
+ }
14552
+ }
14553
+ };
14452
14554
  var runVoiceAgentSquadContract = async (options) => {
14453
14555
  const session = options.session ?? createVoiceSessionRecord(`agent-squad-contract-${options.contract.id}`, options.contract.scenarioId ?? options.contract.id);
14454
14556
  const api = options.api ?? createContractApi(session);
@@ -14515,6 +14617,39 @@ var runVoiceAgentSquadContract = async (options) => {
14515
14617
  }, turn.id);
14516
14618
  }
14517
14619
  }
14620
+ for (const key of ["reason", "summary"]) {
14621
+ if (expectedHandoff[key] && actual[key] !== expectedHandoff[key]) {
14622
+ appendIssue(turnIssues, {
14623
+ code: `agent_squad.handoff_${key}_mismatch`,
14624
+ message: `Expected handoff ${handoffIndex + 1} ${key} ${expectedHandoff[key]}, saw ${actual[key] ?? "none"}.`
14625
+ }, turn.id);
14626
+ }
14627
+ }
14628
+ assertIncludes({
14629
+ actual: actual.reason ? [actual.reason] : undefined,
14630
+ code: "agent_squad.handoff_reason_missing",
14631
+ expected: expectedHandoff.reasonIncludes,
14632
+ handoffIndex,
14633
+ issues: turnIssues,
14634
+ label: "reason",
14635
+ turnId: turn.id
14636
+ });
14637
+ assertIncludes({
14638
+ actual: actual.summary ? [actual.summary] : undefined,
14639
+ code: "agent_squad.handoff_summary_missing",
14640
+ expected: expectedHandoff.summaryIncludes,
14641
+ handoffIndex,
14642
+ issues: turnIssues,
14643
+ label: "summary",
14644
+ turnId: turn.id
14645
+ });
14646
+ assertMetadata({
14647
+ actual: actual.metadata,
14648
+ expected: expectedHandoff.metadata,
14649
+ handoffIndex,
14650
+ issues: turnIssues,
14651
+ turnId: turn.id
14652
+ });
14518
14653
  }
14519
14654
  for (const issue of expected?.result?.({
14520
14655
  result: result.result,
@@ -14672,11 +14807,29 @@ var summarizeVoiceTurnLatency = async (options) => {
14672
14807
  var formatMs = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
14673
14808
  var renderVoiceTurnLatencyHTML = (report, options = {}) => {
14674
14809
  const title = options.title ?? "Voice Turn Latency";
14810
+ const snippet = `app.use(
14811
+ createVoiceTurnLatencyRoutes({
14812
+ failAfterMs: 3200,
14813
+ htmlPath: '/turn-latency',
14814
+ path: '/api/turn-latency',
14815
+ store: sessionStore,
14816
+ traceStore,
14817
+ warnAfterMs: 1800
14818
+ })
14819
+ );
14820
+
14821
+ await traceStore.append({
14822
+ at: Date.now(),
14823
+ payload: { stage: 'assistant_audio_received' },
14824
+ sessionId,
14825
+ turnId,
14826
+ type: 'turn_latency.stage'
14827
+ });`;
14675
14828
  const turns = report.turns.map((turn) => `<article class="turn ${escapeHtml24(turn.status)}">
14676
14829
  <header><div><p class="eyebrow">${escapeHtml24(turn.sessionId)} \xB7 ${escapeHtml24(turn.turnId)}</p><h2>${escapeHtml24(turn.text || "Empty turn")}</h2></div><strong>${escapeHtml24(turn.status)}</strong></header>
14677
14830
  <dl>${turn.stages.map((stage) => `<div><dt>${escapeHtml24(stage.label)}</dt><dd>${escapeHtml24(formatMs(stage.valueMs))}</dd></div>`).join("")}</dl>
14678
14831
  </article>`).join("");
14679
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml24(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(251,191,36,.1))}.eyebrow{color:#5eead4;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.turn header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.empty{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{font-weight:900;margin:0}@media(max-width:800px){main{padding:18px}.turn header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">End-to-end responsiveness</p><h1>${escapeHtml24(title)}</h1><div class="summary"><span class="pill ${escapeHtml24(report.status)}">${escapeHtml24(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">avg ${escapeHtml24(formatMs(report.averageTotalMs))}</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span></div></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
14832
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml24(title)}</title><style>body{background:#101316;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1180px;padding:32px}.hero,.turn,.primitive{background:#181d22;border:1px solid #2a323a;border-radius:20px;margin-bottom:16px;padding:20px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(251,191,36,.1))}.eyebrow{color:#5eead4;font-size:.78rem;font-weight:900;letter-spacing:.08em;text-transform:uppercase}h1{font-size:clamp(2.3rem,6vw,5rem);letter-spacing:-.06em;line-height:.9;margin:.2rem 0 1rem}h2{margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{background:#0f1217;border:1px solid #3f3f46;border-radius:999px;padding:7px 10px}.primitive p{color:#cbd5e1}.primitive pre{background:#0a0d10;border:1px solid #2a323a;border-radius:16px;color:#d9fff7;overflow:auto;padding:16px}.turn header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between}.pass{color:#86efac}.warn,.empty{color:#fde68a}.fail{color:#fca5a5}.turn.fail{border-color:rgba(248,113,113,.45)}dl{display:grid;gap:8px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}dt{color:#a8b0b8;font-size:.8rem}dd{font-weight:900;margin:0}@media(max-width:800px){main{padding:18px}.turn header{display:block}}</style></head><body><main><section class="hero"><p class="eyebrow">End-to-end responsiveness</p><h1>${escapeHtml24(title)}</h1><div class="summary"><span class="pill ${escapeHtml24(report.status)}">${escapeHtml24(report.status)}</span><span class="pill">${String(report.total)} turns</span><span class="pill">avg ${escapeHtml24(formatMs(report.averageTotalMs))}</span><span class="pill">${String(report.warnings)} warnings</span><span class="pill">${String(report.failed)} failed</span></div></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceTurnLatencyRoutes(...)</code> exposes the full turn waterfall</h2><p>Attach stage traces for speech detection, commit, model response, TTS send, and first audio so teams can prove where latency actually comes from.</p><pre><code>${escapeHtml24(snippet)}</code></pre></section>${turns || '<section class="turn"><p>No committed turns found.</p></section>'}</main></body></html>`;
14680
14833
  };
14681
14834
  var createVoiceTurnLatencyJSONHandler = (options) => async () => summarizeVoiceTurnLatency(options);
14682
14835
  var createVoiceTurnLatencyHTMLHandler = (options) => async () => {
@@ -14751,8 +14904,27 @@ var summarizeVoiceLiveLatency = async (options) => {
14751
14904
  var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
14752
14905
  var renderVoiceLiveLatencyHTML = (report, options = {}) => {
14753
14906
  const title = options.title ?? "Voice Live Latency";
14907
+ const snippet = `app.use(
14908
+ createVoiceLiveLatencyRoutes({
14909
+ failAfterMs: 3200,
14910
+ htmlPath: '/live-latency',
14911
+ path: '/api/live-latency',
14912
+ store: traceStore,
14913
+ warnAfterMs: 1800
14914
+ })
14915
+ );
14916
+
14917
+ await traceStore.append({
14918
+ at: Date.now(),
14919
+ payload: {
14920
+ latencyMs,
14921
+ status: 'assistant_audio_started'
14922
+ },
14923
+ sessionId,
14924
+ type: 'client.live_latency'
14925
+ });`;
14754
14926
  const rows = report.recent.map((sample) => `<tr><td>${escapeHtml25(sample.sessionId)}</td><td>${escapeHtml25(formatMs2(sample.latencyMs))}</td><td>${escapeHtml25(sample.status ?? "unknown")}</td><td>${escapeHtml25(new Date(sample.at).toLocaleString())}</td></tr>`).join("");
14755
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml25(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml25(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml25(report.status)}">Status: ${escapeHtml25(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml25(formatMs2(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml25(formatMs2(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml25(formatMs2(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
14927
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml25(title)}</title><style>body{background:#0c0f14;color:#f6f2e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1060px;padding:32px}.hero{background:linear-gradient(135deg,rgba(94,234,212,.16),rgba(245,158,11,.1));border:1px solid #26313d;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.status{border:1px solid #3f3f46;border-radius:999px;display:inline-flex;padding:8px 12px}.pass{color:#86efac}.warn,.empty{color:#fbbf24}.fail{color:#fca5a5}.metrics{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.metrics article,table,.primitive{background:#141922;border:1px solid #26313d;border-radius:18px}.metrics article,.primitive{padding:16px}.metrics span{color:#a8b0b8}.metrics strong{display:block;font-size:2rem;margin-top:.25rem}.primitive{margin:0 0 18px}.primitive h2{margin:.2rem 0 .5rem}.primitive p{color:#cbd5e1}.primitive pre{background:#080b10;border:1px solid #26313d;border-radius:16px;color:#d9fff7;overflow:auto;padding:16px}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #26313d;padding:12px;text-align:left}@media(max-width:760px){main{padding:20px}}</style></head><body><main><section class="hero"><p class="eyebrow">Browser proof</p><h1>${escapeHtml25(title)}</h1><p>Recent real browser speech-to-assistant response measurements from persisted <code>client.live_latency</code> traces.</p><p class="status ${escapeHtml25(report.status)}">Status: ${escapeHtml25(report.status)}</p><section class="metrics"><article><span>p50</span><strong>${escapeHtml25(formatMs2(report.p50LatencyMs))}</strong></article><article><span>p95</span><strong>${escapeHtml25(formatMs2(report.p95LatencyMs))}</strong></article><article><span>Average</span><strong>${escapeHtml25(formatMs2(report.averageLatencyMs))}</strong></article><article><span>Samples</span><strong>${String(report.total)}</strong></article></section></section><section class="primitive"><p class="eyebrow">Copy into your app</p><h2><code>createVoiceLiveLatencyRoutes(...)</code> turns real browser timing into a release gate</h2><p>Persist live timing samples into the trace store so readiness, simulations, and trace timelines all point at the same self-hosted proof.</p><pre><code>${escapeHtml25(snippet)}</code></pre></section><table><thead><tr><th>Session</th><th>Latency</th><th>Status</th><th>Measured</th></tr></thead><tbody>${rows || '<tr><td colspan="4">No live latency samples yet.</td></tr>'}</tbody></table></main></body></html>`;
14756
14928
  };
14757
14929
  var createVoiceLiveLatencyRoutes = (options) => {
14758
14930
  const path = options.path ?? "/api/live-latency";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.180",
3
+ "version": "0.0.22-beta.182",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",