@vellumai/assistant 0.12.2-dev.202609181615.645c0a5 → 0.12.2-dev.202609181713.fb1ff61
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/ARCHITECTURE.md +4 -1
- package/package.json +1 -1
- package/src/__tests__/call-conversation-messages.test.ts +5 -0
- package/src/__tests__/call-recovery.test.ts +7 -2
- package/src/__tests__/call-routes-http.test.ts +12 -0
- package/src/__tests__/call-store.test.ts +7 -2
- package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
- package/src/__tests__/guardian-dispatch.test.ts +13 -0
- package/src/__tests__/notification-guardian-path.test.ts +7 -0
- package/src/__tests__/twilio-routes.test.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +94 -0
- package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
- package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
- package/src/calls/call-controller.ts +177 -1
- package/src/calls/call-domain.ts +4 -0
- package/src/calls/call-store.ts +87 -2
- package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
- package/src/live-voice/live-voice-session.ts +16 -16
- package/src/onboarding/onboarding-events-store.ts +99 -12
- package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
- package/src/telemetry/phone-call-funnel.ts +144 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -614,6 +614,8 @@ Transcription mode is selected once per session in `media-stream-stt-session.ts`
|
|
|
614
614
|
|
|
615
615
|
Every phone turn runs the same two-leg triage as live voice through `startVoiceTurn` (`src/calls/voice-session-bridge.ts`): `call-controller.ts` opens on a toolless front-door leg (`routingLeg: "front-door"`, the `voiceFrontDoor` call site) and drives it through the shared `createFrontDoorLegCoordinator` (`src/calls/voice-leg-coordinator.ts`), which reads the stream through the verdict machine and sequences the hand-off (pause narration, abort the leg, resolve and speak the bridge, mark it as the floor holder, start the escalated leg pinned to the conversation's own model, re-arm narration); each driver supplies only a host for how text and the bridge are spoken, how a leg is started or aborted, and (live voice only) the speculative hold and commit. Phone has no partial transcripts, so the hold verdict is never taught and routing is escalate-only. The controller also passes the bridge's turn callbacks (tool activity is recorded as `tool_use_started` / `tool_use_completed` call events, persisted row ids ride the `assistant_spoke` event), `launchedAtMs` for dispatch timing, and a `voiceTelemetry` bag keyed by the call session with a `phone_inbound` / `phone_outbound` entry. Both drivers share the spoken progress narration cadence (`src/calls/voice-progress-cadence.ts`, tuned by `voice.frontModel.progress`): the cadence owns the tool-activity log, the triggers (an ops burst, a long operation completing, a full interval of audible silence with news, the `maxSilenceMs` heartbeat) and the generated or static phrase, while each driver supplies its own view of audible silence (live voice from its TTS queue and playback-tail estimate; the media-stream transport from `isPlaybackIdle()` and a running sum of sent frame durations) and how to speak a phrase.
|
|
616
616
|
|
|
617
|
+
Both drivers also share the per-turn latency marks (`src/calls/voice-metrics.ts`). The controller opens a turn's marks when it dispatches, seeded with when the caller's transcript arrived, and stamps the leg dispatch, the first assistant delta, the first audible audio, a barge-in, and how the turn settled. Live voice streams a snapshot to its client on every mark; a call has no client on the line, so the aggregate is logged once per turn, cancelled turns included. Each call also records a `phone_call_started` / `phone_call_ended` pair on the onboarding telemetry substrate (`src/telemetry/phone-call-funnel.ts`), written by the call store at session creation and at the single terminal status transition: duration is the gap between the two rows, and a call that took no caller turn carries why on the end row (`silent_no_connect` when it never connected, `silent_no_turn` when it did and nobody spoke).
|
|
618
|
+
|
|
617
619
|
A credential preflight (`resolveTelephonyCredentialReadiness()` in `src/calls/telephony-credential-preflight.ts`) gates every call: it requires a credentialed, telephony-capable STT provider **and** a media-stream-playable TTS provider (the configured one or a credentialed playable fallback). Inbound calls that fail the preflight receive `<Say>` setup-required copy plus `<Hangup/>` instead of a doomed stream; outbound placement fails before dialing via `preflightVoiceIngress()` with the same user-facing message.
|
|
618
620
|
|
|
619
621
|
Key modules:
|
|
@@ -623,6 +625,7 @@ Key modules:
|
|
|
623
625
|
| `src/calls/twilio-routes.ts` | Voice webhook handler; generates `<Connect><Stream>` TwiML, enforces the inbound credential preflight |
|
|
624
626
|
| `src/calls/telephony-credential-preflight.ts` | Combined STT + TTS credential-readiness resolver |
|
|
625
627
|
| `src/calls/media-stream-parser.ts` | Twilio Media Streams protocol parser |
|
|
628
|
+
| `src/calls/voice-metrics.ts` | Per-turn latency marks, shared with live voice |
|
|
626
629
|
| `src/calls/media-turn-detector.ts` | Energy-based VAD turn detector for raw audio (batch mode) |
|
|
627
630
|
| `src/calls/media-stream-stt-session.ts` | STT session — streaming/batch mode selection and transcription via `services.stt` |
|
|
628
631
|
| `src/calls/media-stream-audio-transcode.ts` | Mu-law ↔ PCM16 codecs and resampling |
|
|
@@ -729,7 +732,7 @@ The assistant-side live voice module is intentionally bounded under `src/live-vo
|
|
|
729
732
|
| `live-voice-session.ts` | Session orchestration: streaming STT, push-to-talk release, voice turn bridge callbacks, assistant text deltas, TTS, archive, metrics, interrupt, and close |
|
|
730
733
|
| `live-voice-tts.ts` | Streaming TTS helper that resolves `services.tts`, requires `TtsProvider.synthesizeStream()`, and forwards audio chunks as `tts_audio` frames |
|
|
731
734
|
| `live-voice-archive.ts` | Audio artifact creation/linking for user utterance and assistant response message IDs |
|
|
732
|
-
|
|
|
735
|
+
| `../calls/voice-metrics.ts` | Shared per-session and per-turn latency marks; live voice emits each snapshot as a `metrics` frame, phone calls log the per-turn aggregate |
|
|
733
736
|
|
|
734
737
|
Live voice STT uses the same `resolveStreamingTranscriber()` path as conversation streaming, dialed with the provider the `liveVoice` STT role resolves to after managed-speech defaulting (`resolveEffectiveSpeechProviders`, see `config/managed-speech-defaults.ts`). For V1 latency-sensitive behavior, that provider must resolve to a `daemon-streaming` transcriber whose catalog entry has `conversationStreamingMode: "realtime-ws"` and usable credentials. Providers that only support batch or incremental-batch transcription remain valid for other voice surfaces, but do not satisfy live voice's streaming STT requirement.
|
|
735
738
|
|
package/package.json
CHANGED
|
@@ -102,6 +102,7 @@ describe("call-conversation-messages", () => {
|
|
|
102
102
|
const conversationId = "conv-call-msg-failed";
|
|
103
103
|
ensureConversation(conversationId);
|
|
104
104
|
const session = createCallSession({
|
|
105
|
+
direction: "inbound",
|
|
105
106
|
conversationId,
|
|
106
107
|
provider: "twilio",
|
|
107
108
|
fromNumber: "+15550001111",
|
|
@@ -122,6 +123,7 @@ describe("call-conversation-messages", () => {
|
|
|
122
123
|
const conversationId = "conv-call-msg-cancelled";
|
|
123
124
|
ensureConversation(conversationId);
|
|
124
125
|
const session = createCallSession({
|
|
126
|
+
direction: "inbound",
|
|
125
127
|
conversationId,
|
|
126
128
|
provider: "twilio",
|
|
127
129
|
fromNumber: "+15550001111",
|
|
@@ -142,6 +144,7 @@ describe("call-conversation-messages", () => {
|
|
|
142
144
|
const conversationId = "conv-call-msg-completed";
|
|
143
145
|
ensureConversation(conversationId);
|
|
144
146
|
const session = createCallSession({
|
|
147
|
+
direction: "inbound",
|
|
145
148
|
conversationId,
|
|
146
149
|
provider: "twilio",
|
|
147
150
|
fromNumber: "+15550001111",
|
|
@@ -171,6 +174,7 @@ describe("call-conversation-messages", () => {
|
|
|
171
174
|
const conversationId = "conv-call-msg-fallback";
|
|
172
175
|
ensureConversation(conversationId);
|
|
173
176
|
const session = createCallSession({
|
|
177
|
+
direction: "inbound",
|
|
174
178
|
conversationId,
|
|
175
179
|
provider: "twilio",
|
|
176
180
|
fromNumber: "+12025550101",
|
|
@@ -208,6 +212,7 @@ describe("call-conversation-messages", () => {
|
|
|
208
212
|
const conversationId = "conv-call-msg-schema";
|
|
209
213
|
ensureConversation(conversationId);
|
|
210
214
|
const session = createCallSession({
|
|
215
|
+
direction: "inbound",
|
|
211
216
|
conversationId,
|
|
212
217
|
provider: "twilio",
|
|
213
218
|
fromNumber: "+12025550101",
|
|
@@ -54,9 +54,14 @@ function resetTables() {
|
|
|
54
54
|
ensuredConvIds = new Set();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function createTestCallSession(
|
|
57
|
+
function createTestCallSession(
|
|
58
|
+
opts: Omit<Parameters<typeof createCallSession>[0], "direction"> & {
|
|
59
|
+
direction?: Parameters<typeof createCallSession>[0]["direction"];
|
|
60
|
+
},
|
|
61
|
+
) {
|
|
58
62
|
ensureConversation(opts.conversationId);
|
|
59
|
-
|
|
63
|
+
// Direction only shapes the funnel stamp; these suites test other things.
|
|
64
|
+
return createCallSession({ direction: "inbound", ...opts });
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
/** Backdate a session's createdAt so it appears older than the grace period. */
|
|
@@ -327,6 +327,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
327
327
|
ensureConversation("conv-get-1");
|
|
328
328
|
|
|
329
329
|
const session = createCallSession({
|
|
330
|
+
direction: "outbound",
|
|
330
331
|
conversationId: "conv-get-1",
|
|
331
332
|
provider: "twilio",
|
|
332
333
|
fromNumber: "+15550001111",
|
|
@@ -378,6 +379,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
378
379
|
ensureConversation("conv-cancel-1");
|
|
379
380
|
|
|
380
381
|
const session = createCallSession({
|
|
382
|
+
direction: "inbound",
|
|
381
383
|
conversationId: "conv-cancel-1",
|
|
382
384
|
provider: "twilio",
|
|
383
385
|
fromNumber: "+15550001111",
|
|
@@ -407,6 +409,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
407
409
|
ensureConversation("conv-cancel-2");
|
|
408
410
|
|
|
409
411
|
const session = createCallSession({
|
|
412
|
+
direction: "inbound",
|
|
410
413
|
conversationId: "conv-cancel-2",
|
|
411
414
|
provider: "twilio",
|
|
412
415
|
fromNumber: "+15550001111",
|
|
@@ -447,6 +450,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
447
450
|
ensureConversation("conv-answer-badjson");
|
|
448
451
|
|
|
449
452
|
const session = createCallSession({
|
|
453
|
+
direction: "inbound",
|
|
450
454
|
conversationId: "conv-answer-badjson",
|
|
451
455
|
provider: "twilio",
|
|
452
456
|
fromNumber: "+15550001111",
|
|
@@ -469,6 +473,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
469
473
|
ensureConversation("conv-answer-1");
|
|
470
474
|
|
|
471
475
|
const session = createCallSession({
|
|
476
|
+
direction: "inbound",
|
|
472
477
|
conversationId: "conv-answer-1",
|
|
473
478
|
provider: "twilio",
|
|
474
479
|
fromNumber: "+15550001111",
|
|
@@ -495,6 +500,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
495
500
|
ensureConversation("conv-answer-2");
|
|
496
501
|
|
|
497
502
|
const session = createCallSession({
|
|
503
|
+
direction: "inbound",
|
|
498
504
|
conversationId: "conv-answer-2",
|
|
499
505
|
provider: "twilio",
|
|
500
506
|
fromNumber: "+15550001111",
|
|
@@ -517,6 +523,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
517
523
|
ensureConversation("conv-answer-3");
|
|
518
524
|
|
|
519
525
|
const session = createCallSession({
|
|
526
|
+
direction: "inbound",
|
|
520
527
|
conversationId: "conv-answer-3",
|
|
521
528
|
provider: "twilio",
|
|
522
529
|
fromNumber: "+15550001111",
|
|
@@ -548,6 +555,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
548
555
|
ensureConversation("conv-instr-badjson");
|
|
549
556
|
|
|
550
557
|
const session = createCallSession({
|
|
558
|
+
direction: "inbound",
|
|
551
559
|
conversationId: "conv-instr-badjson",
|
|
552
560
|
provider: "twilio",
|
|
553
561
|
fromNumber: "+15550001111",
|
|
@@ -570,6 +578,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
570
578
|
ensureConversation("conv-instr-empty");
|
|
571
579
|
|
|
572
580
|
const session = createCallSession({
|
|
581
|
+
direction: "inbound",
|
|
573
582
|
conversationId: "conv-instr-empty",
|
|
574
583
|
provider: "twilio",
|
|
575
584
|
fromNumber: "+15550001111",
|
|
@@ -596,6 +605,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
596
605
|
ensureConversation("conv-instr-missing");
|
|
597
606
|
|
|
598
607
|
const session = createCallSession({
|
|
608
|
+
direction: "inbound",
|
|
599
609
|
conversationId: "conv-instr-missing",
|
|
600
610
|
provider: "twilio",
|
|
601
611
|
fromNumber: "+15550001111",
|
|
@@ -640,6 +650,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
640
650
|
ensureConversation("conv-instr-ended");
|
|
641
651
|
|
|
642
652
|
const session = createCallSession({
|
|
653
|
+
direction: "inbound",
|
|
643
654
|
conversationId: "conv-instr-ended",
|
|
644
655
|
provider: "twilio",
|
|
645
656
|
fromNumber: "+15550001111",
|
|
@@ -668,6 +679,7 @@ describe("runtime call routes — HTTP layer", () => {
|
|
|
668
679
|
ensureConversation("conv-instr-no-orch");
|
|
669
680
|
|
|
670
681
|
const session = createCallSession({
|
|
682
|
+
direction: "inbound",
|
|
671
683
|
conversationId: "conv-instr-no-orch",
|
|
672
684
|
provider: "twilio",
|
|
673
685
|
fromNumber: "+15550001111",
|
|
@@ -53,9 +53,14 @@ function resetTables() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Wrapper that ensures the FK conversation row exists before creating a session. */
|
|
56
|
-
function createTestCallSession(
|
|
56
|
+
function createTestCallSession(
|
|
57
|
+
opts: Omit<Parameters<typeof createCallSession>[0], "direction"> & {
|
|
58
|
+
direction?: Parameters<typeof createCallSession>[0]["direction"];
|
|
59
|
+
},
|
|
60
|
+
) {
|
|
57
61
|
ensureConversation(opts.conversationId);
|
|
58
|
-
|
|
62
|
+
// Direction only shapes the funnel stamp; these suites test other things.
|
|
63
|
+
return createCallSession({ direction: "inbound", ...opts });
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
describe("call-store", () => {
|
|
@@ -153,6 +153,7 @@ describe("Call session mode metadata", () => {
|
|
|
153
153
|
|
|
154
154
|
const { conversationId } = getOrCreateConversation("test-conv-mode");
|
|
155
155
|
const session = createCallSession({
|
|
156
|
+
direction: "inbound",
|
|
156
157
|
conversationId,
|
|
157
158
|
provider: "twilio",
|
|
158
159
|
fromNumber: "+15551234567",
|
|
@@ -181,6 +182,7 @@ describe("Call session mode metadata", () => {
|
|
|
181
182
|
"test-conv-mode-default",
|
|
182
183
|
);
|
|
183
184
|
const session = createCallSession({
|
|
185
|
+
direction: "inbound",
|
|
184
186
|
conversationId,
|
|
185
187
|
provider: "twilio",
|
|
186
188
|
fromNumber: "+15551234567",
|
|
@@ -158,6 +158,7 @@ describe("guardian-dispatch", () => {
|
|
|
158
158
|
ensureConversation(convId);
|
|
159
159
|
|
|
160
160
|
const session = createCallSession({
|
|
161
|
+
direction: "inbound",
|
|
161
162
|
conversationId: convId,
|
|
162
163
|
provider: "twilio",
|
|
163
164
|
fromNumber: "+15550001111",
|
|
@@ -209,6 +210,7 @@ describe("guardian-dispatch", () => {
|
|
|
209
210
|
ensureConversation(convId);
|
|
210
211
|
|
|
211
212
|
const session = createCallSession({
|
|
213
|
+
direction: "inbound",
|
|
212
214
|
conversationId: convId,
|
|
213
215
|
provider: "twilio",
|
|
214
216
|
fromNumber: "+15550001111",
|
|
@@ -238,6 +240,7 @@ describe("guardian-dispatch", () => {
|
|
|
238
240
|
ensureConversation(convId);
|
|
239
241
|
|
|
240
242
|
const session = createCallSession({
|
|
243
|
+
direction: "inbound",
|
|
241
244
|
conversationId: convId,
|
|
242
245
|
provider: "twilio",
|
|
243
246
|
fromNumber: "+15550001111",
|
|
@@ -282,6 +285,7 @@ describe("guardian-dispatch", () => {
|
|
|
282
285
|
};
|
|
283
286
|
|
|
284
287
|
const session = createCallSession({
|
|
288
|
+
direction: "inbound",
|
|
285
289
|
conversationId: convId,
|
|
286
290
|
provider: "twilio",
|
|
287
291
|
fromNumber: "+15550001111",
|
|
@@ -324,6 +328,7 @@ describe("guardian-dispatch", () => {
|
|
|
324
328
|
};
|
|
325
329
|
|
|
326
330
|
const session = createCallSession({
|
|
331
|
+
direction: "inbound",
|
|
327
332
|
conversationId: convId,
|
|
328
333
|
provider: "twilio",
|
|
329
334
|
fromNumber: "+15550001111",
|
|
@@ -369,6 +374,7 @@ describe("guardian-dispatch", () => {
|
|
|
369
374
|
};
|
|
370
375
|
|
|
371
376
|
const session = createCallSession({
|
|
377
|
+
direction: "inbound",
|
|
372
378
|
conversationId: convId,
|
|
373
379
|
provider: "twilio",
|
|
374
380
|
fromNumber: "+15550001111",
|
|
@@ -396,6 +402,7 @@ describe("guardian-dispatch", () => {
|
|
|
396
402
|
ensureConversation(convId);
|
|
397
403
|
|
|
398
404
|
const session = createCallSession({
|
|
405
|
+
direction: "inbound",
|
|
399
406
|
conversationId: convId,
|
|
400
407
|
provider: "twilio",
|
|
401
408
|
fromNumber: "+15550001111",
|
|
@@ -431,6 +438,7 @@ describe("guardian-dispatch", () => {
|
|
|
431
438
|
ensureConversation(convId);
|
|
432
439
|
|
|
433
440
|
const session = createCallSession({
|
|
441
|
+
direction: "inbound",
|
|
434
442
|
conversationId: convId,
|
|
435
443
|
provider: "twilio",
|
|
436
444
|
fromNumber: "+15550001111",
|
|
@@ -456,6 +464,7 @@ describe("guardian-dispatch", () => {
|
|
|
456
464
|
ensureConversation(convId);
|
|
457
465
|
|
|
458
466
|
const session = createCallSession({
|
|
467
|
+
direction: "inbound",
|
|
459
468
|
conversationId: convId,
|
|
460
469
|
provider: "twilio",
|
|
461
470
|
fromNumber: "+15550001111",
|
|
@@ -488,6 +497,7 @@ describe("guardian-dispatch", () => {
|
|
|
488
497
|
const sharedConversationId = "conv-shared-guardian";
|
|
489
498
|
|
|
490
499
|
const session = createCallSession({
|
|
500
|
+
direction: "inbound",
|
|
491
501
|
conversationId: convId,
|
|
492
502
|
provider: "twilio",
|
|
493
503
|
fromNumber: "+15550001111",
|
|
@@ -576,6 +586,7 @@ describe("guardian-dispatch", () => {
|
|
|
576
586
|
const sharedConversationId = "conv-affinity-guardian";
|
|
577
587
|
|
|
578
588
|
const session = createCallSession({
|
|
589
|
+
direction: "inbound",
|
|
579
590
|
conversationId: convId,
|
|
580
591
|
provider: "twilio",
|
|
581
592
|
fromNumber: "+15550001111",
|
|
@@ -648,6 +659,7 @@ describe("guardian-dispatch", () => {
|
|
|
648
659
|
const sharedConversationId = "conv-affinity-tool-guardian";
|
|
649
660
|
|
|
650
661
|
const session = createCallSession({
|
|
662
|
+
direction: "inbound",
|
|
651
663
|
conversationId: convId,
|
|
652
664
|
provider: "twilio",
|
|
653
665
|
fromNumber: "+15550001111",
|
|
@@ -727,6 +739,7 @@ describe("guardian-dispatch", () => {
|
|
|
727
739
|
const sharedConversationId = "conv-affinity-triple";
|
|
728
740
|
|
|
729
741
|
const session = createCallSession({
|
|
742
|
+
direction: "inbound",
|
|
730
743
|
conversationId: convId,
|
|
731
744
|
provider: "twilio",
|
|
732
745
|
fromNumber: "+15550001111",
|
|
@@ -160,6 +160,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
160
160
|
ensureConversation(convId);
|
|
161
161
|
|
|
162
162
|
const session = createCallSession({
|
|
163
|
+
direction: "inbound",
|
|
163
164
|
conversationId: convId,
|
|
164
165
|
provider: "twilio",
|
|
165
166
|
fromNumber: "+15550001111",
|
|
@@ -220,6 +221,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
220
221
|
};
|
|
221
222
|
|
|
222
223
|
const session = createCallSession({
|
|
224
|
+
direction: "inbound",
|
|
223
225
|
conversationId: convId,
|
|
224
226
|
provider: "twilio",
|
|
225
227
|
fromNumber: "+15550001111",
|
|
@@ -263,6 +265,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
263
265
|
};
|
|
264
266
|
|
|
265
267
|
const session = createCallSession({
|
|
268
|
+
direction: "inbound",
|
|
266
269
|
conversationId: convId,
|
|
267
270
|
provider: "twilio",
|
|
268
271
|
fromNumber: "+15550001111",
|
|
@@ -308,6 +311,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
308
311
|
};
|
|
309
312
|
|
|
310
313
|
const session = createCallSession({
|
|
314
|
+
direction: "inbound",
|
|
311
315
|
conversationId: convId,
|
|
312
316
|
provider: "twilio",
|
|
313
317
|
fromNumber: "+15550001111",
|
|
@@ -338,6 +342,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
338
342
|
ensureConversation(convId);
|
|
339
343
|
|
|
340
344
|
const session = createCallSession({
|
|
345
|
+
direction: "inbound",
|
|
341
346
|
conversationId: convId,
|
|
342
347
|
provider: "twilio",
|
|
343
348
|
fromNumber: "+15550001111",
|
|
@@ -370,6 +375,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
370
375
|
const sharedConvId = "conv-guardian-shared-thread";
|
|
371
376
|
|
|
372
377
|
const session = createCallSession({
|
|
378
|
+
direction: "inbound",
|
|
373
379
|
conversationId: convId,
|
|
374
380
|
provider: "twilio",
|
|
375
381
|
fromNumber: "+15550001111",
|
|
@@ -463,6 +469,7 @@ describe("ASK_GUARDIAN notification path", () => {
|
|
|
463
469
|
};
|
|
464
470
|
|
|
465
471
|
const session = createCallSession({
|
|
472
|
+
direction: "inbound",
|
|
466
473
|
conversationId: convId,
|
|
467
474
|
provider: "twilio",
|
|
468
475
|
fromNumber: "+15550001111",
|
|
@@ -421,6 +421,7 @@ function setupController(
|
|
|
421
421
|
) {
|
|
422
422
|
ensureConversation("conv-ctrl-test");
|
|
423
423
|
const session = createCallSession({
|
|
424
|
+
direction: "inbound",
|
|
424
425
|
conversationId: "conv-ctrl-test",
|
|
425
426
|
provider: "twilio",
|
|
426
427
|
fromNumber: "+15551111111",
|
|
@@ -471,6 +472,7 @@ function setupControllerWithOrigin(task?: string) {
|
|
|
471
472
|
ensureConversation("conv-ctrl-voice");
|
|
472
473
|
ensureConversation("conv-ctrl-origin");
|
|
473
474
|
const session = createCallSession({
|
|
475
|
+
direction: "inbound",
|
|
474
476
|
conversationId: "conv-ctrl-voice",
|
|
475
477
|
provider: "twilio",
|
|
476
478
|
fromNumber: "+15551111111",
|
|
@@ -5028,4 +5030,96 @@ describe("call-controller", () => {
|
|
|
5028
5030
|
await turnPromise.catch(() => {});
|
|
5029
5031
|
});
|
|
5030
5032
|
});
|
|
5033
|
+
describe("turn metrics", () => {
|
|
5034
|
+
test("a caller turn records the marks the latency numbers are built from", async () => {
|
|
5035
|
+
const { controller } = setupController();
|
|
5036
|
+
|
|
5037
|
+
await controller.handleCallerUtterance("What is the weather?");
|
|
5038
|
+
|
|
5039
|
+
const snapshot = controller.getMetricsSnapshot();
|
|
5040
|
+
expect(snapshot.recentTurns.length).toBe(1);
|
|
5041
|
+
const turn = snapshot.recentTurns[0];
|
|
5042
|
+
expect(turn.status).toBe("completed");
|
|
5043
|
+
// The transcript mark is seeded from when the caller stopped talking,
|
|
5044
|
+
// the dispatch mark from when the leg went out, and the delta mark from
|
|
5045
|
+
// the first token back: the three anchors every phone latency number is
|
|
5046
|
+
// subtracted from.
|
|
5047
|
+
expect(turn.timestamps.finalTranscriptAtMs).not.toBeNull();
|
|
5048
|
+
expect(turn.timestamps.assistantDispatchAtMs).not.toBeNull();
|
|
5049
|
+
expect(turn.timestamps.firstAssistantDeltaAtMs).not.toBeNull();
|
|
5050
|
+
expect(turn.durations.dispatchToFirstAssistantDeltaMs).not.toBeNull();
|
|
5051
|
+
expect(turn.durations.totalTurnDurationMs).not.toBeNull();
|
|
5052
|
+
// The provider's final doubles as the speech-end anchor, so the turn's
|
|
5053
|
+
// round trip is measurable on phone as it is in live voice.
|
|
5054
|
+
expect(turn.timestamps.utteranceEndAtMs).not.toBeNull();
|
|
5055
|
+
expect(turn.durations.roundTripMs).not.toBeNull();
|
|
5056
|
+
|
|
5057
|
+
controller.destroy();
|
|
5058
|
+
});
|
|
5059
|
+
|
|
5060
|
+
test("the transcript mark predates the dispatch it triggered", async () => {
|
|
5061
|
+
// Seeding backdates the turn to the caller's own boundary; a turn that
|
|
5062
|
+
// opened at dispatch time would report a felt latency that starts after
|
|
5063
|
+
// the caller already finished speaking.
|
|
5064
|
+
const { controller } = setupController();
|
|
5065
|
+
|
|
5066
|
+
await controller.handleCallerUtterance("Hello there");
|
|
5067
|
+
|
|
5068
|
+
const turn = controller.getMetricsSnapshot().recentTurns[0];
|
|
5069
|
+
const finalTranscriptAtMs = turn.timestamps.finalTranscriptAtMs;
|
|
5070
|
+
const assistantDispatchAtMs = turn.timestamps.assistantDispatchAtMs;
|
|
5071
|
+
expect(finalTranscriptAtMs).not.toBeNull();
|
|
5072
|
+
expect(assistantDispatchAtMs).not.toBeNull();
|
|
5073
|
+
expect(finalTranscriptAtMs!).toBeLessThanOrEqual(assistantDispatchAtMs!);
|
|
5074
|
+
expect(turn.timestamps.startedAtMs).toBeLessThanOrEqual(
|
|
5075
|
+
finalTranscriptAtMs!,
|
|
5076
|
+
);
|
|
5077
|
+
|
|
5078
|
+
controller.destroy();
|
|
5079
|
+
});
|
|
5080
|
+
|
|
5081
|
+
test("a barge-in ends the turn once, as cancelled", async () => {
|
|
5082
|
+
// One interruption is one cancelled turn: the count downstream reads as
|
|
5083
|
+
// "turns the caller cut off", so a second settle would double it.
|
|
5084
|
+
// A turn that never emits or completes holds the controller in the
|
|
5085
|
+
// pre-speech phase, which a sustained barge-in can still cut off.
|
|
5086
|
+
mockStartVoiceTurn.mockImplementation(
|
|
5087
|
+
async (opts: { onComplete: () => void }) => ({
|
|
5088
|
+
turnId: "run-metrics-slow",
|
|
5089
|
+
abort: () => opts.onComplete(),
|
|
5090
|
+
}),
|
|
5091
|
+
);
|
|
5092
|
+
const { controller } = setupController();
|
|
5093
|
+
const turnPromise = controller.handleCallerUtterance("Tell me a story");
|
|
5094
|
+
for (let i = 0; i < 5; i++) {
|
|
5095
|
+
await Promise.resolve();
|
|
5096
|
+
}
|
|
5097
|
+
expect(controller.getState()).toBe("processing");
|
|
5098
|
+
|
|
5099
|
+
expect(controller.handleBargeIn()).toBe(true);
|
|
5100
|
+
await turnPromise.catch(() => {});
|
|
5101
|
+
controller.destroy();
|
|
5102
|
+
|
|
5103
|
+
const turns = controller.getMetricsSnapshot().recentTurns;
|
|
5104
|
+
expect(turns.length).toBe(1);
|
|
5105
|
+
expect(turns[0].status).toBe("cancelled");
|
|
5106
|
+
expect(turns[0].cancellationReason).toBe("interrupted");
|
|
5107
|
+
expect(turns[0].timestamps.bargeInAtMs).not.toBeNull();
|
|
5108
|
+
});
|
|
5109
|
+
|
|
5110
|
+
test("no turn is invented between turns", async () => {
|
|
5111
|
+
// Every mark opens a turn when none is active, so a barge-in on an idle
|
|
5112
|
+
// controller must leave the collector empty rather than start a turn
|
|
5113
|
+
// that never settles.
|
|
5114
|
+
const { controller } = setupController();
|
|
5115
|
+
|
|
5116
|
+
expect(controller.handleBargeIn()).toBe(false);
|
|
5117
|
+
|
|
5118
|
+
const snapshot = controller.getMetricsSnapshot();
|
|
5119
|
+
expect(snapshot.recentTurns.length).toBe(0);
|
|
5120
|
+
expect(snapshot.activeTurn).toBeNull();
|
|
5121
|
+
|
|
5122
|
+
controller.destroy();
|
|
5123
|
+
});
|
|
5124
|
+
});
|
|
5031
5125
|
});
|