@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 +22 -0
- package/dist/agentSquadContract.d.ts +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +219 -47
- package/package.json +1 -1
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
|
-
|
|
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
|
|
7259
|
+
await appendVoiceAgentSquadHandoff({
|
|
7217
7260
|
agentId: options.id,
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
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
|
|
7300
|
+
await appendVoiceAgentSquadHandoff({
|
|
7255
7301
|
agentId: options.id,
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
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
|
|
7350
|
+
await appendVoiceAgentSquadHandoff({
|
|
7300
7351
|
agentId: options.id,
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
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
|
|
7391
|
+
await appendVoiceAgentSquadHandoff({
|
|
7342
7392
|
agentId: options.id,
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
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";
|