@vellumai/assistant 0.11.3 → 0.11.4-dev.202608190019.b94dbf2
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/AGENTS.md +9 -3
- package/ARCHITECTURE.md +32 -10
- package/docs/architecture/memory.md +43 -5
- package/docs/architecture/security.md +96 -152
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/browser-use-architecture-phase2.md +128 -56
- package/docs/flux-turn-detection-spike.md +248 -0
- package/docs/guardian-request-flow.md +35 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/knip.json +3 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +289 -113
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/scripts/write-plugin-api-shim.ts +10 -0
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/app-control-flow.test.ts +1 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -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__/channel-inbound-disk-pressure.test.ts +4 -4
- package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/compaction-events.test.ts +8 -10
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- 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 +32 -21
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +112 -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-load-history-repair.test.ts +209 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +154 -4
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +187 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/conversation-slash-queue.test.ts +3 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
- package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -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__/document-sync-tags.test.ts +0 -75
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -11
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/gateway-only-guard.test.ts +2 -5
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/http-user-message-parity.test.ts +1 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/media-generate-image.test.ts +131 -21
- package/src/__tests__/memory-retrieval-hook.test.ts +100 -7
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +3 -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__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/plugin-api-store-credential.test.ts +261 -0
- package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -4
- 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__/proxy-approval-callback.test.ts +1 -0
- package/src/__tests__/qdrant-manager.test.ts +14 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +460 -105
- package/src/__tests__/run-due-schedules.test.ts +21 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
- package/src/__tests__/schedule-routes.test.ts +23 -0
- package/src/__tests__/schedule-store.test.ts +17 -0
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +3 -3
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/starter-task-flow.test.ts +5 -4
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
- package/src/__tests__/subagent-spawn-and-await.test.ts +18 -17
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +81 -101
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +68 -39
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-visual-surface.test.ts +1 -1
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/ui-work-result-surface.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +3 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
- package/src/__tests__/voice-session-bridge.test.ts +85 -29
- 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/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -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 +76 -3
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/events/host-file.ts +2 -2
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +12 -3
- 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 +194 -11
- package/src/calls/__tests__/voice-triage-escalate.test.ts +102 -0
- package/src/calls/call-controller.ts +19 -3
- 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 +115 -41
- package/src/calls/voice-triage-escalate.ts +105 -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/bundled-modules.ts +29 -0
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/db/repair.ts +4 -8
- package/src/cli/commands/domain.ts +6 -3
- package/src/cli/commands/email.ts +6 -3
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/keys.ts +8 -3
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +84 -28
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/plugins.ts +85 -36
- package/src/cli/commands/schedules.ts +35 -1
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +278 -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/assistant-feature-flags.ts +36 -15
- package/src/config/balanced-model-experiment.ts +35 -0
- 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/image-studio/SKILL.md +5 -4
- package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
- 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/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -12
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
- 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 +11 -5
- package/src/config/default-profile-catalog.ts +179 -16
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/feature-flag-registry.json +12 -12
- 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/config/skills.ts +9 -2
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -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 +31 -19
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +20 -10
- package/src/daemon/conversation-process.ts +20 -6
- package/src/daemon/conversation-runtime-assembly.ts +12 -6
- package/src/daemon/conversation-store.ts +14 -4
- package/src/daemon/conversation-surfaces.ts +75 -26
- package/src/daemon/conversation-tool-setup.ts +3 -6
- package/src/daemon/conversation.ts +154 -51
- package/src/daemon/doordash-steps.ts +2 -2
- package/src/daemon/lifecycle.ts +14 -1
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -26
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/trust-context.ts +12 -7
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/daemon/windows-compiled-entry.ts +4 -0
- package/src/documents/document-store.ts +143 -240
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +103 -45
- package/src/hooks/types.ts +5 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/gateway-flag-listener.ts +17 -3
- 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 +1050 -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-manager.ts +16 -3
- 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 +598 -323
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/live-voice/windows-compiled-live-voice.ts +4 -0
- 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/control.ts +1 -0
- package/src/monitoring/db-integrity-sample.ts +4 -5
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/approval-card-data.ts +33 -0
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -5
- 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 +182 -52
- package/src/persistence/delivery-crud.ts +150 -36
- package/src/persistence/embeddings/qdrant-manager.ts +84 -49
- 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/360-add-document-workspace-path.ts +5 -14
- 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/schema/documents.ts +4 -4
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/constants.ts +8 -0
- package/src/plugin-api/conversation-turn.ts +195 -5
- package/src/plugin-api/index.ts +38 -6
- package/src/plugin-api/resolve-credential.ts +3 -2
- package/src/plugin-api/store-credential.ts +146 -0
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugin-api/webhook-url.ts +13 -11
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/main.ts +21 -7
- package/src/plugins/defaults/memory/AGENTS.md +15 -3
- 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/__tests__/memory-tier-boundary-guard.test.ts +0 -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/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
- 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 +17 -24
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
- 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/memory-retrospective-job.ts +4 -4
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +147 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +195 -58
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +93 -24
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -33
- 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/__tests__/injection.test.ts +18 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/defaults/memory/v3/injector.ts +8 -0
- 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/v3/shadow-plugin.ts +23 -9
- package/src/plugins/defaults/memory/worker-control.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/defaults/worker-entrypoints.ts +3 -0
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +37 -46
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/prompts/templates/system-sections.ts +0 -7
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
- 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 +25 -2
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +4 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- 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-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +163 -40
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +133 -36
- 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 +625 -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 +679 -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/control.ts +1 -0
- package/src/routes/route-host-client.ts +1 -0
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +48 -15
- package/src/runtime/AGENTS.md +16 -17
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +18 -15
- package/src/runtime/effective-capabilities.test.ts +43 -0
- package/src/runtime/effective-capabilities.ts +27 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- 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 +522 -2
- 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 +132 -35
- package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +133 -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 +181 -23
- package/src/runtime/routes/conversation-management-routes.ts +17 -12
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/conversation-routes.ts +43 -29
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/credential-routes.ts +26 -82
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/documents-routes.ts +3 -222
- package/src/runtime/routes/inbound-message-handler.ts +21 -45
- 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 +101 -79
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -79
- 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/playground/__tests__/inject-failures.test.ts +2 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
- package/src/runtime/routes/playground/inject-failures.ts +2 -2
- package/src/runtime/routes/playground/reset-circuit.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +38 -8
- package/src/runtime/routes/schedule-routes.ts +94 -7
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +69 -15
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/routes/workspace-routes.ts +0 -9
- package/src/runtime/routes/workspace-utils.ts +3 -13
- package/src/runtime/services/conversation-serializer.ts +7 -2
- package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
- package/src/schedule/plugin-schedule-availability.ts +58 -0
- package/src/schedule/plugin-schedule-declarations.ts +23 -27
- package/src/schedule/plugin-schedule-reconciler.ts +12 -3
- package/src/schedule/schedule-store.ts +5 -1
- package/src/schedule/scheduler.ts +9 -4
- package/src/schedule/worker-control.ts +1 -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/__tests__/consult-prompt.test.ts +26 -15
- package/src/subagent/consult-context.ts +11 -11
- package/src/subagent/consult-prompt.ts +26 -35
- package/src/subagent/manager.ts +21 -40
- package/src/subagent/notify.ts +7 -1
- package/src/subagent/types.ts +15 -12
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +38 -4
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/executor.ts +53 -15
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/registry.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- package/src/tools/skills/scaffold-managed.ts +25 -7
- package/src/tools/subagent/spawn.ts +28 -88
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/tools/ui-surface/surface-shape-docs.ts +12 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/__tests__/worker-process-command.test.ts +37 -0
- package/src/util/logger.ts +16 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/util/worker-process.ts +37 -4
- package/src/windows-compiled-cli.ts +32 -0
- package/src/windows-compiled-entry.ts +4 -0
- package/src/windows-compiled-logger.ts +6 -0
- package/src/windows-compiled-worker-entry.ts +29 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- 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/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/__tests__/document-workspace-file.test.ts +0 -467
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
- package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
- package/src/subagent/consult-transcript.ts +0 -90
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
import {
|
|
18
18
|
getConversationTurnTeardown,
|
|
19
19
|
resolveProcessingWaitMs,
|
|
20
|
-
VOICE_NO_SETUP_FLOWS_RULE,
|
|
21
20
|
waitForPriorTurnTeardown,
|
|
22
21
|
} from "../calls/voice-session-bridge.js";
|
|
23
22
|
import {
|
|
@@ -33,6 +32,8 @@ import {
|
|
|
33
32
|
} from "../calls/voice-triage-escalate.js";
|
|
34
33
|
import { getConfig } from "../config/loader.js";
|
|
35
34
|
import {
|
|
35
|
+
type LiveVoiceFluxConfig,
|
|
36
|
+
LiveVoiceFluxConfigSchema,
|
|
36
37
|
type LiveVoiceFrontModelConfig,
|
|
37
38
|
LiveVoiceFrontModelConfigSchema,
|
|
38
39
|
} from "../config/schemas/live-voice.js";
|
|
@@ -49,6 +50,7 @@ import {
|
|
|
49
50
|
listProviderIds,
|
|
50
51
|
pinnedListeningLanguage,
|
|
51
52
|
supportsBoundary,
|
|
53
|
+
supportsProviderTurnDetection,
|
|
52
54
|
} from "../providers/speech-to-text/provider-catalog.js";
|
|
53
55
|
import type { ResolveStreamingTranscriberOptions } from "../providers/speech-to-text/resolve.js";
|
|
54
56
|
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
@@ -71,6 +73,10 @@ import type {
|
|
|
71
73
|
import { getSubagentManager } from "../subagent/index.js";
|
|
72
74
|
import { liveVoiceEndScreen } from "../telemetry/live-voice-funnel.js";
|
|
73
75
|
import { getToolOwner } from "../tools/registry.js";
|
|
76
|
+
import {
|
|
77
|
+
createReasoningTagFilter,
|
|
78
|
+
type ReasoningTagFilter,
|
|
79
|
+
} from "../tts/reasoning-tag-filter.js";
|
|
74
80
|
import { extractSpeakableSegments } from "../tts/speakable-segments.js";
|
|
75
81
|
import { createAbortReason } from "../util/abort-reasons.js";
|
|
76
82
|
import { hasLocalizedEntry } from "../util/language-subtag.js";
|
|
@@ -81,11 +87,6 @@ import {
|
|
|
81
87
|
dismissesUiSurface,
|
|
82
88
|
revealsUiSurface,
|
|
83
89
|
} from "./activity-label.js";
|
|
84
|
-
import {
|
|
85
|
-
createVoiceFrontDecider,
|
|
86
|
-
type VoiceFrontDecider,
|
|
87
|
-
type VoiceProgressTextInput,
|
|
88
|
-
} from "./front-decision.js";
|
|
89
90
|
import { LiveActivityReporter } from "./live-activity-reporter.js";
|
|
90
91
|
import type {
|
|
91
92
|
LiveVoiceAudioArchiveResult,
|
|
@@ -97,9 +98,9 @@ import {
|
|
|
97
98
|
type LiveVoiceMetricsClock,
|
|
98
99
|
LiveVoiceMetricsCollector,
|
|
99
100
|
type LiveVoiceMetricsEvent,
|
|
100
|
-
type LiveVoiceSpokenAckKind,
|
|
101
101
|
type LiveVoiceTurnSeedMarks,
|
|
102
102
|
type VoiceEndpointAction,
|
|
103
|
+
type VoiceEndpointSource,
|
|
103
104
|
} from "./live-voice-metrics.js";
|
|
104
105
|
import { persistLiveVoicePhoto } from "./live-voice-photo.js";
|
|
105
106
|
import {
|
|
@@ -113,6 +114,11 @@ import type {
|
|
|
113
114
|
LiveVoiceTtsOptions,
|
|
114
115
|
LiveVoiceTtsResult,
|
|
115
116
|
} from "./live-voice-tts.js";
|
|
117
|
+
import {
|
|
118
|
+
createVoiceProgressNarrator,
|
|
119
|
+
type VoiceProgressNarrator,
|
|
120
|
+
type VoiceProgressTextInput,
|
|
121
|
+
} from "./progress-narration.js";
|
|
116
122
|
import {
|
|
117
123
|
APPROVAL_PENDING_PHRASE_BY_LANGUAGE,
|
|
118
124
|
approvalPendingPhraseFor,
|
|
@@ -161,6 +167,14 @@ const FINALIZE_GRACE_MS = 1_000;
|
|
|
161
167
|
// cancellation cannot kill a reply mid-sentence. Mirrors the
|
|
162
168
|
// liveVoice.vad.bargeInMinSpeechMs schema default; 0 disables the guard for
|
|
163
169
|
// instant barge-in.
|
|
170
|
+
//
|
|
171
|
+
// Local energy detection owns barge-in in every mode, including when a
|
|
172
|
+
// turn-detecting provider owns the turn boundary: an interrupt during TTS has
|
|
173
|
+
// to feel instant, and no provider roundtrip beats a local gate on the audio
|
|
174
|
+
// already in hand. The echo-adaptive part of this guard also has to stay
|
|
175
|
+
// upstream of the provider, which hears only the microphone: it cannot tell
|
|
176
|
+
// our own TTS bleeding through imperfect echo cancellation from the caller,
|
|
177
|
+
// and would report a user turn nobody took.
|
|
164
178
|
const DEFAULT_BARGE_IN_MIN_SPEECH_MS = 250;
|
|
165
179
|
// The playback echo gate learns microphone energy while assistant audio is
|
|
166
180
|
// expected at the speaker. Input must rise above the learned level by this
|
|
@@ -203,6 +217,12 @@ const BARGE_IN_GAP_TOLERANCE_MS = 200;
|
|
|
203
217
|
// (1 / (1 + ratio) ≈ 20%): once the run is mostly silence it resets, so genuine
|
|
204
218
|
// choppy speech still lands but periodic noise cannot accumulate into one.
|
|
205
219
|
const BARGE_IN_MAX_TOLERATED_SILENCE_RATIO = 4;
|
|
220
|
+
// Slack added to the configured end-of-turn timeout before the session stops
|
|
221
|
+
// waiting for an event that is not coming and falls the utterance back onto
|
|
222
|
+
// the silence-boundary path. A turn-detecting provider force-ends its own turn
|
|
223
|
+
// at that timeout, so anything past it plus a frame's network and parse hop
|
|
224
|
+
// means the stream is gone, not slow.
|
|
225
|
+
const PROVIDER_TURN_END_FALLBACK_MARGIN_MS = 1_000;
|
|
206
226
|
// At most this many TTS segment jobs are open (provider stream started,
|
|
207
227
|
// frames not yet fully emitted) per turn: the emitting job plus one
|
|
208
228
|
// prefetching job. The prefetch buffers its chunks in memory until promoted;
|
|
@@ -328,19 +348,24 @@ export interface LiveVoiceSessionOptions {
|
|
|
328
348
|
*/
|
|
329
349
|
finalizeGraceMs?: number;
|
|
330
350
|
/**
|
|
331
|
-
*
|
|
351
|
+
* Voice front-door endpointing and progress narration tuning. The
|
|
332
352
|
* production factory seeds this from `liveVoice.frontModel` config when
|
|
333
353
|
* unset; absent fields fall back to the schema defaults (the constructor
|
|
334
354
|
* schema-parses the partial into a complete config).
|
|
335
355
|
*/
|
|
336
356
|
frontModelConfig?: Partial<LiveVoiceFrontModelConfig>;
|
|
337
357
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* and narration falls back to its static phrases.
|
|
358
|
+
* Deepgram Flux turn-detection tuning. The production factory seeds this
|
|
359
|
+
* from `liveVoice.flux` config when unset; absent fields fall back to the
|
|
360
|
+
* schema defaults, which leave `turnEnd.enabled` false.
|
|
342
361
|
*/
|
|
343
|
-
|
|
362
|
+
fluxConfig?: Partial<LiveVoiceFluxConfig>;
|
|
363
|
+
/**
|
|
364
|
+
* Progress narration service. The production factory constructs it from
|
|
365
|
+
* `liveVoice.frontModel.progress` config when unset. `null` disables
|
|
366
|
+
* generated narration, so progress updates use their static fallback.
|
|
367
|
+
*/
|
|
368
|
+
progressNarrator?: VoiceProgressNarrator | null;
|
|
344
369
|
/**
|
|
345
370
|
* Spawns the background continuation for a barged-in turn. The factory wires
|
|
346
371
|
* the real SubagentManager-backed implementation; tests inject a stub.
|
|
@@ -418,6 +443,29 @@ interface UtteranceCycle {
|
|
|
418
443
|
// Consecutive semantic-endpointing "hold" extensions this utterance has
|
|
419
444
|
// consumed, bounded by `endpointMaxExtensions`.
|
|
420
445
|
endpointExtensionCount: number;
|
|
446
|
+
// The provider's end-of-turn never arrived for this cycle, so it fell back
|
|
447
|
+
// to the silence-boundary path and stays there: a late event must not
|
|
448
|
+
// commit a boundary the fallback already owns.
|
|
449
|
+
providerTurnEndTimedOut: boolean;
|
|
450
|
+
// The provider's own end-of-turn released this cycle, so its transcript is
|
|
451
|
+
// already complete. A provider that decides turn ends commits the transcript
|
|
452
|
+
// in the same event, so such a release needs no caller-side flush and must
|
|
453
|
+
// not tear the stream down. False on every caller-side release (the fail-open
|
|
454
|
+
// deadline, a max-duration force-end, barge-in), where a turn may still be
|
|
455
|
+
// open provider-side and only a stream close can make it answer.
|
|
456
|
+
providerClosedTurn: boolean;
|
|
457
|
+
// `vadSpeechGeneration` as of the local silence boundary that handed this
|
|
458
|
+
// cycle to the provider, or null while no boundary has fired yet (a turn
|
|
459
|
+
// model routinely beats the trailing-silence countdown, and that fast commit
|
|
460
|
+
// is the point).
|
|
461
|
+
// The staleness signal of last resort: it stands in for the provider's own
|
|
462
|
+
// turn numbering when the provider sends none (see isStaleProviderTurnEnd).
|
|
463
|
+
turnBoundaryGeneration: number | null;
|
|
464
|
+
// Index of the newest provider turn opened in this cycle, from the turn
|
|
465
|
+
// model's own numbering, or null when the provider does not number its
|
|
466
|
+
// turns. An end-of-turn closing an older index describes a turn the
|
|
467
|
+
// provider has already superseded (see isStaleProviderTurnEnd).
|
|
468
|
+
openProviderTurnIndex: number | null;
|
|
421
469
|
// The transcript the most recent hold verdict judged (unified front-door).
|
|
422
470
|
// A final segment arriving during the extension window that extends this
|
|
423
471
|
// text replays the boundary immediately — the hold was judged on stale
|
|
@@ -668,27 +716,11 @@ interface ActiveAssistantTurn {
|
|
|
668
716
|
// context. A bump since then means a stop/interrupt/supersede deliberately
|
|
669
717
|
// dropped the continuation, so a rollback must not resurrect it.
|
|
670
718
|
pendingContextStopGeneration: number;
|
|
671
|
-
// The agent run started a definitive tool use this turn — tool use implies
|
|
672
|
-
// a guaranteed-slow turn, so acknowledgment logic can key off this.
|
|
673
|
-
toolUseStarted: boolean;
|
|
674
|
-
// The model's first streamed delta reached this session — the spoken-ack
|
|
675
|
-
// timer is moot once real output is flowing.
|
|
676
|
-
firstDeltaSeen: boolean;
|
|
677
719
|
// Counts assistant text deltas seen this turn. A narration generation
|
|
678
720
|
// captures it at launch and discards its result if it moved: text the model
|
|
679
721
|
// produced mid-generation makes the narration stale, and proves the model
|
|
680
722
|
// is alive — which is exactly what narration exists to paper over.
|
|
681
723
|
deltaEpoch: number;
|
|
682
|
-
// A spoken ack was enqueued this turn. Every ack trigger shares this
|
|
683
|
-
// one-per-turn budget so a slow turn never stacks fillers.
|
|
684
|
-
ackSpoken: boolean;
|
|
685
|
-
// An ack generation is awaiting the decider. While true, the ack has not
|
|
686
|
-
// yet stamped `lastFloorHolderAtMs`, so narration must treat it as a
|
|
687
|
-
// floor-holder-in-waiting and stand down —
|
|
688
|
-
// otherwise a progress phrase could land back-to-back with the ack.
|
|
689
|
-
ackGenerationPending: boolean;
|
|
690
|
-
// Pending slow-first-delta ack timer; null once cleared or fired.
|
|
691
|
-
ackTimer: ReturnType<typeof setTimeout> | null;
|
|
692
724
|
// Triage-and-escalate (Voice Mode): the front-door leg gave the escalate
|
|
693
725
|
// verdict and the strong "escalated" leg has taken over this same turn.
|
|
694
726
|
// Guards the
|
|
@@ -696,6 +728,10 @@ interface ActiveAssistantTurn {
|
|
|
696
728
|
// the hand-off idempotent.
|
|
697
729
|
escalationHandedOff: boolean;
|
|
698
730
|
ttsBuffer: string;
|
|
731
|
+
// Strips inline <think> reasoning spans from the spoken stream. Stateful
|
|
732
|
+
// per turn because spans and tags cross delta boundaries; display frames
|
|
733
|
+
// keep the raw text.
|
|
734
|
+
ttsReasoningFilter: ReasoningTagFilter;
|
|
699
735
|
// A non-empty speakable segment reached the TTS queue — gates the eager
|
|
700
736
|
// first-segment flush that trades clause quality for speech onset.
|
|
701
737
|
ttsSegmentEnqueued: boolean;
|
|
@@ -783,6 +819,20 @@ const LIVE_VOICE_CONTROL_PROMPT_BASE =
|
|
|
783
819
|
const LIVE_VOICE_SCREEN_REVEAL_TEACHING =
|
|
784
820
|
"The call renders as a full-screen overlay covering the app. Whenever you put something on screen, the overlay minimizes by itself as soon as you finish speaking, and the user is looking at what you made. So speak as if you are showing it to them right now (for example, close with something like: take a look), and never say you cannot show it, that this is a voice call, or that they should check it later. Never emit bracketed markers of any kind. ";
|
|
785
821
|
|
|
822
|
+
// The setup-flow case, spelled out because it is the one the model gets wrong
|
|
823
|
+
// on its own: connecting an account reads as something a call cannot do, so it
|
|
824
|
+
// declines and offers to do it later in text, which is the exact thing the base
|
|
825
|
+
// prompt above forbids.
|
|
826
|
+
//
|
|
827
|
+
// It can do it. The connect card is a ui surface like any other, so showing it
|
|
828
|
+
// minimizes the room on the same latch (`revealsUiSurface`), and the user is at
|
|
829
|
+
// the screen the browser window opens on. Appended alongside the screen-reveal
|
|
830
|
+
// teaching, to the same legs, for the same reason: the front-door leg is
|
|
831
|
+
// toolless, so a setup flow is not its to run, and its capability digest
|
|
832
|
+
// already tells it to escalate anything needing a tool rather than refuse.
|
|
833
|
+
const LIVE_VOICE_SETUP_FLOW_TEACHING =
|
|
834
|
+
"This includes connecting accounts. If a task needs an account connected or a sign-in completed, put the connection up on screen and let the user do it now. Do not decline it, and do not defer it to text chat. Say what you are connecting and that it is in front of them. ";
|
|
835
|
+
|
|
786
836
|
// System-level guidance appended to a barge-in turn's control prompt so the
|
|
787
837
|
// model treats the new utterance as a continuation of the request it was cut
|
|
788
838
|
// off answering, rather than a fresh follow-up. Reaches the model only; it is
|
|
@@ -839,21 +889,21 @@ function buildLiveDeliveryNote(request: string, answer: string): string {
|
|
|
839
889
|
return `The user interrupted you earlier, and in the background you finished ${what}. The call is still live and nobody is speaking, so tell them briefly that it is done and give them the result. Do not re-run any tool calls; the work is already complete, and do not repeat it verbatim if it no longer fits. What you produced was:\n\n${answer}`;
|
|
840
890
|
}
|
|
841
891
|
|
|
842
|
-
// Assemble a leg's model-facing control prompt: the base live-voice rules,
|
|
843
|
-
//
|
|
844
|
-
// LIVE_VOICE_SCREEN_REVEAL_TEACHING),
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
// never render as user bubbles.
|
|
892
|
+
// Assemble a leg's model-facing control prompt: the base live-voice rules, the
|
|
893
|
+
// screen-reveal and setup-flow teaching (both withheld from the front-door leg,
|
|
894
|
+
// see LIVE_VOICE_SCREEN_REVEAL_TEACHING), plus any pending barge-in merge
|
|
895
|
+
// context, completed-continuation context, and/or the announcement instruction.
|
|
896
|
+
// A turn can carry several (a barge-in follow-up that also has a continuation
|
|
897
|
+
// result waiting); the notes are model-only and never render as user bubbles.
|
|
849
898
|
function buildVoiceControlPrompt(
|
|
850
899
|
turn: ActiveAssistantTurn,
|
|
851
900
|
leg: { frontDoor?: boolean },
|
|
852
901
|
): string {
|
|
853
902
|
let prompt =
|
|
854
903
|
LIVE_VOICE_CONTROL_PROMPT_BASE +
|
|
855
|
-
(leg.frontDoor === true
|
|
856
|
-
|
|
904
|
+
(leg.frontDoor === true
|
|
905
|
+
? ""
|
|
906
|
+
: LIVE_VOICE_SCREEN_REVEAL_TEACHING + LIVE_VOICE_SETUP_FLOW_TEACHING);
|
|
857
907
|
if (turn.language !== undefined) {
|
|
858
908
|
prompt = `${prompt}\n\nThe caller has been speaking the language with code "${turn.language}" this turn. Reply in that language unless they clearly switch to another.`;
|
|
859
909
|
}
|
|
@@ -904,6 +954,10 @@ function createUtteranceCycle(): UtteranceCycle {
|
|
|
904
954
|
finalTranscriptSegments: [],
|
|
905
955
|
latestPartialText: null,
|
|
906
956
|
endpointExtensionCount: 0,
|
|
957
|
+
providerTurnEndTimedOut: false,
|
|
958
|
+
providerClosedTurn: false,
|
|
959
|
+
turnBoundaryGeneration: null,
|
|
960
|
+
openProviderTurnIndex: null,
|
|
907
961
|
heldSpeculativeContent: null,
|
|
908
962
|
languageTally: new Map(),
|
|
909
963
|
latestPartialLanguages: null,
|
|
@@ -1170,12 +1224,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1170
1224
|
// replayed once the parked speech flushes into the next armed utterance.
|
|
1171
1225
|
private vadPendingTurnEnd: "silence" | "max-duration" | null = null;
|
|
1172
1226
|
private readonly metricsClock: LiveVoiceMetricsClock;
|
|
1173
|
-
// Persistent mode: server-VAD
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1227
|
+
// Persistent mode: a server-VAD session keeps one streaming transcriber for
|
|
1228
|
+
// the whole session when the stream can be sealed per utterance without
|
|
1229
|
+
// closing it. Either the provider is finalize-capable (release flushes via
|
|
1230
|
+
// finalizeUtterance()) or the provider decides turn ends itself (release is
|
|
1231
|
+
// already sealed by the end-of-turn that triggered it). Re-arm reuses this
|
|
1232
|
+
// instance synchronously. Null in manual mode, for a stream that offers
|
|
1233
|
+
// neither seal, and after an unexpected stream close (the next arm resolves
|
|
1234
|
+
// a fresh transcriber).
|
|
1179
1235
|
private sharedTranscriber: StreamingTranscriber | null = null;
|
|
1180
1236
|
// The `services.stt.language` value the shared stream was dialed with.
|
|
1181
1237
|
// Providers pin the language per connection, so the persistent re-arm
|
|
@@ -1200,13 +1256,38 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1200
1256
|
private readonly finalizeGraceMs: number;
|
|
1201
1257
|
// Rotates through the progress fallback phrases across the session's turns.
|
|
1202
1258
|
private progressPhraseCounter = 0;
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
private readonly
|
|
1259
|
+
// Progress-only phrasing service. It never makes routing decisions and
|
|
1260
|
+
// never emits an answer.
|
|
1261
|
+
private readonly progressNarrator: VoiceProgressNarrator | null;
|
|
1206
1262
|
// Complete front-model tunables: the constructor schema-parses the partial
|
|
1207
1263
|
// option once, so every field carries its `liveVoice.frontModel` schema
|
|
1208
1264
|
// default when unset.
|
|
1209
1265
|
private readonly frontModelConfig: LiveVoiceFrontModelConfig;
|
|
1266
|
+
// Complete Flux tunables (the constructor schema-parses the partial option
|
|
1267
|
+
// once, so every field carries its `liveVoice.flux` schema default).
|
|
1268
|
+
private readonly fluxConfig: LiveVoiceFluxConfig;
|
|
1269
|
+
/**
|
|
1270
|
+
* Per-session latch: the provider's committed end-of-turn owns the turn
|
|
1271
|
+
* boundary instead of the silence boundary's front-door hold verdict. Set
|
|
1272
|
+
* when the config flag is on AND the resolved streaming provider declares
|
|
1273
|
+
* `turnDetection: "provider"` in the STT catalog AND the session runs server
|
|
1274
|
+
* VAD. Push-to-talk is excluded deliberately: there the client's release IS
|
|
1275
|
+
* the boundary, and answering while the caller still holds the button is not
|
|
1276
|
+
* turn detection, it is a bug. False leaves every other code path exactly as
|
|
1277
|
+
* it is, with no provider turn detection in the picture.
|
|
1278
|
+
*/
|
|
1279
|
+
private providerTurnEndActive = false;
|
|
1280
|
+
// Wall-clock of the newest above-gate audio chunk, tracked in every
|
|
1281
|
+
// server-VAD session. It is the local VAD's speech-stop mark: the one anchor
|
|
1282
|
+
// the reported end-of-turn latency is measured from whichever decider
|
|
1283
|
+
// commits the turn, and the anchor for the provider fallback deadline. Null
|
|
1284
|
+
// in push-to-talk, where no local VAD runs.
|
|
1285
|
+
private localSpeechStopAtMs: number | null = null;
|
|
1286
|
+
// Fail-open deadline for a provider end-of-turn that never arrives. On
|
|
1287
|
+
// expiry the utterance falls back to the silence-boundary path, so a dead
|
|
1288
|
+
// provider stream degrades to the silence-path behavior instead of a hung
|
|
1289
|
+
// turn.
|
|
1290
|
+
private providerTurnEndTimer: ReturnType<typeof setTimeout> | null = null;
|
|
1210
1291
|
// Effective trailing-silence threshold, mirroring the detector's private
|
|
1211
1292
|
// copy (constructor seed + update_config), reported to the endpoint decider.
|
|
1212
1293
|
private silenceThresholdMs: number;
|
|
@@ -1276,10 +1357,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1276
1357
|
this.echoDrainSlackMs =
|
|
1277
1358
|
options.echoDrainSlackMs ?? DEFAULT_ECHO_DRAIN_SLACK_MS;
|
|
1278
1359
|
this.finalizeGraceMs = options.finalizeGraceMs ?? FINALIZE_GRACE_MS;
|
|
1279
|
-
this.
|
|
1360
|
+
this.progressNarrator = options.progressNarrator ?? null;
|
|
1280
1361
|
this.frontModelConfig = LiveVoiceFrontModelConfigSchema.parse(
|
|
1281
1362
|
options.frontModelConfig ?? {},
|
|
1282
1363
|
);
|
|
1364
|
+
this.fluxConfig = LiveVoiceFluxConfigSchema.parse(options.fluxConfig ?? {});
|
|
1283
1365
|
const turnDetectorConfig: TurnDetectorConfig = {
|
|
1284
1366
|
...(options.turnDetectorConfig ?? {}),
|
|
1285
1367
|
...(context.startFrame.silenceThresholdMs !== undefined
|
|
@@ -1455,6 +1537,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1455
1537
|
this.liveActivityReporter.end();
|
|
1456
1538
|
this.turnDetector?.dispose();
|
|
1457
1539
|
this.clearEndpointExtensionTimer();
|
|
1540
|
+
this.clearProviderTurnEndTimer();
|
|
1458
1541
|
// There is no longer anyone on the call to speak to, so a queued
|
|
1459
1542
|
// announcement cannot be spoken — but the answer behind it is finished
|
|
1460
1543
|
// work the user asked for, so it takes the same conversation route a
|
|
@@ -1506,11 +1589,28 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1506
1589
|
}
|
|
1507
1590
|
|
|
1508
1591
|
try {
|
|
1509
|
-
// One
|
|
1510
|
-
//
|
|
1511
|
-
// language and the recorded sharedTranscriberLanguage
|
|
1512
|
-
// when config changes while the resolver awaits
|
|
1513
|
-
|
|
1592
|
+
// One config snapshot serves the dial, the re-arm comparison and the
|
|
1593
|
+
// latch seed below: passing the language to the resolver keeps the
|
|
1594
|
+
// stream's actual language and the recorded sharedTranscriberLanguage
|
|
1595
|
+
// identical even when config changes while the resolver awaits
|
|
1596
|
+
// credentials.
|
|
1597
|
+
const stt = getConfig().services.stt;
|
|
1598
|
+
const sttLanguage = stt.language;
|
|
1599
|
+
// Seed the latch from the CONFIGURED provider before the dial, not
|
|
1600
|
+
// only from the resolved one after it. `start()` sends `ready` without
|
|
1601
|
+
// waiting on this resolve, so the caller can speak and close an entire
|
|
1602
|
+
// silence boundary while the provider handshake is still in flight. A
|
|
1603
|
+
// latch still on its default `false` at that boundary hands the first
|
|
1604
|
+
// turn of the session to the silence path and then ignores the
|
|
1605
|
+
// provider end-of-turn that follows, which is invisible from the
|
|
1606
|
+
// outside: the session looks like a turn-detecting session while its
|
|
1607
|
+
// opening turn is not one. The resolved provider reconciles this guess
|
|
1608
|
+
// below.
|
|
1609
|
+
this.setProviderTurnEndActive(
|
|
1610
|
+
this.fluxConfig.turnEnd.enabled &&
|
|
1611
|
+
supportsProviderTurnDetection(stt.provider as SttProviderId) &&
|
|
1612
|
+
this.turnDetector !== null,
|
|
1613
|
+
);
|
|
1514
1614
|
const transcriber = await this.resolveTranscriber({
|
|
1515
1615
|
sampleRate: this.context.startFrame.audio.sampleRate,
|
|
1516
1616
|
...(sttLanguage ? { language: sttLanguage } : {}),
|
|
@@ -1522,6 +1622,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1522
1622
|
}
|
|
1523
1623
|
|
|
1524
1624
|
if (!transcriber) {
|
|
1625
|
+
// No stream answered, so no end-of-turn ever will: the guess above
|
|
1626
|
+
// must not outlive the dial that disproved it.
|
|
1627
|
+
this.setProviderTurnEndActive(false);
|
|
1525
1628
|
return {
|
|
1526
1629
|
status: "unavailable",
|
|
1527
1630
|
message: unavailableTranscriberMessage(),
|
|
@@ -1530,9 +1633,27 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1530
1633
|
|
|
1531
1634
|
utterance.transcriber = transcriber;
|
|
1532
1635
|
utterance.dialedSttProvider = transcriber.providerId;
|
|
1636
|
+
// Reconcile the pre-dial guess with the provider that actually
|
|
1637
|
+
// answered. The resolver reads the provider from config, which cannot
|
|
1638
|
+
// change under a live session, so this normally confirms the guess; it
|
|
1639
|
+
// clears it when the dial fell back to another provider or resolved
|
|
1640
|
+
// one the config did not name.
|
|
1641
|
+
this.setProviderTurnEndActive(
|
|
1642
|
+
this.fluxConfig.turnEnd.enabled &&
|
|
1643
|
+
supportsProviderTurnDetection(transcriber.providerId) &&
|
|
1644
|
+
this.turnDetector !== null,
|
|
1645
|
+
);
|
|
1533
1646
|
if (
|
|
1534
1647
|
this.turnDetector &&
|
|
1535
|
-
typeof transcriber.finalizeUtterance === "function"
|
|
1648
|
+
(typeof transcriber.finalizeUtterance === "function" ||
|
|
1649
|
+
// A provider that owns the turn boundary needs no caller-side flush
|
|
1650
|
+
// on the normal path: its end-of-turn IS the flush, and it commits
|
|
1651
|
+
// the transcript in the same event. Gating persistence on
|
|
1652
|
+
// `finalizeUtterance` alone asks "can the caller flush this stream"
|
|
1653
|
+
// when the question is "will the caller ever have to", and answering
|
|
1654
|
+
// it wrong costs a socket per utterance, which drops the audio that
|
|
1655
|
+
// arrives while the next one dials.
|
|
1656
|
+
this.providerTurnEndActive)
|
|
1536
1657
|
) {
|
|
1537
1658
|
// Adopt persistent mode: this stream serves the whole session, so
|
|
1538
1659
|
// its events route by cycle ownership instead of binding to this
|
|
@@ -1563,6 +1684,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1563
1684
|
if (this.isUtteranceStale(utterance)) {
|
|
1564
1685
|
return { status: "stale" };
|
|
1565
1686
|
}
|
|
1687
|
+
// The dial threw, so the pre-dial guess is disproved the same way the
|
|
1688
|
+
// unavailable case disproves it: nothing will send an end-of-turn.
|
|
1689
|
+
this.setProviderTurnEndActive(false);
|
|
1566
1690
|
return {
|
|
1567
1691
|
status: "error",
|
|
1568
1692
|
message: `Live voice transcription could not be started: ${errorMessage(
|
|
@@ -1690,6 +1814,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1690
1814
|
|
|
1691
1815
|
this.state = "failed";
|
|
1692
1816
|
this.clearEndpointExtensionTimer();
|
|
1817
|
+
this.clearProviderTurnEndTimer();
|
|
1693
1818
|
await this.sendFrame({
|
|
1694
1819
|
type: "error",
|
|
1695
1820
|
code:
|
|
@@ -1763,6 +1888,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1763
1888
|
const hasSpeech = energyClassification === "speech";
|
|
1764
1889
|
detector.onMediaChunk(hasSpeech);
|
|
1765
1890
|
this.trackBargeInGuard(energyClassification, chunk);
|
|
1891
|
+
if (hasSpeech) {
|
|
1892
|
+
this.localSpeechStopAtMs = Date.now();
|
|
1893
|
+
}
|
|
1766
1894
|
|
|
1767
1895
|
// Playback echo is neither user audio nor useful pre-roll. Dropping it
|
|
1768
1896
|
// prevents the assistant's reply from reaching transcription as a ghost
|
|
@@ -2163,6 +2291,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2163
2291
|
// utterance keeps accumulating and the detector fires a fresh turn-end.
|
|
2164
2292
|
this.vadSpeechGeneration += 1;
|
|
2165
2293
|
this.clearEndpointExtensionTimer();
|
|
2294
|
+
// ...and so is a fallback deadline armed for the boundary the caller just
|
|
2295
|
+
// spoke through. The next silence boundary arms a fresh one.
|
|
2296
|
+
this.clearProviderTurnEndTimer();
|
|
2166
2297
|
|
|
2167
2298
|
// Speech resumed while a speculative leg was awaiting its verdict: the
|
|
2168
2299
|
// pause was mid-thought after all. Discard silently (no frames were ever
|
|
@@ -2319,7 +2450,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2319
2450
|
// Tagged reason: provider catch-sites classify untagged caller aborts as
|
|
2320
2451
|
// retryable transport failures (ERROR log + futile retry against the
|
|
2321
2452
|
// aborted signal). This signal reaches the brain leg and any in-flight
|
|
2322
|
-
//
|
|
2453
|
+
// progress narration.
|
|
2323
2454
|
turn.abortController.abort(
|
|
2324
2455
|
createAbortReason("voice_session_aborted", "live-voice-barge-in"),
|
|
2325
2456
|
);
|
|
@@ -3072,8 +3203,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3072
3203
|
this.pendingBargeIn = null;
|
|
3073
3204
|
if (reason === "max-duration") {
|
|
3074
3205
|
// A max-duration boundary always releases: drop any pending hold
|
|
3075
|
-
// replay so it cannot re-fire a
|
|
3206
|
+
// replay (or provider fallback deadline) so it cannot re-fire a
|
|
3207
|
+
// boundary this one already owns.
|
|
3076
3208
|
this.clearEndpointExtensionTimer();
|
|
3209
|
+
this.clearProviderTurnEndTimer();
|
|
3077
3210
|
}
|
|
3078
3211
|
const utterance = this.currentUtterance;
|
|
3079
3212
|
if (!utterance || utterance.released || utterance.completed) {
|
|
@@ -3095,6 +3228,20 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3095
3228
|
// release (ptt_release forced the boundary) is the user saying "answer
|
|
3096
3229
|
// now" — never second-guess it.
|
|
3097
3230
|
if (reason === "silence" && !manualRelease) {
|
|
3231
|
+
// The provider owns this boundary: its end-of-turn commits the
|
|
3232
|
+
// utterance, so the silence timer only arms the fail-open deadline. An
|
|
3233
|
+
// utterance whose deadline already elapsed falls through to the hold
|
|
3234
|
+
// path below, which is the whole point of the fallback.
|
|
3235
|
+
if (this.providerTurnEndActive && !utterance.providerTurnEndTimedOut) {
|
|
3236
|
+
// Stamp the speech run this boundary closed. This is the staleness
|
|
3237
|
+
// signal of last resort, used only for a provider that does not
|
|
3238
|
+
// number its turns: where the provider's own turn index is on the
|
|
3239
|
+
// wire it answers the question directly (see
|
|
3240
|
+
// isStaleProviderTurnEnd).
|
|
3241
|
+
utterance.turnBoundaryGeneration = this.vadSpeechGeneration;
|
|
3242
|
+
this.armProviderTurnEndFallbackTimer(utterance);
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3098
3245
|
if (await this.launchSpeculativeAssistantTurn(utterance)) {
|
|
3099
3246
|
return;
|
|
3100
3247
|
}
|
|
@@ -3125,6 +3272,221 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3125
3272
|
}, this.frontModelConfig.endpointExtensionMs);
|
|
3126
3273
|
}
|
|
3127
3274
|
|
|
3275
|
+
// Arms the fail-open deadline for a provider end-of-turn. A turn-detecting
|
|
3276
|
+
// provider force-ends its own turn after `eotTimeoutMs` of silence, so once
|
|
3277
|
+
// that budget plus a margin has passed since the caller stopped speaking, no
|
|
3278
|
+
// end-of-turn is coming and the utterance replays this boundary on the
|
|
3279
|
+
// silence path.
|
|
3280
|
+
// `waitMsOverride` collapses that wait when the caller already knows no
|
|
3281
|
+
// end-of-turn is coming (see setProviderTurnEndActive).
|
|
3282
|
+
private armProviderTurnEndFallbackTimer(
|
|
3283
|
+
utterance: UtteranceCycle,
|
|
3284
|
+
waitMsOverride?: number,
|
|
3285
|
+
): void {
|
|
3286
|
+
this.clearProviderTurnEndTimer();
|
|
3287
|
+
const budgetMs =
|
|
3288
|
+
this.fluxConfig.eotTimeoutMs + PROVIDER_TURN_END_FALLBACK_MARGIN_MS;
|
|
3289
|
+
const waitMs =
|
|
3290
|
+
waitMsOverride ?? Math.max(0, budgetMs - this.msSinceLocalSpeechStop());
|
|
3291
|
+
this.providerTurnEndTimer = setTimeout(() => {
|
|
3292
|
+
this.providerTurnEndTimer = null;
|
|
3293
|
+
if (
|
|
3294
|
+
this.currentUtterance !== utterance ||
|
|
3295
|
+
utterance.released ||
|
|
3296
|
+
utterance.completed ||
|
|
3297
|
+
utterance.assistantTurnStarted ||
|
|
3298
|
+
// Speech resumed (the detector owns the next boundary). Onset also
|
|
3299
|
+
// clears this timer, so this is a belt to that suspender.
|
|
3300
|
+
this.turnDetector?.isActive
|
|
3301
|
+
) {
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
utterance.providerTurnEndTimedOut = true;
|
|
3305
|
+
log.warn(
|
|
3306
|
+
{ budgetMs, waitMs },
|
|
3307
|
+
"No provider end-of-turn is coming; falling back to the silence boundary for this utterance",
|
|
3308
|
+
);
|
|
3309
|
+
this.handleVadUtteranceEnd("silence");
|
|
3310
|
+
}, waitMs);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
/**
|
|
3314
|
+
* Flips the provider end-of-turn latch, unwinding an optimistic arm.
|
|
3315
|
+
*
|
|
3316
|
+
* `beginUtterance` arms the latch from the configured provider before the
|
|
3317
|
+
* dial resolves, so a silence boundary can already have deferred to the
|
|
3318
|
+
* provider by the time the resolved one says otherwise. That deferred
|
|
3319
|
+
* boundary is parked on the fail-open deadline, a whole end-of-turn budget
|
|
3320
|
+
* away, waiting for an event that will now never arrive. Collapse the wait to
|
|
3321
|
+
* zero rather than burn the budget: the deadline body re-checks the cycle
|
|
3322
|
+
* and replays the silence boundary, which with the latch down takes the
|
|
3323
|
+
* ordinary hold path. Replaying through the deadline instead of calling
|
|
3324
|
+
* `handleVadUtteranceEnd` directly keeps the release off the arming
|
|
3325
|
+
* caller's stack, which is still mid-dial.
|
|
3326
|
+
*
|
|
3327
|
+
* A cleared latch with no deadline armed needs no unwind: either no
|
|
3328
|
+
* boundary ever deferred, or the caller resumed speaking and
|
|
3329
|
+
* `handleVadSpeechStart` already dropped the deadline, leaving the detector
|
|
3330
|
+
* owning the next boundary. The cycle's `turnBoundaryGeneration` stamp is
|
|
3331
|
+
* left as it is: `isStaleProviderTurnEnd` is only ever consulted from
|
|
3332
|
+
* `handleProviderTurnEnd`, which returns immediately once the latch is down.
|
|
3333
|
+
*/
|
|
3334
|
+
private setProviderTurnEndActive(active: boolean): void {
|
|
3335
|
+
if (active === this.providerTurnEndActive) {
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
this.providerTurnEndActive = active;
|
|
3339
|
+
if (active || this.providerTurnEndTimer === null) {
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
const utterance = this.currentUtterance;
|
|
3343
|
+
this.clearProviderTurnEndTimer();
|
|
3344
|
+
if (utterance && !utterance.released && !utterance.completed) {
|
|
3345
|
+
this.armProviderTurnEndFallbackTimer(utterance, 0);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
/**
|
|
3350
|
+
* Record the provider turn a cycle is currently inside. Providers without
|
|
3351
|
+
* turn numbering send no index, which leaves the cycle on the local speech
|
|
3352
|
+
* generation as its only staleness signal (see isStaleProviderTurnEnd).
|
|
3353
|
+
*/
|
|
3354
|
+
private recordProviderTurnStart(
|
|
3355
|
+
utterance: UtteranceCycle,
|
|
3356
|
+
turnIndex: number | undefined,
|
|
3357
|
+
): void {
|
|
3358
|
+
if (turnIndex === undefined) {
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
utterance.openProviderTurnIndex = turnIndex;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/**
|
|
3365
|
+
* A provider end-of-turn that lost its race with the caller's next breath:
|
|
3366
|
+
* it describes speech the caller has already spoken past. Acting on one
|
|
3367
|
+
* would send utterance_end and stop the transcriber while local VAD is
|
|
3368
|
+
* still routing the resumed speech into this same cycle, cutting off the
|
|
3369
|
+
* next words or folding them into the wrong turn.
|
|
3370
|
+
*
|
|
3371
|
+
* The provider's own turn numbering answers this directly and is preferred
|
|
3372
|
+
* wherever it is available: the cycle records the newest turn the provider
|
|
3373
|
+
* has opened, so an end-of-turn for an older index closes a turn the
|
|
3374
|
+
* provider itself has already superseded. An end-of-turn for the turn still
|
|
3375
|
+
* in progress is never stale, however many times the caller drew breath
|
|
3376
|
+
* inside it: the mid-thought pause is exactly the case a provider's turn
|
|
3377
|
+
* model exists to judge, and its verdict covers the resumed speech too.
|
|
3378
|
+
*
|
|
3379
|
+
* With no turn numbering the local speech generation is the only signal
|
|
3380
|
+
* left. The silence boundary stamps the cycle with the generation it
|
|
3381
|
+
* closed and `handleVadSpeechStart` bumps that generation when the caller
|
|
3382
|
+
* resumes, so a turn-end arriving on a newer generation is treated as
|
|
3383
|
+
* stale. That is conservative: it also drops the fast end-of-turn this
|
|
3384
|
+
* feature exists for, which the turn index would have committed. Before
|
|
3385
|
+
* any boundary has fired the stamp is null and nothing is stale.
|
|
3386
|
+
*/
|
|
3387
|
+
private isStaleProviderTurnEnd(
|
|
3388
|
+
utterance: UtteranceCycle,
|
|
3389
|
+
turnIndex: number | undefined,
|
|
3390
|
+
): boolean {
|
|
3391
|
+
if (turnIndex !== undefined && utterance.openProviderTurnIndex !== null) {
|
|
3392
|
+
return turnIndex < utterance.openProviderTurnIndex;
|
|
3393
|
+
}
|
|
3394
|
+
return (
|
|
3395
|
+
utterance.turnBoundaryGeneration !== null &&
|
|
3396
|
+
utterance.turnBoundaryGeneration !== this.vadSpeechGeneration
|
|
3397
|
+
);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
private clearProviderTurnEndTimer(): void {
|
|
3401
|
+
if (this.providerTurnEndTimer !== null) {
|
|
3402
|
+
clearTimeout(this.providerTurnEndTimer);
|
|
3403
|
+
this.providerTurnEndTimer = null;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
// Time since the local VAD last heard above-gate audio: the speech-stop
|
|
3408
|
+
// mark both the fallback deadline and the reported end-of-turn latency are
|
|
3409
|
+
// measured from. Zero when no speech has been heard at all.
|
|
3410
|
+
private msSinceLocalSpeechStop(): number {
|
|
3411
|
+
if (this.localSpeechStopAtMs === null) {
|
|
3412
|
+
return 0;
|
|
3413
|
+
}
|
|
3414
|
+
return Math.max(0, Date.now() - this.localSpeechStopAtMs);
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
/**
|
|
3418
|
+
* The provider committed an end of turn: the caller has finished, so the utterance
|
|
3419
|
+
* commits now instead of waiting out the trailing-silence timer and asking
|
|
3420
|
+
* the front door whether the pause was mid-thought. This runs the ordinary
|
|
3421
|
+
* post-release path (utterance_end, then release), so the front-door leg it
|
|
3422
|
+
* dispatches is non-speculative: `speculativeHoldAllowed` is false, its
|
|
3423
|
+
* decision rule is built with `includeHold: false`, and the model is told
|
|
3424
|
+
* the caller has finished. Escalation and progress narration are untouched;
|
|
3425
|
+
* only the hold verdict is bypassed.
|
|
3426
|
+
*/
|
|
3427
|
+
private async handleProviderTurnEnd(
|
|
3428
|
+
utterance: UtteranceCycle,
|
|
3429
|
+
turnIndex: number | undefined,
|
|
3430
|
+
): Promise<void> {
|
|
3431
|
+
if (
|
|
3432
|
+
!this.providerTurnEndActive ||
|
|
3433
|
+
this.currentUtterance !== utterance ||
|
|
3434
|
+
utterance.released ||
|
|
3435
|
+
utterance.completed ||
|
|
3436
|
+
utterance.assistantTurnStarted ||
|
|
3437
|
+
// The fallback already took this utterance back to the silence path.
|
|
3438
|
+
utterance.providerTurnEndTimedOut
|
|
3439
|
+
) {
|
|
3440
|
+
return;
|
|
3441
|
+
}
|
|
3442
|
+
if (this.isStaleProviderTurnEnd(utterance, turnIndex)) {
|
|
3443
|
+
// At `info`, not `debug`: a drop is rare and is the only signal of the
|
|
3444
|
+
// one known latency-outlier mode, so an operator measuring the two
|
|
3445
|
+
// paths has to be able to see it at the default log level.
|
|
3446
|
+
log.info(
|
|
3447
|
+
{
|
|
3448
|
+
turnIndex,
|
|
3449
|
+
openTurnIndex: utterance.openProviderTurnIndex,
|
|
3450
|
+
boundaryGeneration: utterance.turnBoundaryGeneration,
|
|
3451
|
+
speechGeneration: this.vadSpeechGeneration,
|
|
3452
|
+
},
|
|
3453
|
+
"Dropping a stale provider end-of-turn: the caller resumed speaking past the boundary it closed",
|
|
3454
|
+
);
|
|
3455
|
+
// The cycle stays open and local VAD keeps routing the resumed speech
|
|
3456
|
+
// into it. The detector's next silence boundary re-stamps the
|
|
3457
|
+
// generation and re-arms the fail-open deadline, so the turn still
|
|
3458
|
+
// commits: on the end-of-turn for the resumed speech, or on that
|
|
3459
|
+
// deadline. Re-arm here only if the detector has somehow already gone
|
|
3460
|
+
// idle, so the deadline handleVadSpeechStart cleared can never strand
|
|
3461
|
+
// the cycle with nothing left to close it.
|
|
3462
|
+
if (this.turnDetector?.isActive !== true) {
|
|
3463
|
+
utterance.turnBoundaryGeneration = this.vadSpeechGeneration;
|
|
3464
|
+
this.armProviderTurnEndFallbackTimer(utterance);
|
|
3465
|
+
}
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
this.clearProviderTurnEndTimer();
|
|
3469
|
+
this.markEndpointDecision(
|
|
3470
|
+
utterance,
|
|
3471
|
+
"release",
|
|
3472
|
+
this.msSinceLocalSpeechStop(),
|
|
3473
|
+
"provider",
|
|
3474
|
+
);
|
|
3475
|
+
await this.sendFrame({ type: "utterance_end", reason: "silence" });
|
|
3476
|
+
// The provider committed this cycle's transcript in the event being
|
|
3477
|
+
// handled, so the release below seals it in place instead of flushing or
|
|
3478
|
+
// closing the stream.
|
|
3479
|
+
utterance.providerClosedTurn = true;
|
|
3480
|
+
await this.releaseUtterance();
|
|
3481
|
+
// Leave the local detector idle, which is where every other commit path
|
|
3482
|
+
// leaves it. A provider can close a turn while the trailing-silence
|
|
3483
|
+
// countdown is still running, and barge-in fires from the detector's speech
|
|
3484
|
+
// ONSET: a detector left mid-turn reports no onset, so the caller could
|
|
3485
|
+
// not interrupt the reply they just triggered. The forced boundary
|
|
3486
|
+
// reaches an already-released utterance and returns.
|
|
3487
|
+
this.turnDetector?.forceEnd();
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3128
3490
|
/**
|
|
3129
3491
|
* Unified front-door: dispatch the assistant turn speculatively at the
|
|
3130
3492
|
* silence boundary, judging the transcript accumulated so far — the same
|
|
@@ -3204,13 +3566,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3204
3566
|
if (!alreadyReleased) {
|
|
3205
3567
|
void this.releaseUtterance();
|
|
3206
3568
|
}
|
|
3207
|
-
if (this.streamTtsAudio) {
|
|
3208
|
-
this.armAckTimer(turn);
|
|
3209
|
-
}
|
|
3210
3569
|
if (
|
|
3211
3570
|
this.frontModelConfig.progress.enabled &&
|
|
3212
3571
|
this.streamTtsAudio &&
|
|
3213
|
-
this.
|
|
3572
|
+
this.progressNarrator
|
|
3214
3573
|
) {
|
|
3215
3574
|
this.armProgressIdleTimer(turn);
|
|
3216
3575
|
}
|
|
@@ -3344,6 +3703,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3344
3703
|
!utterance.completed
|
|
3345
3704
|
) {
|
|
3346
3705
|
this.clearEndpointExtensionTimer();
|
|
3706
|
+
this.clearProviderTurnEndTimer();
|
|
3347
3707
|
await this.sendFrame({ type: "utterance_end", reason: "silence" });
|
|
3348
3708
|
}
|
|
3349
3709
|
await this.releaseUtterance();
|
|
@@ -3358,6 +3718,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3358
3718
|
if (utterance.phase === "transcriber_closed") {
|
|
3359
3719
|
utterance.released = true;
|
|
3360
3720
|
this.markUtteranceReleased(utterance);
|
|
3721
|
+
this.markEndpointCommit(utterance);
|
|
3361
3722
|
await this.startAssistantTurnIfReady();
|
|
3362
3723
|
await this.drainOutboundFrames();
|
|
3363
3724
|
return;
|
|
@@ -3369,6 +3730,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3369
3730
|
|
|
3370
3731
|
utterance.released = true;
|
|
3371
3732
|
this.markUtteranceReleased(utterance);
|
|
3733
|
+
this.markEndpointCommit(utterance);
|
|
3372
3734
|
|
|
3373
3735
|
if (utterance.phase === "pending") {
|
|
3374
3736
|
// The transcriber is still starting; beginUtterance completes the release.
|
|
@@ -3383,8 +3745,24 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3383
3745
|
): Promise<void> {
|
|
3384
3746
|
const shared = this.sharedTranscriber;
|
|
3385
3747
|
if (shared && utterance.transcriber === shared) {
|
|
3386
|
-
|
|
3387
|
-
|
|
3748
|
+
if (typeof shared.finalizeUtterance === "function") {
|
|
3749
|
+
await this.finalizeUtteranceForRelease(utterance);
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
if (utterance.providerClosedTurn) {
|
|
3753
|
+
await this.sealProviderClosedUtterance(utterance);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
// A caller-side boundary on a stream with no flush: only closing it can
|
|
3757
|
+
// make the provider answer for a turn still open, so this one release
|
|
3758
|
+
// costs the stream. The stream stays installed as the shared one across
|
|
3759
|
+
// the close: its flush `final` and its `closed` both arrive after
|
|
3760
|
+
// `stop()`, and the shared router drops events from a stream it no
|
|
3761
|
+
// longer recognizes. `closed` is what retires it, seals this cycle and
|
|
3762
|
+
// dispatches the turn, so clearing the reference here would strand the
|
|
3763
|
+
// cycle in `released` with the flushed transcript discarded. Nothing can
|
|
3764
|
+
// re-arm onto the closing stream in the meantime: the next arm waits on
|
|
3765
|
+
// the assistant turn, which waits on that same `closed`.
|
|
3388
3766
|
}
|
|
3389
3767
|
|
|
3390
3768
|
utterance.phase = "released";
|
|
@@ -3404,6 +3782,22 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3404
3782
|
await this.drainOutboundFrames();
|
|
3405
3783
|
}
|
|
3406
3784
|
|
|
3785
|
+
// Persistent-mode release for a provider-decided turn end: the event that
|
|
3786
|
+
// released this cycle already carried its final transcript, so there is
|
|
3787
|
+
// nothing to flush and no reason to close the stream. Sealing the phase is
|
|
3788
|
+
// the whole of the teardown: the same "transcript complete, stream open"
|
|
3789
|
+
// state the `finalized` path reaches, arrived at without a round trip.
|
|
3790
|
+
private async sealProviderClosedUtterance(
|
|
3791
|
+
utterance: UtteranceCycle,
|
|
3792
|
+
): Promise<void> {
|
|
3793
|
+
if (utterance.phase === "transcriber_closed") {
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
utterance.phase = "transcriber_closed";
|
|
3797
|
+
await this.startAssistantTurnIfReady();
|
|
3798
|
+
await this.drainOutboundFrames();
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3407
3801
|
// Persistent-mode release: flush the shared stream's buffered audio with
|
|
3408
3802
|
// finalizeUtterance() instead of tearing the stream down. The assistant
|
|
3409
3803
|
// turn starts on the `finalized` event; the grace timer bounds a flush
|
|
@@ -3546,6 +3940,24 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3546
3940
|
// Per-cycle transcribers are torn down with stop(); the finalize
|
|
3547
3941
|
// completion signal has no cycle to advance here.
|
|
3548
3942
|
return;
|
|
3943
|
+
case "turn-start":
|
|
3944
|
+
// Barge-in is deliberately untouched: local VAD still owns it,
|
|
3945
|
+
// because a provider roundtrip cannot beat a local energy gate on an
|
|
3946
|
+
// interrupt during playback (see DEFAULT_BARGE_IN_MIN_SPEECH_MS). The
|
|
3947
|
+
// index is recorded so a later end-of-turn can be told apart from one
|
|
3948
|
+
// this turn superseded (see isStaleProviderTurnEnd).
|
|
3949
|
+
this.recordProviderTurnStart(utterance, event.turnIndex);
|
|
3950
|
+
return;
|
|
3951
|
+
case "eager-turn-end":
|
|
3952
|
+
case "turn-resumed":
|
|
3953
|
+
// Speculative end-of-turn stays off (the config leaves
|
|
3954
|
+
// `eagerEotThreshold` unset, so Deepgram never emits these). Wiring
|
|
3955
|
+
// them onto the speculative dispatch machinery is a deferred
|
|
3956
|
+
// follow-up.
|
|
3957
|
+
return;
|
|
3958
|
+
case "turn-end":
|
|
3959
|
+
await this.handleProviderTurnEnd(utterance, event.turnIndex);
|
|
3960
|
+
return;
|
|
3549
3961
|
case "error":
|
|
3550
3962
|
await this.sendTranscriberErrorFrame(event);
|
|
3551
3963
|
return;
|
|
@@ -3567,6 +3979,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3567
3979
|
}
|
|
3568
3980
|
await this.startAssistantTurnIfReady();
|
|
3569
3981
|
return;
|
|
3982
|
+
default: {
|
|
3983
|
+
const _exhaustive: never = event;
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3570
3986
|
}
|
|
3571
3987
|
}
|
|
3572
3988
|
|
|
@@ -3670,6 +4086,34 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3670
4086
|
await this.startAssistantTurnIfReady();
|
|
3671
4087
|
return;
|
|
3672
4088
|
}
|
|
4089
|
+
case "turn-start": {
|
|
4090
|
+
// Barge-in is deliberately untouched: local VAD still owns it,
|
|
4091
|
+
// because a provider roundtrip cannot beat a local energy gate on an
|
|
4092
|
+
// interrupt during playback (see DEFAULT_BARGE_IN_MIN_SPEECH_MS). The
|
|
4093
|
+
// index is recorded so a later end-of-turn can be told apart from one
|
|
4094
|
+
// this turn superseded (see isStaleProviderTurnEnd).
|
|
4095
|
+
const target = this.pendingTranscriptCycle();
|
|
4096
|
+
if (target) {
|
|
4097
|
+
this.recordProviderTurnStart(target, event.turnIndex);
|
|
4098
|
+
}
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
case "eager-turn-end":
|
|
4102
|
+
case "turn-resumed":
|
|
4103
|
+
// Speculative end-of-turn stays off (the config leaves
|
|
4104
|
+
// `eagerEotThreshold` unset, so Deepgram never emits these). Wiring
|
|
4105
|
+
// them onto the speculative dispatch machinery is a deferred
|
|
4106
|
+
// follow-up.
|
|
4107
|
+
return;
|
|
4108
|
+
case "turn-end": {
|
|
4109
|
+
// Same routing as transcript events: the cycle awaiting its
|
|
4110
|
+
// transcript owns the turn the provider just closed.
|
|
4111
|
+
const target = this.pendingTranscriptCycle();
|
|
4112
|
+
if (target) {
|
|
4113
|
+
await this.handleProviderTurnEnd(target, event.turnIndex);
|
|
4114
|
+
}
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
3673
4117
|
case "error":
|
|
3674
4118
|
await this.sendTranscriberErrorFrame(event);
|
|
3675
4119
|
return;
|
|
@@ -3709,6 +4153,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3709
4153
|
await this.startAssistantTurnIfReady();
|
|
3710
4154
|
return;
|
|
3711
4155
|
}
|
|
4156
|
+
default: {
|
|
4157
|
+
const _exhaustive: never = event;
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
3712
4160
|
}
|
|
3713
4161
|
}
|
|
3714
4162
|
|
|
@@ -3844,8 +4292,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3844
4292
|
this.assistantPlaybackTailUntilMs = 0;
|
|
3845
4293
|
this.takeVadPreRoll();
|
|
3846
4294
|
this.vadPendingTurnEnd = null;
|
|
3847
|
-
// ...and abandons any semantic-endpointing hold still awaiting replay
|
|
4295
|
+
// ...and abandons any semantic-endpointing hold still awaiting replay,
|
|
4296
|
+
// along with any provider end-of-turn the session was still waiting on.
|
|
3848
4297
|
this.clearEndpointExtensionTimer();
|
|
4298
|
+
this.clearProviderTurnEndTimer();
|
|
3849
4299
|
// A client interrupt is a hard reset: any barge-in merge context waiting for
|
|
3850
4300
|
// the next turn is now stale (the interrupted utterance may be discarded
|
|
3851
4301
|
// without ever reaching finalizePendingUtterance).
|
|
@@ -4112,14 +4562,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4112
4562
|
consumedAnnouncement: pending?.announcement ?? null,
|
|
4113
4563
|
pendingContextStopGeneration: this.detachStopGeneration,
|
|
4114
4564
|
continuationDelivery: opts?.continuationDelivery ?? null,
|
|
4115
|
-
toolUseStarted: false,
|
|
4116
|
-
firstDeltaSeen: false,
|
|
4117
4565
|
deltaEpoch: 0,
|
|
4118
|
-
ackSpoken: false,
|
|
4119
|
-
ackGenerationPending: false,
|
|
4120
|
-
ackTimer: null,
|
|
4121
4566
|
escalationHandedOff: false,
|
|
4122
4567
|
ttsBuffer: "",
|
|
4568
|
+
ttsReasoningFilter: createReasoningTagFilter(),
|
|
4123
4569
|
ttsSegmentEnqueued: false,
|
|
4124
4570
|
ttsJobs: [],
|
|
4125
4571
|
ttsQueue: Promise.resolve(),
|
|
@@ -4142,8 +4588,8 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4142
4588
|
// Verdict-deadline fail-open: the deferred-everything window is only
|
|
4143
4589
|
// safe while the verdict is fast. If the leg produces no verdict
|
|
4144
4590
|
// within the endpoint budget (provider TTFT tail), commit anyway so
|
|
4145
|
-
// the thinking frame shows and
|
|
4146
|
-
//
|
|
4591
|
+
// the thinking frame shows and progress narration can begin, bounding
|
|
4592
|
+
// the structural silence. A verdict that arrives
|
|
4147
4593
|
// after the commit still works: escalate hands off normally, and a
|
|
4148
4594
|
// late hold token is stripped like any stray token (the utterance is
|
|
4149
4595
|
// already released, so holding is no longer possible).
|
|
@@ -4166,22 +4612,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4166
4612
|
}, this.frontModelConfig.endpointDecisionTimeoutMs);
|
|
4167
4613
|
}
|
|
4168
4614
|
|
|
4169
|
-
// Slow-first-delta spoken ack: if the model produces no delta within the
|
|
4170
|
-
// budget, a short front-model-phrased sentence holds the floor. Only
|
|
4171
|
-
// meaningful on TTS sessions — a text-only session has no audio gap to
|
|
4172
|
-
// bridge.
|
|
4173
|
-
if (this.streamTtsAudio && !opts?.speculative) {
|
|
4174
|
-
this.armAckTimer(activeTurn);
|
|
4175
|
-
}
|
|
4176
|
-
|
|
4177
4615
|
// Progress narration speaks into the turn's audible dead air on a
|
|
4178
|
-
// cadence, wherever in the turn it occurs; without TTS or a
|
|
4616
|
+
// cadence, wherever in the turn it occurs; without TTS or a narrator there
|
|
4179
4617
|
// is nothing to speak (the idle trigger's static fallback still needs a
|
|
4180
4618
|
// generation attempt to fall back from).
|
|
4181
4619
|
if (
|
|
4182
4620
|
this.frontModelConfig.progress.enabled &&
|
|
4183
4621
|
this.streamTtsAudio &&
|
|
4184
|
-
this.
|
|
4622
|
+
this.progressNarrator &&
|
|
4185
4623
|
!opts?.speculative
|
|
4186
4624
|
) {
|
|
4187
4625
|
this.armProgressIdleTimer(activeTurn);
|
|
@@ -4218,9 +4656,10 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4218
4656
|
* escalate verdict the post-verdict stream buffers into the capped bridge
|
|
4219
4657
|
* and `escalateTurn` starts a second "escalated" leg that shares this same
|
|
4220
4658
|
* ActiveAssistantTurn. The persisted assistant row is reduced to the capped
|
|
4221
|
-
* bridge by the bridge's teardown transcript-hygiene pass
|
|
4222
|
-
* conversation-hub broadcast
|
|
4223
|
-
*
|
|
4659
|
+
* bridge by the bridge's teardown transcript-hygiene pass, and the shared
|
|
4660
|
+
* conversation-hub broadcast releases the same capped bridge through the
|
|
4661
|
+
* bridge's front-door stream gate, so no verdict token reaches a hub
|
|
4662
|
+
* subscriber mid-turn.
|
|
4224
4663
|
*/
|
|
4225
4664
|
private async startAssistantLeg(
|
|
4226
4665
|
activeTurn: ActiveAssistantTurn,
|
|
@@ -4254,7 +4693,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4254
4693
|
return;
|
|
4255
4694
|
}
|
|
4256
4695
|
this.markFirstAssistantDelta(utterance, turnId);
|
|
4257
|
-
this.
|
|
4696
|
+
this.markAssistantDelta(activeTurn);
|
|
4258
4697
|
// Send-time abort gate: a delta queued behind a backed-up outbound
|
|
4259
4698
|
// frame must not be written once barge-in aborts the turn, or the
|
|
4260
4699
|
// cancelled reply's text leaks ahead of turn_cancelled. Key off this
|
|
@@ -4492,7 +4931,6 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4492
4931
|
if (current?.token !== token) {
|
|
4493
4932
|
return;
|
|
4494
4933
|
}
|
|
4495
|
-
current.toolUseStarted = true;
|
|
4496
4934
|
// Foreground wins the workspace: the continuation runs with full
|
|
4497
4935
|
// subagent abilities (it can write files, run commands), so the
|
|
4498
4936
|
// moment a live turn starts a consequential tool the two could
|
|
@@ -4541,21 +4979,6 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4541
4979
|
current.progress.stateEpoch += 1;
|
|
4542
4980
|
this.refreshActivity(current);
|
|
4543
4981
|
log.debug({ turnId, toolName }, "Live voice turn started tool use");
|
|
4544
|
-
// Definitive tool use means the turn is guaranteed slow: speak
|
|
4545
|
-
// the floor-holding ack now instead of waiting out the
|
|
4546
|
-
// first-delta timer (same one-ack-per-turn `ackSpoken` budget).
|
|
4547
|
-
// Post-handoff the escalation bridge phrase holds the floor and
|
|
4548
|
-
// front-door deltas never set firstDeltaSeen, so the escalated
|
|
4549
|
-
// leg's tool starts must not stack another filler on the bridge.
|
|
4550
|
-
if (
|
|
4551
|
-
this.streamTtsAudio &&
|
|
4552
|
-
!current.firstDeltaSeen &&
|
|
4553
|
-
!current.ackSpoken &&
|
|
4554
|
-
!current.escalationHandedOff
|
|
4555
|
-
) {
|
|
4556
|
-
this.clearAckTimer(current);
|
|
4557
|
-
this.speakAck(current, "tool_use", toolName);
|
|
4558
|
-
}
|
|
4559
4982
|
this.maybeNarrateProgress(current, "ops");
|
|
4560
4983
|
},
|
|
4561
4984
|
tool_result: (event) => {
|
|
@@ -4709,9 +5132,8 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4709
5132
|
return;
|
|
4710
5133
|
}
|
|
4711
5134
|
activeTurn.escalationHandedOff = true;
|
|
4712
|
-
// The escalation bridge below holds the floor, so
|
|
4713
|
-
//
|
|
4714
|
-
// filler on top of it.
|
|
5135
|
+
// The escalation bridge below holds the floor, so pending progress
|
|
5136
|
+
// narration would only stack a second filler on top of it.
|
|
4715
5137
|
this.clearFillerTimers(activeTurn);
|
|
4716
5138
|
// Abort the front-door leg so a model that keeps generating past the
|
|
4717
5139
|
// bridge cap adds no latency before the escalated leg starts.
|
|
@@ -4728,7 +5150,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4728
5150
|
: cappedBridge;
|
|
4729
5151
|
if (!usesFallbackBridge) {
|
|
4730
5152
|
this.markFirstAssistantDelta(activeTurn.utterance, activeTurn.turnId);
|
|
4731
|
-
this.
|
|
5153
|
+
this.markAssistantDelta(activeTurn);
|
|
4732
5154
|
void this.sendFrame(
|
|
4733
5155
|
{ type: "assistant_text_delta", text: spokenBridge },
|
|
4734
5156
|
() => !activeTurn.abortController.signal.aborted && !this.isClosed,
|
|
@@ -4774,7 +5196,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4774
5196
|
if (
|
|
4775
5197
|
this.frontModelConfig.progress.enabled &&
|
|
4776
5198
|
this.streamTtsAudio &&
|
|
4777
|
-
this.
|
|
5199
|
+
this.progressNarrator
|
|
4778
5200
|
) {
|
|
4779
5201
|
this.armProgressIdleTimer(activeTurn);
|
|
4780
5202
|
}
|
|
@@ -4846,42 +5268,11 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4846
5268
|
);
|
|
4847
5269
|
}
|
|
4848
5270
|
|
|
4849
|
-
//
|
|
4850
|
-
//
|
|
4851
|
-
//
|
|
4852
|
-
private
|
|
4853
|
-
const { token } = activeTurn;
|
|
4854
|
-
activeTurn.ackTimer = setTimeout(() => {
|
|
4855
|
-
activeTurn.ackTimer = null;
|
|
4856
|
-
if (
|
|
4857
|
-
!this.isActiveAssistantTurn(token) ||
|
|
4858
|
-
activeTurn.firstDeltaSeen ||
|
|
4859
|
-
activeTurn.ackSpoken
|
|
4860
|
-
) {
|
|
4861
|
-
return;
|
|
4862
|
-
}
|
|
4863
|
-
this.speakAck(activeTurn, "first_delta");
|
|
4864
|
-
}, this.frontModelConfig.ackFirstDeltaTimeoutMs);
|
|
4865
|
-
}
|
|
4866
|
-
|
|
4867
|
-
private clearAckTimer(turn: ActiveAssistantTurn): void {
|
|
4868
|
-
if (turn.ackTimer !== null) {
|
|
4869
|
-
clearTimeout(turn.ackTimer);
|
|
4870
|
-
turn.ackTimer = null;
|
|
4871
|
-
}
|
|
4872
|
-
}
|
|
4873
|
-
|
|
4874
|
-
// The model produced real output for this turn: the pending slow-first-delta
|
|
4875
|
-
// ack is moot, and any in-flight narration generation is stale (it captures
|
|
4876
|
-
// deltaEpoch to notice). The narration idle timer stays armed — the dead air
|
|
4877
|
-
// narration exists to fill is almost always MID-turn (between tool phases,
|
|
4878
|
-
// during long silent LLM calls after the model's opening words), so it must
|
|
4879
|
-
// keep watching after the model speaks and re-trigger once the audio drains
|
|
4880
|
-
// and silence stretches again.
|
|
4881
|
-
private markFirstDeltaForAck(turn: ActiveAssistantTurn): void {
|
|
4882
|
-
turn.firstDeltaSeen = true;
|
|
5271
|
+
// A real assistant delta makes any in-flight progress narration stale. The
|
|
5272
|
+
// idle timer stays armed so it can detect a later stretch of mid-turn
|
|
5273
|
+
// silence.
|
|
5274
|
+
private markAssistantDelta(turn: ActiveAssistantTurn): void {
|
|
4883
5275
|
turn.deltaEpoch += 1;
|
|
4884
|
-
this.clearAckTimer(turn);
|
|
4885
5276
|
}
|
|
4886
5277
|
|
|
4887
5278
|
// Arms (or re-arms) the dead-air narration timer. The countdown measures
|
|
@@ -4957,14 +5348,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4957
5348
|
}
|
|
4958
5349
|
}
|
|
4959
5350
|
|
|
4960
|
-
// Clears
|
|
4961
|
-
//
|
|
4962
|
-
// lifecycle: escalation hand-off (which then re-arms narration for the
|
|
4963
|
-
// escalated leg), barge-in, cancellation, tts-completion, finalize. Real
|
|
4964
|
-
// output moots only the ack (markFirstDeltaForAck) — the narration timer
|
|
4965
|
-
// keeps watching for mid-turn dead air.
|
|
5351
|
+
// Clears progress narration and verdict timers for events that end the
|
|
5352
|
+
// current filler lifecycle.
|
|
4966
5353
|
private clearFillerTimers(turn: ActiveAssistantTurn): void {
|
|
4967
|
-
this.clearAckTimer(turn);
|
|
4968
5354
|
this.clearProgressIdleTimer(turn);
|
|
4969
5355
|
if (turn.verdictDeadlineTimer !== null) {
|
|
4970
5356
|
clearTimeout(turn.verdictDeadlineTimer);
|
|
@@ -4972,26 +5358,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4972
5358
|
}
|
|
4973
5359
|
}
|
|
4974
5360
|
|
|
4975
|
-
//
|
|
4976
|
-
// Once the turn is stale, the brain's first delta has arrived, an
|
|
4977
|
-
// escalation hand-off has enqueued its bridge phrase (which holds the floor
|
|
4978
|
-
// itself), or the turn has completed outright — a tool-only/no-text turn
|
|
4979
|
-
// completes with firstDeltaSeen still false — an ack is moot and must not
|
|
4980
|
-
// speak over real output, stack on the escalation bridge, or voice a
|
|
4981
|
-
// finished turn.
|
|
4982
|
-
private turnCanSpeakFiller(turn: ActiveAssistantTurn): boolean {
|
|
4983
|
-
return (
|
|
4984
|
-
this.isActiveAssistantTurn(turn.token) &&
|
|
4985
|
-
!turn.firstDeltaSeen &&
|
|
4986
|
-
!turn.escalationHandedOff &&
|
|
4987
|
-
!turn.assistantCompleted
|
|
4988
|
-
);
|
|
4989
|
-
}
|
|
4990
|
-
|
|
4991
|
-
// Narration, unlike the ack, is not confined to the pre-first-delta window:
|
|
4992
|
-
// the dead air it exists to fill is almost always mid-turn, after the
|
|
4993
|
-
// model's opening words. It may speak whenever the live turn is audibly
|
|
4994
|
-
// silent right now — nothing streaming, queued, or still playing — and has
|
|
5361
|
+
// Narration may speak whenever the live turn is audibly silent and has
|
|
4995
5362
|
// not completed. Escalated turns qualify too: the bridge phrase holds the
|
|
4996
5363
|
// floor only while its audio plays (covered by the audible-idle check),
|
|
4997
5364
|
// and the strong leg's tool loops are the longest dead air in the system.
|
|
@@ -5028,31 +5395,20 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5028
5395
|
return false;
|
|
5029
5396
|
}
|
|
5030
5397
|
|
|
5031
|
-
// Gatekeeper for
|
|
5032
|
-
//
|
|
5033
|
-
// narration), one generation at a time, and — per trigger — only once the
|
|
5034
|
-
// ops trigger has `opsThreshold` ops accumulated or the idle trigger has
|
|
5035
|
-
// something new to report. No per-turn count cap: the cadence guards bound
|
|
5036
|
-
// the rate, and going quiet deep into a long turn is the failure mode
|
|
5037
|
-
// narration exists to prevent. Every failing guard short-circuits silently;
|
|
5038
|
-
// a skipped ops trigger keeps its accumulated count, so the next tool event
|
|
5039
|
-
// or idle tick retries.
|
|
5398
|
+
// Gatekeeper for progress narration. It speaks only while the turn is
|
|
5399
|
+
// audibly silent, with one generation at a time and the configured spacing.
|
|
5040
5400
|
private maybeNarrateProgress(
|
|
5041
5401
|
turn: ActiveAssistantTurn,
|
|
5042
5402
|
trigger: "ops" | "idle" | "op_complete",
|
|
5043
5403
|
): void {
|
|
5044
5404
|
const cfg = this.frontModelConfig.progress;
|
|
5045
5405
|
const { progress } = turn;
|
|
5046
|
-
const
|
|
5406
|
+
const progressNarrator = this.progressNarrator;
|
|
5047
5407
|
if (
|
|
5048
5408
|
!cfg.enabled ||
|
|
5049
5409
|
!this.streamTtsAudio ||
|
|
5050
|
-
!
|
|
5410
|
+
!progressNarrator ||
|
|
5051
5411
|
!this.turnCanNarrateProgress(turn) ||
|
|
5052
|
-
// A pending ack generation is a floor-holder-in-waiting: starting a
|
|
5053
|
-
// narration generation now would only be discarded by the post-await
|
|
5054
|
-
// re-check once the ack enqueues — a guaranteed wasted provider call.
|
|
5055
|
-
turn.ackGenerationPending ||
|
|
5056
5412
|
progress.narrationInFlight ||
|
|
5057
5413
|
(progress.lastFloorHolderAtMs !== null &&
|
|
5058
5414
|
Date.now() - progress.lastFloorHolderAtMs < cfg.minGapMs) ||
|
|
@@ -5061,25 +5417,20 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5061
5417
|
) {
|
|
5062
5418
|
return;
|
|
5063
5419
|
}
|
|
5064
|
-
void this.speakProgressUpdate(turn,
|
|
5420
|
+
void this.speakProgressUpdate(turn, progressNarrator, trigger);
|
|
5065
5421
|
}
|
|
5066
5422
|
|
|
5067
|
-
// Generate and speak one progress narration.
|
|
5068
|
-
//
|
|
5069
|
-
//
|
|
5070
|
-
// by `progress.generationTimeoutMs` and resolves null on every failure
|
|
5071
|
-
// mode; on null the idle trigger falls back to a static phrase (silence is
|
|
5072
|
-
// actively harmful there) while the tool-activity triggers stay silent — a
|
|
5073
|
-
// generic filler is not worth it when narration was merely opportunistic.
|
|
5423
|
+
// Generate and speak one audio-only progress narration. On a null result,
|
|
5424
|
+
// idle narration falls back to a static phrase while activity-triggered
|
|
5425
|
+
// narration stays silent.
|
|
5074
5426
|
private async speakProgressUpdate(
|
|
5075
5427
|
turn: ActiveAssistantTurn,
|
|
5076
|
-
|
|
5428
|
+
progressNarrator: VoiceProgressNarrator,
|
|
5077
5429
|
trigger: "ops" | "idle" | "op_complete",
|
|
5078
5430
|
): Promise<void> {
|
|
5079
5431
|
const { progress } = turn;
|
|
5080
5432
|
progress.narrationInFlight = true;
|
|
5081
|
-
// Any delta that lands while
|
|
5082
|
-
// generated text stale — and proves the model is speaking again.
|
|
5433
|
+
// Any delta that lands while generation is in flight makes the text stale.
|
|
5083
5434
|
const deltaEpochAtLaunch = turn.deltaEpoch;
|
|
5084
5435
|
// The activity this update describes. Tool events that land mid-generation
|
|
5085
5436
|
// are news the generated text cannot carry, so they must leave the idle
|
|
@@ -5097,7 +5448,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5097
5448
|
(op): op is TurnProgressOp & { completedAtMs: number } =>
|
|
5098
5449
|
op.completedAtMs !== undefined,
|
|
5099
5450
|
)
|
|
5100
|
-
// `ops` is in start order; the
|
|
5451
|
+
// `ops` is in start order; the narrator wants completion
|
|
5101
5452
|
// order, which differs when parallel tools finish out of order.
|
|
5102
5453
|
.sort((a, b) => a.completedAtMs - b.completedAtMs)
|
|
5103
5454
|
.map((op) => ({
|
|
@@ -5117,26 +5468,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5117
5468
|
updateIndex: progress.updatesSpoken + 1,
|
|
5118
5469
|
...(turn.language !== undefined ? { languageHint: turn.language } : {}),
|
|
5119
5470
|
};
|
|
5120
|
-
const generated = await
|
|
5471
|
+
const generated = await progressNarrator
|
|
5121
5472
|
.generateProgressText(input, turn.abortController.signal)
|
|
5122
|
-
// The
|
|
5473
|
+
// The narrator contract never rejects. Keep test stubs fail-soft too.
|
|
5123
5474
|
.catch(() => null);
|
|
5124
|
-
//
|
|
5125
|
-
// condition may have flipped while generating, and a delta that landed
|
|
5126
|
-
// mid-generation (deltaEpoch moved) makes the text stale even when the
|
|
5127
|
-
// resulting audio has already drained by now. The ack-generation and
|
|
5128
|
-
// floor re-checks close the race with a generated ack that
|
|
5129
|
-
// STARTED after this generation did — the entry guard rejects a
|
|
5130
|
-
// narration while an ack generation is already pending, but cannot see
|
|
5131
|
-
// one that begins mid-generation: while that ack is pending it has not
|
|
5132
|
-
// yet stamped `lastFloorHolderAtMs`, and once it enqueues the minGapMs
|
|
5133
|
-
// spacing must be re-applied from that stamp — either way, enqueueing
|
|
5134
|
-
// now would stack back-to-back filler. A bail here is silent and keeps
|
|
5135
|
-
// the update budget, exactly like the stale-turn bail.
|
|
5475
|
+
// Re-check liveness and staleness after the provider call.
|
|
5136
5476
|
if (
|
|
5137
5477
|
!this.turnCanNarrateProgress(turn) ||
|
|
5138
5478
|
turn.deltaEpoch !== deltaEpochAtLaunch ||
|
|
5139
|
-
turn.ackGenerationPending ||
|
|
5140
5479
|
(progress.lastFloorHolderAtMs !== null &&
|
|
5141
5480
|
Date.now() - progress.lastFloorHolderAtMs <
|
|
5142
5481
|
this.frontModelConfig.progress.minGapMs)
|
|
@@ -5144,7 +5483,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5144
5483
|
return;
|
|
5145
5484
|
}
|
|
5146
5485
|
let raw = generated;
|
|
5147
|
-
//
|
|
5486
|
+
// Generated text is in the turn's language; only the static
|
|
5148
5487
|
// fallback comes from a localized table and may need the "en" override.
|
|
5149
5488
|
let fillerLanguage: string | undefined;
|
|
5150
5489
|
if (raw === null) {
|
|
@@ -5163,8 +5502,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5163
5502
|
progress.opsSinceNarration = 0;
|
|
5164
5503
|
progress.narratedEpoch = stateEpochAtLaunch;
|
|
5165
5504
|
progress.updatesSpoken += 1;
|
|
5166
|
-
//
|
|
5167
|
-
// enqueued (decider text or static fallback alike).
|
|
5505
|
+
// Record only when narration audio actually enqueued.
|
|
5168
5506
|
this.markProgressSpoken(turn);
|
|
5169
5507
|
// Restart the dead-air countdown from this narration.
|
|
5170
5508
|
this.armProgressIdleTimer(turn);
|
|
@@ -5173,93 +5511,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5173
5511
|
}
|
|
5174
5512
|
}
|
|
5175
5513
|
|
|
5176
|
-
//
|
|
5177
|
-
// `ackSpoken` budget). The ack is audio-only — no assistant_text_delta frame
|
|
5178
|
-
// — so captions and the persisted assistant message carry only the model's
|
|
5179
|
-
// own output. It enqueues as a complete standalone sentence on the turn's
|
|
5180
|
-
// ordered TTS queue, so the model's audio always follows it cleanly; it
|
|
5181
|
-
// never consumes the eager first-clause flush reserved for the model's real
|
|
5182
|
-
// first segment.
|
|
5183
|
-
private speakAck(
|
|
5184
|
-
activeTurn: ActiveAssistantTurn,
|
|
5185
|
-
kind: LiveVoiceSpokenAckKind,
|
|
5186
|
-
toolName?: string,
|
|
5187
|
-
): void {
|
|
5188
|
-
if (!this.frontDecider) {
|
|
5189
|
-
// No decider, no ack: a canned line is worse than the silence, which
|
|
5190
|
-
// progress narration already covers once the turn runs long.
|
|
5191
|
-
return;
|
|
5192
|
-
}
|
|
5193
|
-
activeTurn.ackSpoken = true;
|
|
5194
|
-
// Optimistic ackSpoken above keeps the one-per-turn budget honest for
|
|
5195
|
-
// any other ack trigger while the generation is in flight; the async
|
|
5196
|
-
// path undoes it if the ack turns out moot.
|
|
5197
|
-
void this.speakGeneratedAck(activeTurn, this.frontDecider, kind, toolName);
|
|
5198
|
-
}
|
|
5199
|
-
|
|
5200
|
-
// Ask the front model for one short contextual sentence. The decider
|
|
5201
|
-
// internally bounds the call by `ackGenerationTimeoutMs` and resolves null
|
|
5202
|
-
// on every failure mode, so the enqueue never waits beyond that budget; a
|
|
5203
|
-
// null generation speaks nothing.
|
|
5204
|
-
private async speakGeneratedAck(
|
|
5205
|
-
activeTurn: ActiveAssistantTurn,
|
|
5206
|
-
frontDecider: VoiceFrontDecider,
|
|
5207
|
-
kind: LiveVoiceSpokenAckKind,
|
|
5208
|
-
toolName?: string,
|
|
5209
|
-
): Promise<void> {
|
|
5210
|
-
activeTurn.ackGenerationPending = true;
|
|
5211
|
-
try {
|
|
5212
|
-
const transcript = activeTurn.utterance.finalTranscriptSegments
|
|
5213
|
-
.join(" ")
|
|
5214
|
-
.trim();
|
|
5215
|
-
const generated = await frontDecider
|
|
5216
|
-
.generateAckText(
|
|
5217
|
-
{
|
|
5218
|
-
transcriptSoFar: transcript,
|
|
5219
|
-
toolName,
|
|
5220
|
-
...(activeTurn.language !== undefined
|
|
5221
|
-
? { languageHint: activeTurn.language }
|
|
5222
|
-
: {}),
|
|
5223
|
-
},
|
|
5224
|
-
activeTurn.abortController.signal,
|
|
5225
|
-
)
|
|
5226
|
-
// The decider contract never rejects; belt-and-braces for a stub.
|
|
5227
|
-
.catch(() => null);
|
|
5228
|
-
// Liveness re-check after the await: any turnCanSpeakFiller condition
|
|
5229
|
-
// may have flipped while generating, mooting the ack. A null generation
|
|
5230
|
-
// (no provider, timeout, unusable output) speaks nothing. Either way,
|
|
5231
|
-
// undo the optimistic ackSpoken so the flag reflects reality and a
|
|
5232
|
-
// later trigger this turn can still hold the floor.
|
|
5233
|
-
if (generated === null || !this.turnCanSpeakFiller(activeTurn)) {
|
|
5234
|
-
activeTurn.ackSpoken = false;
|
|
5235
|
-
return;
|
|
5236
|
-
}
|
|
5237
|
-
this.enqueueAckPhrase(activeTurn, generated, kind);
|
|
5238
|
-
} finally {
|
|
5239
|
-
activeTurn.ackGenerationPending = false;
|
|
5240
|
-
}
|
|
5241
|
-
}
|
|
5242
|
-
|
|
5243
|
-
// Records the ack-spoken metric only when a phrase actually enqueues; a
|
|
5244
|
-
// phrase that sanitizes away releases the turn's ack budget again.
|
|
5245
|
-
private enqueueAckPhrase(
|
|
5246
|
-
activeTurn: ActiveAssistantTurn,
|
|
5247
|
-
raw: string,
|
|
5248
|
-
kind: LiveVoiceSpokenAckKind,
|
|
5249
|
-
): void {
|
|
5250
|
-
if (this.enqueueFillerPhrase(activeTurn, raw)) {
|
|
5251
|
-
this.markAckSpoken(activeTurn, kind);
|
|
5252
|
-
return;
|
|
5253
|
-
}
|
|
5254
|
-
activeTurn.ackSpoken = false;
|
|
5255
|
-
}
|
|
5256
|
-
|
|
5257
|
-
// Sanitize and enqueue one filler sentence (spoken ack or progress
|
|
5258
|
-
// narration) on the turn's ordered TTS queue — the shared tail of every
|
|
5259
|
-
// filler path. Returns whether a phrase actually enqueued; per-kind metric
|
|
5260
|
-
// marks and bookkeeping are the caller's. `language` is a per-segment
|
|
5261
|
-
// hint override (see fixedPhraseLanguage); omit it for generated text,
|
|
5262
|
-
// which is already in the turn's language.
|
|
5514
|
+
// Sanitize and enqueue one progress sentence on the ordered TTS queue.
|
|
5263
5515
|
private enqueueFillerPhrase(
|
|
5264
5516
|
turn: ActiveAssistantTurn,
|
|
5265
5517
|
raw: string,
|
|
@@ -5303,7 +5555,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5303
5555
|
return;
|
|
5304
5556
|
}
|
|
5305
5557
|
|
|
5306
|
-
activeTurn.ttsBuffer += text;
|
|
5558
|
+
activeTurn.ttsBuffer += activeTurn.ttsReasoningFilter.push(text);
|
|
5307
5559
|
this.flushTtsBuffer(token, false);
|
|
5308
5560
|
}
|
|
5309
5561
|
|
|
@@ -5314,6 +5566,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5314
5566
|
}
|
|
5315
5567
|
|
|
5316
5568
|
this.clearFillerTimers(activeTurn);
|
|
5569
|
+
activeTurn.ttsBuffer += activeTurn.ttsReasoningFilter.flush();
|
|
5317
5570
|
this.flushTtsBuffer(token, true);
|
|
5318
5571
|
activeTurn.ttsQueue = activeTurn.ttsQueue
|
|
5319
5572
|
.catch(() => {})
|
|
@@ -5420,8 +5673,8 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5420
5673
|
return;
|
|
5421
5674
|
}
|
|
5422
5675
|
|
|
5423
|
-
//
|
|
5424
|
-
//
|
|
5676
|
+
// Progress narration leaves the eager first-clause flush available for
|
|
5677
|
+
// the model's real first segment.
|
|
5425
5678
|
if (options.countsAsFirstSegment ?? true) {
|
|
5426
5679
|
activeTurn.ttsSegmentEnqueued = true;
|
|
5427
5680
|
}
|
|
@@ -5724,23 +5977,42 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5724
5977
|
utterance: UtteranceCycle,
|
|
5725
5978
|
action: VoiceEndpointAction,
|
|
5726
5979
|
latencyMs: number,
|
|
5980
|
+
// Absent means the front door decided, which is what the collector
|
|
5981
|
+
// defaults to; the provider path names itself so one metrics frame can
|
|
5982
|
+
// compare the two.
|
|
5983
|
+
source?: VoiceEndpointSource,
|
|
5727
5984
|
): void {
|
|
5728
5985
|
const turnId = this.ensureTurnId(utterance);
|
|
5729
5986
|
if (!this.startMetricsTurnIfNeeded(utterance, turnId)) {
|
|
5730
5987
|
return;
|
|
5731
5988
|
}
|
|
5732
|
-
this.metrics.markEndpointDecision(turnId, {
|
|
5989
|
+
this.metrics.markEndpointDecision(turnId, {
|
|
5990
|
+
action,
|
|
5991
|
+
latencyMs,
|
|
5992
|
+
...(source ? { source } : {}),
|
|
5993
|
+
});
|
|
5733
5994
|
}
|
|
5734
5995
|
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5996
|
+
/**
|
|
5997
|
+
* The turn's end-of-turn latency, stamped at the moment it commits. Called
|
|
5998
|
+
* from `releaseUtterance`, which every committed turn passes through
|
|
5999
|
+
* whichever decider released it, so the front-door and provider samples span
|
|
6000
|
+
* the same thing from the same anchor over the same population. That is
|
|
6001
|
+
* what `endpointDecisionMaxLatencyMs` cannot offer, and why this exists
|
|
6002
|
+
* alongside it.
|
|
6003
|
+
*
|
|
6004
|
+
* Skipped with no speech-stop mark, which is push-to-talk: there the
|
|
6005
|
+
* client's release is the boundary and no local VAD runs.
|
|
6006
|
+
*/
|
|
6007
|
+
private markEndpointCommit(utterance: UtteranceCycle): void {
|
|
6008
|
+
if (this.localSpeechStopAtMs === null) {
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
const turnId = this.ensureTurnId(utterance);
|
|
6012
|
+
if (!this.startMetricsTurnIfNeeded(utterance, turnId)) {
|
|
5741
6013
|
return;
|
|
5742
6014
|
}
|
|
5743
|
-
this.metrics.
|
|
6015
|
+
this.metrics.markEndpointCommit(turnId, this.msSinceLocalSpeechStop());
|
|
5744
6016
|
}
|
|
5745
6017
|
|
|
5746
6018
|
private markProgressSpoken(activeTurn: ActiveAssistantTurn): void {
|
|
@@ -6096,14 +6368,16 @@ export function createLiveVoiceSession(
|
|
|
6096
6368
|
options: LiveVoiceSessionOptions = {},
|
|
6097
6369
|
): LiveVoiceSession {
|
|
6098
6370
|
// Workspace-tunable server-VAD thresholds. The `liveVoice.vad` schema
|
|
6099
|
-
// defaults
|
|
6100
|
-
//
|
|
6101
|
-
//
|
|
6102
|
-
//
|
|
6103
|
-
//
|
|
6371
|
+
// defaults are 800 energy / 1200 ms silence / 30 s max turn / 250 ms
|
|
6372
|
+
// barge-in guard. Three of the four match their in-code fallback; the
|
|
6373
|
+
// trailing-silence threshold does not, so a session built with a
|
|
6374
|
+
// `liveVoice` config waits 1200 ms and one built without waits the in-code
|
|
6375
|
+
// DEFAULT_SILENCE_THRESHOLD_MS of 800. Optional-chained because hand-built
|
|
6376
|
+
// test configs may predate the liveVoice namespace; absent config falls
|
|
6377
|
+
// through to the in-code defaults.
|
|
6104
6378
|
const liveVoiceConfig = getConfig().liveVoice;
|
|
6105
6379
|
const vadConfig = liveVoiceConfig?.vad;
|
|
6106
|
-
// Parsed once here into a complete config, shared by
|
|
6380
|
+
// Parsed once here into a complete config, shared by endpointing and the
|
|
6107
6381
|
// session (the constructor's own parse is then a no-op re-validation).
|
|
6108
6382
|
const frontModelConfig = LiveVoiceFrontModelConfigSchema.parse(
|
|
6109
6383
|
options.frontModelConfig ?? liveVoiceConfig?.frontModel ?? {},
|
|
@@ -6128,13 +6402,14 @@ export function createLiveVoiceSession(
|
|
|
6128
6402
|
options.echoEmaHalfLifeMs ?? vadConfig?.echoEmaHalfLifeMs,
|
|
6129
6403
|
echoDrainSlackMs: options.echoDrainSlackMs ?? vadConfig?.echoDrainSlackMs,
|
|
6130
6404
|
frontModelConfig,
|
|
6131
|
-
//
|
|
6132
|
-
//
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6405
|
+
// Absent config leaves the schema defaults, which keep provider turn
|
|
6406
|
+
// detection off.
|
|
6407
|
+
fluxConfig: options.fluxConfig ?? liveVoiceConfig?.flux,
|
|
6408
|
+
// An explicit option, including null, always wins for the test seam.
|
|
6409
|
+
progressNarrator:
|
|
6410
|
+
options.progressNarrator !== undefined
|
|
6411
|
+
? options.progressNarrator
|
|
6412
|
+
: createVoiceProgressNarrator({ config: frontModelConfig.progress }),
|
|
6138
6413
|
resolveCredentialReadiness:
|
|
6139
6414
|
options.resolveCredentialReadiness === undefined
|
|
6140
6415
|
? defaultResolveLiveVoiceCredentialReadiness
|