@vellumai/assistant 0.11.3 → 0.11.4-staging.1
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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -14,11 +14,6 @@ import type {
|
|
|
14
14
|
StreamingTranscriber,
|
|
15
15
|
SttStreamServerEvent,
|
|
16
16
|
} from "../../stt/types.js";
|
|
17
|
-
import type {
|
|
18
|
-
VoiceAckTextInput,
|
|
19
|
-
VoiceFrontDecider,
|
|
20
|
-
VoiceProgressTextInput,
|
|
21
|
-
} from "../front-decision.js";
|
|
22
17
|
import {
|
|
23
18
|
LiveVoiceSession,
|
|
24
19
|
type LiveVoiceTtsStreamer,
|
|
@@ -26,6 +21,10 @@ import {
|
|
|
26
21
|
} from "../live-voice-session.js";
|
|
27
22
|
import type { LiveVoiceSessionFactoryContext } from "../live-voice-session-manager.js";
|
|
28
23
|
import type { LiveVoiceTtsOptions } from "../live-voice-tts.js";
|
|
24
|
+
import type {
|
|
25
|
+
VoiceProgressNarrator,
|
|
26
|
+
VoiceProgressTextInput,
|
|
27
|
+
} from "../progress-narration.js";
|
|
29
28
|
import {
|
|
30
29
|
pickProgressPhrase,
|
|
31
30
|
PROGRESS_FALLBACK_PHRASES,
|
|
@@ -47,16 +46,7 @@ const START_FRAME = {
|
|
|
47
46
|
},
|
|
48
47
|
} as const satisfies LiveVoiceClientStartFrame;
|
|
49
48
|
|
|
50
|
-
// Generous enough that the slow-first-delta ack never fires during a test;
|
|
51
|
-
// the immediate tool-start ack still speaks and is asserted where relevant.
|
|
52
|
-
const GENEROUS_ACK_TIMEOUT_MS = 60_000;
|
|
53
|
-
|
|
54
49
|
const GENERATED_NARRATION = "Progress text.";
|
|
55
|
-
// Acks are front-model-phrased; narrations fall back to a static phrase.
|
|
56
|
-
// Both pass through the same TTS sanitizer, and each fresh session's
|
|
57
|
-
// narration phrase counter starts at 0.
|
|
58
|
-
const GENERATED_TOOL_ACK = "Let me look that up.";
|
|
59
|
-
const EXPECTED_TOOL_ACK = sanitizeForTts(GENERATED_TOOL_ACK).trim();
|
|
60
50
|
const EXPECTED_PROGRESS_FALLBACK = sanitizeForTts(pickProgressPhrase(0)).trim();
|
|
61
51
|
|
|
62
52
|
class MockStreamingTranscriber implements StreamingTranscriber {
|
|
@@ -143,11 +133,10 @@ function createRecordingTtsStreamer(
|
|
|
143
133
|
return { streamTtsAudio, ttsTexts, ttsCalls };
|
|
144
134
|
}
|
|
145
135
|
|
|
146
|
-
function
|
|
147
|
-
generateProgressText:
|
|
148
|
-
):
|
|
136
|
+
function makeProgressNarrator(
|
|
137
|
+
generateProgressText: VoiceProgressNarrator["generateProgressText"],
|
|
138
|
+
): VoiceProgressNarrator {
|
|
149
139
|
return {
|
|
150
|
-
generateAckText: async () => GENERATED_TOOL_ACK,
|
|
151
140
|
generateProgressText,
|
|
152
141
|
};
|
|
153
142
|
}
|
|
@@ -157,7 +146,6 @@ function progressConfig(
|
|
|
157
146
|
): Partial<LiveVoiceFrontModelConfig> {
|
|
158
147
|
const idleIntervalMs = overrides.idleIntervalMs ?? 60_000;
|
|
159
148
|
return {
|
|
160
|
-
ackFirstDeltaTimeoutMs: GENEROUS_ACK_TIMEOUT_MS,
|
|
161
149
|
progress: {
|
|
162
150
|
enabled: true,
|
|
163
151
|
opsThreshold: 3,
|
|
@@ -176,7 +164,7 @@ function progressConfig(
|
|
|
176
164
|
|
|
177
165
|
function createProgressHarness(options: {
|
|
178
166
|
frontModelConfig: Partial<LiveVoiceFrontModelConfig>;
|
|
179
|
-
|
|
167
|
+
progressNarrator: VoiceProgressNarrator;
|
|
180
168
|
emitMetrics?: boolean;
|
|
181
169
|
gateTtsText?: (text: string) => Promise<void> | null;
|
|
182
170
|
// Events the transcriber flushes at utterance release; defaults to a plain
|
|
@@ -195,7 +183,7 @@ function createProgressHarness(options: {
|
|
|
195
183
|
startVoiceTurn,
|
|
196
184
|
streamTtsAudio,
|
|
197
185
|
frontModelConfig: options.frontModelConfig,
|
|
198
|
-
|
|
186
|
+
progressNarrator: options.progressNarrator,
|
|
199
187
|
createTurnId: () => "live-turn-1",
|
|
200
188
|
emitMetrics: options.emitMetrics ?? false,
|
|
201
189
|
});
|
|
@@ -282,7 +270,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
282
270
|
);
|
|
283
271
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
284
272
|
frontModelConfig: progressConfig({ idleIntervalMs: 40 }),
|
|
285
|
-
|
|
273
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
286
274
|
});
|
|
287
275
|
await startReleasedTurn(session, getCallbacks);
|
|
288
276
|
|
|
@@ -308,22 +296,18 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
308
296
|
});
|
|
309
297
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
310
298
|
frontModelConfig: progressConfig({ opsThreshold: 3, minGapMs: 10 }),
|
|
311
|
-
|
|
299
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
312
300
|
});
|
|
313
301
|
|
|
314
302
|
await startReleasedTurn(session, getCallbacks);
|
|
315
303
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
316
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
317
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
|
|
318
304
|
|
|
319
305
|
emitToolResult(getCallbacks, "web_search", "tool-1", "found 3 results");
|
|
320
306
|
emitToolStart(getCallbacks, "file_read", "tool-2");
|
|
321
307
|
emitToolResult(getCallbacks, "file_read", "tool-2", "file contents");
|
|
322
|
-
// Let the ack's minGapMs spacing elapse before the threshold-crossing op.
|
|
323
|
-
await sleep(30);
|
|
324
308
|
emitToolStart(getCallbacks, "web_search", "tool-3");
|
|
325
|
-
await waitFor(() => ttsTexts.length ===
|
|
326
|
-
expect(ttsTexts).toEqual([
|
|
309
|
+
await waitFor(() => ttsTexts.length === 1);
|
|
310
|
+
expect(ttsTexts).toEqual([GENERATED_NARRATION]);
|
|
327
311
|
expect(generateProgressText).toHaveBeenCalledTimes(1);
|
|
328
312
|
expect(inputs[0]).toMatchObject({
|
|
329
313
|
transcriptSoFar: "hello",
|
|
@@ -339,7 +323,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
339
323
|
// below the threshold: still exactly one narration.
|
|
340
324
|
emitToolResult(getCallbacks, "web_search", "tool-3");
|
|
341
325
|
await sleep(30);
|
|
342
|
-
expect(ttsTexts).toEqual([
|
|
326
|
+
expect(ttsTexts).toEqual([GENERATED_NARRATION]);
|
|
343
327
|
expect(generateProgressText).toHaveBeenCalledTimes(1);
|
|
344
328
|
|
|
345
329
|
emitMessageComplete(getCallbacks);
|
|
@@ -355,12 +339,11 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
355
339
|
});
|
|
356
340
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
357
341
|
frontModelConfig: progressConfig({ opsThreshold: 3, minGapMs: 10 }),
|
|
358
|
-
|
|
342
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
359
343
|
});
|
|
360
344
|
|
|
361
345
|
await startReleasedTurn(session, getCallbacks);
|
|
362
346
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
363
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
364
347
|
|
|
365
348
|
// Name-only result: no toolUseId to correlate by.
|
|
366
349
|
getCallbacks()?.tool_result?.({
|
|
@@ -368,11 +351,9 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
368
351
|
resultPreview: "found it",
|
|
369
352
|
});
|
|
370
353
|
emitToolStart(getCallbacks, "file_read", "tool-2");
|
|
371
|
-
// Let the ack's minGapMs spacing elapse before the threshold-crossing op.
|
|
372
|
-
await sleep(30);
|
|
373
354
|
emitToolStart(getCallbacks, "bash", "tool-3");
|
|
374
|
-
await waitFor(() => ttsTexts.length ===
|
|
375
|
-
expect(ttsTexts).toEqual([
|
|
355
|
+
await waitFor(() => ttsTexts.length === 1);
|
|
356
|
+
expect(ttsTexts).toEqual([GENERATED_NARRATION]);
|
|
376
357
|
|
|
377
358
|
// The name fallback completed the web_search op: it reports as completed
|
|
378
359
|
// (with its preview) rather than lingering incomplete.
|
|
@@ -384,7 +365,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
384
365
|
emitMessageComplete(getCallbacks);
|
|
385
366
|
});
|
|
386
367
|
|
|
387
|
-
test("completedOps reach the
|
|
368
|
+
test("completedOps reach the narrator in completion order, not start order", async () => {
|
|
388
369
|
const inputs: VoiceProgressTextInput[] = [];
|
|
389
370
|
const generateProgressText = mock(async (input: VoiceProgressTextInput) => {
|
|
390
371
|
inputs.push(input);
|
|
@@ -392,21 +373,20 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
392
373
|
});
|
|
393
374
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
394
375
|
frontModelConfig: progressConfig({ opsThreshold: 3, minGapMs: 10 }),
|
|
395
|
-
|
|
376
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
396
377
|
});
|
|
397
378
|
|
|
398
379
|
await startReleasedTurn(session, getCallbacks);
|
|
399
380
|
// Two parallel tools start in one order…
|
|
400
381
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
401
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
402
382
|
emitToolStart(getCallbacks, "file_read", "tool-2");
|
|
403
|
-
//
|
|
404
|
-
// timestamps
|
|
383
|
+
// They complete in the other order. The sleep separates their completion
|
|
384
|
+
// timestamps on the millisecond clock.
|
|
405
385
|
emitToolResult(getCallbacks, "file_read", "tool-2", "file contents");
|
|
406
386
|
await sleep(30);
|
|
407
387
|
emitToolResult(getCallbacks, "web_search", "tool-1", "found 3 results");
|
|
408
388
|
emitToolStart(getCallbacks, "bash", "tool-3");
|
|
409
|
-
await waitFor(() => ttsTexts.length ===
|
|
389
|
+
await waitFor(() => ttsTexts.length === 1);
|
|
410
390
|
|
|
411
391
|
expect(inputs[0]?.completedOps).toEqual([
|
|
412
392
|
{ toolName: "file_read", resultPreview: "file contents" },
|
|
@@ -416,7 +396,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
416
396
|
emitMessageComplete(getCallbacks);
|
|
417
397
|
});
|
|
418
398
|
|
|
419
|
-
test("idle trigger: dead air narrates with
|
|
399
|
+
test("idle trigger: dead air narrates with generated text, audio-only", async () => {
|
|
420
400
|
const inputs: VoiceProgressTextInput[] = [];
|
|
421
401
|
const generateProgressText = mock(async (input: VoiceProgressTextInput) => {
|
|
422
402
|
inputs.push(input);
|
|
@@ -427,7 +407,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
427
407
|
idleIntervalMs: 40,
|
|
428
408
|
minGapMs: 60_000,
|
|
429
409
|
}),
|
|
430
|
-
|
|
410
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
431
411
|
});
|
|
432
412
|
|
|
433
413
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -448,14 +428,14 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
448
428
|
emitMessageComplete(getCallbacks);
|
|
449
429
|
});
|
|
450
430
|
|
|
451
|
-
test("idle trigger with a null
|
|
431
|
+
test("idle trigger with a null narrator result speaks the static fallback", async () => {
|
|
452
432
|
const generateProgressText = mock(async () => null);
|
|
453
433
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
454
434
|
frontModelConfig: progressConfig({
|
|
455
435
|
idleIntervalMs: 40,
|
|
456
436
|
minGapMs: 60_000,
|
|
457
437
|
}),
|
|
458
|
-
|
|
438
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
459
439
|
});
|
|
460
440
|
|
|
461
441
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -488,7 +468,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
488
468
|
idleIntervalMs: 40,
|
|
489
469
|
minGapMs: 60_000,
|
|
490
470
|
}),
|
|
491
|
-
|
|
471
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
492
472
|
});
|
|
493
473
|
|
|
494
474
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -500,7 +480,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
500
480
|
emitMessageComplete(getCallbacks);
|
|
501
481
|
});
|
|
502
482
|
|
|
503
|
-
test("a Hindi turn's idle fallback speaks the Hindi phrase and hints the
|
|
483
|
+
test("a Hindi turn's idle fallback speaks the Hindi phrase and hints the narrator", async () => {
|
|
504
484
|
const inputs: VoiceProgressTextInput[] = [];
|
|
505
485
|
const generateProgressText = mock(async (input: VoiceProgressTextInput) => {
|
|
506
486
|
inputs.push(input);
|
|
@@ -511,7 +491,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
511
491
|
idleIntervalMs: 40,
|
|
512
492
|
minGapMs: 60_000,
|
|
513
493
|
}),
|
|
514
|
-
|
|
494
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
515
495
|
sttStopEvents: [
|
|
516
496
|
{ type: "final", text: "नमस्ते", languages: ["hi"] },
|
|
517
497
|
{ type: "closed" },
|
|
@@ -522,7 +502,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
522
502
|
await waitFor(() => ttsTexts.length === 1);
|
|
523
503
|
|
|
524
504
|
// The static fallback rotates through the caller's language's list, not
|
|
525
|
-
// the English default, and the
|
|
505
|
+
// the English default, and the narrator was told the language too.
|
|
526
506
|
expect(pickProgressPhrase(0, "hi")).toBe(
|
|
527
507
|
PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE.hi![0]!,
|
|
528
508
|
);
|
|
@@ -560,7 +540,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
560
540
|
idleIntervalMs: 40,
|
|
561
541
|
minGapMs: 60_000,
|
|
562
542
|
}),
|
|
563
|
-
|
|
543
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
564
544
|
},
|
|
565
545
|
);
|
|
566
546
|
|
|
@@ -583,41 +563,9 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
583
563
|
}
|
|
584
564
|
});
|
|
585
565
|
|
|
586
|
-
test("
|
|
587
|
-
const ackInputs: VoiceAckTextInput[] = [];
|
|
588
|
-
const frontDecider: VoiceFrontDecider = {
|
|
589
|
-
generateAckText: async (input) => {
|
|
590
|
-
ackInputs.push(input);
|
|
591
|
-
return GENERATED_TOOL_ACK;
|
|
592
|
-
},
|
|
593
|
-
generateProgressText: async () => null,
|
|
594
|
-
};
|
|
595
|
-
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
596
|
-
frontModelConfig: progressConfig(),
|
|
597
|
-
frontDecider,
|
|
598
|
-
sttStopEvents: [
|
|
599
|
-
{ type: "final", text: "नमस्ते", languages: ["hi"] },
|
|
600
|
-
{ type: "closed" },
|
|
601
|
-
],
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
await startReleasedTurn(session, getCallbacks);
|
|
605
|
-
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
606
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
607
|
-
|
|
608
|
-
expect(ackInputs[0]?.languageHint).toBe("hi");
|
|
609
|
-
|
|
610
|
-
emitMessageComplete(getCallbacks);
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
test("with no detected language the decider inputs carry no hint and the fallback stays English", async () => {
|
|
566
|
+
test("with no detected language the narrator gets no hint and fallback stays English", async () => {
|
|
614
567
|
const inputs: VoiceProgressTextInput[] = [];
|
|
615
|
-
const
|
|
616
|
-
const frontDecider: VoiceFrontDecider = {
|
|
617
|
-
generateAckText: async (input) => {
|
|
618
|
-
ackInputs.push(input);
|
|
619
|
-
return GENERATED_TOOL_ACK;
|
|
620
|
-
},
|
|
568
|
+
const progressNarrator: VoiceProgressNarrator = {
|
|
621
569
|
generateProgressText: async (input) => {
|
|
622
570
|
inputs.push(input);
|
|
623
571
|
return null;
|
|
@@ -625,24 +573,22 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
625
573
|
};
|
|
626
574
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
627
575
|
frontModelConfig: progressConfig({ idleIntervalMs: 40, minGapMs: 10 }),
|
|
628
|
-
|
|
576
|
+
progressNarrator,
|
|
629
577
|
});
|
|
630
578
|
|
|
631
579
|
await startReleasedTurn(session, getCallbacks);
|
|
632
580
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
633
581
|
await waitFor(() => ttsTexts.length === 1);
|
|
634
|
-
await waitFor(() => ttsTexts.length === 2);
|
|
635
582
|
|
|
636
|
-
// Language unknown: the
|
|
637
|
-
//
|
|
638
|
-
expect(ackInputs[0]).not.toHaveProperty("languageHint");
|
|
583
|
+
// Language unknown: the input has no languageHint key and the static
|
|
584
|
+
// fallback comes from the English list.
|
|
639
585
|
expect(inputs[0]).not.toHaveProperty("languageHint");
|
|
640
|
-
expect(ttsTexts).toEqual([
|
|
586
|
+
expect(ttsTexts).toEqual([EXPECTED_PROGRESS_FALLBACK]);
|
|
641
587
|
|
|
642
588
|
emitMessageComplete(getCallbacks);
|
|
643
589
|
});
|
|
644
590
|
|
|
645
|
-
test("ops trigger with a null
|
|
591
|
+
test("ops trigger with a null narrator result stays silent and keeps the update budget", async () => {
|
|
646
592
|
const generateProgressText = mock(async () => null);
|
|
647
593
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
648
594
|
frontModelConfig: progressConfig({
|
|
@@ -650,96 +596,19 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
650
596
|
idleIntervalMs: 120,
|
|
651
597
|
minGapMs: 10,
|
|
652
598
|
}),
|
|
653
|
-
|
|
599
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
654
600
|
});
|
|
655
601
|
|
|
656
602
|
await startReleasedTurn(session, getCallbacks);
|
|
657
603
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
658
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
659
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
|
|
660
|
-
|
|
661
|
-
// Past the ack gap, the ops trigger fires but generation returns null:
|
|
662
|
-
// nothing speaks and no update is consumed.
|
|
663
|
-
await sleep(30);
|
|
664
|
-
emitToolResult(getCallbacks, "web_search", "tool-1");
|
|
665
604
|
await waitFor(() => generateProgressText.mock.calls.length === 1);
|
|
666
605
|
await sleep(10);
|
|
667
|
-
expect(ttsTexts).toEqual([
|
|
606
|
+
expect(ttsTexts).toEqual([]);
|
|
668
607
|
|
|
669
608
|
// The idle trigger still has the full budget: its own null falls back to
|
|
670
609
|
// the static phrase.
|
|
671
|
-
await waitFor(() => ttsTexts.length === 2);
|
|
672
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, EXPECTED_PROGRESS_FALLBACK]);
|
|
673
|
-
|
|
674
|
-
emitMessageComplete(getCallbacks);
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
test("minGapMs: a tool-start ack suppresses narration until the gap elapses", async () => {
|
|
678
|
-
const generateProgressText = mock(async () => GENERATED_NARRATION);
|
|
679
|
-
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
680
|
-
frontModelConfig: progressConfig({ opsThreshold: 1, minGapMs: 150 }),
|
|
681
|
-
frontDecider: makeProgressDecider(generateProgressText),
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
await startReleasedTurn(session, getCallbacks);
|
|
685
|
-
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
686
610
|
await waitFor(() => ttsTexts.length === 1);
|
|
687
|
-
|
|
688
|
-
emitToolResult(getCallbacks, "web_search", "tool-1");
|
|
689
|
-
await sleep(30);
|
|
690
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
|
|
691
|
-
expect(generateProgressText).not.toHaveBeenCalled();
|
|
692
|
-
|
|
693
|
-
await sleep(150);
|
|
694
|
-
emitToolStart(getCallbacks, "file_read", "tool-2");
|
|
695
|
-
await waitFor(() => ttsTexts.length === 2);
|
|
696
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, GENERATED_NARRATION]);
|
|
697
|
-
expect(generateProgressText).toHaveBeenCalledTimes(1);
|
|
698
|
-
|
|
699
|
-
emitMessageComplete(getCallbacks);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
test("no narration generation launches while an ack generation is pending", async () => {
|
|
703
|
-
const ackGeneration = deferred<string | null>();
|
|
704
|
-
const generateAckText = mock(() => ackGeneration.promise);
|
|
705
|
-
const generateProgressText = mock(async () => GENERATED_NARRATION);
|
|
706
|
-
const frontDecider: VoiceFrontDecider = {
|
|
707
|
-
generateAckText,
|
|
708
|
-
generateProgressText,
|
|
709
|
-
};
|
|
710
|
-
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
711
|
-
frontModelConfig: progressConfig({ opsThreshold: 1, minGapMs: 150 }),
|
|
712
|
-
frontDecider,
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
await startReleasedTurn(session, getCallbacks);
|
|
716
|
-
// The tool start speaks a generated ack (generation still pending) and
|
|
717
|
-
// simultaneously crosses the ops threshold; the entry guard must stand
|
|
718
|
-
// down instead of launching a narration generation whose result the
|
|
719
|
-
// post-await re-check would only discard.
|
|
720
|
-
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
721
|
-
await sleep(30);
|
|
722
|
-
expect(generateProgressText).not.toHaveBeenCalled();
|
|
723
|
-
expect(ttsTexts).toEqual([]);
|
|
724
|
-
|
|
725
|
-
ackGeneration.resolve("Generated ack.");
|
|
726
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
727
|
-
expect(ttsTexts).toEqual(["Generated ack."]);
|
|
728
|
-
|
|
729
|
-
// Inside the ack's minGapMs window nothing else speaks even as tool
|
|
730
|
-
// activity continues.
|
|
731
|
-
emitToolResult(getCallbacks, "web_search", "tool-1");
|
|
732
|
-
await sleep(30);
|
|
733
|
-
expect(generateProgressText).not.toHaveBeenCalled();
|
|
734
|
-
expect(ttsTexts).toEqual(["Generated ack."]);
|
|
735
|
-
|
|
736
|
-
// The stand-down preserved the update budget: once the gap elapses, the
|
|
737
|
-
// next ops trigger narrates normally.
|
|
738
|
-
await sleep(150);
|
|
739
|
-
emitToolStart(getCallbacks, "file_read", "tool-2");
|
|
740
|
-
await waitFor(() => ttsTexts.length === 2);
|
|
741
|
-
expect(ttsTexts).toEqual(["Generated ack.", GENERATED_NARRATION]);
|
|
742
|
-
expect(generateProgressText).toHaveBeenCalledTimes(1);
|
|
611
|
+
expect(ttsTexts).toEqual([EXPECTED_PROGRESS_FALLBACK]);
|
|
743
612
|
|
|
744
613
|
emitMessageComplete(getCallbacks);
|
|
745
614
|
});
|
|
@@ -752,7 +621,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
752
621
|
});
|
|
753
622
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
754
623
|
frontModelConfig: progressConfig({ idleIntervalMs: 30, minGapMs: 1 }),
|
|
755
|
-
|
|
624
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
756
625
|
});
|
|
757
626
|
|
|
758
627
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -786,7 +655,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
786
655
|
idleIntervalMs: 40,
|
|
787
656
|
minGapMs: 10,
|
|
788
657
|
}),
|
|
789
|
-
|
|
658
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
790
659
|
});
|
|
791
660
|
|
|
792
661
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -818,7 +687,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
818
687
|
idleIntervalMs: 40,
|
|
819
688
|
minGapMs: 60_000,
|
|
820
689
|
}),
|
|
821
|
-
|
|
690
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
822
691
|
gateTtsText: (text) => (text === "Hello there." ? ttsGate.promise : null),
|
|
823
692
|
});
|
|
824
693
|
|
|
@@ -843,7 +712,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
843
712
|
const generateProgressText = mock(() => generation.promise);
|
|
844
713
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
845
714
|
frontModelConfig: progressConfig({ idleIntervalMs: 40 }),
|
|
846
|
-
|
|
715
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
847
716
|
});
|
|
848
717
|
|
|
849
718
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -864,7 +733,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
864
733
|
idleIntervalMs: 40,
|
|
865
734
|
minGapMs: 60_000,
|
|
866
735
|
}),
|
|
867
|
-
|
|
736
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
868
737
|
emitMetrics: true,
|
|
869
738
|
});
|
|
870
739
|
|
|
@@ -889,7 +758,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
889
758
|
});
|
|
890
759
|
});
|
|
891
760
|
|
|
892
|
-
test("progress.enabled false: zero narrations and zero
|
|
761
|
+
test("progress.enabled false: zero narrations and zero narrator calls", async () => {
|
|
893
762
|
const generateProgressText = mock(async () => GENERATED_NARRATION);
|
|
894
763
|
const { session, getCallbacks, ttsTexts } = createProgressHarness({
|
|
895
764
|
frontModelConfig: progressConfig({
|
|
@@ -898,7 +767,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
898
767
|
idleIntervalMs: 30,
|
|
899
768
|
minGapMs: 1,
|
|
900
769
|
}),
|
|
901
|
-
|
|
770
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
902
771
|
});
|
|
903
772
|
|
|
904
773
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -908,8 +777,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
908
777
|
emitToolResult(getCallbacks, "file_read", "tool-2");
|
|
909
778
|
await sleep(100);
|
|
910
779
|
|
|
911
|
-
|
|
912
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
|
|
780
|
+
expect(ttsTexts).toEqual([]);
|
|
913
781
|
expect(generateProgressText).not.toHaveBeenCalled();
|
|
914
782
|
|
|
915
783
|
emitMessageComplete(getCallbacks);
|
|
@@ -922,7 +790,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
922
790
|
idleIntervalMs: 20,
|
|
923
791
|
maxSilenceMs: 150,
|
|
924
792
|
}),
|
|
925
|
-
|
|
793
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
926
794
|
});
|
|
927
795
|
|
|
928
796
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -949,7 +817,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
949
817
|
idleIntervalMs: 20,
|
|
950
818
|
maxSilenceMs: 600_000,
|
|
951
819
|
}),
|
|
952
|
-
|
|
820
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
953
821
|
});
|
|
954
822
|
|
|
955
823
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -958,14 +826,14 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
958
826
|
|
|
959
827
|
// A tool starting is news: the next tick narrates it.
|
|
960
828
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
961
|
-
await waitFor(() => ttsTexts.length ===
|
|
962
|
-
expect(ttsTexts).toEqual([
|
|
829
|
+
await waitFor(() => ttsTexts.length === 1);
|
|
830
|
+
expect(ttsTexts).toEqual([GENERATED_NARRATION]);
|
|
963
831
|
|
|
964
832
|
// Nothing has happened since, so the ticks that follow stay quiet
|
|
965
833
|
// instead of repeating the same update on a clock.
|
|
966
834
|
await sleep(80);
|
|
967
835
|
expect(generateProgressText).toHaveBeenCalledTimes(1);
|
|
968
|
-
expect(ttsTexts).toEqual([
|
|
836
|
+
expect(ttsTexts).toEqual([GENERATED_NARRATION]);
|
|
969
837
|
|
|
970
838
|
emitMessageComplete(getCallbacks);
|
|
971
839
|
});
|
|
@@ -984,17 +852,16 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
984
852
|
idleIntervalMs: 60_000,
|
|
985
853
|
longOpMs: 25,
|
|
986
854
|
}),
|
|
987
|
-
|
|
855
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
988
856
|
});
|
|
989
857
|
|
|
990
858
|
await startReleasedTurn(session, getCallbacks);
|
|
991
859
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
992
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
993
860
|
await sleep(40);
|
|
994
861
|
|
|
995
862
|
emitToolResult(getCallbacks, "web_search", "tool-1");
|
|
996
|
-
await waitFor(() => ttsTexts.length ===
|
|
997
|
-
expect(ttsTexts).toEqual([
|
|
863
|
+
await waitFor(() => ttsTexts.length === 1);
|
|
864
|
+
expect(ttsTexts).toEqual([GENERATED_NARRATION]);
|
|
998
865
|
expect(inputs[0]?.completedOps).toEqual([
|
|
999
866
|
{ toolName: "web_search", resultPreview: "ok" },
|
|
1000
867
|
]);
|
|
@@ -1010,18 +877,17 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
1010
877
|
idleIntervalMs: 60_000,
|
|
1011
878
|
longOpMs: 60_000,
|
|
1012
879
|
}),
|
|
1013
|
-
|
|
880
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
1014
881
|
});
|
|
1015
882
|
|
|
1016
883
|
await startReleasedTurn(session, getCallbacks);
|
|
1017
884
|
emitToolStart(getCallbacks, "web_search", "tool-1");
|
|
1018
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
1019
885
|
emitToolResult(getCallbacks, "web_search", "tool-1");
|
|
1020
886
|
await sleep(60);
|
|
1021
887
|
|
|
1022
888
|
// Narrating every quick lookup is the chatter the cadence avoids.
|
|
1023
889
|
expect(generateProgressText).not.toHaveBeenCalled();
|
|
1024
|
-
expect(ttsTexts).toEqual([
|
|
890
|
+
expect(ttsTexts).toEqual([]);
|
|
1025
891
|
|
|
1026
892
|
emitMessageComplete(getCallbacks);
|
|
1027
893
|
});
|
|
@@ -1041,7 +907,7 @@ describe("LiveVoiceSession progress narration", () => {
|
|
|
1041
907
|
idleIntervalMs: 20,
|
|
1042
908
|
maxSilenceMs: 600_000,
|
|
1043
909
|
}),
|
|
1044
|
-
|
|
910
|
+
progressNarrator: makeProgressNarrator(generateProgressText),
|
|
1045
911
|
});
|
|
1046
912
|
|
|
1047
913
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -261,6 +261,20 @@ describe("LiveVoiceSession STT", () => {
|
|
|
261
261
|
]);
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
test("ignores model-integrated turn-detection events", async () => {
|
|
265
|
+
const { frames, session, transcriber } = createSessionWithTranscriber();
|
|
266
|
+
|
|
267
|
+
await session.start();
|
|
268
|
+
transcriber.emit({ type: "turn-start" });
|
|
269
|
+
transcriber.emit({ type: "eager-turn-end", text: "eager" });
|
|
270
|
+
transcriber.emit({ type: "turn-resumed" });
|
|
271
|
+
transcriber.emit({ type: "turn-end", text: "committed", confidence: 0.9 });
|
|
272
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
273
|
+
|
|
274
|
+
expect(frames.map((frame) => frame.type)).toEqual(["ready"]);
|
|
275
|
+
expect(session.finalTranscriptText).toBe("");
|
|
276
|
+
});
|
|
277
|
+
|
|
264
278
|
test("marks transient transcriber errors recoverable while the session continues", async () => {
|
|
265
279
|
const { frames, session, transcriber } = createSessionWithTranscriber();
|
|
266
280
|
|
|
@@ -201,6 +201,35 @@ describe("live-voice triage-and-escalate routing", () => {
|
|
|
201
201
|
expect(escalated?.content).toBe(ESCALATION_CONTINUATION_CONTENT);
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
test("no leg is told to refuse setup flows, and the escalated leg is told to run them", async () => {
|
|
205
|
+
const { starter } = scriptedStartVoiceTurn({
|
|
206
|
+
frontDoor: ["[1] ", "Let me think about that."],
|
|
207
|
+
escalated: ["The detailed answer is 42."],
|
|
208
|
+
});
|
|
209
|
+
const { frames, session } = createHarness(starter);
|
|
210
|
+
|
|
211
|
+
await driveTurn(session);
|
|
212
|
+
await waitFor(() => starter.mock.calls.length >= 2);
|
|
213
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
214
|
+
|
|
215
|
+
const frontDoorPrompt =
|
|
216
|
+
starter.mock.calls[0]?.[0]?.voiceControlPrompt ?? "";
|
|
217
|
+
const escalatedPrompt =
|
|
218
|
+
starter.mock.calls[1]?.[0]?.voiceControlPrompt ?? "";
|
|
219
|
+
// The phone's no-setup-flows rule must not leak onto a channel that has a
|
|
220
|
+
// screen: it contradicts the base prompt's "never tell the user you cannot
|
|
221
|
+
// show them something".
|
|
222
|
+
expect(frontDoorPrompt).not.toContain("Never start account connections");
|
|
223
|
+
expect(escalatedPrompt).not.toContain("Never start account connections");
|
|
224
|
+
expect(frontDoorPrompt).not.toContain("finish it in text chat");
|
|
225
|
+
expect(escalatedPrompt).not.toContain("finish it in text chat");
|
|
226
|
+
// The leg that can actually run one is told to.
|
|
227
|
+
expect(escalatedPrompt).toContain("This includes connecting accounts");
|
|
228
|
+
// The toolless fast leg is not: a connection is not its to run, and its
|
|
229
|
+
// capability digest already routes anything needing a tool here.
|
|
230
|
+
expect(frontDoorPrompt).not.toContain("This includes connecting accounts");
|
|
231
|
+
});
|
|
232
|
+
|
|
204
233
|
test("the screen-reveal teaching reaches the escalated leg's prompt but never the front-door leg's", async () => {
|
|
205
234
|
const { starter } = scriptedStartVoiceTurn({
|
|
206
235
|
frontDoor: ["[1] ", "Let me think about that."],
|