@vellumai/assistant 0.12.1 → 0.12.2-staging.2
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 +4 -0
- package/ARCHITECTURE.md +20 -21
- package/docs/architecture/turn-actor.md +15 -8
- package/docs/guardian-request-flow.md +16 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +360 -51
- package/package.json +3 -1
- package/scripts/smoke-desktop-window-theme.ts +186 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
- package/src/__tests__/anthropic-provider.test.ts +0 -63
- package/src/__tests__/app-compiler.test.ts +6 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
- package/src/__tests__/app-dir-path-guard.test.ts +1 -3
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
- package/src/__tests__/assistant-event-hub.test.ts +44 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
- package/src/__tests__/computer-use-tools.test.ts +19 -7
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +28 -29
- package/src/__tests__/config-watcher.test.ts +23 -8
- package/src/__tests__/connection-model-compat.test.ts +2 -2
- package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +26 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
- package/src/__tests__/conversation-queue.test.ts +48 -207
- package/src/__tests__/conversation-retry-discard.test.ts +54 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
- package/src/__tests__/conversation-skill-tools.test.ts +27 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
- package/src/__tests__/conversation-title-service.test.ts +14 -10
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
- package/src/__tests__/credential-record-retire.test.ts +171 -1
- package/src/__tests__/credential-routes.test.ts +250 -3
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/cu-unified-flow.test.ts +14 -4
- package/src/__tests__/document-sync-tags.test.ts +25 -0
- package/src/__tests__/drain-kick-guard.test.ts +0 -2
- package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
- package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
- package/src/__tests__/git-porcelain.test.ts +47 -0
- package/src/__tests__/heartbeat-service.test.ts +56 -40
- package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
- package/src/__tests__/host-bash-proxy.test.ts +26 -45
- package/src/__tests__/host-browser-proxy.test.ts +6 -17
- package/src/__tests__/host-cu-proxy.test.ts +596 -0
- package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
- package/src/__tests__/host-file-proxy.test.ts +11 -23
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
- package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
- package/src/__tests__/llm-catalog-parity.test.ts +25 -0
- package/src/__tests__/mcp-auth-routes.test.ts +6 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
- package/src/__tests__/mcp-health-check.test.ts +8 -11
- package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
- package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
- package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
- package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
- package/src/__tests__/messaging-send-tool.test.ts +355 -548
- package/src/__tests__/model-intents.test.ts +3 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
- package/src/__tests__/oauth-commands-routes.test.ts +168 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/openai-provider.test.ts +1 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
- package/src/__tests__/platform-callback-registration.test.ts +8 -16
- package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
- package/src/__tests__/qdrant-manager.test.ts +132 -0
- package/src/__tests__/resolve-app.test.ts +135 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
- package/src/__tests__/script-proxy-certs.test.ts +35 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/service-account-token.test.ts +247 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/skills.test.ts +9 -4
- package/src/__tests__/source-fingerprint.test.ts +184 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
- package/src/__tests__/subagent-tools.test.ts +3 -2
- package/src/__tests__/telephony-tts-guard.test.ts +38 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/unicode.test.ts +13 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
- package/src/__tests__/workspace-git-service.test.ts +26 -0
- package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/agent-process.test.ts +258 -4
- package/src/acp/__tests__/auth-required.test.ts +96 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/client-handler.test.ts +90 -2
- package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
- package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
- package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
- package/src/acp/__tests__/session-manager.test.ts +895 -4
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/agent-process.test.ts +4 -1
- package/src/acp/agent-process.ts +96 -10
- package/src/acp/auth-required.ts +50 -1
- package/src/acp/auto-install.test.ts +9 -7
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/client-handler.ts +16 -3
- package/src/acp/model-config.test.ts +166 -0
- package/src/acp/model-config.ts +138 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/acp/resolve-agent.test.ts +220 -6
- package/src/acp/resolve-agent.ts +88 -23
- package/src/acp/session-manager.test.ts +196 -3
- package/src/acp/session-manager.ts +441 -18
- package/src/acp/types.ts +50 -5
- package/src/agent/loop.ts +1 -0
- package/src/api/events/acp-session-model-update.test.ts +88 -0
- package/src/api/events/acp-session-model-update.ts +46 -0
- package/src/api/events/app-files-changed.ts +4 -3
- package/src/api/events/notification-intent.ts +2 -0
- package/src/api/events/user-message-echo.ts +7 -1
- package/src/api/index.ts +6 -0
- package/src/api/responses/conversation-message.ts +5 -0
- package/src/apps/app-store.ts +1 -1
- package/src/apps/resolve-app.ts +81 -0
- package/src/apps/source-fingerprint.ts +266 -0
- package/src/avatar/identity-avatar.ts +7 -2
- package/src/bundler/app-compiler.ts +15 -0
- package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
- package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
- package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
- package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
- package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
- package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
- package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
- package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
- package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
- package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
- package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
- package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
- package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
- package/src/calls/barge-in-guard.ts +118 -0
- package/src/calls/call-constants.ts +24 -4
- package/src/calls/call-controller.ts +499 -199
- package/src/calls/call-transport.ts +16 -0
- package/src/calls/media-stream-output.ts +32 -0
- package/src/calls/media-stream-server.ts +82 -30
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/{live-voice → calls}/progress-narration.ts +2 -2
- package/src/calls/resolve-call-tts-provider.ts +33 -2
- package/src/calls/tts-call-strategy.ts +9 -5
- package/src/calls/types.ts +2 -0
- package/src/calls/voice-control-protocol.ts +155 -21
- package/src/calls/voice-leg-coordinator.ts +189 -0
- package/src/calls/voice-progress-cadence.ts +423 -0
- package/src/calls/voice-session-bridge.ts +92 -72
- package/src/calls/voice-triage-escalate.ts +178 -59
- package/src/cli/commands/__tests__/apps.test.ts +245 -0
- package/src/cli/commands/__tests__/bash.test.ts +53 -0
- package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
- package/src/cli/commands/apps.help.ts +67 -13
- package/src/cli/commands/apps.ts +183 -0
- package/src/cli/commands/bash.help.ts +8 -13
- package/src/cli/commands/bash.ts +7 -25
- package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
- package/src/cli/commands/channels/index.help.ts +70 -2
- package/src/cli/commands/channels/index.ts +16 -3
- package/src/cli/commands/channels/send.ts +128 -0
- package/src/cli/commands/credentials.help.ts +6 -2
- package/src/cli/commands/credentials.ts +2 -2
- package/src/cli/commands/mcp.help.ts +8 -8
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/roadmap.help.ts +5 -1
- package/src/cli/commands/roadmap.ts +1 -1
- package/src/cli/lib/bundled-marketplace.json +103 -90
- package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
- package/src/config/__tests__/platform-identity.test.ts +255 -0
- package/src/config/__tests__/platform-rehydration.test.ts +99 -9
- package/src/config/acp-agent-write.ts +43 -0
- package/src/config/acp-defaults.test.ts +13 -3
- package/src/config/acp-defaults.ts +23 -6
- package/src/config/acp-schema.test.ts +81 -0
- package/src/config/acp-schema.ts +30 -2
- package/src/config/bundled-skills/acp/SKILL.md +22 -14
- package/src/config/bundled-skills/acp/TOOLS.json +5 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
- package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
- package/src/config/bundled-skills/schedule/SKILL.md +3 -4
- package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
- package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/env.ts +2 -2
- package/src/config/feature-flag-registry.json +33 -13
- package/src/config/loader.ts +2 -0
- package/src/config/memory-v3-gate.test.ts +31 -0
- package/src/config/memory-v3-gate.ts +20 -16
- package/src/config/platform-identity.ts +201 -0
- package/src/config/platform-rehydration.ts +11 -27
- package/src/config/schema.ts +2 -2
- package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
- package/src/config/schemas/__tests__/voice.test.ts +148 -0
- package/src/config/schemas/calls.ts +1 -28
- package/src/config/schemas/live-voice.ts +1 -173
- package/src/config/schemas/mcp.ts +1 -1
- package/src/config/schemas/memory-retrospective.ts +10 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/schemas/skills.ts +0 -14
- package/src/config/schemas/voice.ts +150 -0
- package/src/credential-execution/process-manager.test.ts +18 -19
- package/src/credential-health/credential-health-service.ts +11 -17
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
- package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
- package/src/daemon/app-change-notify.ts +45 -0
- package/src/daemon/config-watcher.ts +13 -194
- package/src/daemon/conversation-error.ts +3 -2
- package/src/daemon/conversation-media-retry.ts +2 -1
- package/src/daemon/conversation-process.ts +35 -64
- package/src/daemon/conversation-runtime-assembly.ts +20 -7
- package/src/daemon/conversation-skill-tools.ts +13 -31
- package/src/daemon/conversation-store.ts +1 -1
- package/src/daemon/conversation-surfaces.ts +21 -10
- package/src/daemon/conversation-tool-setup.ts +45 -59
- package/src/daemon/conversation.ts +0 -3
- package/src/daemon/dictation-profile-store.ts +2 -1
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +6 -3
- package/src/daemon/host-bash-proxy.ts +8 -12
- package/src/daemon/host-browser-proxy.ts +15 -1
- package/src/daemon/host-cu-proxy.ts +224 -19
- package/src/daemon/host-file-proxy.ts +8 -10
- package/src/daemon/host-proxy-preactivation.ts +2 -1
- package/src/daemon/host-transfer-proxy.ts +14 -18
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/mcp-reload-service.ts +5 -5
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/orphan-reaper.ts +1 -1
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/reaction-history-render.ts +2 -1
- package/src/daemon/shutdown-handlers.ts +1 -9
- package/src/daemon/skill-memory-refresh.ts +64 -0
- package/src/daemon/tool-side-effects.ts +7 -18
- package/src/desktop/__tests__/fake-desktop.ts +122 -0
- package/src/desktop/desktop-chrome-policy.test.ts +79 -0
- package/src/desktop/desktop-chrome-policy.ts +47 -0
- package/src/desktop/desktop-chrome-session.ts +37 -0
- package/src/desktop/desktop-dependencies.test.ts +102 -0
- package/src/desktop/desktop-dependencies.ts +308 -0
- package/src/desktop/desktop-feature.test.ts +18 -0
- package/src/desktop/desktop-feature.ts +19 -0
- package/src/desktop/desktop-panel-config.test.ts +135 -0
- package/src/desktop/desktop-panel-config.ts +149 -0
- package/src/desktop/desktop-session-manager.test.ts +730 -0
- package/src/desktop/desktop-session-manager.ts +850 -0
- package/src/desktop/desktop-stream-bridge.test.ts +395 -0
- package/src/desktop/desktop-stream-bridge.ts +214 -0
- package/src/desktop/desktop-wallpaper.test.ts +191 -0
- package/src/desktop/desktop-wallpaper.ts +113 -0
- package/src/desktop/desktop-window-drag.ts +177 -0
- package/src/desktop/desktop-window-manager-config.test.ts +230 -0
- package/src/desktop/desktop-window-manager-config.ts +238 -0
- package/src/desktop/desktop-window-theme.ts +170 -0
- package/src/heartbeat/heartbeat-service.ts +21 -3
- package/src/home/__tests__/feed-writer.test.ts +22 -0
- package/src/home/feed-writer.ts +11 -2
- package/src/hooks/types.ts +33 -0
- package/src/i18n/AGENTS.md +22 -0
- package/src/i18n/__tests__/i18n.test.ts +108 -0
- package/src/i18n/index.ts +16 -0
- package/src/i18n/locales.ts +93 -0
- package/src/i18n/messages.ts +61 -0
- package/src/i18n/title.ts +25 -0
- package/src/inbound/platform-callback-registration.ts +4 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
- package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
- package/src/live-voice/__tests__/protocol.test.ts +39 -0
- package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
- package/src/live-voice/__tests__/session-controls.test.ts +79 -0
- package/src/live-voice/live-voice-photo.ts +25 -6
- package/src/live-voice/live-voice-session.ts +297 -699
- package/src/live-voice/protocol.ts +60 -2
- package/src/live-voice/session-controls.ts +111 -0
- package/src/mcp/__tests__/effective-config.test.ts +2 -2
- package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
- package/src/mcp/effective-config.ts +3 -3
- package/src/mcp/mcp-header-store.ts +29 -45
- package/src/mcp/spec-schema.ts +111 -0
- package/src/mcp/startup.ts +8 -4
- package/src/mcp/workspace-mcp-config.ts +188 -0
- package/src/messaging/provider.ts +10 -3
- package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
- package/src/messaging/providers/callback-routing.ts +18 -0
- package/src/messaging/providers/channel-transport.ts +56 -0
- package/src/messaging/providers/discord/transport.ts +17 -0
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
- package/src/messaging/providers/slack/adapter.ts +7 -44
- package/src/messaging/providers/slack/transport.ts +16 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
- package/src/messaging/providers/telegram-bot/client.ts +0 -26
- package/src/messaging/providers/telegram-bot/transport.ts +20 -0
- package/src/messaging/providers/whatsapp/adapter.ts +1 -48
- package/src/messaging/providers/whatsapp/transport.ts +18 -0
- package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
- package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/assistant-reply-producer.ts +8 -3
- package/src/notifications/conversation-pairing.ts +36 -16
- package/src/notifications/delivered-post-record.ts +11 -1
- package/src/notifications/guardian-feed-projection.ts +125 -5
- package/src/notifications/home-feed-side-effect.ts +55 -7
- package/src/notifications/schedule-result-producer.ts +66 -22
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
- package/src/oauth/byo-connection.test.ts +105 -0
- package/src/oauth/byo-connection.ts +36 -6
- package/src/oauth/connection-resolver.test.ts +100 -0
- package/src/oauth/connection-resolver.ts +120 -3
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/scope-utils.ts +24 -0
- package/src/oauth/seed-providers.ts +314 -27
- package/src/outbound-proxy/certs.ts +3 -5
- package/src/permissions/checker.test.ts +11 -6
- package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
- package/src/persistence/conversation-crud.ts +71 -63
- package/src/persistence/conversation-lineage.ts +12 -4
- package/src/persistence/conversation-queries.ts +2 -2
- package/src/persistence/conversation-title-placeholders.ts +23 -0
- package/src/persistence/conversation-title-service.ts +9 -26
- package/src/persistence/embeddings/qdrant-manager.ts +163 -18
- package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
- package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
- package/src/persistence/message-cursor.ts +78 -0
- package/src/persistence/message-reads.ts +30 -0
- package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
- package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
- package/src/persistence/schema/memory-core.ts +5 -0
- package/src/platform/client.test.ts +4 -2
- package/src/platform/client.ts +2 -10
- package/src/plugin-api/constants.ts +2 -0
- package/src/plugin-api/index.ts +1 -0
- package/src/plugin-api/types.ts +2 -0
- package/src/plugins/defaults/index.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
- package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
- package/src/plugins/defaults/memory/context-search/format.ts +3 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
- package/src/plugins/defaults/memory/hooks/init.ts +10 -0
- package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
- package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
- package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
- package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
- package/src/plugins/defaults/memory/startup.ts +28 -2
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
- package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
- package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
- package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
- package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
- package/src/plugins/defaults/memory/v3/card.ts +3 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
- package/src/plugins/defaults/memory/v3/sections.ts +9 -3
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
- package/src/plugins/defaults/memory/worker.ts +6 -0
- package/src/plugins/mcp-servers.ts +15 -75
- package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
- package/src/providers/__tests__/retry-callsite.test.ts +41 -0
- package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
- package/src/providers/anthropic/client.ts +0 -47
- package/src/providers/gemini/client.ts +9 -23
- package/src/providers/gemini-thought-signature.test.ts +63 -0
- package/src/providers/gemini-thought-signature.ts +44 -0
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
- package/src/providers/inference/auth.ts +2 -3
- package/src/providers/inference/connection-availability.ts +27 -8
- package/src/providers/inference/endpoint-probe.ts +10 -1
- package/src/providers/inference/resolve-auth.ts +22 -4
- package/src/providers/inference/service-account-token.ts +248 -0
- package/src/providers/model-catalog.ts +34 -0
- package/src/providers/model-intents.ts +10 -5
- package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
- package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
- package/src/providers/openai/chat-completions-provider.ts +212 -79
- package/src/providers/openai/codex-models.ts +0 -4
- package/src/providers/openai/google-thought-signature.ts +320 -0
- package/src/providers/opencode/client.test.ts +73 -4
- package/src/providers/opencode/client.ts +60 -3
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/outbound-request-sanitize.ts +96 -0
- package/src/providers/retry.ts +30 -19
- package/src/providers/unparseable-tool-args.ts +5 -1
- package/src/runtime/AGENTS.md +3 -1
- package/src/runtime/__tests__/channel-send.test.ts +443 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
- package/src/runtime/__tests__/host-observe.test.ts +40 -1
- package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
- package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
- package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
- package/src/runtime/agent-wake.ts +2 -1
- package/src/runtime/assistant-event-hub.ts +21 -3
- package/src/runtime/auth/same-actor.ts +38 -3
- package/src/runtime/channel-send.ts +316 -0
- package/src/runtime/host-observe.ts +11 -1
- package/src/runtime/http-server.ts +160 -151
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
- package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/acp-routes.test.ts +15 -0
- package/src/runtime/routes/acp-routes.ts +72 -9
- package/src/runtime/routes/app-management-routes.ts +90 -2
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/channel-send-routes.ts +85 -0
- package/src/runtime/routes/conversation-list-routes.ts +14 -3
- package/src/runtime/routes/conversation-query-routes.ts +40 -0
- package/src/runtime/routes/conversation-routes.ts +16 -3
- package/src/runtime/routes/credential-routes.ts +20 -14
- package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
- package/src/runtime/routes/desktop-setup-routes.ts +35 -0
- package/src/runtime/routes/diagnostics-routes.ts +61 -4
- package/src/runtime/routes/events-routes.ts +4 -0
- package/src/runtime/routes/host-cu-routes.ts +6 -0
- package/src/runtime/routes/index.ts +4 -2
- package/src/runtime/routes/log-export-routes.ts +2 -0
- package/src/runtime/routes/mcp-auth-routes.ts +39 -47
- package/src/runtime/routes/migration-routes.ts +6 -20
- package/src/runtime/routes/oauth-commands-routes.ts +50 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/platform-routes.ts +18 -15
- package/src/runtime/routes/workspace-routes.test.ts +55 -26
- package/src/runtime/routes/workspace-routes.ts +46 -63
- package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
- package/src/runtime/routes/workspace-tree-walk.ts +240 -0
- package/src/runtime/routes/workspace-utils.ts +17 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +16 -3
- package/src/runtime/sync/resource-sync-events.ts +2 -1
- package/src/schedule/run-script.ts +2 -1
- package/src/schedule/schedule-store.ts +9 -2
- package/src/schedule/worker.ts +1 -1
- package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
- package/src/security/ces-http-record-backend.ts +120 -0
- package/src/security/ces-rpc-record-backend.ts +7 -0
- package/src/security/untrusted-content.ts +3 -1
- package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
- package/src/skills/workspace-skill-md-signature.ts +50 -0
- package/src/subagent/consult-context.ts +12 -25
- package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
- package/src/tools/acp/list-agents.test.ts +3 -1
- package/src/tools/acp/spawn.test.ts +146 -11
- package/src/tools/acp/spawn.ts +21 -3
- package/src/tools/acp/status.test.ts +94 -0
- package/src/tools/acp/status.ts +40 -3
- package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
- package/src/tools/browser/network-recorder.ts +2 -1
- package/src/tools/capability-offer.test.ts +22 -2
- package/src/tools/capability-offer.ts +16 -1
- package/src/tools/computer-use/definitions.ts +121 -2
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
- package/src/tools/credentials/metadata-store.ts +100 -0
- package/src/tools/filesystem/search.ts +2 -1
- package/src/tools/host-filesystem/edit.test.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.test.ts +2 -1
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.test.ts +2 -1
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.test.ts +2 -1
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +16 -0
- package/src/tools/host-terminal/host-shell.ts +4 -20
- package/src/tools/network/web-search.ts +6 -1
- package/src/tools/policy-context.ts +6 -7
- package/src/tools/skills/scaffold-managed.ts +61 -3
- package/src/tools/subagent/spawn.ts +5 -8
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
- package/src/tools/terminal/sanitized-bash.ts +105 -0
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/tools/watch/watch-retro-report.ts +0 -8
- package/src/types/host-capabilities.ts +5 -3
- package/src/util/__tests__/language-subtag.test.ts +23 -1
- package/src/util/ca-bundle.ts +13 -0
- package/src/util/language-subtag.ts +16 -0
- package/src/util/platform.ts +8 -0
- package/src/util/unicode.ts +15 -1
- package/src/watch/watch-retro.ts +6 -8
- package/src/watch/watch-timeline.ts +2 -2
- package/src/workflows/run-manager.ts +2 -1
- package/src/workspace/git-porcelain.ts +66 -0
- package/src/workspace/git-service.ts +6 -55
- package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
- package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
- package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
- package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/app-source-watcher.test.ts +0 -218
- package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
- package/src/__tests__/speaker-identification.test.ts +0 -56
- package/src/calls/speaker-identification.ts +0 -233
- package/src/daemon/app-source-watcher.ts +0 -218
- package/src/messaging/providers/whatsapp/client.ts +0 -29
- package/src/runtime/routes/debug-bash-routes.ts +0 -173
- package/src/util/fs-watcher-error.ts +0 -36
- /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
type BargeInGuard,
|
|
6
|
+
createBargeInGuard,
|
|
7
|
+
DEFAULT_BARGE_IN_MIN_SPEECH_MS,
|
|
8
|
+
} from "../calls/barge-in-guard.js";
|
|
4
9
|
import {
|
|
5
10
|
MediaTurnDetector,
|
|
6
11
|
type TurnDetectorConfig,
|
|
7
12
|
} from "../calls/media-turn-detector.js";
|
|
13
|
+
import {
|
|
14
|
+
createVoiceProgressNarrator,
|
|
15
|
+
type VoiceProgressNarrator,
|
|
16
|
+
} from "../calls/progress-narration.js";
|
|
17
|
+
import {
|
|
18
|
+
APPROVAL_PENDING_PHRASE_BY_LANGUAGE,
|
|
19
|
+
approvalPendingPhraseFor,
|
|
20
|
+
} from "../calls/progress-phrases.js";
|
|
8
21
|
import {
|
|
9
22
|
SPOKEN_REPLY_LENGTH_RULE,
|
|
10
23
|
SPOKEN_REPLY_PLAIN_TEXT_RULE,
|
|
11
24
|
} from "../calls/spoken-reply-rules.js";
|
|
12
25
|
import { sanitizeForTts } from "../calls/tts-text-sanitizer.js";
|
|
26
|
+
import { createControlMarkerHoldback } from "../calls/voice-control-protocol.js";
|
|
27
|
+
import {
|
|
28
|
+
createFrontDoorLegCoordinator,
|
|
29
|
+
type FrontDoorLegCoordinator,
|
|
30
|
+
type SpokenEscalationBridge,
|
|
31
|
+
} from "../calls/voice-leg-coordinator.js";
|
|
13
32
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "../calls/voice-
|
|
33
|
+
createProgressCadence,
|
|
34
|
+
type ProgressCadence,
|
|
35
|
+
} from "../calls/voice-progress-cadence.js";
|
|
17
36
|
import type {
|
|
18
37
|
VoiceTurnHandle,
|
|
19
38
|
VoiceTurnOptions,
|
|
@@ -23,24 +42,16 @@ import {
|
|
|
23
42
|
resolveProcessingWaitMs,
|
|
24
43
|
waitForPriorTurnTeardown,
|
|
25
44
|
} from "../calls/voice-session-bridge.js";
|
|
26
|
-
import {
|
|
27
|
-
capEscalationBridge,
|
|
28
|
-
classifyFrontDoorLeading,
|
|
29
|
-
ESCALATE_VERDICT_TOKEN,
|
|
30
|
-
ESCALATION_CONTINUATION_CONTENT,
|
|
31
|
-
FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
|
|
32
|
-
fallbackEscalationBridgeFor,
|
|
33
|
-
isEscalationBridgeComplete,
|
|
34
|
-
MIN_SPOKEN_BRIDGE_CHARS,
|
|
35
|
-
type VoiceRoutingLeg,
|
|
36
|
-
} from "../calls/voice-triage-escalate.js";
|
|
45
|
+
import { type VoiceRoutingLeg } from "../calls/voice-triage-escalate.js";
|
|
37
46
|
import { getConfig } from "../config/loader.js";
|
|
38
47
|
import {
|
|
39
48
|
type LiveVoiceFluxConfig,
|
|
40
49
|
LiveVoiceFluxConfigSchema,
|
|
41
|
-
type LiveVoiceFrontModelConfig,
|
|
42
|
-
LiveVoiceFrontModelConfigSchema,
|
|
43
50
|
} from "../config/schemas/live-voice.js";
|
|
51
|
+
import {
|
|
52
|
+
type VoiceFrontModelConfig,
|
|
53
|
+
VoiceFrontModelConfigSchema,
|
|
54
|
+
} from "../config/schemas/voice.js";
|
|
44
55
|
import { ABORT_WATCHDOG_MS } from "../daemon/abort-watchdog.js";
|
|
45
56
|
import { isRefusedInReadOnlyPass } from "../daemon/conversation-tool-setup.js";
|
|
46
57
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
@@ -93,7 +104,7 @@ import {
|
|
|
93
104
|
} from "../tts/reasoning-tag-filter.js";
|
|
94
105
|
import { extractSpeakableSegments } from "../tts/speakable-segments.js";
|
|
95
106
|
import { createAbortReason } from "../util/abort-reasons.js";
|
|
96
|
-
import {
|
|
107
|
+
import { fixedPhraseLanguage } from "../util/language-subtag.js";
|
|
97
108
|
import { getLogger } from "../util/logger.js";
|
|
98
109
|
import {
|
|
99
110
|
activityLabelForTool,
|
|
@@ -136,17 +147,6 @@ import type {
|
|
|
136
147
|
LiveVoiceTtsOptions,
|
|
137
148
|
LiveVoiceTtsResult,
|
|
138
149
|
} from "./live-voice-tts.js";
|
|
139
|
-
import {
|
|
140
|
-
createVoiceProgressNarrator,
|
|
141
|
-
type VoiceProgressNarrator,
|
|
142
|
-
type VoiceProgressTextInput,
|
|
143
|
-
} from "./progress-narration.js";
|
|
144
|
-
import {
|
|
145
|
-
APPROVAL_PENDING_PHRASE_BY_LANGUAGE,
|
|
146
|
-
approvalPendingPhraseFor,
|
|
147
|
-
pickProgressPhrase,
|
|
148
|
-
PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE,
|
|
149
|
-
} from "./progress-phrases.js";
|
|
150
150
|
import {
|
|
151
151
|
type LiveVoiceClientAttachFrameFrame,
|
|
152
152
|
type LiveVoiceClientAttachImageFrame,
|
|
@@ -156,7 +156,14 @@ import {
|
|
|
156
156
|
type LiveVoiceClientUpdateConfigFrame,
|
|
157
157
|
LiveVoiceProtocolErrorCode,
|
|
158
158
|
type LiveVoiceServerFramePayload,
|
|
159
|
+
type LiveVoiceSessionControl,
|
|
159
160
|
} from "./protocol.js";
|
|
161
|
+
import {
|
|
162
|
+
type ClientSessionControlRequest,
|
|
163
|
+
progressConfigForCadence,
|
|
164
|
+
requestedSessionControl,
|
|
165
|
+
sessionControlTeaching,
|
|
166
|
+
} from "./session-controls.js";
|
|
160
167
|
|
|
161
168
|
const log = getLogger("live-voice-session");
|
|
162
169
|
|
|
@@ -200,7 +207,6 @@ const FINALIZE_GRACE_MS = 1_000;
|
|
|
200
207
|
// upstream of the provider, which hears only the microphone: it cannot tell
|
|
201
208
|
// our own TTS bleeding through imperfect echo cancellation from the caller,
|
|
202
209
|
// and would report a user turn nobody took.
|
|
203
|
-
const DEFAULT_BARGE_IN_MIN_SPEECH_MS = 250;
|
|
204
210
|
// The playback echo gate learns microphone energy while assistant audio is
|
|
205
211
|
// expected at the speaker. Input must rise above the learned level by this
|
|
206
212
|
// margin to count as user speech.
|
|
@@ -236,24 +242,6 @@ const NOISE_FLOOR_MAX_BASE_MULTIPLE = 4;
|
|
|
236
242
|
// tracks the effective trailing-silence threshold (the detector keeps its own
|
|
237
243
|
// copy private) so the endpoint decider can report the pause length.
|
|
238
244
|
const DEFAULT_SILENCE_THRESHOLD_MS = 800;
|
|
239
|
-
// Longest continuous sub-threshold gap the sustained-speech barge-in run
|
|
240
|
-
// tolerates without resetting. A gap this short is a syllable boundary, or the
|
|
241
|
-
// choppy energy the browser's half-duplex echo canceller produces while the
|
|
242
|
-
// assistant is still playing (it ducks the user's near-end voice, so post-AEC
|
|
243
|
-
// user speech arrives as intermittent above-gate chunks) — so the run keeps
|
|
244
|
-
// accumulating across it and a barge-in during playback still lands. Only a
|
|
245
|
-
// longer continuous silence (a real end of speech, or an isolated cough) resets
|
|
246
|
-
// the run.
|
|
247
|
-
const BARGE_IN_GAP_TOLERANCE_MS = 200;
|
|
248
|
-
// Ceiling on cumulative sub-threshold time across a whole barge-in run, as a
|
|
249
|
-
// multiple of bargeInMinSpeechMs. Per-gap tolerance alone lets sparse isolated
|
|
250
|
-
// blips (e.g. a 10 ms echo spike every 200 ms) each clear the consecutive-gap
|
|
251
|
-
// timer while retaining prior speech, so they would sum to the guard over
|
|
252
|
-
// several seconds and fire a barge-in with no sustained user speech. Capping the
|
|
253
|
-
// run's total tolerated silence imposes a minimum above-gate duty cycle
|
|
254
|
-
// (1 / (1 + ratio) ≈ 20%): once the run is mostly silence it resets, so genuine
|
|
255
|
-
// choppy speech still lands but periodic noise cannot accumulate into one.
|
|
256
|
-
const BARGE_IN_MAX_TOLERATED_SILENCE_RATIO = 4;
|
|
257
245
|
// Slack added to the configured end-of-turn timeout before the session stops
|
|
258
246
|
// waiting for an event that is not coming and falls the utterance back onto
|
|
259
247
|
// the silence-boundary path. A turn-detecting provider force-ends its own turn
|
|
@@ -407,11 +395,11 @@ export interface LiveVoiceSessionOptions {
|
|
|
407
395
|
finalizeGraceMs?: number;
|
|
408
396
|
/**
|
|
409
397
|
* Voice front-door endpointing and progress narration tuning. The
|
|
410
|
-
* production factory seeds this from `
|
|
398
|
+
* production factory seeds this from `voice.frontModel` config when
|
|
411
399
|
* unset; absent fields fall back to the schema defaults (the constructor
|
|
412
400
|
* schema-parses the partial into a complete config).
|
|
413
401
|
*/
|
|
414
|
-
frontModelConfig?: Partial<
|
|
402
|
+
frontModelConfig?: Partial<VoiceFrontModelConfig>;
|
|
415
403
|
/**
|
|
416
404
|
* Deepgram Flux turn-detection tuning. The production factory seeds this
|
|
417
405
|
* from `liveVoice.flux` config when unset; absent fields fall back to the
|
|
@@ -421,7 +409,7 @@ export interface LiveVoiceSessionOptions {
|
|
|
421
409
|
fluxConfig?: Partial<LiveVoiceFluxConfig>;
|
|
422
410
|
/**
|
|
423
411
|
* Progress narration service. The production factory constructs it from
|
|
424
|
-
* `
|
|
412
|
+
* `voice.frontModel.progress` config when unset. `null` disables
|
|
425
413
|
* generated narration, so progress updates use their static fallback.
|
|
426
414
|
*/
|
|
427
415
|
progressNarrator?: VoiceProgressNarrator | null;
|
|
@@ -604,70 +592,6 @@ interface TtsSegmentJob {
|
|
|
604
592
|
frames: Promise<void>;
|
|
605
593
|
}
|
|
606
594
|
|
|
607
|
-
// One tool operation observed on a turn, fed by the bridge's structured tool
|
|
608
|
-
// callbacks. `completedAtMs`/`isError`/`resultPreview` land with tool_result.
|
|
609
|
-
interface TurnProgressOp {
|
|
610
|
-
toolName: string;
|
|
611
|
-
toolUseId?: string;
|
|
612
|
-
startedAtMs: number;
|
|
613
|
-
completedAtMs?: number;
|
|
614
|
-
isError?: boolean;
|
|
615
|
-
resultPreview?: string;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// Per-turn tool-activity log and narration cadence state for spoken progress
|
|
619
|
-
// updates (liveVoice.frontModel.progress).
|
|
620
|
-
interface TurnProgressState {
|
|
621
|
-
// Tool operations this turn, in start order.
|
|
622
|
-
ops: TurnProgressOp[];
|
|
623
|
-
// Ops accumulated toward the next ops-triggered narration. Counted once per
|
|
624
|
-
// op, on start (not completion), so a burst of slow tools still trips the
|
|
625
|
-
// threshold while they run.
|
|
626
|
-
opsSinceNarration: number;
|
|
627
|
-
// Bumped by every observable change to the turn's tool activity — an op
|
|
628
|
-
// starting or finishing. The idle trigger compares it against
|
|
629
|
-
// `narratedEpoch` so a tick with nothing new to report stays silent.
|
|
630
|
-
stateEpoch: number;
|
|
631
|
-
// The `stateEpoch` the last spoken narration described: the activity the
|
|
632
|
-
// user has already been told about.
|
|
633
|
-
narratedEpoch: number;
|
|
634
|
-
// Narrations actually spoken this turn — the metrics count and the
|
|
635
|
-
// decider's 1-based updateIndex. Rate, not count, bounds narration:
|
|
636
|
-
// idleIntervalMs/minGapMs cap the cadence and the session duration cap
|
|
637
|
-
// bounds the turn.
|
|
638
|
-
updatesSpoken: number;
|
|
639
|
-
// When the last spoken floor-holder (ack or narration) enqueued — gates the
|
|
640
|
-
// progress.minGapMs spacing guard. Null until something speaks.
|
|
641
|
-
lastFloorHolderAtMs: number | null;
|
|
642
|
-
// When the turn's last TTS segment finished emitting (turn launch until
|
|
643
|
-
// anything speaks). Together with the session-level playback-tail estimate
|
|
644
|
-
// this anchors the dead-air countdown: idle time is measured from when the
|
|
645
|
-
// user last heard something, not from when the turn started.
|
|
646
|
-
lastAudibleAtMs: number;
|
|
647
|
-
// Self-re-arming dead-air narration timer; null once cleared or fired.
|
|
648
|
-
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
649
|
-
// A narration generation is awaiting the decider; serializes narrations.
|
|
650
|
-
narrationInFlight: boolean;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Newest incomplete op, optionally restricted to a tool name (the
|
|
654
|
-
// tool_result fallback match when no toolUseId correlates).
|
|
655
|
-
function findLastIncompleteOp(
|
|
656
|
-
ops: TurnProgressOp[],
|
|
657
|
-
toolName?: string,
|
|
658
|
-
): TurnProgressOp | undefined {
|
|
659
|
-
for (let i = ops.length - 1; i >= 0; i -= 1) {
|
|
660
|
-
const op = ops[i];
|
|
661
|
-
if (
|
|
662
|
-
op.completedAtMs === undefined &&
|
|
663
|
-
(toolName === undefined || op.toolName === toolName)
|
|
664
|
-
) {
|
|
665
|
-
return op;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
return undefined;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
595
|
interface ActiveAssistantTurn {
|
|
672
596
|
token: symbol;
|
|
673
597
|
turnId: string;
|
|
@@ -683,7 +607,9 @@ interface ActiveAssistantTurn {
|
|
|
683
607
|
handle: VoiceTurnHandle | null;
|
|
684
608
|
// When the turn launched, for narration's turnElapsedMs.
|
|
685
609
|
launchedAtMs: number;
|
|
686
|
-
progress
|
|
610
|
+
// Tool-activity log and spoken progress narration for the turn
|
|
611
|
+
// (voice.frontModel.progress), shared with the phone driver.
|
|
612
|
+
progress: ProgressCadence;
|
|
687
613
|
assistantCompleted: boolean;
|
|
688
614
|
ttsDone: boolean;
|
|
689
615
|
// Latched when the turn puts something on screen (a ui tool ran); consumed
|
|
@@ -691,6 +617,10 @@ interface ActiveAssistantTurn {
|
|
|
691
617
|
// Never set from anything the model says: the reveal is a consequence of
|
|
692
618
|
// showing a surface, not a token the model has to remember.
|
|
693
619
|
minimizeRequested: boolean;
|
|
620
|
+
// The session control the completed reply ended with (see
|
|
621
|
+
// session-controls.ts); consumed at TTS drain like the minimize, where the
|
|
622
|
+
// session_control frame goes out once the acknowledgement has been spoken.
|
|
623
|
+
sessionControlRequested: ClientSessionControlRequest | null;
|
|
694
624
|
// The activity label the client was last told about, so a run of tools that
|
|
695
625
|
// map to the same line sends one frame rather than one per call. Empty means
|
|
696
626
|
// the client believes nothing is running, which is also where a turn ends.
|
|
@@ -803,7 +733,9 @@ interface ActiveAssistantTurn {
|
|
|
803
733
|
// Guards the
|
|
804
734
|
// front-door leg's trailing completion from finalizing the turn, and makes
|
|
805
735
|
// the hand-off idempotent.
|
|
806
|
-
|
|
736
|
+
// The front-door leg's coordinator: whether it handed the turn off to the
|
|
737
|
+
// escalated leg. Null until the front-door leg starts.
|
|
738
|
+
frontDoor: FrontDoorLegCoordinator | null;
|
|
807
739
|
ttsBuffer: string;
|
|
808
740
|
// What the caller actually hears this turn, summed over the model's own
|
|
809
741
|
// segments (acks and progress narration do not count). Logged at tts_done
|
|
@@ -829,55 +761,6 @@ interface ActiveAssistantTurn {
|
|
|
829
761
|
assistantAudioSampleRate?: number;
|
|
830
762
|
}
|
|
831
763
|
|
|
832
|
-
/**
|
|
833
|
-
* Control-marker hygiene for one model leg's delta stream, shared by the
|
|
834
|
-
* front-door answer stage and the default/escalated leg. The returned flush
|
|
835
|
-
* forwards the stripped (stripInternalSpeechMarkers) prefix of `raw` that has
|
|
836
|
-
* not been emitted yet and cannot contain a still-streaming control marker:
|
|
837
|
-
* the flush stops at the first "[" whose tail is an incomplete marker
|
|
838
|
-
* (isIncompleteControlMarkerTail) and holds from there until a later delta
|
|
839
|
-
* completes or disproves it; `force` (leg completion) emits the held tail so
|
|
840
|
-
* real text that merely resembles a marker prefix is not dropped. The scan
|
|
841
|
-
* runs forward from the emitted boundary — not from the last "[" — so
|
|
842
|
-
* brackets INSIDE a streaming marker body (a JSON array or "]"-bearing string
|
|
843
|
-
* in ASK_GUARDIAN_APPROVAL) can neither mask the marker's start nor pass as
|
|
844
|
-
* its terminator.
|
|
845
|
-
*
|
|
846
|
-
* **Markers are stripped, never acted on.** Nothing the model can say
|
|
847
|
-
* minimizes the room any more: that is decided by whether a ui tool ran (see
|
|
848
|
-
* the `tool_use_start` handler), so the reveal cannot depend on the model
|
|
849
|
-
* remembering a token, and a reply whose content happens to contain "[-1]" (an
|
|
850
|
-
* array literal, a temperature) cannot move the room either. No prompt teaches
|
|
851
|
-
* a marker, so this stripping is defense against a model that emits one
|
|
852
|
-
* regardless: an unspoken, unpersisted "[-1]" is the correct handling of a
|
|
853
|
-
* token that now means nothing.
|
|
854
|
-
*/
|
|
855
|
-
function createControlMarkerHoldback(
|
|
856
|
-
turn: ActiveAssistantTurn,
|
|
857
|
-
emit: (chunk: string) => void,
|
|
858
|
-
): (raw: string, opts?: { force?: boolean }) => void {
|
|
859
|
-
let emitted = 0;
|
|
860
|
-
return (raw, opts) => {
|
|
861
|
-
let safeEnd = raw.length;
|
|
862
|
-
if (opts?.force !== true) {
|
|
863
|
-
for (
|
|
864
|
-
let i = raw.indexOf("[", emitted);
|
|
865
|
-
i !== -1;
|
|
866
|
-
i = raw.indexOf("[", i + 1)
|
|
867
|
-
) {
|
|
868
|
-
if (isIncompleteControlMarkerTail(raw.slice(i))) {
|
|
869
|
-
safeEnd = i;
|
|
870
|
-
break;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
if (safeEnd > emitted) {
|
|
875
|
-
emit(stripInternalSpeechMarkers(raw.slice(emitted, safeEnd)));
|
|
876
|
-
emitted = safeEnd;
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
|
|
881
764
|
// Base control prompt for every live-voice turn. Opens with the spoken-reply
|
|
882
765
|
// rules shared with the phone path (spoken-reply-rules.ts): this is the only
|
|
883
766
|
// place the model learns that its text is spoken, since the system prompt has
|
|
@@ -885,7 +768,7 @@ function createControlMarkerHoldback(
|
|
|
885
768
|
// message. When a turn starts from a barge-in, the interruption merge note is
|
|
886
769
|
// appended to it (see buildInterruptionMergeNote) so the model reconciles the
|
|
887
770
|
// interrupted request with the new utterance.
|
|
888
|
-
const LIVE_VOICE_CONTROL_PROMPT_BASE = `You are speaking in a local live voice session. ${SPOKEN_REPLY_LENGTH_RULE} ${SPOKEN_REPLY_PLAIN_TEXT_RULE} Speech is the main channel: say the answer, and do not narrate a surface instead of answering. You can also put something on screen when it genuinely helps (a form, a list to pick from, a progress card for long work); the call overlay minimizes by itself once you finish speaking, so the user sees it without doing anything. Never tell the user you cannot show them something. Reply in the language the caller is speaking; if they switch languages, switch with them. `;
|
|
771
|
+
const LIVE_VOICE_CONTROL_PROMPT_BASE = `You are speaking in a local live voice session. ${SPOKEN_REPLY_LENGTH_RULE} ${SPOKEN_REPLY_PLAIN_TEXT_RULE} Speech is the main channel: say the answer, and do not narrate a surface instead of answering. You can also put something on screen when it genuinely helps (a form, a list to pick from, a progress card for long work, or a card with an image or a link instead of reading an address out); the call overlay minimizes by itself once you finish speaking, so the user sees it without doing anything. Never tell the user you cannot show them something. Reply in the language the caller is speaking; if they switch languages, switch with them. `;
|
|
889
772
|
|
|
890
773
|
// Appended for the legs that can actually put something on screen: the main
|
|
891
774
|
// leg and the escalated leg. The front-door (fast) leg never receives it, for
|
|
@@ -901,7 +784,7 @@ const LIVE_VOICE_CONTROL_PROMPT_BASE = `You are speaking in a local live voice s
|
|
|
901
784
|
// model can get right, which is speaking as though the thing is already in
|
|
902
785
|
// front of the user, because by the time it stops talking it is.
|
|
903
786
|
const LIVE_VOICE_SCREEN_REVEAL_TEACHING =
|
|
904
|
-
"When the complete answer would run past a few sentences, say the short version out loud and put the detail on screen instead of reading it out. 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.
|
|
787
|
+
"When the complete answer would run past a few sentences, say the short version out loud and put the detail on screen instead of reading it out. 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. ";
|
|
905
788
|
|
|
906
789
|
// The setup-flow case, spelled out because it is the one the model gets wrong
|
|
907
790
|
// on its own: connecting an account reads as something a call cannot do, so it
|
|
@@ -982,12 +865,14 @@ function buildLiveDeliveryNote(request: string, answer: string): string {
|
|
|
982
865
|
function buildVoiceControlPrompt(
|
|
983
866
|
turn: ActiveAssistantTurn,
|
|
984
867
|
leg: { frontDoor?: boolean },
|
|
868
|
+
sessionControls: readonly LiveVoiceSessionControl[],
|
|
985
869
|
): string {
|
|
986
870
|
let prompt =
|
|
987
871
|
LIVE_VOICE_CONTROL_PROMPT_BASE +
|
|
988
872
|
(leg.frontDoor === true
|
|
989
873
|
? ""
|
|
990
|
-
: LIVE_VOICE_SCREEN_REVEAL_TEACHING + LIVE_VOICE_SETUP_FLOW_TEACHING)
|
|
874
|
+
: LIVE_VOICE_SCREEN_REVEAL_TEACHING + LIVE_VOICE_SETUP_FLOW_TEACHING) +
|
|
875
|
+
sessionControlTeaching(sessionControls, leg);
|
|
991
876
|
if (turn.language !== undefined) {
|
|
992
877
|
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.`;
|
|
993
878
|
}
|
|
@@ -1247,6 +1132,12 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1247
1132
|
// take a turn without the microphone. Governs one thing only: whether a
|
|
1248
1133
|
// missing speech-to-text leg is fatal to startup (see start()).
|
|
1249
1134
|
private readonly textInput: boolean;
|
|
1135
|
+
// The session controls the client declared it can carry out; the only ones
|
|
1136
|
+
// the model is taught and the only ones a reply's marker can trigger.
|
|
1137
|
+
private readonly sessionControls: readonly LiveVoiceSessionControl[];
|
|
1138
|
+
// How often progress updates are spoken, as the user last asked out loud.
|
|
1139
|
+
// Session-scoped: it applies from the next turn to the end of the call.
|
|
1140
|
+
private progressCadence: "fewer" | "normal" = "normal";
|
|
1250
1141
|
// Whether this session's speech-to-text leg came up. False only when the
|
|
1251
1142
|
// preflight found it missing and `textInput` let the session open anyway, in
|
|
1252
1143
|
// which case nothing arms a transcriber and typed turns are the only input.
|
|
@@ -1289,22 +1180,17 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1289
1180
|
// assistant turn is audibly speaking: above-gate speech-chunk duration
|
|
1290
1181
|
// accumulates until it reaches bargeInMinSpeechMs, then the deferred
|
|
1291
1182
|
// speech_started + barge-in fire (at most once per onset). Brief sub-threshold
|
|
1292
|
-
// gaps are tolerated (see
|
|
1183
|
+
// gaps are tolerated (see barge-in-guard.ts); the run resets on a
|
|
1293
1184
|
// single longer continuous silence, or once cumulative tolerated silence
|
|
1294
|
-
// exceeds the duty-cycle ceiling (see
|
|
1185
|
+
// exceeds the duty-cycle ceiling (see barge-in-guard.ts).
|
|
1295
1186
|
// The detector's utterance end discards the guard.
|
|
1296
1187
|
private pendingBargeIn: {
|
|
1297
1188
|
// Null when guarding only the post-tts_done drain window (the turn is
|
|
1298
1189
|
// already finalized but the client is still playing its tail).
|
|
1299
1190
|
turn: ActiveAssistantTurn | null;
|
|
1300
|
-
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
silenceMs: number;
|
|
1304
|
-
// Cumulative sub-threshold time over the whole run (not reset by speech
|
|
1305
|
-
// chunks); resets speechMs once it exceeds the duty-cycle ceiling so sparse
|
|
1306
|
-
// periodic blips cannot sum into a barge-in.
|
|
1307
|
-
toleratedSilenceMs: number;
|
|
1191
|
+
// The shared sustained-speech accounting (gap tolerance, duty-cycle
|
|
1192
|
+
// ceiling, threshold).
|
|
1193
|
+
guard: BargeInGuard;
|
|
1308
1194
|
} | null = null;
|
|
1309
1195
|
// Estimated wall-clock ms until the client finishes draining the
|
|
1310
1196
|
// assistant audio sent so far. The server clears the turn right after
|
|
@@ -1400,9 +1286,9 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1400
1286
|
// never emits an answer.
|
|
1401
1287
|
private readonly progressNarrator: VoiceProgressNarrator | null;
|
|
1402
1288
|
// Complete front-model tunables: the constructor schema-parses the partial
|
|
1403
|
-
// option once, so every field carries its `
|
|
1289
|
+
// option once, so every field carries its `voice.frontModel` schema
|
|
1404
1290
|
// default when unset.
|
|
1405
|
-
private readonly frontModelConfig:
|
|
1291
|
+
private readonly frontModelConfig: VoiceFrontModelConfig;
|
|
1406
1292
|
// Complete Flux tunables (the constructor schema-parses the partial option
|
|
1407
1293
|
// once, so every field carries its `liveVoice.flux` schema default).
|
|
1408
1294
|
private readonly fluxConfig: LiveVoiceFluxConfig;
|
|
@@ -1502,7 +1388,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1502
1388
|
options.echoDrainSlackMs ?? DEFAULT_ECHO_DRAIN_SLACK_MS;
|
|
1503
1389
|
this.finalizeGraceMs = options.finalizeGraceMs ?? FINALIZE_GRACE_MS;
|
|
1504
1390
|
this.progressNarrator = options.progressNarrator ?? null;
|
|
1505
|
-
this.frontModelConfig =
|
|
1391
|
+
this.frontModelConfig = VoiceFrontModelConfigSchema.parse(
|
|
1506
1392
|
options.frontModelConfig ?? {},
|
|
1507
1393
|
);
|
|
1508
1394
|
this.fluxConfig = LiveVoiceFluxConfigSchema.parse(options.fluxConfig ?? {});
|
|
@@ -1526,6 +1412,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1526
1412
|
2 *
|
|
1527
1413
|
SERVER_VAD_PENDING_AUDIO_MAX_SECONDS;
|
|
1528
1414
|
this.textInput = context.startFrame.textInput === true;
|
|
1415
|
+
this.sessionControls = context.startFrame.sessionControls ?? [];
|
|
1529
1416
|
}
|
|
1530
1417
|
|
|
1531
1418
|
get finalTranscriptText(): string {
|
|
@@ -2488,7 +2375,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2488
2375
|
|
|
2489
2376
|
if (this.echoWindowTotalAudioMs === 0) {
|
|
2490
2377
|
this.echoWindowGuardCarryover =
|
|
2491
|
-
this.pendingBargeIn !== null && this.pendingBargeIn.speechMs > 0;
|
|
2378
|
+
this.pendingBargeIn !== null && this.pendingBargeIn.guard.speechMs > 0;
|
|
2492
2379
|
} else if (this.pendingBargeIn === null) {
|
|
2493
2380
|
this.echoWindowGuardCarryover = false;
|
|
2494
2381
|
}
|
|
@@ -2533,7 +2420,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2533
2420
|
);
|
|
2534
2421
|
if (meanAmplitude > speechThreshold) {
|
|
2535
2422
|
const guardHasSpeech =
|
|
2536
|
-
this.pendingBargeIn !== null && this.pendingBargeIn.speechMs > 0;
|
|
2423
|
+
this.pendingBargeIn !== null && this.pendingBargeIn.guard.speechMs > 0;
|
|
2537
2424
|
if (!guardHasSpeech && this.echoMatchesAssistant(chunk)) {
|
|
2538
2425
|
this.updateEchoEnergy(meanAmplitude, chunkMs);
|
|
2539
2426
|
return [{ chunk, classification: "echo" }];
|
|
@@ -2831,9 +2718,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2831
2718
|
// accumulates (trackBargeInGuard), so no speech is lost either way.
|
|
2832
2719
|
this.pendingBargeIn = {
|
|
2833
2720
|
turn: bargeableTurn,
|
|
2834
|
-
|
|
2835
|
-
silenceMs: 0,
|
|
2836
|
-
toleratedSilenceMs: 0,
|
|
2721
|
+
guard: createBargeInGuard(this.bargeInMinSpeechMs),
|
|
2837
2722
|
};
|
|
2838
2723
|
return;
|
|
2839
2724
|
}
|
|
@@ -2857,71 +2742,42 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2857
2742
|
|
|
2858
2743
|
// Advance the sustained-speech barge-in guard by one server-VAD chunk.
|
|
2859
2744
|
// Speech accumulates toward bargeInMinSpeechMs, short true-silence gaps are
|
|
2860
|
-
// tolerated, and classified playback echo resets the run immediately
|
|
2861
|
-
//
|
|
2745
|
+
// tolerated, and classified playback echo resets the run immediately (the
|
|
2746
|
+
// shared guard owns that accounting). A reset also drops the echo state a
|
|
2747
|
+
// run carried over, so the classifier re-learns the playback level.
|
|
2862
2748
|
private trackBargeInGuard(
|
|
2863
2749
|
classification: VadEnergyClassification,
|
|
2864
2750
|
chunk: Buffer,
|
|
2865
2751
|
): void {
|
|
2866
|
-
const
|
|
2867
|
-
if (!
|
|
2752
|
+
const pending = this.pendingBargeIn;
|
|
2753
|
+
if (!pending) {
|
|
2868
2754
|
return;
|
|
2869
2755
|
}
|
|
2870
2756
|
const chunkMs = pcm16DurationMs(
|
|
2871
2757
|
chunk.byteLength,
|
|
2872
2758
|
this.context.startFrame.audio.sampleRate,
|
|
2873
2759
|
);
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
guard.toleratedSilenceMs += chunkMs;
|
|
2881
|
-
// Strictly greater on the per-gap check: a gap of exactly
|
|
2882
|
-
// BARGE_IN_GAP_TOLERANCE_MS is still tolerated. The web client batches PCM
|
|
2883
|
-
// into 50 ms frames, so a run of ducked frames lands on the boundary
|
|
2884
|
-
// exactly (e.g. four frames = 200 ms). The run also resets once its total
|
|
2885
|
-
// tolerated silence outweighs the speech by the duty-cycle ceiling, so
|
|
2886
|
-
// sparse periodic blips can never sum to the guard.
|
|
2887
|
-
if (
|
|
2888
|
-
guard.silenceMs > BARGE_IN_GAP_TOLERANCE_MS ||
|
|
2889
|
-
guard.toleratedSilenceMs >
|
|
2890
|
-
this.bargeInMinSpeechMs * BARGE_IN_MAX_TOLERATED_SILENCE_RATIO
|
|
2891
|
-
) {
|
|
2892
|
-
this.resetBargeInGuardRun();
|
|
2760
|
+
const step = pending.guard.track(classification, chunkMs);
|
|
2761
|
+
if (step === "reset") {
|
|
2762
|
+
if (this.echoWindowGuardCarryover) {
|
|
2763
|
+
this.echoWindowGuardCarryover = false;
|
|
2764
|
+
this.echoEnergyEma = 0;
|
|
2765
|
+
this.echoProbeChunks = [];
|
|
2893
2766
|
}
|
|
2894
2767
|
return;
|
|
2895
2768
|
}
|
|
2896
|
-
|
|
2897
|
-
guard.speechMs += chunkMs;
|
|
2898
|
-
if (guard.speechMs < this.bargeInMinSpeechMs) {
|
|
2769
|
+
if (step !== "fired") {
|
|
2899
2770
|
return;
|
|
2900
2771
|
}
|
|
2901
2772
|
this.pendingBargeIn = null;
|
|
2902
2773
|
this.assistantPlaybackTailUntilMs = 0;
|
|
2903
2774
|
this.sendSpeechStarted();
|
|
2904
|
-
const { turn } =
|
|
2775
|
+
const { turn } = pending;
|
|
2905
2776
|
if (turn && turn === this.activeAssistantTurn && !turn.finalized) {
|
|
2906
2777
|
this.bargeIn(turn);
|
|
2907
2778
|
}
|
|
2908
2779
|
}
|
|
2909
2780
|
|
|
2910
|
-
private resetBargeInGuardRun(): void {
|
|
2911
|
-
const guard = this.pendingBargeIn;
|
|
2912
|
-
if (!guard) {
|
|
2913
|
-
return;
|
|
2914
|
-
}
|
|
2915
|
-
guard.speechMs = 0;
|
|
2916
|
-
guard.silenceMs = 0;
|
|
2917
|
-
guard.toleratedSilenceMs = 0;
|
|
2918
|
-
if (this.echoWindowGuardCarryover) {
|
|
2919
|
-
this.echoWindowGuardCarryover = false;
|
|
2920
|
-
this.echoEnergyEma = 0;
|
|
2921
|
-
this.echoProbeChunks = [];
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
2781
|
private bargeIn(turn: ActiveAssistantTurn): void {
|
|
2926
2782
|
// Abort synchronously so no tts_audio frame can follow turn_cancelled,
|
|
2927
2783
|
// and settle the cancelled turn's metrics so the next utterance's marks
|
|
@@ -3561,11 +3417,15 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
3561
3417
|
// down. One line, not narration: the turn is not working, it is waiting,
|
|
3562
3418
|
// and it says which, in the turn's spoken language, like every other
|
|
3563
3419
|
// filler phrase.
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3420
|
+
if (
|
|
3421
|
+
this.enqueueFillerPhrase(
|
|
3422
|
+
turn,
|
|
3423
|
+
approvalPendingPhraseFor(turn.language),
|
|
3424
|
+
this.fixedPhraseLanguage(turn, APPROVAL_PENDING_PHRASE_BY_LANGUAGE),
|
|
3425
|
+
)
|
|
3426
|
+
) {
|
|
3427
|
+
turn.progress.noteFloorHolder();
|
|
3428
|
+
}
|
|
3569
3429
|
}
|
|
3570
3430
|
|
|
3571
3431
|
/** Clear the wait once a decision lands, so the turn narrates normally again. */
|
|
@@ -4172,13 +4032,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
4172
4032
|
if (!alreadyReleased) {
|
|
4173
4033
|
void this.releaseUtterance();
|
|
4174
4034
|
}
|
|
4175
|
-
|
|
4176
|
-
this.frontModelConfig.progress.enabled &&
|
|
4177
|
-
this.streamTtsAudio &&
|
|
4178
|
-
this.progressNarrator
|
|
4179
|
-
) {
|
|
4180
|
-
this.armProgressIdleTimer(turn);
|
|
4181
|
-
}
|
|
4035
|
+
turn.progress.arm();
|
|
4182
4036
|
return true;
|
|
4183
4037
|
}
|
|
4184
4038
|
|
|
@@ -5169,23 +5023,46 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5169
5023
|
abortController,
|
|
5170
5024
|
handle: null,
|
|
5171
5025
|
launchedAtMs: Date.now(),
|
|
5172
|
-
progress: {
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
//
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5026
|
+
progress: createProgressCadence({
|
|
5027
|
+
config: progressConfigForCadence(
|
|
5028
|
+
this.frontModelConfig.progress,
|
|
5029
|
+
this.progressCadence,
|
|
5030
|
+
),
|
|
5031
|
+
// Without TTS there is nothing to speak (the idle trigger's static
|
|
5032
|
+
// fallback still needs a generation attempt to fall back from).
|
|
5033
|
+
narrator: this.streamTtsAudio ? this.progressNarrator : null,
|
|
5034
|
+
nextFallbackPhraseIndex: () => this.progressPhraseCounter++,
|
|
5035
|
+
host: {
|
|
5036
|
+
turnId,
|
|
5037
|
+
launchedAtMs: Date.now(),
|
|
5038
|
+
signal: abortController.signal,
|
|
5039
|
+
// Narration may speak whenever the live turn is audibly silent and
|
|
5040
|
+
// has not completed. Escalated turns qualify too: the bridge phrase
|
|
5041
|
+
// holds the floor only while its audio plays (covered by the
|
|
5042
|
+
// audible-idle check), and the strong leg's tool loops are the
|
|
5043
|
+
// longest dead air in the system. Nothing is in flight while a
|
|
5044
|
+
// decision is pending, so every phrase narration has would be a lie
|
|
5045
|
+
// about who the call is waiting on: the turn says so once, when it
|
|
5046
|
+
// starts waiting, and is quiet after that.
|
|
5047
|
+
canNarrate: () =>
|
|
5048
|
+
this.isActiveAssistantTurn(token) &&
|
|
5049
|
+
!activeTurn.assistantCompleted &&
|
|
5050
|
+
activeTurn.pendingApproval === null,
|
|
5051
|
+
isAudioIdle: () => this.turnAudioIdle(activeTurn),
|
|
5052
|
+
playbackTailUntilMs: () => this.assistantPlaybackTailUntilMs,
|
|
5053
|
+
transcriptSoFar: () =>
|
|
5054
|
+
activeTurn.utterance.finalTranscriptSegments.join(" ").trim(),
|
|
5055
|
+
language: () => activeTurn.language,
|
|
5056
|
+
deltaEpoch: () => activeTurn.deltaEpoch,
|
|
5057
|
+
speak: (phrase, language) =>
|
|
5058
|
+
this.enqueueFillerPhrase(activeTurn, phrase, language),
|
|
5059
|
+
onNarrationSpoken: () => this.markProgressSpoken(activeTurn),
|
|
5060
|
+
},
|
|
5061
|
+
}),
|
|
5186
5062
|
assistantCompleted: false,
|
|
5187
5063
|
ttsDone: false,
|
|
5188
5064
|
minimizeRequested: false,
|
|
5065
|
+
sessionControlRequested: null,
|
|
5189
5066
|
activityLabel: "",
|
|
5190
5067
|
publishedApprovalRequestId: null,
|
|
5191
5068
|
pendingApproval: null,
|
|
@@ -5210,7 +5087,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5210
5087
|
continuationDelivery: opts?.continuationDelivery ?? null,
|
|
5211
5088
|
hiddenPrompt: opts?.hiddenPrompt === true,
|
|
5212
5089
|
deltaEpoch: 0,
|
|
5213
|
-
|
|
5090
|
+
frontDoor: null,
|
|
5214
5091
|
ttsBuffer: "",
|
|
5215
5092
|
spokenSegments: 0,
|
|
5216
5093
|
spokenChars: 0,
|
|
@@ -5268,13 +5145,8 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5268
5145
|
// cadence, wherever in the turn it occurs; without TTS or a narrator there
|
|
5269
5146
|
// is nothing to speak (the idle trigger's static fallback still needs a
|
|
5270
5147
|
// generation attempt to fall back from).
|
|
5271
|
-
if (
|
|
5272
|
-
|
|
5273
|
-
this.streamTtsAudio &&
|
|
5274
|
-
this.progressNarrator &&
|
|
5275
|
-
!opts?.speculative
|
|
5276
|
-
) {
|
|
5277
|
-
this.armProgressIdleTimer(activeTurn);
|
|
5148
|
+
if (!opts?.speculative) {
|
|
5149
|
+
activeTurn.progress.arm();
|
|
5278
5150
|
}
|
|
5279
5151
|
|
|
5280
5152
|
// Front-door leg: a fast model fronts every turn (the `voiceFrontDoor`
|
|
@@ -5337,17 +5209,15 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5337
5209
|
}
|
|
5338
5210
|
const { token, utterance, turnId } = activeTurn;
|
|
5339
5211
|
|
|
5340
|
-
// `rawText` accumulates this leg's
|
|
5341
|
-
//
|
|
5342
|
-
//
|
|
5343
|
-
// escalation buffers the post-verdict stream
|
|
5344
|
-
// bridge is complete, then hands off. A default
|
|
5345
|
-
// every delta through the same holdback, so a
|
|
5346
|
-
// the main model is stripped instead of
|
|
5212
|
+
// `rawText` accumulates this leg's spoken stream. A front-door leg runs
|
|
5213
|
+
// through the shared coordinator, which reads its stream through the
|
|
5214
|
+
// verdict machine: released answer text flushes through the shared
|
|
5215
|
+
// marker holdback, while an escalation buffers the post-verdict stream
|
|
5216
|
+
// until the capped bridge is complete, then hands off. A default or
|
|
5217
|
+
// escalated leg flushes every delta through the same holdback, so a
|
|
5218
|
+
// stray control marker from the main model is stripped instead of
|
|
5219
|
+
// spoken.
|
|
5347
5220
|
let rawText = "";
|
|
5348
|
-
let frontDoorStage: "deciding" | "answer" | "bridging" | "handedOff" =
|
|
5349
|
-
"deciding";
|
|
5350
|
-
let bridgeRaw = "";
|
|
5351
5221
|
|
|
5352
5222
|
const emitLegText = (chunk: string): void => {
|
|
5353
5223
|
if (chunk.length === 0) {
|
|
@@ -5370,20 +5240,54 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5370
5240
|
this.bufferAssistantTextForTts(token, chunk);
|
|
5371
5241
|
};
|
|
5372
5242
|
|
|
5373
|
-
const flushLegText = createControlMarkerHoldback(
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5243
|
+
const flushLegText = createControlMarkerHoldback(emitLegText);
|
|
5244
|
+
|
|
5245
|
+
const coordinator = leg.frontDoor
|
|
5246
|
+
? createFrontDoorLegCoordinator({
|
|
5247
|
+
// The hold branch is only classifiable while the leg is
|
|
5248
|
+
// speculative: its decision rule is the only one that teaches the
|
|
5249
|
+
// hold token.
|
|
5250
|
+
holdEnabled:
|
|
5251
|
+
activeTurn.speculativePending && activeTurn.speculativeHoldAllowed,
|
|
5252
|
+
host: {
|
|
5253
|
+
isLive: () => !activeTurn.finalized,
|
|
5254
|
+
language: () => activeTurn.language,
|
|
5255
|
+
progress: {
|
|
5256
|
+
// The verdict deadline goes with the narration timer: the
|
|
5257
|
+
// hand-off settles the verdict.
|
|
5258
|
+
clear: () => this.clearFillerTimers(activeTurn),
|
|
5259
|
+
noteFloorHolder: () => activeTurn.progress.noteFloorHolder(),
|
|
5260
|
+
arm: () => activeTurn.progress.arm(),
|
|
5261
|
+
},
|
|
5262
|
+
// Hold discards a speculative turn (mid-thought pause, keep
|
|
5263
|
+
// listening); escalate and answer both commit it: utterance
|
|
5264
|
+
// release, thinking frame, and timers all happen inside
|
|
5265
|
+
// commitSpeculativeTurn.
|
|
5266
|
+
onHold: () => {
|
|
5267
|
+
void this.holdSpeculativeTurn(activeTurn);
|
|
5268
|
+
},
|
|
5269
|
+
commit: () =>
|
|
5270
|
+
!activeTurn.speculativePending ||
|
|
5271
|
+
this.commitSpeculativeTurn(activeTurn),
|
|
5272
|
+
onAnswerText: (text) => {
|
|
5273
|
+
rawText += text;
|
|
5274
|
+
flushLegText(rawText);
|
|
5275
|
+
},
|
|
5276
|
+
abortLeg: () => {
|
|
5277
|
+
activeTurn.handle?.abort();
|
|
5278
|
+
activeTurn.handle = null;
|
|
5279
|
+
},
|
|
5280
|
+
speakBridge: (bridge) =>
|
|
5281
|
+
this.speakEscalationBridge(activeTurn, bridge),
|
|
5282
|
+
startEscalatedLeg: (escalated) => {
|
|
5283
|
+
void this.startAssistantLeg(activeTurn, escalated);
|
|
5284
|
+
},
|
|
5285
|
+
},
|
|
5286
|
+
})
|
|
5287
|
+
: null;
|
|
5288
|
+
if (coordinator !== null) {
|
|
5289
|
+
activeTurn.frontDoor = coordinator;
|
|
5290
|
+
}
|
|
5387
5291
|
|
|
5388
5292
|
try {
|
|
5389
5293
|
// Latched before the await, not after: this flag only decides whether the
|
|
@@ -5427,9 +5331,15 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5427
5331
|
? { entry: this.context.startFrame.entry }
|
|
5428
5332
|
: {}),
|
|
5429
5333
|
},
|
|
5430
|
-
voiceControlPrompt: buildVoiceControlPrompt(
|
|
5431
|
-
|
|
5432
|
-
|
|
5334
|
+
voiceControlPrompt: buildVoiceControlPrompt(
|
|
5335
|
+
activeTurn,
|
|
5336
|
+
{
|
|
5337
|
+
...(leg.frontDoor !== undefined
|
|
5338
|
+
? { frontDoor: leg.frontDoor }
|
|
5339
|
+
: {}),
|
|
5340
|
+
},
|
|
5341
|
+
this.sessionControls,
|
|
5342
|
+
),
|
|
5433
5343
|
onApprovalPending: (requestId) => {
|
|
5434
5344
|
this.revealRoomForPendingApproval(activeTurn, requestId);
|
|
5435
5345
|
},
|
|
@@ -5470,53 +5380,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5470
5380
|
if (!this.isForwardingAssistantText(token)) {
|
|
5471
5381
|
return;
|
|
5472
5382
|
}
|
|
5473
|
-
if (
|
|
5474
|
-
rawText += msg.text;
|
|
5475
|
-
if (frontDoorStage === "handedOff") {
|
|
5476
|
-
return;
|
|
5477
|
-
}
|
|
5478
|
-
if (frontDoorStage === "bridging") {
|
|
5479
|
-
bridgeRaw += msg.text;
|
|
5480
|
-
maybeHandOffBridge();
|
|
5481
|
-
return;
|
|
5482
|
-
}
|
|
5383
|
+
if (coordinator !== null) {
|
|
5483
5384
|
// Verdict-first: the leg's leading tokens decide the turn's
|
|
5484
|
-
// fate
|
|
5485
|
-
//
|
|
5486
|
-
//
|
|
5487
|
-
//
|
|
5488
|
-
|
|
5489
|
-
// rule is the only one that teaches the hold token).
|
|
5490
|
-
if (frontDoorStage === "deciding") {
|
|
5491
|
-
const verdict = classifyFrontDoorLeading(
|
|
5492
|
-
rawText.trimStart(),
|
|
5493
|
-
activeTurn.speculativePending &&
|
|
5494
|
-
activeTurn.speculativeHoldAllowed,
|
|
5495
|
-
);
|
|
5496
|
-
if (verdict === "pending") {
|
|
5497
|
-
return;
|
|
5498
|
-
}
|
|
5499
|
-
if (verdict === "hold") {
|
|
5500
|
-
void this.holdSpeculativeTurn(activeTurn);
|
|
5501
|
-
return;
|
|
5502
|
-
}
|
|
5503
|
-
if (
|
|
5504
|
-
activeTurn.speculativePending &&
|
|
5505
|
-
!this.commitSpeculativeTurn(activeTurn)
|
|
5506
|
-
) {
|
|
5507
|
-
return;
|
|
5508
|
-
}
|
|
5509
|
-
if (verdict === "escalate") {
|
|
5510
|
-
frontDoorStage = "bridging";
|
|
5511
|
-
bridgeRaw = rawText
|
|
5512
|
-
.trimStart()
|
|
5513
|
-
.slice(ESCALATE_VERDICT_TOKEN.length);
|
|
5514
|
-
maybeHandOffBridge();
|
|
5515
|
-
return;
|
|
5516
|
-
}
|
|
5517
|
-
frontDoorStage = "answer";
|
|
5518
|
-
}
|
|
5519
|
-
flushLegText(rawText);
|
|
5385
|
+
// fate, and the coordinator acts on them. The bridge hands
|
|
5386
|
+
// off in one piece once the machine caps it, so the audio,
|
|
5387
|
+
// the persisted row, and the phrase quoted to the escalated
|
|
5388
|
+
// leg are all the same text.
|
|
5389
|
+
coordinator.push(msg.text);
|
|
5520
5390
|
return;
|
|
5521
5391
|
}
|
|
5522
5392
|
// Defensive: speculative legs are always front-door today, but a
|
|
@@ -5552,27 +5422,20 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5552
5422
|
if (current.speculativePending) {
|
|
5553
5423
|
this.commitSpeculativeTurn(current);
|
|
5554
5424
|
}
|
|
5555
|
-
// A front-door leg that stopped mid-bridge
|
|
5556
|
-
//
|
|
5557
|
-
//
|
|
5558
|
-
//
|
|
5559
|
-
//
|
|
5560
|
-
//
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
return;
|
|
5570
|
-
}
|
|
5571
|
-
// A front-door leg that handed off is finished; the escalated leg
|
|
5572
|
-
// drives completion. The front-door leg's own trailing completion
|
|
5573
|
-
// (including the generation_cancelled from its abort) is a no-op.
|
|
5574
|
-
if (leg.frontDoor && current.escalationHandedOff) {
|
|
5575
|
-
return;
|
|
5425
|
+
// A front-door leg that stopped mid-bridge hands off now with
|
|
5426
|
+
// whatever arrived. A cancellation mid-bridge falls through to
|
|
5427
|
+
// normal cancelled finalization instead: a dead turn must not
|
|
5428
|
+
// spawn an escalated leg. A front-door leg that handed off is
|
|
5429
|
+
// finished; the escalated leg drives completion, so this leg's
|
|
5430
|
+
// own trailing completion (including the generation_cancelled
|
|
5431
|
+
// from its abort) is a no-op.
|
|
5432
|
+
if (coordinator !== null) {
|
|
5433
|
+
if (msg.type === "message_complete") {
|
|
5434
|
+
coordinator.complete();
|
|
5435
|
+
}
|
|
5436
|
+
if (coordinator.handedOff) {
|
|
5437
|
+
return;
|
|
5438
|
+
}
|
|
5576
5439
|
}
|
|
5577
5440
|
// A held "[…"-tail that never completed a marker is real text —
|
|
5578
5441
|
// force-flush it before assistantCompleted closes the TTS buffer
|
|
@@ -5581,6 +5444,26 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5581
5444
|
if (!leg.frontDoor && msg.type === "message_complete") {
|
|
5582
5445
|
flushLegText(rawText, { force: true });
|
|
5583
5446
|
}
|
|
5447
|
+
// Read off the leg that finished the reply: a front-door answer
|
|
5448
|
+
// or the escalated leg. A handed-off front-door leg returned
|
|
5449
|
+
// above, so its holding phrase can never end a call.
|
|
5450
|
+
if (msg.type === "message_complete") {
|
|
5451
|
+
const request = requestedSessionControl(
|
|
5452
|
+
rawText,
|
|
5453
|
+
this.sessionControls,
|
|
5454
|
+
);
|
|
5455
|
+
if (request?.action === "updates") {
|
|
5456
|
+
// The session's own control: nothing to send, and nothing to
|
|
5457
|
+
// wait for, since it shapes turns that have not started yet.
|
|
5458
|
+
this.progressCadence = request.cadence;
|
|
5459
|
+
log.info(
|
|
5460
|
+
{ turnId, cadence: request.cadence },
|
|
5461
|
+
"Live voice progress cadence changed",
|
|
5462
|
+
);
|
|
5463
|
+
} else {
|
|
5464
|
+
current.sessionControlRequested = request;
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5584
5467
|
current.assistantCompleted = true;
|
|
5585
5468
|
if (msg.type === "generation_cancelled") {
|
|
5586
5469
|
void this.finalizeAssistantTurn(
|
|
@@ -5649,57 +5532,16 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5649
5532
|
toolName,
|
|
5650
5533
|
});
|
|
5651
5534
|
}
|
|
5652
|
-
|
|
5653
|
-
// completion) so a burst of slow tools still trips the ops
|
|
5654
|
-
// trigger while they run.
|
|
5655
|
-
current.progress.ops.push({
|
|
5656
|
-
toolName,
|
|
5657
|
-
...(detail?.toolUseId !== undefined
|
|
5658
|
-
? { toolUseId: detail.toolUseId }
|
|
5659
|
-
: {}),
|
|
5660
|
-
startedAtMs: Date.now(),
|
|
5661
|
-
});
|
|
5662
|
-
current.progress.opsSinceNarration += 1;
|
|
5663
|
-
current.progress.stateEpoch += 1;
|
|
5535
|
+
current.progress.toolStarted(toolName, detail?.toolUseId);
|
|
5664
5536
|
this.refreshActivity(current);
|
|
5665
5537
|
log.debug({ turnId, toolName }, "Live voice turn started tool use");
|
|
5666
|
-
this.maybeNarrateProgress(current, "ops");
|
|
5667
5538
|
},
|
|
5668
5539
|
tool_result: (event) => {
|
|
5669
5540
|
const current = this.activeAssistantTurn;
|
|
5670
5541
|
if (current?.token !== token) {
|
|
5671
5542
|
return;
|
|
5672
5543
|
}
|
|
5673
|
-
|
|
5674
|
-
// incomplete op with the same name (parallel same-name ops
|
|
5675
|
-
// resolve newest-first).
|
|
5676
|
-
const op =
|
|
5677
|
-
(event.toolUseId !== undefined
|
|
5678
|
-
? current.progress.ops.find(
|
|
5679
|
-
(o) => o.toolUseId === event.toolUseId,
|
|
5680
|
-
)
|
|
5681
|
-
: undefined) ??
|
|
5682
|
-
findLastIncompleteOp(current.progress.ops, event.toolName);
|
|
5683
|
-
// A long-running op finishing is the beat the user has been
|
|
5684
|
-
// waiting through: it narrates immediately rather than waiting for
|
|
5685
|
-
// `opsThreshold` more ops, which on a one-slow-tool turn never
|
|
5686
|
-
// arrive. Short ops stay on the ops trigger — narrating every
|
|
5687
|
-
// quick lookup is the chatter this cadence exists to avoid.
|
|
5688
|
-
let trigger: "ops" | "op_complete" = "ops";
|
|
5689
|
-
if (op) {
|
|
5690
|
-
op.completedAtMs = Date.now();
|
|
5691
|
-
if (event.isError !== undefined) {
|
|
5692
|
-
op.isError = event.isError;
|
|
5693
|
-
}
|
|
5694
|
-
op.resultPreview = event.resultPreview;
|
|
5695
|
-
if (
|
|
5696
|
-
op.completedAtMs - op.startedAtMs >=
|
|
5697
|
-
this.frontModelConfig.progress.longOpMs
|
|
5698
|
-
) {
|
|
5699
|
-
trigger = "op_complete";
|
|
5700
|
-
}
|
|
5701
|
-
}
|
|
5702
|
-
current.progress.stateEpoch += 1;
|
|
5544
|
+
current.progress.toolFinished(event);
|
|
5703
5545
|
// Showing a surface implies revealing it: the room is a
|
|
5704
5546
|
// full-screen overlay, so a surface rendered behind it is a
|
|
5705
5547
|
// surface nobody sees.
|
|
@@ -5722,7 +5564,6 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5722
5564
|
}
|
|
5723
5565
|
}
|
|
5724
5566
|
this.refreshActivity(current);
|
|
5725
|
-
this.maybeNarrateProgress(current, trigger);
|
|
5726
5567
|
},
|
|
5727
5568
|
},
|
|
5728
5569
|
onError: (message) => {
|
|
@@ -5781,7 +5622,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5781
5622
|
}
|
|
5782
5623
|
// The front-door leg may have handed off before its handle resolved;
|
|
5783
5624
|
// abort it rather than exposing it as the turn's live handle.
|
|
5784
|
-
if (
|
|
5625
|
+
if (coordinator?.handedOff === true) {
|
|
5785
5626
|
handle.abort();
|
|
5786
5627
|
return true;
|
|
5787
5628
|
}
|
|
@@ -5809,37 +5650,17 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5809
5650
|
}
|
|
5810
5651
|
|
|
5811
5652
|
/**
|
|
5812
|
-
*
|
|
5813
|
-
*
|
|
5814
|
-
*
|
|
5815
|
-
*
|
|
5816
|
-
* starts the escalated leg on the same ActiveAssistantTurn. Idempotent.
|
|
5653
|
+
* Speak the escalation bridge so the strong-model call has no dead air.
|
|
5654
|
+
* The model's own bridge is real assistant speech (captions + TTS); the
|
|
5655
|
+
* canned fallback stays audio-only, matching the persisted-row hygiene (a
|
|
5656
|
+
* deleted row for a bridge the model never produced).
|
|
5817
5657
|
*/
|
|
5818
|
-
private
|
|
5658
|
+
private speakEscalationBridge(
|
|
5819
5659
|
activeTurn: ActiveAssistantTurn,
|
|
5820
|
-
|
|
5660
|
+
bridge: SpokenEscalationBridge,
|
|
5821
5661
|
): void {
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
}
|
|
5825
|
-
activeTurn.escalationHandedOff = true;
|
|
5826
|
-
// The escalation bridge below holds the floor, so pending progress
|
|
5827
|
-
// narration would only stack a second filler on top of it.
|
|
5828
|
-
this.clearFillerTimers(activeTurn);
|
|
5829
|
-
// Abort the front-door leg so a model that keeps generating past the
|
|
5830
|
-
// bridge cap adds no latency before the escalated leg starts.
|
|
5831
|
-
activeTurn.handle?.abort();
|
|
5832
|
-
activeTurn.handle = null;
|
|
5833
|
-
|
|
5834
|
-
// Speak the bridge so the strong-model call has no dead air. The model's
|
|
5835
|
-
// own bridge is real assistant speech (captions + TTS); the canned
|
|
5836
|
-
// fallback stays audio-only, matching the persisted-row hygiene (a
|
|
5837
|
-
// deleted row for a bridge the model never produced).
|
|
5838
|
-
const usesFallbackBridge = cappedBridge.length < MIN_SPOKEN_BRIDGE_CHARS;
|
|
5839
|
-
const spokenBridge = usesFallbackBridge
|
|
5840
|
-
? fallbackEscalationBridgeFor(activeTurn.language)
|
|
5841
|
-
: cappedBridge;
|
|
5842
|
-
if (!usesFallbackBridge) {
|
|
5662
|
+
const { spokenBridge, usesFallback, language } = bridge;
|
|
5663
|
+
if (!usesFallback) {
|
|
5843
5664
|
this.markFirstAssistantDelta(activeTurn.utterance, activeTurn.turnId);
|
|
5844
5665
|
this.markAssistantDelta(activeTurn);
|
|
5845
5666
|
void this.sendFrame(
|
|
@@ -5851,46 +5672,14 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5851
5672
|
// otherwise sit buffered until a sentence boundary and leave the
|
|
5852
5673
|
// caller in silence during the escalated model's call.
|
|
5853
5674
|
this.flushTtsBuffer(activeTurn.token, true);
|
|
5854
|
-
|
|
5855
|
-
// The canned bridge is a fixed localized-table phrase, enqueued
|
|
5856
|
-
// directly (it is already one complete sentence) so the segment can
|
|
5857
|
-
// carry the "en" override when the table lacks the turn's language.
|
|
5858
|
-
const speakable = sanitizeForTts(spokenBridge).trim();
|
|
5859
|
-
if (speakable.length > 0) {
|
|
5860
|
-
this.enqueueTtsSegment(activeTurn.token, speakable, {
|
|
5861
|
-
language: this.fixedPhraseLanguage(
|
|
5862
|
-
activeTurn,
|
|
5863
|
-
FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
|
|
5864
|
-
),
|
|
5865
|
-
});
|
|
5866
|
-
}
|
|
5675
|
+
return;
|
|
5867
5676
|
}
|
|
5868
|
-
|
|
5869
|
-
//
|
|
5870
|
-
//
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
// row).
|
|
5875
|
-
void this.startAssistantLeg(activeTurn, {
|
|
5876
|
-
content: ESCALATION_CONTINUATION_CONTENT,
|
|
5877
|
-
routingLeg: "escalated",
|
|
5878
|
-
spokenEscalationBridge: spokenBridge,
|
|
5879
|
-
});
|
|
5880
|
-
|
|
5881
|
-
// Escalated legs run the slowest work in the system (strong-model
|
|
5882
|
-
// thinking + tool loops), and the bridge only covers the first couple of
|
|
5883
|
-
// seconds of it — exactly the dead air progress narration exists for.
|
|
5884
|
-
// Re-arm the idle narration timer (cleared above with the ack): its
|
|
5885
|
-
// audible-silence gating means nothing speaks until the bridge audio has
|
|
5886
|
-
// fully drained plus a whole idle interval. Acks stay suppressed
|
|
5887
|
-
// post-handoff — the bridge already served that role.
|
|
5888
|
-
if (
|
|
5889
|
-
this.frontModelConfig.progress.enabled &&
|
|
5890
|
-
this.streamTtsAudio &&
|
|
5891
|
-
this.progressNarrator
|
|
5892
|
-
) {
|
|
5893
|
-
this.armProgressIdleTimer(activeTurn);
|
|
5677
|
+
// The canned bridge is a fixed localized-table phrase, enqueued
|
|
5678
|
+
// directly (it is already one complete sentence) so the segment can
|
|
5679
|
+
// carry the "en" override when the table lacks the turn's language.
|
|
5680
|
+
const speakable = sanitizeForTts(spokenBridge).trim();
|
|
5681
|
+
if (speakable.length > 0) {
|
|
5682
|
+
this.enqueueTtsSegment(activeTurn.token, speakable, { language });
|
|
5894
5683
|
}
|
|
5895
5684
|
}
|
|
5896
5685
|
|
|
@@ -5967,61 +5756,6 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
5967
5756
|
turn.deltaEpoch += 1;
|
|
5968
5757
|
}
|
|
5969
5758
|
|
|
5970
|
-
// Arms (or re-arms) the dead-air narration timer. The countdown measures
|
|
5971
|
-
// audible silence — time since the turn's audio last (estimatedly) reached
|
|
5972
|
-
// the user's ears — not time since launch, so it covers mid-turn silences
|
|
5973
|
-
// for the whole turn. On expiry with audio still pending, or with the
|
|
5974
|
-
// silence not yet a full interval old, it re-arms for the remainder; only a
|
|
5975
|
-
// full interval of audible silence reaches the narration gatekeeper. The
|
|
5976
|
-
// interval is a polling cadence, not a speaking cadence: most ticks find
|
|
5977
|
-
// nothing new to report and stay quiet, so what the user hears follows the
|
|
5978
|
-
// turn's tool activity (with `maxSilenceMs` as the heartbeat ceiling).
|
|
5979
|
-
private armProgressIdleTimer(
|
|
5980
|
-
turn: ActiveAssistantTurn,
|
|
5981
|
-
delayMs?: number,
|
|
5982
|
-
): void {
|
|
5983
|
-
const { token } = turn;
|
|
5984
|
-
this.clearProgressIdleTimer(turn);
|
|
5985
|
-
turn.progress.idleTimer = setTimeout(() => {
|
|
5986
|
-
turn.progress.idleTimer = null;
|
|
5987
|
-
if (!this.isActiveAssistantTurn(token) || turn.assistantCompleted) {
|
|
5988
|
-
return;
|
|
5989
|
-
}
|
|
5990
|
-
if (!this.turnAudioIdle(turn)) {
|
|
5991
|
-
// Audio is buffered, queued, or still playing: a fresh silence can
|
|
5992
|
-
// only be a full interval old one interval from now.
|
|
5993
|
-
this.armProgressIdleTimer(turn);
|
|
5994
|
-
return;
|
|
5995
|
-
}
|
|
5996
|
-
const remaining = this.progressIdleDeadlineMs(turn) - Date.now();
|
|
5997
|
-
if (remaining > 0) {
|
|
5998
|
-
this.armProgressIdleTimer(turn, remaining);
|
|
5999
|
-
return;
|
|
6000
|
-
}
|
|
6001
|
-
this.maybeNarrateProgress(turn, "idle");
|
|
6002
|
-
this.armProgressIdleTimer(turn);
|
|
6003
|
-
}, delayMs ?? this.frontModelConfig.progress.idleIntervalMs);
|
|
6004
|
-
}
|
|
6005
|
-
|
|
6006
|
-
// Wall-clock instant the current audible silence turns a full interval old.
|
|
6007
|
-
private progressIdleDeadlineMs(turn: ActiveAssistantTurn): number {
|
|
6008
|
-
return (
|
|
6009
|
-
this.progressSilenceSinceMs(turn) +
|
|
6010
|
-
this.frontModelConfig.progress.idleIntervalMs
|
|
6011
|
-
);
|
|
6012
|
-
}
|
|
6013
|
-
|
|
6014
|
-
// When the turn's current audible silence began: the latest of the last
|
|
6015
|
-
// emitted segment, the estimated client playback end, and the last enqueued
|
|
6016
|
-
// filler.
|
|
6017
|
-
private progressSilenceSinceMs(turn: ActiveAssistantTurn): number {
|
|
6018
|
-
return Math.max(
|
|
6019
|
-
turn.progress.lastAudibleAtMs,
|
|
6020
|
-
this.assistantPlaybackTailUntilMs,
|
|
6021
|
-
turn.progress.lastFloorHolderAtMs ?? 0,
|
|
6022
|
-
);
|
|
6023
|
-
}
|
|
6024
|
-
|
|
6025
5759
|
// No assistant audio is pending or (estimatedly) still playing: nothing
|
|
6026
5760
|
// buffered toward the next sentence, every queued TTS segment fully
|
|
6027
5761
|
// emitted, and the client-side playback-tail estimate expired.
|
|
@@ -6033,177 +5767,18 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6033
5767
|
);
|
|
6034
5768
|
}
|
|
6035
5769
|
|
|
6036
|
-
private clearProgressIdleTimer(turn: ActiveAssistantTurn): void {
|
|
6037
|
-
if (turn.progress.idleTimer !== null) {
|
|
6038
|
-
clearTimeout(turn.progress.idleTimer);
|
|
6039
|
-
turn.progress.idleTimer = null;
|
|
6040
|
-
}
|
|
6041
|
-
}
|
|
6042
|
-
|
|
6043
5770
|
// Clears progress narration and verdict timers for events that end the
|
|
6044
5771
|
// current filler lifecycle.
|
|
6045
5772
|
private clearFillerTimers(turn: ActiveAssistantTurn): void {
|
|
6046
|
-
|
|
5773
|
+
turn.progress.clear();
|
|
6047
5774
|
if (turn.verdictDeadlineTimer !== null) {
|
|
6048
5775
|
clearTimeout(turn.verdictDeadlineTimer);
|
|
6049
5776
|
turn.verdictDeadlineTimer = null;
|
|
6050
5777
|
}
|
|
6051
5778
|
}
|
|
6052
5779
|
|
|
6053
|
-
//
|
|
6054
|
-
//
|
|
6055
|
-
// floor only while its audio plays (covered by the audible-idle check),
|
|
6056
|
-
// and the strong leg's tool loops are the longest dead air in the system.
|
|
6057
|
-
private turnCanNarrateProgress(turn: ActiveAssistantTurn): boolean {
|
|
6058
|
-
return (
|
|
6059
|
-
this.isActiveAssistantTurn(turn.token) &&
|
|
6060
|
-
!turn.assistantCompleted &&
|
|
6061
|
-
// Nothing is in flight while a decision is pending, so every phrase
|
|
6062
|
-
// narration has would be a lie about who the call is waiting on. The
|
|
6063
|
-
// turn says so once, when it starts waiting, and is quiet after that.
|
|
6064
|
-
turn.pendingApproval === null &&
|
|
6065
|
-
this.turnAudioIdle(turn)
|
|
6066
|
-
);
|
|
6067
|
-
}
|
|
6068
|
-
|
|
6069
|
-
// The idle tick has something worth saying when the turn's tool activity has
|
|
6070
|
-
// moved since the last narration described it, or when the silence has run
|
|
6071
|
-
// past `maxSilenceMs` — the heartbeat ceiling that proves the assistant is
|
|
6072
|
-
// still alive on a turn with no observable activity at all. Every other tick
|
|
6073
|
-
// stays quiet, so the cadence follows the work rather than the clock.
|
|
6074
|
-
private progressIdleHasSomethingToSay(turn: ActiveAssistantTurn): boolean {
|
|
6075
|
-
const { progress } = turn;
|
|
6076
|
-
if (progress.stateEpoch !== progress.narratedEpoch) {
|
|
6077
|
-
return true;
|
|
6078
|
-
}
|
|
6079
|
-
const silentForMs = Date.now() - this.progressSilenceSinceMs(turn);
|
|
6080
|
-
if (silentForMs >= this.frontModelConfig.progress.maxSilenceMs) {
|
|
6081
|
-
return true;
|
|
6082
|
-
}
|
|
6083
|
-
log.debug(
|
|
6084
|
-
{ turnId: turn.turnId, silentForMs },
|
|
6085
|
-
"Live voice progress narration held — nothing new since the last update",
|
|
6086
|
-
);
|
|
6087
|
-
return false;
|
|
6088
|
-
}
|
|
6089
|
-
|
|
6090
|
-
// Gatekeeper for progress narration. It speaks only while the turn is
|
|
6091
|
-
// audibly silent, with one generation at a time and the configured spacing.
|
|
6092
|
-
private maybeNarrateProgress(
|
|
6093
|
-
turn: ActiveAssistantTurn,
|
|
6094
|
-
trigger: "ops" | "idle" | "op_complete",
|
|
6095
|
-
): void {
|
|
6096
|
-
const cfg = this.frontModelConfig.progress;
|
|
6097
|
-
const { progress } = turn;
|
|
6098
|
-
const progressNarrator = this.progressNarrator;
|
|
6099
|
-
if (
|
|
6100
|
-
!cfg.enabled ||
|
|
6101
|
-
!this.streamTtsAudio ||
|
|
6102
|
-
!progressNarrator ||
|
|
6103
|
-
!this.turnCanNarrateProgress(turn) ||
|
|
6104
|
-
progress.narrationInFlight ||
|
|
6105
|
-
(progress.lastFloorHolderAtMs !== null &&
|
|
6106
|
-
Date.now() - progress.lastFloorHolderAtMs < cfg.minGapMs) ||
|
|
6107
|
-
(trigger === "ops" && progress.opsSinceNarration < cfg.opsThreshold) ||
|
|
6108
|
-
(trigger === "idle" && !this.progressIdleHasSomethingToSay(turn))
|
|
6109
|
-
) {
|
|
6110
|
-
return;
|
|
6111
|
-
}
|
|
6112
|
-
void this.speakProgressUpdate(turn, progressNarrator, trigger);
|
|
6113
|
-
}
|
|
6114
|
-
|
|
6115
|
-
// Generate and speak one audio-only progress narration. On a null result,
|
|
6116
|
-
// idle narration falls back to a static phrase while activity-triggered
|
|
6117
|
-
// narration stays silent.
|
|
6118
|
-
private async speakProgressUpdate(
|
|
6119
|
-
turn: ActiveAssistantTurn,
|
|
6120
|
-
progressNarrator: VoiceProgressNarrator,
|
|
6121
|
-
trigger: "ops" | "idle" | "op_complete",
|
|
6122
|
-
): Promise<void> {
|
|
6123
|
-
const { progress } = turn;
|
|
6124
|
-
progress.narrationInFlight = true;
|
|
6125
|
-
// Any delta that lands while generation is in flight makes the text stale.
|
|
6126
|
-
const deltaEpochAtLaunch = turn.deltaEpoch;
|
|
6127
|
-
// The activity this update describes. Tool events that land mid-generation
|
|
6128
|
-
// are news the generated text cannot carry, so they must leave the idle
|
|
6129
|
-
// trigger armed rather than count as already narrated.
|
|
6130
|
-
const stateEpochAtLaunch = progress.stateEpoch;
|
|
6131
|
-
try {
|
|
6132
|
-
const now = Date.now();
|
|
6133
|
-
const currentOp = findLastIncompleteOp(progress.ops);
|
|
6134
|
-
const input: VoiceProgressTextInput = {
|
|
6135
|
-
transcriptSoFar: turn.utterance.finalTranscriptSegments
|
|
6136
|
-
.join(" ")
|
|
6137
|
-
.trim(),
|
|
6138
|
-
completedOps: progress.ops
|
|
6139
|
-
.filter(
|
|
6140
|
-
(op): op is TurnProgressOp & { completedAtMs: number } =>
|
|
6141
|
-
op.completedAtMs !== undefined,
|
|
6142
|
-
)
|
|
6143
|
-
// `ops` is in start order; the narrator wants completion
|
|
6144
|
-
// order, which differs when parallel tools finish out of order.
|
|
6145
|
-
.sort((a, b) => a.completedAtMs - b.completedAtMs)
|
|
6146
|
-
.map((op) => ({
|
|
6147
|
-
toolName: op.toolName,
|
|
6148
|
-
...(op.isError !== undefined ? { isError: op.isError } : {}),
|
|
6149
|
-
...(op.resultPreview !== undefined
|
|
6150
|
-
? { resultPreview: op.resultPreview }
|
|
6151
|
-
: {}),
|
|
6152
|
-
})),
|
|
6153
|
-
currentOp: currentOp
|
|
6154
|
-
? {
|
|
6155
|
-
toolName: currentOp.toolName,
|
|
6156
|
-
elapsedMs: now - currentOp.startedAtMs,
|
|
6157
|
-
}
|
|
6158
|
-
: null,
|
|
6159
|
-
turnElapsedMs: now - turn.launchedAtMs,
|
|
6160
|
-
updateIndex: progress.updatesSpoken + 1,
|
|
6161
|
-
...(turn.language !== undefined ? { languageHint: turn.language } : {}),
|
|
6162
|
-
};
|
|
6163
|
-
const generated = await progressNarrator
|
|
6164
|
-
.generateProgressText(input, turn.abortController.signal)
|
|
6165
|
-
// The narrator contract never rejects. Keep test stubs fail-soft too.
|
|
6166
|
-
.catch(() => null);
|
|
6167
|
-
// Re-check liveness and staleness after the provider call.
|
|
6168
|
-
if (
|
|
6169
|
-
!this.turnCanNarrateProgress(turn) ||
|
|
6170
|
-
turn.deltaEpoch !== deltaEpochAtLaunch ||
|
|
6171
|
-
(progress.lastFloorHolderAtMs !== null &&
|
|
6172
|
-
Date.now() - progress.lastFloorHolderAtMs <
|
|
6173
|
-
this.frontModelConfig.progress.minGapMs)
|
|
6174
|
-
) {
|
|
6175
|
-
return;
|
|
6176
|
-
}
|
|
6177
|
-
let raw = generated;
|
|
6178
|
-
// Generated text is in the turn's language; only the static
|
|
6179
|
-
// fallback comes from a localized table and may need the "en" override.
|
|
6180
|
-
let fillerLanguage: string | undefined;
|
|
6181
|
-
if (raw === null) {
|
|
6182
|
-
if (trigger !== "idle") {
|
|
6183
|
-
return;
|
|
6184
|
-
}
|
|
6185
|
-
raw = pickProgressPhrase(this.progressPhraseCounter++, turn.language);
|
|
6186
|
-
fillerLanguage = this.fixedPhraseLanguage(
|
|
6187
|
-
turn,
|
|
6188
|
-
PROGRESS_FALLBACK_PHRASES_BY_LANGUAGE,
|
|
6189
|
-
);
|
|
6190
|
-
}
|
|
6191
|
-
if (!this.enqueueFillerPhrase(turn, raw, fillerLanguage)) {
|
|
6192
|
-
return;
|
|
6193
|
-
}
|
|
6194
|
-
progress.opsSinceNarration = 0;
|
|
6195
|
-
progress.narratedEpoch = stateEpochAtLaunch;
|
|
6196
|
-
progress.updatesSpoken += 1;
|
|
6197
|
-
// Record only when narration audio actually enqueued.
|
|
6198
|
-
this.markProgressSpoken(turn);
|
|
6199
|
-
// Restart the dead-air countdown from this narration.
|
|
6200
|
-
this.armProgressIdleTimer(turn);
|
|
6201
|
-
} finally {
|
|
6202
|
-
progress.narrationInFlight = false;
|
|
6203
|
-
}
|
|
6204
|
-
}
|
|
6205
|
-
|
|
6206
|
-
// Sanitize and enqueue one progress sentence on the ordered TTS queue.
|
|
5780
|
+
// Sanitize and enqueue one filler sentence (narration, the approval line)
|
|
5781
|
+
// on the ordered TTS queue, audio-only.
|
|
6207
5782
|
private enqueueFillerPhrase(
|
|
6208
5783
|
turn: ActiveAssistantTurn,
|
|
6209
5784
|
raw: string,
|
|
@@ -6217,8 +5792,6 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6217
5792
|
countsAsFirstSegment: false,
|
|
6218
5793
|
...(language !== undefined ? { language } : {}),
|
|
6219
5794
|
});
|
|
6220
|
-
// A spoken filler holds the floor, so narration's minGapMs spaces from it.
|
|
6221
|
-
turn.progress.lastFloorHolderAtMs = Date.now();
|
|
6222
5795
|
return true;
|
|
6223
5796
|
}
|
|
6224
5797
|
|
|
@@ -6231,10 +5804,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6231
5804
|
turn: ActiveAssistantTurn,
|
|
6232
5805
|
table: Readonly<Record<string, unknown>>,
|
|
6233
5806
|
): string | undefined {
|
|
6234
|
-
return turn.language
|
|
6235
|
-
!hasLocalizedEntry(table, turn.language)
|
|
6236
|
-
? "en"
|
|
6237
|
-
: undefined;
|
|
5807
|
+
return fixedPhraseLanguage(table, turn.language);
|
|
6238
5808
|
}
|
|
6239
5809
|
|
|
6240
5810
|
private bufferAssistantTextForTts(token: symbol, text: string): void {
|
|
@@ -6277,7 +5847,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6277
5847
|
log.info(
|
|
6278
5848
|
{
|
|
6279
5849
|
turnId: currentTurn.turnId,
|
|
6280
|
-
escalated: currentTurn.
|
|
5850
|
+
escalated: currentTurn.frontDoor?.handedOff ?? false,
|
|
6281
5851
|
spokenSegments: currentTurn.spokenSegments,
|
|
6282
5852
|
spokenChars: currentTurn.spokenChars,
|
|
6283
5853
|
},
|
|
@@ -6306,6 +5876,34 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6306
5876
|
// leave the last tool it touched on screen through the next silence.
|
|
6307
5877
|
this.publishActivity(currentTurn, "");
|
|
6308
5878
|
|
|
5879
|
+
// Drain-scoped session control, under the same terms as the minimize
|
|
5880
|
+
// below: after the acknowledgement has been spoken, never for a
|
|
5881
|
+
// barged-in turn (talking over "okay, bye" means they are not
|
|
5882
|
+
// leaving), at most once per turn. Ending the call makes revealing
|
|
5883
|
+
// the screen moot, so an end takes the minimize's place.
|
|
5884
|
+
const sessionControl = currentTurn.sessionControlRequested;
|
|
5885
|
+
currentTurn.sessionControlRequested = null;
|
|
5886
|
+
if (
|
|
5887
|
+
sessionControl !== null &&
|
|
5888
|
+
!currentTurn.abortController.signal.aborted
|
|
5889
|
+
) {
|
|
5890
|
+
log.info(
|
|
5891
|
+
{ turnId: currentTurn.turnId, action: sessionControl.action },
|
|
5892
|
+
"Live voice reply requested a session control",
|
|
5893
|
+
);
|
|
5894
|
+
if (sessionControl.action === "end") {
|
|
5895
|
+
currentTurn.minimizeRequested = false;
|
|
5896
|
+
}
|
|
5897
|
+
await this.sendFrame(
|
|
5898
|
+
{
|
|
5899
|
+
type: "session_control",
|
|
5900
|
+
turnId: currentTurn.turnId,
|
|
5901
|
+
...sessionControl,
|
|
5902
|
+
},
|
|
5903
|
+
() => !this.isClosed,
|
|
5904
|
+
);
|
|
5905
|
+
}
|
|
5906
|
+
|
|
6309
5907
|
// Drain-scoped minimize: the latched marker is consumed here, after
|
|
6310
5908
|
// the turn's speech has fully drained — never mid-speech, never for
|
|
6311
5909
|
// a barged-in turn, at most once per turn.
|
|
@@ -6518,7 +6116,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6518
6116
|
if (settledTurn?.token === token) {
|
|
6519
6117
|
// Anchor the dead-air countdown to the end of emission; the playback
|
|
6520
6118
|
// -tail estimate covers any client-side buffer still draining.
|
|
6521
|
-
settledTurn.progress.
|
|
6119
|
+
settledTurn.progress.noteAudioSettled();
|
|
6522
6120
|
}
|
|
6523
6121
|
this.pumpTtsSynthesis(token);
|
|
6524
6122
|
}
|
|
@@ -7105,8 +6703,8 @@ export function createLiveVoiceSession(
|
|
|
7105
6703
|
const vadConfig = liveVoiceConfig?.vad;
|
|
7106
6704
|
// Parsed once here into a complete config, shared by endpointing and the
|
|
7107
6705
|
// session (the constructor's own parse is then a no-op re-validation).
|
|
7108
|
-
const frontModelConfig =
|
|
7109
|
-
options.frontModelConfig ??
|
|
6706
|
+
const frontModelConfig = VoiceFrontModelConfigSchema.parse(
|
|
6707
|
+
options.frontModelConfig ?? getConfig().voice?.frontModel ?? {},
|
|
7110
6708
|
);
|
|
7111
6709
|
return new LiveVoiceSession(context, {
|
|
7112
6710
|
...options,
|
|
@@ -7271,10 +6869,10 @@ export async function defaultSpawnBackgroundContinuation(args: {
|
|
|
7271
6869
|
// spoken by a session turn, on the session's terms; it was never meant to
|
|
7272
6870
|
// mean invisible.
|
|
7273
6871
|
//
|
|
7274
|
-
// NOT
|
|
7275
|
-
//
|
|
7276
|
-
//
|
|
7277
|
-
// spawning
|
|
6872
|
+
// NOT a per-turn sender: the conversation's event sink is fixed at
|
|
6873
|
+
// construction (see "Conversation event delivery" in assistant/AGENTS.md),
|
|
6874
|
+
// and the detach deliberately waits for the interrupted turn's bridge
|
|
6875
|
+
// teardown before spawning, so nothing turn-scoped is alive by the time
|
|
7278
6876
|
// these events fire. `broadcastMessage` is the same path the bridge itself
|
|
7279
6877
|
// uses to reach an attached web client. The subagent events carry
|
|
7280
6878
|
// `parentConversationId`, not `conversationId`, so scope explicitly.
|