@vellumai/assistant 0.12.3-staging.1 → 0.12.3-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/ARCHITECTURE.md +43 -29
- package/Dockerfile +63 -0
- package/docs/architecture/security.md +10 -5
- package/docs/architecture/turn-actor.md +2 -0
- package/docs/desktop-browser-cli.md +45 -4
- package/docs/live-voice-task-outcomes.md +100 -0
- package/docs/mode-sessions.md +183 -0
- package/docs/voice-input-diagnostics.md +58 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +38 -7
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +5 -6
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/integration-categories.ts +36 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +4 -8
- package/openapi.yaml +665 -14
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +10 -26
- package/scripts/smoke-desktop-window-theme.ts +2 -3
- package/src/__tests__/agent-loop-output-hooks.test.ts +150 -1
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +87 -19
- package/src/__tests__/annotate-activity-metadata.test.ts +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/answered-question-persistence.test.ts +2 -0
- package/src/__tests__/anthropic-gateway-shared.test.ts +22 -0
- package/src/__tests__/approval-message-composer.test.ts +1 -118
- package/src/__tests__/approval-routes-http.test.ts +3 -0
- package/src/__tests__/assistant-event-hub-connection-history.test.ts +111 -0
- package/src/__tests__/assistant-event-hub-targeted.test.ts +50 -0
- package/src/__tests__/assistant-event-targeting.test.ts +186 -0
- package/src/__tests__/assistant-stream-state.test.ts +29 -9
- package/src/__tests__/browser-fill-credential.test.ts +11 -22
- package/src/__tests__/btw-routes.test.ts +4 -1
- package/src/__tests__/build-persisted-content.test.ts +1 -1
- package/src/__tests__/call-conversation-messages.test.ts +5 -0
- package/src/__tests__/call-recovery.test.ts +7 -2
- package/src/__tests__/call-routes-http.test.ts +12 -0
- package/src/__tests__/call-store.test.ts +7 -2
- package/src/__tests__/channel-retry-sweep.test.ts +9 -0
- package/src/__tests__/chat-reveal-guard-priming.test.ts +2 -0
- package/src/__tests__/checker.test.ts +11 -3
- package/src/__tests__/client-os-metadata-persistence.test.ts +35 -1
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +0 -25
- package/src/__tests__/computer-use-tools.test.ts +1 -233
- package/src/__tests__/config-model-image-provider.test.ts +39 -0
- package/src/__tests__/config-schema.test.ts +16 -0
- package/src/__tests__/config-watcher.test.ts +24 -0
- package/src/__tests__/context-search-agent-runner.test.ts +260 -1
- package/src/__tests__/context-search-fanout.test.ts +1 -1
- package/src/__tests__/context-search-types.test.ts +7 -7
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +8 -2
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +546 -16
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +53 -1
- package/src/__tests__/conversation-evictor.test.ts +62 -37
- package/src/__tests__/conversation-history-boundary-repair-failure.test.ts +174 -0
- package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-retry-route.test.ts +22 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +2 -0
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +2 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +10 -0
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +29 -0
- package/src/__tests__/conversation-surface-snapshots.test.ts +543 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +151 -0
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +63 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +23 -0
- package/src/__tests__/conversation-undo.test.ts +61 -0
- package/src/__tests__/credential-execution-client.test.ts +6 -6
- package/src/__tests__/credential-prompt-route.test.ts +35 -3
- package/src/__tests__/cu-unified-flow.test.ts +82 -1
- package/src/__tests__/db-mode-sessions-migration.test.ts +175 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -0
- package/src/__tests__/dm-backfill.test.ts +91 -1
- package/src/__tests__/document-create-route.test.ts +88 -0
- package/src/__tests__/document-sync-tags.test.ts +27 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +142 -2
- package/src/__tests__/events-tail-route.test.ts +25 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +57 -8
- package/src/__tests__/guardian-action-sweep.test.ts +16 -0
- package/src/__tests__/guardian-dispatch.test.ts +13 -0
- package/src/__tests__/headless-browser-interactions.test.ts +64 -20
- package/src/__tests__/heartbeat-service.test.ts +2 -59
- package/src/__tests__/helpers/mock-conversation.ts +21 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +2 -0
- package/src/__tests__/helpers/verification-sessions-ipc-sim.ts +5 -0
- package/src/__tests__/host-cu-proxy.test.ts +46 -2
- package/src/__tests__/http-user-message-parity.test.ts +2 -0
- package/src/__tests__/image-credentials.test.ts +49 -0
- package/src/__tests__/image-service-dispatcher.test.ts +45 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +1 -1
- package/src/__tests__/interrupt-on-send.test.ts +57 -43
- package/src/__tests__/interrupt-turn-note.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +2 -2
- package/src/__tests__/media-generate-image.test.ts +49 -1
- package/src/__tests__/message-provenance-contact-id.test.ts +69 -0
- package/src/__tests__/migration-export-to-gcs.test.ts +142 -0
- package/src/__tests__/mode-session-history.test.ts +245 -0
- package/src/__tests__/mode-session-recovery.test.ts +222 -0
- package/src/__tests__/mode-session-wire.test.ts +141 -0
- package/src/__tests__/notification-guardian-path.test.ts +7 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +2 -2
- package/src/__tests__/notification-vellum-adapter.test.ts +34 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +12 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-api-provider.test.ts +5 -0
- package/src/__tests__/plugin-api-resolve-credential.test.ts +97 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +9 -0
- package/src/__tests__/process-message-display-content.test.ts +56 -0
- package/src/__tests__/reaction-persistence.test.ts +5 -0
- package/src/__tests__/request-secret-standalone-channel-gate.test.ts +25 -9
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +1 -1
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +73 -0
- package/src/__tests__/secret-ingress-http.test.ts +2 -0
- package/src/__tests__/secure-keys.test.ts +43 -1
- package/src/__tests__/send-endpoint-busy.test.ts +163 -1
- package/src/__tests__/server-history-render.test.ts +91 -1
- package/src/__tests__/settings-routes.test.ts +50 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +32 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -2
- package/src/__tests__/subagent-manager-notify.test.ts +1 -2
- package/src/__tests__/subagent-notify-parent.test.ts +146 -39
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +2 -4
- package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +2 -0
- package/src/__tests__/thread-backfill.test.ts +53 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +3 -1
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/twilio-routes.test.ts +1 -0
- package/src/__tests__/web-search-backend-failure.test.ts +1 -1
- package/src/activation/progress-store.test.ts +12 -7
- package/src/agent/loop.ts +99 -41
- package/src/api/events/assistant-turn-start.ts +3 -0
- package/src/api/events/generation-handoff.ts +2 -0
- package/src/api/events/message-complete.ts +2 -0
- package/src/api/events/notification-conversation-created.ts +14 -16
- package/src/api/events/notification-intent.ts +4 -2
- package/src/api/events/tool-result.ts +85 -0
- package/src/api/events/tool-use-start.ts +3 -0
- package/src/api/events/user-message-echo.ts +3 -0
- package/src/api/index.ts +31 -6
- package/src/api/mode-session.test.ts +131 -0
- package/src/api/mode-session.ts +148 -0
- package/src/api/remember-tool.ts +15 -0
- package/src/api/responses/conversation-message.ts +6 -0
- package/src/approvals/AGENTS.md +4 -2
- package/src/browser/__tests__/operations.test.ts +29 -0
- package/src/browser/operation-meta.ts +2 -1
- package/src/browser/operations.ts +29 -0
- package/src/browser/virtual-desktop-target.ts +2 -8
- package/src/bundler/bundle-signer.ts +1 -3
- package/src/calls/__tests__/barge-in-guard.test.ts +3 -0
- package/src/calls/__tests__/call-controller.test.ts +94 -0
- package/src/calls/__tests__/call-funnel-telemetry.test.ts +204 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +15 -0
- package/src/calls/__tests__/voice-escalation-judge.test.ts +209 -0
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +168 -0
- package/src/{live-voice/__tests__/live-voice-metrics.test.ts → calls/__tests__/voice-metrics.test.ts} +32 -38
- package/src/calls/__tests__/voice-session-bridge.test.ts +576 -122
- package/src/calls/barge-in-guard.ts +5 -0
- package/src/calls/call-controller.ts +196 -1
- package/src/calls/call-domain.ts +4 -0
- package/src/calls/call-store.ts +87 -2
- package/src/calls/guardian-action-sweep.ts +22 -8
- package/src/calls/media-stream-audio-transcode.ts +1 -3
- package/src/calls/typesafe-noul.ts +156 -0
- package/src/calls/voice-control-protocol.ts +6 -0
- package/src/calls/voice-escalation-judge.ts +145 -0
- package/src/calls/voice-escalation-target.ts +10 -0
- package/src/calls/voice-leg-coordinator.ts +121 -3
- package/src/{live-voice/live-voice-metrics.ts → calls/voice-metrics.ts} +94 -87
- package/src/calls/voice-session-bridge.ts +256 -112
- package/src/calls/voice-triage-escalate.ts +7 -0
- package/src/cli/__tests__/roadmap-cli.test.ts +4 -41
- package/src/cli/commands/__tests__/notifications.test.ts +35 -10
- package/src/cli/commands/browser.help.ts +4 -5
- package/src/cli/commands/browser.ts +0 -1
- package/src/cli/commands/clients.help.ts +46 -0
- package/src/cli/commands/clients.ts +116 -0
- package/src/cli/commands/credentials.help.ts +6 -1
- package/src/cli/commands/notifications.help.ts +16 -2
- package/src/cli/commands/notifications.ts +30 -8
- package/src/cli/commands/roadmap.help.ts +18 -21
- package/src/cli/commands/roadmap.ts +4 -21
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +5 -2
- package/src/cli/lib/bundled-marketplace.json +84 -34
- package/src/cli/lib/plugin-marketplace.ts +8 -0
- package/src/cli/lib/publish-plugin.ts +1 -2
- package/src/config/__tests__/default-provider.test.ts +3 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +14 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -1
- package/src/config/bundled-skills/computer-use/TOOLS.json +89 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +17 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +7 -11
- package/src/config/bundled-skills/sequences/TOOLS.json +1 -5
- package/src/config/call-site-defaults.ts +9 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/profile-text-generation.test.ts +2 -2
- package/src/config/schemas/__tests__/tools.test.ts +31 -0
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -18
- package/src/config/schemas/llm.ts +3 -1
- package/src/config/schemas/mcp.ts +2 -1
- package/src/config/schemas/memory-retrospective.ts +11 -2
- package/src/config/schemas/services.ts +15 -6
- package/src/config/schemas/tools.ts +10 -0
- package/src/config/session-groups-gate.ts +21 -0
- package/src/config/skills.ts +2 -6
- package/src/conversations/__tests__/message-consolidation.test.ts +149 -0
- package/src/conversations/message-consolidation.ts +102 -5
- package/src/credential-execution/ces-connect.test.ts +69 -0
- package/src/credential-execution/ces-connect.ts +115 -0
- package/src/credential-execution/ces-runtime.ts +55 -93
- package/src/credential-execution/executable-discovery.ts +1 -11
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +10 -1
- package/src/daemon/__tests__/inference-profile-notification.test.ts +2 -0
- package/src/daemon/browser-mode-session.test.ts +402 -0
- package/src/daemon/browser-mode-session.ts +186 -0
- package/src/daemon/computer-use-mode-session.test.ts +210 -0
- package/src/daemon/computer-use-mode-session.ts +105 -0
- package/src/daemon/config-watcher.ts +10 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +58 -10
- package/src/daemon/conversation-agent-loop.ts +219 -25
- package/src/daemon/conversation-evictor.ts +5 -24
- package/src/daemon/conversation-history.ts +10 -0
- package/src/daemon/conversation-interrupt-repair.ts +4 -43
- package/src/daemon/conversation-interrupt.ts +7 -11
- package/src/daemon/conversation-messaging.ts +58 -5
- package/src/daemon/conversation-mode-session.test.ts +1667 -0
- package/src/daemon/conversation-mode-session.ts +1008 -0
- package/src/daemon/conversation-process.ts +5 -0
- package/src/daemon/conversation-runtime-assembly.ts +10 -20
- package/src/daemon/conversation-skill-tools.ts +1 -0
- package/src/daemon/conversation-store.ts +4 -4
- package/src/daemon/conversation-surface-snapshots.ts +135 -0
- package/src/daemon/conversation-surfaces.ts +113 -5
- package/src/daemon/conversation-tool-setup.ts +8 -3
- package/src/daemon/conversation.ts +143 -41
- package/src/daemon/handlers/config-model.ts +7 -5
- package/src/daemon/handlers/shared.ts +55 -16
- package/src/daemon/host-cu-proxy.ts +51 -22
- package/src/daemon/lifecycle.ts +30 -13
- package/src/daemon/message-protocol.ts +0 -1
- package/src/daemon/message-provenance.ts +28 -4
- package/src/daemon/mode-session-startup-recovery.ts +23 -0
- package/src/daemon/mode-session-tracking.ts +54 -0
- package/src/daemon/process-message.ts +13 -4
- package/src/daemon/virtual-desktop-context.ts +21 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-accessibility-script.ts +88 -0
- package/src/desktop/desktop-accessibility.test.ts +160 -0
- package/src/desktop/desktop-accessibility.ts +169 -0
- package/src/desktop/desktop-automation-lease.test.ts +76 -68
- package/src/desktop/desktop-automation-lease.ts +41 -50
- package/src/desktop/desktop-browser-endpoint.ts +4 -0
- package/src/desktop/desktop-computer-use-routing.test.ts +330 -0
- package/src/desktop/desktop-computer-use.test.ts +449 -0
- package/src/desktop/desktop-computer-use.ts +502 -0
- package/src/desktop/desktop-dependencies.ts +47 -293
- package/src/desktop/desktop-display.ts +2 -2
- package/src/desktop/desktop-panel-config.test.ts +13 -3
- package/src/desktop/desktop-panel-config.ts +109 -15
- package/src/desktop/desktop-session-manager.test.ts +69 -21
- package/src/desktop/desktop-session-manager.ts +76 -22
- package/src/desktop/desktop-stream-bridge.test.ts +11 -122
- package/src/desktop/desktop-stream-bridge.ts +6 -8
- package/src/desktop/desktop-window-theme.ts +5 -25
- package/src/desktop/virtual-desktop-feature.ts +52 -2
- package/src/desktop/virtual-desktop-platform.test.ts +5 -9
- package/src/documents/document-store.ts +23 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +22 -26
- package/src/heartbeat/heartbeat-service.ts +5 -33
- package/src/ipc/__tests__/browser-ipc.test.ts +215 -6
- package/src/ipc/assistant-server.ts +2 -0
- package/src/ipc/routes/__tests__/channel-reply-ipc-routes.test.ts +111 -0
- package/src/ipc/routes/channel-reply-ipc-routes.ts +73 -0
- package/src/live-voice/__tests__/continuation-judge.test.ts +151 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +154 -15
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +743 -31
- package/src/live-voice/__tests__/live-voice-look-follow-up.test.ts +191 -10
- package/src/live-voice/__tests__/live-voice-session-groups-gate.test.ts +87 -0
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +5 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +41 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +1017 -9
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +224 -1
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +101 -0
- package/src/live-voice/__tests__/live-voice-tts.test.ts +44 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +939 -75
- package/src/live-voice/__tests__/microphone-idle-gate.test.ts +87 -0
- package/src/live-voice/__tests__/session-controls.test.ts +23 -0
- package/src/live-voice/__tests__/task-outcomes.test.ts +117 -0
- package/src/live-voice/__tests__/voice-input-diagnostics.test.ts +117 -0
- package/src/live-voice/camera-mode-session.test.ts +265 -0
- package/src/live-voice/camera-mode-session.ts +277 -0
- package/src/live-voice/continuation-judge.ts +123 -0
- package/src/live-voice/live-activity-reporter.ts +115 -6
- package/src/live-voice/live-voice-manager.ts +36 -1
- package/src/live-voice/live-voice-photo.ts +88 -7
- package/src/live-voice/live-voice-session-manager.ts +15 -0
- package/src/live-voice/live-voice-session.ts +1443 -747
- package/src/live-voice/live-voice-tts.ts +22 -4
- package/src/live-voice/microphone-idle-gate.ts +56 -0
- package/src/live-voice/protocol.ts +171 -4
- package/src/live-voice/session-controls.ts +11 -3
- package/src/live-voice/task-outcomes.ts +131 -0
- package/src/live-voice/voice-input-diagnostics.ts +117 -0
- package/src/mcp/__tests__/manager-tool-caps.test.ts +63 -18
- package/src/mcp/__tests__/tool-caps.test.ts +28 -1
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/tool-caps.ts +12 -4
- package/src/media/__tests__/image-models.test.ts +58 -0
- package/src/media/image-credentials.ts +5 -0
- package/src/media/image-models.ts +52 -0
- package/src/media/image-service.ts +8 -0
- package/src/media/openrouter-image-service.test.ts +133 -0
- package/src/media/openrouter-image-service.ts +153 -0
- package/src/media/types.ts +1 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +65 -11
- package/src/messaging/providers/discord/send.ts +4 -17
- package/src/messaging/providers/discord/transport.ts +2 -2
- package/src/messaging/providers/outlook/client.ts +16 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +236 -1
- package/src/messaging/providers/telegram-bot/send.ts +10 -7
- package/src/messaging/providers/telegram-bot/transport.ts +3 -10
- package/src/messaging/providers/whatsapp/send.test.ts +169 -4
- package/src/monitoring/__tests__/control.test.ts +17 -5
- package/src/monitoring/control.ts +15 -5
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/README.md +83 -99
- package/src/notifications/__tests__/broadcaster.test.ts +1 -1
- package/src/notifications/__tests__/channel-allowlist.test.ts +39 -0
- package/src/notifications/__tests__/copy-composer.test.ts +1 -1
- package/src/notifications/__tests__/decision-engine.test.ts +79 -2
- package/src/notifications/__tests__/deterministic-checks.test.ts +10 -10
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +93 -4
- package/src/notifications/__tests__/notification-utils.test.ts +18 -0
- package/src/notifications/adapters/macos.ts +37 -46
- package/src/notifications/broadcaster.ts +5 -6
- package/src/notifications/channel-allowlist.ts +44 -0
- package/src/notifications/copy-composer.ts +0 -23
- package/src/notifications/decision-engine.ts +24 -14
- package/src/notifications/deterministic-checks.ts +1 -1
- package/src/notifications/emit-signal.ts +123 -37
- package/src/notifications/home-feed-side-effect.ts +9 -7
- package/src/notifications/notification-utils.ts +19 -0
- package/src/notifications/signal.ts +4 -19
- package/src/notifications/types.ts +15 -0
- package/src/oauth/connection-resolver.ts +1 -2
- package/src/oauth/oauth-store.ts +1 -3
- package/src/oauth/provider-serializer.ts +7 -0
- package/src/oauth/seed-providers.ts +30 -0
- package/src/onboarding/onboarding-events-store.ts +99 -12
- package/src/persistence/__tests__/client-connection-events-store.test.ts +322 -0
- package/src/persistence/client-connection-events-store.ts +377 -0
- package/src/persistence/conversation-crud.ts +55 -6
- package/src/persistence/conversation-mode-sessions.test.ts +424 -0
- package/src/persistence/conversation-mode-sessions.ts +460 -0
- package/src/persistence/conversation-queries.ts +6 -19
- package/src/persistence/llm-usage-store.ts +3 -9
- package/src/persistence/message-metadata.ts +25 -0
- package/src/persistence/migrations/381-create-conversation-mode-sessions.ts +41 -0
- package/src/persistence/migrations/382-create-client-connection-events.ts +40 -0
- package/src/persistence/migrations/__tests__/382-create-client-connection-events.test.ts +56 -0
- package/src/persistence/schema/conversations.ts +67 -0
- package/src/persistence/schema/infrastructure.ts +29 -0
- package/src/persistence/steps.ts +17 -0
- package/src/plugin-api/__tests__/plugin-name-env.test.ts +249 -0
- package/src/plugin-api/effective-context-window.ts +33 -0
- package/src/plugin-api/index.ts +24 -5
- package/src/plugin-api/plugin-name-env.ts +125 -0
- package/src/plugin-api/resolve-credential.ts +79 -20
- package/src/plugin-api/vision-support.test.ts +22 -0
- package/src/plugin-api/vision-support.ts +17 -5
- package/src/plugins/__tests__/active-plugin-env.test.ts +136 -0
- package/src/plugins/active-plugin-env.ts +51 -0
- package/src/plugins/defaults/index.ts +24 -0
- package/src/plugins/defaults/memory/__tests__/archive-removal-framing.test.ts +64 -0
- package/src/plugins/defaults/memory/__tests__/worker-control.test.ts +19 -6
- package/src/plugins/defaults/memory/context-search/agent-protocol.ts +9 -12
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +78 -22
- package/src/plugins/defaults/memory/context-search/format.ts +51 -7
- package/src/plugins/defaults/memory/context-search/limits.ts +10 -12
- package/src/plugins/defaults/memory/context-search/recall-input.ts +38 -0
- package/src/plugins/defaults/memory/context-search/search.ts +8 -14
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +1 -0
- package/src/plugins/defaults/memory/context-search/types.ts +1 -11
- package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +11 -4
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +12 -21
- package/src/plugins/defaults/memory/graph/tools.ts +35 -64
- package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +5 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/tools.test.ts +124 -9
- package/src/plugins/defaults/memory/tools.ts +38 -11
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +165 -27
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +626 -27
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +46 -27
- package/src/plugins/defaults/memory/v3/orchestrate.ts +78 -38
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +65 -79
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +14 -5
- package/src/plugins/defaults/memory/v3/pool-select.ts +683 -44
- package/src/plugins/defaults/memory/worker-control.ts +15 -5
- package/src/plugins/defaults/task-progress/__tests__/registration.test.ts +70 -0
- package/src/plugins/defaults/task-progress/__tests__/task-progress-context.test.ts +380 -0
- package/src/plugins/defaults/task-progress/hooks/post-compact.ts +19 -0
- package/src/plugins/defaults/task-progress/hooks/user-prompt-submit.ts +24 -0
- package/src/plugins/defaults/task-progress/package.json +14 -0
- package/src/plugins/defaults/task-progress/src/apply-task-progress-context.ts +93 -0
- package/src/plugins/defaults/task-progress/src/format-task-progress-context.ts +67 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +10 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +18 -0
- package/src/providers/anthropic/client.ts +10 -1
- package/src/providers/anthropic-gateway-shared.ts +6 -0
- package/src/providers/fetch-provider-catalog.ts +11 -0
- package/src/providers/gemini/client.ts +25 -8
- package/src/providers/inference/adapter-factory.ts +1 -1
- package/src/providers/jev/client.test.ts +10 -0
- package/src/providers/jev/client.ts +20 -4
- package/src/providers/model-catalog.ts +4 -3
- package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +94 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +106 -1
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +24 -0
- package/src/providers/openai/__tests__/google-thought-signature.test.ts +1 -2
- package/src/providers/openai/chat-completions-provider.ts +100 -29
- package/src/providers/openai/google-thought-signature.ts +0 -6
- package/src/providers/openai/responses-provider.ts +23 -1
- package/src/providers/ratelimit.ts +1 -3
- package/src/providers/search-provider-catalog.ts +24 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +24 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +7 -1
- package/src/providers/speech-to-text/__tests__/vellum-managed-realtime.test.ts +50 -1
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +11 -0
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +13 -0
- package/src/providers/types.ts +5 -0
- package/src/routes/control.ts +7 -5
- package/src/runtime/AGENTS.md +4 -2
- package/src/runtime/actor-trust-resolver.ts +3 -3
- package/src/runtime/agent-wake.ts +1 -1
- package/src/runtime/approval-message-composer.ts +3 -69
- package/src/runtime/assistant-event-hub.ts +73 -75
- package/src/runtime/assistant-event-publish-options.ts +7 -0
- package/src/runtime/assistant-event-targeting.ts +79 -0
- package/src/runtime/assistant-stream-state.ts +13 -100
- package/src/runtime/background-job-runner.ts +5 -5
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/guardian-action-message-composer.ts +8 -161
- package/src/runtime/guardian-reply-router.ts +28 -32
- package/src/runtime/http-types.ts +6 -0
- package/src/runtime/message-composer-types.ts +3 -61
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +67 -1
- package/src/runtime/migrations/vbundle-builder.ts +24 -1
- package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -4
- package/src/runtime/migrations/vbundle-import-policy.ts +10 -0
- package/src/runtime/migrations/vbundle-importer.ts +4 -1
- package/src/runtime/migrations/vbundle-streaming-importer.ts +4 -1
- package/src/runtime/migrations/vbundle-validator.ts +1 -0
- package/src/runtime/pending-interactions-mode-session.test.ts +62 -0
- package/src/runtime/pending-interactions.ts +29 -0
- package/src/runtime/routes/__tests__/client-history-routes.test.ts +104 -0
- package/src/runtime/routes/__tests__/image-generation-routes.test.ts +97 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/roadmap-routes.test.ts +29 -6
- package/src/runtime/routes/browser-context.ts +31 -11
- package/src/runtime/routes/browser-routes.ts +43 -4
- package/src/runtime/routes/btw-routes.ts +9 -4
- package/src/runtime/routes/client-routes.ts +108 -0
- package/src/runtime/routes/conversation-management-routes.ts +4 -1
- package/src/runtime/routes/conversation-routes.ts +113 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -3
- package/src/runtime/routes/credential-routes.ts +1 -2
- package/src/runtime/routes/desktop-setup-routes.test.ts +9 -18
- package/src/runtime/routes/desktop-setup-routes.ts +4 -6
- package/src/runtime/routes/documents-routes.ts +51 -0
- package/src/runtime/routes/email-routes.ts +3 -7
- package/src/runtime/routes/events-routes.ts +75 -54
- package/src/runtime/routes/identity-routes.ts +1 -5
- package/src/runtime/routes/image-generation-routes.ts +8 -14
- package/src/runtime/routes/inbound-message-handler.ts +35 -38
- package/src/runtime/routes/inbound-stages/backfill-sender-provenance.test.ts +168 -0
- package/src/runtime/routes/inbound-stages/backfill-sender-provenance.ts +96 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +38 -9
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +2 -5
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +73 -2
- package/src/runtime/routes/notification-routes.ts +10 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/plugins-routes.ts +6 -0
- package/src/runtime/routes/roadmap-routes.ts +47 -8
- package/src/runtime/routes/secret-routes.ts +1 -1
- package/src/runtime/routes/settings-routes.ts +10 -4
- package/src/runtime/routes/watcher-routes.ts +1 -3
- package/src/runtime/trust-class.test.ts +7 -6
- package/src/runtime/trust-class.ts +9 -17
- package/src/schedule/__tests__/worker-control.test.ts +36 -5
- package/src/schedule/worker-control.ts +15 -5
- package/src/security/secure-keys.ts +53 -103
- package/src/skills/clawhub.ts +3 -8
- package/src/skills/managed-store.ts +1 -5
- package/src/stt/types.ts +4 -0
- package/src/subagent/notify.ts +54 -34
- package/src/subagent/parent-notification.ts +6 -0
- package/src/telemetry/__tests__/phone-call-funnel.test.ts +72 -0
- package/src/telemetry/phone-call-funnel.ts +144 -0
- package/src/tools/__tests__/tool-schema-root-combinator-guard.test.ts +81 -0
- package/src/tools/ask-question/ask-question-tool.ts +6 -4
- package/src/tools/browser/browser-execution.ts +22 -59
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +561 -39
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +435 -39
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
- package/src/tools/client-os.ts +15 -0
- package/src/tools/computer-use/skill-proxy-bridge.ts +1 -1
- package/src/tools/computer-use/target.ts +42 -0
- package/src/tools/document/document-tool.ts +13 -18
- package/src/tools/execution-target.ts +14 -16
- package/src/tools/host-terminal/host-shell.ts +4 -1
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
- package/src/tools/network/__tests__/web-search.test.ts +203 -0
- package/src/tools/network/web-fetch.ts +274 -7
- package/src/tools/network/web-search.ts +468 -6
- package/src/tools/permission-checker.ts +3 -3
- package/src/tools/policy-context.ts +3 -1
- package/src/tools/skills/load.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +11 -2
- package/src/tools/skills/skill-script-runner.ts +7 -0
- package/src/tools/skills/skill-tool-factory.ts +15 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +11 -0
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/terminal/shell.ts +2 -0
- package/src/tools/tool-approval-handler.ts +5 -1
- package/src/tools/tool-types.ts +3 -3
- package/src/tools/types.ts +8 -2
- package/src/util/__tests__/worker-process-command.test.ts +5 -4
- package/src/util/abort-reasons.ts +18 -26
- package/src/util/canonicalize-identity.test.ts +21 -0
- package/src/util/canonicalize-identity.ts +16 -0
- package/src/util/errors.ts +13 -3
- package/src/util/provider-error-patterns.ts +3 -0
- package/src/util/worker-process.ts +77 -35
- package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
- package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/computer-use-window-schema.test.ts +0 -24
- package/src/api/events/heartbeat-alert.ts +0 -20
- package/src/daemon/message-types/web-activity.ts +0 -65
- package/src/desktop/desktop-dependencies.test.ts +0 -131
- package/src/tools/computer-use/definitions.ts +0 -607
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from "../../stt/types.js";
|
|
14
14
|
import {
|
|
15
15
|
LiveVoiceSession,
|
|
16
|
+
type LiveVoiceSessionAudioArchiver,
|
|
16
17
|
type LiveVoiceTtsStreamer,
|
|
17
18
|
} from "../live-voice-session.js";
|
|
18
19
|
import type { LiveVoiceSessionFactoryContext } from "../live-voice-session-manager.js";
|
|
@@ -54,15 +55,15 @@ const LOUD_CHUNK = pcm(8_000);
|
|
|
54
55
|
const SUSTAINED_LOUD_CHUNK = pcm(8_000, 7_200);
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
|
-
* Flux
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* exactly as Flux does: its transcript is committed by EndOfTurn.
|
|
58
|
+
* Flux has no `finalizeUtterance`: the session keeps the stream open when
|
|
59
|
+
* provider end-of-turn owns the boundary, otherwise it closes each cycle.
|
|
60
|
+
* Events are scripted, and `stop()` only flushes explicitly supplied text.
|
|
61
61
|
*/
|
|
62
62
|
class MockFluxTranscriber implements StreamingTranscriber {
|
|
63
63
|
readonly boundaryId = "daemon-streaming" as const;
|
|
64
64
|
readonly received: Buffer[] = [];
|
|
65
65
|
stopped = false;
|
|
66
|
+
onAudio: ((chunk: Buffer) => void) | null = null;
|
|
66
67
|
private onEvent: ((event: SttStreamServerEvent) => void) | null = null;
|
|
67
68
|
|
|
68
69
|
constructor(readonly providerId: SttProviderId) {}
|
|
@@ -73,17 +74,25 @@ class MockFluxTranscriber implements StreamingTranscriber {
|
|
|
73
74
|
|
|
74
75
|
sendAudio(chunk: Buffer): void {
|
|
75
76
|
this.received.push(Buffer.from(chunk));
|
|
77
|
+
this.onAudio?.(chunk);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// Transcript Flux answers `CloseStream` with, for a turn still in flight
|
|
79
81
|
// when the caller released. Null models a stop with nothing left to flush.
|
|
80
82
|
pendingFlushText: string | null = null;
|
|
83
|
+
holdStopEvents = false;
|
|
81
84
|
|
|
82
85
|
stop(): void {
|
|
83
86
|
if (this.stopped) {
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
this.stopped = true;
|
|
90
|
+
if (!this.holdStopEvents) {
|
|
91
|
+
this.flushStopEvents();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
flushStopEvents(): void {
|
|
87
96
|
if (this.pendingFlushText !== null) {
|
|
88
97
|
this.onEvent?.({ type: "final", text: this.pendingFlushText });
|
|
89
98
|
}
|
|
@@ -112,15 +121,19 @@ class MockFluxTranscriber implements StreamingTranscriber {
|
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
function createHarness(options: {
|
|
124
|
+
startFrame?: LiveVoiceClientStartFrame;
|
|
115
125
|
providerId?: SttProviderId;
|
|
116
126
|
fluxConfig?: Partial<LiveVoiceFluxConfig>;
|
|
117
127
|
silenceThresholdMs?: number;
|
|
128
|
+
continuationAnnounceSilenceMs?: number;
|
|
118
129
|
startVoiceTurn?: (options: VoiceTurnOptions) => Promise<{
|
|
119
130
|
turnId: string;
|
|
120
131
|
abort: () => void;
|
|
121
132
|
}>;
|
|
122
133
|
streamTtsAudio?: LiveVoiceTtsStreamer | null;
|
|
123
134
|
emitMetrics?: boolean;
|
|
135
|
+
archiveAudio?: LiveVoiceSessionAudioArchiver;
|
|
136
|
+
bargeInMinSpeechMs?: number;
|
|
124
137
|
// Holds the STT dial open so a test can drive a whole utterance through the
|
|
125
138
|
// window between `ready` and the resolved provider.
|
|
126
139
|
resolveGate?: Promise<unknown>;
|
|
@@ -129,7 +142,7 @@ function createHarness(options: {
|
|
|
129
142
|
const frames: LiveVoiceServerFrame[] = [];
|
|
130
143
|
const context: LiveVoiceSessionFactoryContext = {
|
|
131
144
|
sessionId: "session-123",
|
|
132
|
-
startFrame: VAD_START_FRAME,
|
|
145
|
+
startFrame: options.startFrame ?? VAD_START_FRAME,
|
|
133
146
|
sendFrame: mock(async (payload) => {
|
|
134
147
|
const frame = sequencer.next(payload);
|
|
135
148
|
frames.push(frame);
|
|
@@ -163,6 +176,9 @@ function createHarness(options: {
|
|
|
163
176
|
startVoiceTurn,
|
|
164
177
|
streamTtsAudio: options.streamTtsAudio ?? null,
|
|
165
178
|
emitMetrics: options.emitMetrics ?? false,
|
|
179
|
+
archiveAudio: options.archiveAudio,
|
|
180
|
+
bargeInMinSpeechMs: options.bargeInMinSpeechMs,
|
|
181
|
+
continuationAnnounceSilenceMs: options.continuationAnnounceSilenceMs,
|
|
166
182
|
spawnBackgroundContinuation: mock(async () => ""),
|
|
167
183
|
turnDetectorConfig: {
|
|
168
184
|
silenceThresholdMs: options.silenceThresholdMs ?? 40,
|
|
@@ -285,6 +301,405 @@ const FLUX_OFF = {
|
|
|
285
301
|
} as const satisfies Partial<LiveVoiceFluxConfig>;
|
|
286
302
|
|
|
287
303
|
describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
304
|
+
test("holds a task outcome through a Flux pause and reply, then announces over idle input", async () => {
|
|
305
|
+
const completeTurn = autoCompletingTurn(
|
|
306
|
+
"The investigation found the cause.",
|
|
307
|
+
);
|
|
308
|
+
const { session, transcribers, turnCalls, frames } = createHarness({
|
|
309
|
+
fluxConfig: { ...FLUX_ON, eotTimeoutMs: 2_000 },
|
|
310
|
+
silenceThresholdMs: 30,
|
|
311
|
+
continuationAnnounceSilenceMs: 10,
|
|
312
|
+
startVoiceTurn: async (options) => {
|
|
313
|
+
if (options.subagentNotification) {
|
|
314
|
+
return completeTurn(options);
|
|
315
|
+
}
|
|
316
|
+
return { turnId: "question-turn", abort: mock() };
|
|
317
|
+
},
|
|
318
|
+
streamTtsAudio: async (options) => {
|
|
319
|
+
const audio = pcm(100);
|
|
320
|
+
options.onAudioChunk({
|
|
321
|
+
type: "tts_audio",
|
|
322
|
+
contentType: "audio/pcm",
|
|
323
|
+
sampleRate: SAMPLE_RATE,
|
|
324
|
+
dataBase64: Buffer.from(audio).toString("base64"),
|
|
325
|
+
});
|
|
326
|
+
return {
|
|
327
|
+
provider: "fish-audio",
|
|
328
|
+
contentType: "audio/pcm",
|
|
329
|
+
sampleRate: SAMPLE_RATE,
|
|
330
|
+
chunks: 1,
|
|
331
|
+
bytes: audio.byteLength,
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
const feedQuietAudio = async () => {
|
|
336
|
+
for (let index = 0; index < 12; index += 1) {
|
|
337
|
+
await session.handleBinaryAudio(pcm(100));
|
|
338
|
+
await sleep(10);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
try {
|
|
342
|
+
await session.start();
|
|
343
|
+
await waitFor(() => transcribers.length === 1);
|
|
344
|
+
const transcriber = transcribers[0]!;
|
|
345
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
346
|
+
transcriber.startOfTurn(0);
|
|
347
|
+
transcriber.emit({ type: "partial", text: "Could you explain" });
|
|
348
|
+
session.receiveSubagentNotification({
|
|
349
|
+
taskId: "task-1",
|
|
350
|
+
message: "The investigation found the cause.",
|
|
351
|
+
metadata: {
|
|
352
|
+
subagentNotification: {
|
|
353
|
+
subagentId: "task-1",
|
|
354
|
+
label: "Investigation",
|
|
355
|
+
status: "completed",
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const submittedBeforePause = transcriber.received.length;
|
|
361
|
+
await feedQuietAudio();
|
|
362
|
+
expect(transcriber.received.length - submittedBeforePause).toBe(12);
|
|
363
|
+
expect(turnCalls).toHaveLength(0);
|
|
364
|
+
|
|
365
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
366
|
+
transcriber.endOfTurn("Could you explain how this works?", 0);
|
|
367
|
+
await waitFor(() => turnCalls.length === 1);
|
|
368
|
+
expect(turnCalls[0]?.content).toBe("Could you explain how this works?");
|
|
369
|
+
await feedQuietAudio();
|
|
370
|
+
expect(turnCalls).toHaveLength(1);
|
|
371
|
+
|
|
372
|
+
turnCalls[0]?.callbacks?.assistant_text_delta?.(
|
|
373
|
+
makeTextDelta("Here is how it works."),
|
|
374
|
+
);
|
|
375
|
+
turnCalls[0]?.callbacks?.message_complete?.(makeMessageComplete());
|
|
376
|
+
await feedQuietAudio();
|
|
377
|
+
await waitFor(() => countFrames(frames, "tts_done") === 2);
|
|
378
|
+
expect(turnCalls).toHaveLength(2);
|
|
379
|
+
expect(turnCalls[1]?.hiddenSyntheticPrompt).toBe(true);
|
|
380
|
+
expect(turnCalls[1]?.subagentNotification?.taskId).toBe("task-1");
|
|
381
|
+
expect(transcribers).toHaveLength(1);
|
|
382
|
+
} finally {
|
|
383
|
+
await session.close("client_end");
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("keeps continuous idle audio out of the request recording", async () => {
|
|
388
|
+
const recordings: Buffer[] = [];
|
|
389
|
+
const { session, transcribers } = createHarness({
|
|
390
|
+
fluxConfig: FLUX_ON,
|
|
391
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
392
|
+
archiveAudio: async (input) => {
|
|
393
|
+
recordings.push(Buffer.from(input.audio.dataBase64, "base64"));
|
|
394
|
+
return {
|
|
395
|
+
type: "warning",
|
|
396
|
+
warning: {
|
|
397
|
+
code: "message_id_unavailable",
|
|
398
|
+
message: "No persisted message in this test.",
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
await session.start();
|
|
405
|
+
await waitFor(() => transcribers.length === 1);
|
|
406
|
+
for (let index = 0; index < 50; index++) {
|
|
407
|
+
await session.handleBinaryAudio(pcm(100));
|
|
408
|
+
}
|
|
409
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
410
|
+
transcribers[0]!.endOfTurn("a complete question", 0);
|
|
411
|
+
await waitFor(() => recordings.length === 1);
|
|
412
|
+
expect(recordings).toEqual([Buffer.from(LOUD_CHUNK)]);
|
|
413
|
+
} finally {
|
|
414
|
+
await session.close("client_end");
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("replaces confirmed playback echo with equal-duration silence for Flux", async () => {
|
|
419
|
+
const echo = Buffer.alloc(SAMPLE_RATE * 2 * 2);
|
|
420
|
+
for (let index = 0; index < echo.byteLength / 2; index++) {
|
|
421
|
+
echo.writeInt16LE(
|
|
422
|
+
Math.round(4_700 * Math.sin((2 * Math.PI * 200 * index) / SAMPLE_RATE)),
|
|
423
|
+
index * 2,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const { session, frames, transcribers } = createHarness({
|
|
427
|
+
fluxConfig: FLUX_ON,
|
|
428
|
+
startVoiceTurn: async (options) => {
|
|
429
|
+
options.callbacks?.assistant_text_delta?.(
|
|
430
|
+
makeTextDelta("Here is an answer."),
|
|
431
|
+
);
|
|
432
|
+
return { turnId: "bridge-turn", abort: mock() };
|
|
433
|
+
},
|
|
434
|
+
streamTtsAudio: async (options) => {
|
|
435
|
+
options.onAudioChunk({
|
|
436
|
+
type: "tts_audio",
|
|
437
|
+
contentType: "audio/pcm",
|
|
438
|
+
sampleRate: SAMPLE_RATE,
|
|
439
|
+
dataBase64: echo.toString("base64"),
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
provider: "fish-audio",
|
|
443
|
+
contentType: "audio/pcm",
|
|
444
|
+
sampleRate: SAMPLE_RATE,
|
|
445
|
+
chunks: 1,
|
|
446
|
+
bytes: echo.byteLength,
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
await session.start();
|
|
452
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
453
|
+
await waitFor(() => transcribers.length === 1);
|
|
454
|
+
const transcriber = transcribers[0]!;
|
|
455
|
+
transcriber.endOfTurn("a complete question", 0);
|
|
456
|
+
await waitFor(() => countFrames(frames, "tts_audio") === 1);
|
|
457
|
+
const before = transcriber.received.length;
|
|
458
|
+
const echoChunk = echo.subarray(0, 480);
|
|
459
|
+
for (let index = 0; index < 40; index++) {
|
|
460
|
+
await session.handleBinaryAudio(echoChunk);
|
|
461
|
+
}
|
|
462
|
+
const submitted = Buffer.concat(transcriber.received.slice(before));
|
|
463
|
+
expect(submitted.byteLength).toBe(echoChunk.byteLength * 40);
|
|
464
|
+
expect(submitted.every((byte) => byte === 0)).toBe(true);
|
|
465
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
466
|
+
} finally {
|
|
467
|
+
await session.close("client_end");
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("keeps idle quiet audio flowing without starting a user request", async () => {
|
|
472
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
473
|
+
fluxConfig: FLUX_ON,
|
|
474
|
+
});
|
|
475
|
+
try {
|
|
476
|
+
await session.start();
|
|
477
|
+
await waitFor(() => transcribers.length === 1);
|
|
478
|
+
const quiet = pcm(100);
|
|
479
|
+
for (let index = 0; index < 30; index++) {
|
|
480
|
+
await session.handleBinaryAudio(quiet);
|
|
481
|
+
}
|
|
482
|
+
expect(transcribers[0]?.received).toEqual(
|
|
483
|
+
Array.from({ length: 30 }, () => Buffer.from(quiet)),
|
|
484
|
+
);
|
|
485
|
+
expect(turnCalls).toHaveLength(0);
|
|
486
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
487
|
+
expect(countFrames(frames, "utterance_end")).toBe(0);
|
|
488
|
+
} finally {
|
|
489
|
+
await session.close("client_end");
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("gates idle room audio after one second while keeping Flux supplied with silence", async () => {
|
|
494
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
495
|
+
fluxConfig: FLUX_ON,
|
|
496
|
+
});
|
|
497
|
+
try {
|
|
498
|
+
await session.start();
|
|
499
|
+
await waitFor(() => transcribers.length === 1);
|
|
500
|
+
for (let index = 0; index < 200; index += 1) {
|
|
501
|
+
await session.handleBinaryAudio(pcm(100));
|
|
502
|
+
}
|
|
503
|
+
const submitted = Buffer.concat(transcribers[0]!.received);
|
|
504
|
+
expect(submitted).toEqual(
|
|
505
|
+
Buffer.concat([
|
|
506
|
+
Buffer.from(pcm(100, SAMPLE_RATE)),
|
|
507
|
+
Buffer.alloc((SAMPLE_RATE * 2 * 800) / 1_000),
|
|
508
|
+
]),
|
|
509
|
+
);
|
|
510
|
+
expect(transcribers[0]!.stopped).toBe(false);
|
|
511
|
+
expect(turnCalls).toHaveLength(0);
|
|
512
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
513
|
+
expect(countFrames(frames, "utterance_end")).toBe(0);
|
|
514
|
+
} finally {
|
|
515
|
+
await session.close("client_end");
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("a long pause gates noise without releasing the question or clipping resumed speech", async () => {
|
|
520
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
521
|
+
fluxConfig: { ...FLUX_ON, eotTimeoutMs: 5_000 },
|
|
522
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
await session.start();
|
|
526
|
+
await waitFor(() => transcribers.length === 1);
|
|
527
|
+
const transcriber = transcribers[0]!;
|
|
528
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
529
|
+
transcriber.startOfTurn(0);
|
|
530
|
+
transcriber.emit({ type: "partial", text: "Could you explain" });
|
|
531
|
+
for (let index = 0; index < 200; index += 1) {
|
|
532
|
+
await session.handleBinaryAudio(pcm(100));
|
|
533
|
+
}
|
|
534
|
+
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
535
|
+
expect(turnCalls).toHaveLength(0);
|
|
536
|
+
expect(countFrames(frames, "utterance_end")).toBe(0);
|
|
537
|
+
|
|
538
|
+
const onset = pcm(300, SAMPLE_RATE / 5);
|
|
539
|
+
await session.handleBinaryAudio(onset);
|
|
540
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
541
|
+
expect(transcriber.received.at(-1)).toEqual(
|
|
542
|
+
Buffer.concat([Buffer.from(onset), Buffer.from(LOUD_CHUNK)]),
|
|
543
|
+
);
|
|
544
|
+
const submitted = Buffer.concat(transcriber.received);
|
|
545
|
+
expect(submitted.byteLength).toBe(
|
|
546
|
+
LOUD_CHUNK.byteLength * 2 + SAMPLE_RATE * 2 * 2 + onset.byteLength,
|
|
547
|
+
);
|
|
548
|
+
transcriber.endOfTurn("Could you explain how penguins stay warm?", 0);
|
|
549
|
+
await waitFor(() => turnCalls.length === 1);
|
|
550
|
+
expect(turnCalls[0]?.content).toBe(
|
|
551
|
+
"Could you explain how penguins stay warm?",
|
|
552
|
+
);
|
|
553
|
+
expect(transcribers).toHaveLength(1);
|
|
554
|
+
} finally {
|
|
555
|
+
await session.close("client_end");
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("a replacement Flux stream does not receive the closed stream's buffered room audio", async () => {
|
|
560
|
+
const { session, transcribers } = createHarness({ fluxConfig: FLUX_ON });
|
|
561
|
+
try {
|
|
562
|
+
await session.start();
|
|
563
|
+
await waitFor(() => transcribers.length === 1);
|
|
564
|
+
for (let index = 0; index < 200; index += 1) {
|
|
565
|
+
await session.handleBinaryAudio(pcm(100));
|
|
566
|
+
}
|
|
567
|
+
transcribers[0]!.emit({ type: "closed" });
|
|
568
|
+
await flushAsyncCallbacks();
|
|
569
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
570
|
+
await waitFor(() => transcribers.length === 2);
|
|
571
|
+
await waitFor(() => transcribers[1]!.received.length > 0);
|
|
572
|
+
expect(Buffer.concat(transcribers[1]!.received)).toEqual(
|
|
573
|
+
Buffer.from(LOUD_CHUNK),
|
|
574
|
+
);
|
|
575
|
+
} finally {
|
|
576
|
+
await session.close("client_end");
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("a provider can finish from trailing audio after the local silence boundary", async () => {
|
|
581
|
+
const { session, transcribers, turnCalls } = createHarness({
|
|
582
|
+
fluxConfig: FLUX_ON,
|
|
583
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
584
|
+
});
|
|
585
|
+
try {
|
|
586
|
+
await session.start();
|
|
587
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
588
|
+
await waitFor(() => transcribers.length === 1);
|
|
589
|
+
const transcriber = transcribers[0]!;
|
|
590
|
+
transcriber.emit({ type: "partial", text: "a complete question" });
|
|
591
|
+
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
592
|
+
let quietChunks = 0;
|
|
593
|
+
transcriber.onAudio = (chunk) => {
|
|
594
|
+
if (chunk.every((byte) => byte === 0)) {
|
|
595
|
+
quietChunks += 1;
|
|
596
|
+
if (quietChunks === 20) {
|
|
597
|
+
transcriber.endOfTurn("a complete question", 0);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
for (let index = 0; index < 20; index++) {
|
|
602
|
+
await session.handleBinaryAudio(pcm(0));
|
|
603
|
+
}
|
|
604
|
+
await waitFor(() => turnCalls.length === 1);
|
|
605
|
+
expect(turnCalls[0]?.content).toBe("a complete question");
|
|
606
|
+
expect(transcriber.stopped).toBe(false);
|
|
607
|
+
expect(transcribers).toHaveLength(1);
|
|
608
|
+
} finally {
|
|
609
|
+
await session.close("client_end");
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("preserves quiet continuation audio across a pause until Flux commits", async () => {
|
|
614
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
615
|
+
fluxConfig: FLUX_ON,
|
|
616
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
617
|
+
});
|
|
618
|
+
try {
|
|
619
|
+
await session.start();
|
|
620
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
621
|
+
await waitFor(() => transcribers.length === 1);
|
|
622
|
+
const transcriber = transcribers[0]!;
|
|
623
|
+
transcriber.emit({ type: "partial", text: "did they help invent" });
|
|
624
|
+
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
625
|
+
const continuation = [pcm(0), pcm(120, 7_200), pcm(0)];
|
|
626
|
+
const before = transcriber.received.length;
|
|
627
|
+
for (const chunk of continuation) {
|
|
628
|
+
await session.handleBinaryAudio(chunk);
|
|
629
|
+
}
|
|
630
|
+
expect(transcriber.received.slice(before)).toEqual(
|
|
631
|
+
continuation.map((chunk) => Buffer.from(chunk)),
|
|
632
|
+
);
|
|
633
|
+
expect(turnCalls).toHaveLength(0);
|
|
634
|
+
expect(countFrames(frames, "utterance_end")).toBe(0);
|
|
635
|
+
transcriber.endOfTurn("did they help invent ChatGPT", 0);
|
|
636
|
+
await waitFor(() => turnCalls.length === 1);
|
|
637
|
+
expect(turnCalls[0]?.content).toBe("did they help invent ChatGPT");
|
|
638
|
+
} finally {
|
|
639
|
+
await session.close("client_end");
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test("gates room noise during an active answer while preserving real barge-in", async () => {
|
|
644
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
645
|
+
fluxConfig: FLUX_ON,
|
|
646
|
+
});
|
|
647
|
+
try {
|
|
648
|
+
await session.start();
|
|
649
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
650
|
+
await waitFor(() => transcribers.length === 1);
|
|
651
|
+
const transcriber = transcribers[0]!;
|
|
652
|
+
transcriber.endOfTurn("a complete question", 0);
|
|
653
|
+
await waitFor(() => turnCalls.length === 1);
|
|
654
|
+
const before = transcriber.received.length;
|
|
655
|
+
for (let index = 0; index < 200; index++) {
|
|
656
|
+
await session.handleBinaryAudio(pcm(100));
|
|
657
|
+
}
|
|
658
|
+
expect(Buffer.concat(transcriber.received.slice(before))).toEqual(
|
|
659
|
+
Buffer.concat([
|
|
660
|
+
Buffer.from(pcm(100, SAMPLE_RATE)),
|
|
661
|
+
Buffer.alloc((SAMPLE_RATE * 2 * 800) / 1_000),
|
|
662
|
+
]),
|
|
663
|
+
);
|
|
664
|
+
expect(transcribers).toHaveLength(1);
|
|
665
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
666
|
+
expect(turnCalls).toHaveLength(1);
|
|
667
|
+
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
668
|
+
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
669
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
670
|
+
transcriber.startOfTurn(1);
|
|
671
|
+
await waitFor(() => countFrames(frames, "turn_cancelled") === 1);
|
|
672
|
+
expect(transcriber.received.at(-1)).toEqual(
|
|
673
|
+
Buffer.concat([
|
|
674
|
+
Buffer.from(pcm(100, SAMPLE_RATE / 5)),
|
|
675
|
+
Buffer.from(SUSTAINED_LOUD_CHUNK),
|
|
676
|
+
]),
|
|
677
|
+
);
|
|
678
|
+
} finally {
|
|
679
|
+
await session.close("client_end");
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test.each([
|
|
684
|
+
{ providerId: "deepgram-flux" as const, fluxConfig: FLUX_OFF },
|
|
685
|
+
{ providerId: "deepgram" as const, fluxConfig: FLUX_ON },
|
|
686
|
+
])("locally endpointed streams retain idle pre-roll: %j", async (options) => {
|
|
687
|
+
const { session, transcribers } = createHarness(options);
|
|
688
|
+
try {
|
|
689
|
+
await session.start();
|
|
690
|
+
await waitFor(() => transcribers.length === 1);
|
|
691
|
+
await session.handleBinaryAudio(pcm(0));
|
|
692
|
+
expect(transcribers[0]?.received).toHaveLength(0);
|
|
693
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
694
|
+
expect(transcribers[0]?.received).toEqual([
|
|
695
|
+
Buffer.from(pcm(0)),
|
|
696
|
+
Buffer.from(LOUD_CHUNK),
|
|
697
|
+
]);
|
|
698
|
+
} finally {
|
|
699
|
+
await session.close("client_end");
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
288
703
|
test("commits the turn on turn-end without an endpoint-decision leg", async () => {
|
|
289
704
|
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
290
705
|
fluxConfig: FLUX_ON,
|
|
@@ -555,10 +970,7 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
555
970
|
// The caller draws breath and keeps going before the provider's turn-end
|
|
556
971
|
// for the speech that boundary closed reaches the session.
|
|
557
972
|
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
558
|
-
|
|
559
|
-
() => countFrames(frames, "speech_started") === 2,
|
|
560
|
-
"The resumed speech never re-opened a detector turn",
|
|
561
|
-
);
|
|
973
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
562
974
|
const receivedBeforeResume = transcriber?.received.length ?? 0;
|
|
563
975
|
|
|
564
976
|
transcriber?.emit({
|
|
@@ -602,10 +1014,7 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
602
1014
|
// the cycle to Flux, and then the caller keeps going.
|
|
603
1015
|
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
604
1016
|
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
605
|
-
|
|
606
|
-
() => countFrames(frames, "speech_started") === 2,
|
|
607
|
-
"The resumed speech never re-opened a detector turn",
|
|
608
|
-
);
|
|
1017
|
+
expect(countFrames(frames, "speech_started")).toBe(1);
|
|
609
1018
|
|
|
610
1019
|
// Flux ends the turn it kept open across the pause, beating the next local
|
|
611
1020
|
// silence boundary: the fast commit this feature exists for. Nothing here
|
|
@@ -649,10 +1058,7 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
649
1058
|
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
650
1059
|
|
|
651
1060
|
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
652
|
-
|
|
653
|
-
() => countFrames(frames, "speech_started") === 2,
|
|
654
|
-
"The resumed speech never re-opened a detector turn",
|
|
655
|
-
);
|
|
1061
|
+
expect(countFrames(frames, "speech_started")).toBe(1);
|
|
656
1062
|
const receivedBeforeResume = transcriber?.received.length ?? 0;
|
|
657
1063
|
|
|
658
1064
|
// Flux closed turn 0 and opened turn 1 for the resumed speech, so the
|
|
@@ -701,7 +1107,7 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
701
1107
|
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
702
1108
|
|
|
703
1109
|
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
704
|
-
|
|
1110
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
705
1111
|
transcribers[0]?.emit({
|
|
706
1112
|
type: "turn-end",
|
|
707
1113
|
text: "what is the",
|
|
@@ -761,10 +1167,14 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
761
1167
|
await session.close("client_end");
|
|
762
1168
|
}, 10_000);
|
|
763
1169
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1170
|
+
test.each([
|
|
1171
|
+
{ providerId: "deepgram-flux" as const, flagOn: true },
|
|
1172
|
+
{ providerId: "deepgram-flux" as const, flagOn: false },
|
|
1173
|
+
{ providerId: "vellum-flux" as const, flagOn: true },
|
|
1174
|
+
{ providerId: "vellum-flux" as const, flagOn: false },
|
|
1175
|
+
])(
|
|
1176
|
+
"only provider speech starts interrupt playback: %j",
|
|
1177
|
+
async ({ providerId, flagOn }) => {
|
|
768
1178
|
const streamTtsAudio = mock(async (ttsOptions: LiveVoiceTtsOptions) => {
|
|
769
1179
|
ttsOptions.onAudioChunk({
|
|
770
1180
|
type: "tts_audio",
|
|
@@ -781,7 +1191,8 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
781
1191
|
} satisfies LiveVoiceTtsResult;
|
|
782
1192
|
});
|
|
783
1193
|
const { frames, session, transcribers } = createHarness({
|
|
784
|
-
|
|
1194
|
+
providerId,
|
|
1195
|
+
fluxConfig: flagOn ? FLUX_ON : FLUX_OFF,
|
|
785
1196
|
silenceThresholdMs: 10_000,
|
|
786
1197
|
streamTtsAudio,
|
|
787
1198
|
// The leg never completes, so the turn is still in flight when the
|
|
@@ -797,28 +1208,182 @@ describe("LiveVoiceSession Flux end-of-turn", () => {
|
|
|
797
1208
|
await session.start();
|
|
798
1209
|
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
799
1210
|
await waitFor(() => transcribers.length > 0);
|
|
1211
|
+
transcribers[0]?.startOfTurn(0);
|
|
800
1212
|
if (flagOn) {
|
|
801
1213
|
transcribers[0]?.endOfTurn("tell me a story");
|
|
802
1214
|
} else {
|
|
803
1215
|
transcribers[0]?.emit({ type: "final", text: "tell me a story" });
|
|
804
1216
|
await session.handleClientFrame({ type: "ptt_release" });
|
|
805
1217
|
}
|
|
806
|
-
await waitFor(() => frames.some((frame) => frame.type === "
|
|
1218
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_audio"));
|
|
807
1219
|
|
|
808
|
-
|
|
809
|
-
// interrupts the assistant.
|
|
1220
|
+
const speechStarts = countFrames(frames, "speech_started");
|
|
810
1221
|
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
1222
|
+
await flushAsyncCallbacks();
|
|
1223
|
+
expect(countFrames(frames, "speech_started")).toBe(speechStarts);
|
|
1224
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1225
|
+
|
|
1226
|
+
// Per-cycle streams restart the provider's turn numbering.
|
|
1227
|
+
transcribers.at(-1)?.startOfTurn(flagOn ? 1 : 0);
|
|
811
1228
|
await waitFor(
|
|
812
|
-
() => frames
|
|
813
|
-
"
|
|
1229
|
+
() => countFrames(frames, "speech_started") === speechStarts + 1,
|
|
1230
|
+
"Provider speech start did not interrupt playback",
|
|
814
1231
|
);
|
|
815
1232
|
await waitFor(() =>
|
|
816
1233
|
frames.some((frame) => frame.type === "turn_cancelled"),
|
|
817
1234
|
);
|
|
1235
|
+
const types = frames.map((frame) => frame.type);
|
|
1236
|
+
expect(types.lastIndexOf("speech_started")).toBeLessThan(
|
|
1237
|
+
types.indexOf("turn_cancelled"),
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
await session.close("client_end");
|
|
1241
|
+
},
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
test("provider speech starts are immediate and ignore duplicate or older turn indices", async () => {
|
|
1245
|
+
const { frames, session, transcribers } = createHarness({
|
|
1246
|
+
fluxConfig: FLUX_ON,
|
|
1247
|
+
bargeInMinSpeechMs: 5_000,
|
|
1248
|
+
});
|
|
1249
|
+
try {
|
|
1250
|
+
await session.start();
|
|
1251
|
+
await waitFor(() => transcribers.length === 1);
|
|
1252
|
+
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
1253
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
818
1254
|
|
|
1255
|
+
const transcriber = transcribers[0]!;
|
|
1256
|
+
transcriber.startOfTurn(2);
|
|
1257
|
+
await waitFor(() => countFrames(frames, "speech_started") === 1);
|
|
1258
|
+
transcriber.startOfTurn(2);
|
|
1259
|
+
transcriber.startOfTurn(1);
|
|
1260
|
+
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
1261
|
+
expect(countFrames(frames, "speech_started")).toBe(1);
|
|
1262
|
+
transcriber.startOfTurn(3);
|
|
1263
|
+
await waitFor(() => countFrames(frames, "speech_started") === 2);
|
|
1264
|
+
} finally {
|
|
819
1265
|
await session.close("client_end");
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
test("provider-confirmed quiet speech interrupts a thinking turn", async () => {
|
|
1270
|
+
const abort = mock();
|
|
1271
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
1272
|
+
fluxConfig: FLUX_ON,
|
|
1273
|
+
bargeInMinSpeechMs: 5_000,
|
|
1274
|
+
startVoiceTurn: async () => ({ turnId: "thinking-turn", abort }),
|
|
820
1275
|
});
|
|
821
|
-
|
|
1276
|
+
try {
|
|
1277
|
+
await session.start();
|
|
1278
|
+
await waitFor(() => transcribers.length === 1);
|
|
1279
|
+
const transcriber = transcribers[0]!;
|
|
1280
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1281
|
+
transcriber.startOfTurn(0);
|
|
1282
|
+
transcriber.endOfTurn("start an investigation", 0);
|
|
1283
|
+
await waitFor(() => turnCalls.length === 1);
|
|
1284
|
+
await session.handleBinaryAudio(pcm(100));
|
|
1285
|
+
|
|
1286
|
+
// The previous turn's duplicate cannot interrupt the reply it caused.
|
|
1287
|
+
transcriber.startOfTurn(0);
|
|
1288
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1289
|
+
transcriber.startOfTurn(1);
|
|
1290
|
+
await waitFor(() => countFrames(frames, "turn_cancelled") === 1);
|
|
1291
|
+
expect(countFrames(frames, "speech_started")).toBe(2);
|
|
1292
|
+
expect(abort).toHaveBeenCalledTimes(1);
|
|
1293
|
+
} finally {
|
|
1294
|
+
await session.close("client_end");
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test("provider speech start flushes the playback tail of a completed turn", async () => {
|
|
1299
|
+
const { frames, session, transcribers } = createHarness({
|
|
1300
|
+
fluxConfig: FLUX_ON,
|
|
1301
|
+
startVoiceTurn: autoCompletingTurn(),
|
|
1302
|
+
streamTtsAudio: async (options) => {
|
|
1303
|
+
const audio = pcm(100, SAMPLE_RATE * 2);
|
|
1304
|
+
options.onAudioChunk({
|
|
1305
|
+
type: "tts_audio",
|
|
1306
|
+
contentType: "audio/pcm",
|
|
1307
|
+
sampleRate: SAMPLE_RATE,
|
|
1308
|
+
dataBase64: Buffer.from(audio).toString("base64"),
|
|
1309
|
+
});
|
|
1310
|
+
return {
|
|
1311
|
+
provider: "fish-audio",
|
|
1312
|
+
contentType: "audio/pcm",
|
|
1313
|
+
sampleRate: SAMPLE_RATE,
|
|
1314
|
+
chunks: 1,
|
|
1315
|
+
bytes: audio.byteLength,
|
|
1316
|
+
};
|
|
1317
|
+
},
|
|
1318
|
+
});
|
|
1319
|
+
try {
|
|
1320
|
+
await session.start();
|
|
1321
|
+
await waitFor(() => transcribers.length === 1);
|
|
1322
|
+
const transcriber = transcribers[0]!;
|
|
1323
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1324
|
+
transcriber.startOfTurn(0);
|
|
1325
|
+
transcriber.endOfTurn("tell me a story", 0);
|
|
1326
|
+
await waitFor(() => countFrames(frames, "tts_done") === 1);
|
|
1327
|
+
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
1328
|
+
expect(countFrames(frames, "speech_started")).toBe(1);
|
|
1329
|
+
|
|
1330
|
+
transcriber.startOfTurn(1);
|
|
1331
|
+
await waitFor(() => countFrames(frames, "speech_started") === 2);
|
|
1332
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1333
|
+
} finally {
|
|
1334
|
+
await session.close("client_end");
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
test("provider speech start does not interrupt manual sessions", async () => {
|
|
1339
|
+
const { frames, session, transcribers } = createHarness({
|
|
1340
|
+
startFrame: { ...VAD_START_FRAME, turnDetection: "manual" },
|
|
1341
|
+
fluxConfig: FLUX_ON,
|
|
1342
|
+
});
|
|
1343
|
+
try {
|
|
1344
|
+
await session.start();
|
|
1345
|
+
await waitFor(() => transcribers.length === 1);
|
|
1346
|
+
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
1347
|
+
transcribers[0]?.startOfTurn(0);
|
|
1348
|
+
await flushAsyncCallbacks();
|
|
1349
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
1350
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1351
|
+
} finally {
|
|
1352
|
+
await session.close("client_end");
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
test("provider speech start discards a pending speculative reply", async () => {
|
|
1357
|
+
const discard = mock(async () => {});
|
|
1358
|
+
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
1359
|
+
fluxConfig: FLUX_OFF,
|
|
1360
|
+
startVoiceTurn: async () => ({
|
|
1361
|
+
turnId: "speculative-turn",
|
|
1362
|
+
abort: mock(),
|
|
1363
|
+
discard,
|
|
1364
|
+
}),
|
|
1365
|
+
});
|
|
1366
|
+
try {
|
|
1367
|
+
await session.start();
|
|
1368
|
+
await waitFor(() => transcribers.length === 1);
|
|
1369
|
+
const transcriber = transcribers[0]!;
|
|
1370
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1371
|
+
transcriber.startOfTurn(0);
|
|
1372
|
+
transcriber.emit({ type: "final", text: "what is the" });
|
|
1373
|
+
await waitFor(() => turnCalls.length === 1);
|
|
1374
|
+
expect(turnCalls[0]?.unifiedVerdict).toBeDefined();
|
|
1375
|
+
|
|
1376
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1377
|
+
expect(discard).not.toHaveBeenCalled();
|
|
1378
|
+
transcriber.startOfTurn(1);
|
|
1379
|
+
await waitFor(() => discard.mock.calls.length === 1);
|
|
1380
|
+
await flushAsyncCallbacks();
|
|
1381
|
+
expect(countFrames(frames, "speech_started")).toBe(2);
|
|
1382
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1383
|
+
} finally {
|
|
1384
|
+
await session.close("client_end");
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
822
1387
|
});
|
|
823
1388
|
|
|
824
1389
|
/**
|
|
@@ -843,6 +1408,29 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
843
1408
|
setConfig("services", { stt: { provider: "deepgram", providers: {} } });
|
|
844
1409
|
});
|
|
845
1410
|
|
|
1411
|
+
test("empty Flux updates do not prevent a language change during idle input", async () => {
|
|
1412
|
+
const { session, transcribers } = createHarness({ fluxConfig: FLUX_ON });
|
|
1413
|
+
try {
|
|
1414
|
+
await session.start();
|
|
1415
|
+
await waitFor(() => transcribers.length === 1);
|
|
1416
|
+
transcribers[0]!.emit({ type: "partial", text: "" });
|
|
1417
|
+
setConfig("services", {
|
|
1418
|
+
stt: {
|
|
1419
|
+
provider: "deepgram",
|
|
1420
|
+
providers: { deepgram: { model: "flux" } },
|
|
1421
|
+
language: "es",
|
|
1422
|
+
},
|
|
1423
|
+
});
|
|
1424
|
+
await session.handleBinaryAudio(pcm(0));
|
|
1425
|
+
await waitFor(() => transcribers.length === 2);
|
|
1426
|
+
expect(transcribers[0]!.stopped).toBe(true);
|
|
1427
|
+
await session.handleBinaryAudio(pcm(100));
|
|
1428
|
+
expect(transcribers[1]!.received.at(-1)).toEqual(Buffer.from(pcm(100)));
|
|
1429
|
+
} finally {
|
|
1430
|
+
await session.close("client_end");
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
|
|
846
1434
|
test("waits for turn-end when the boundary lands before the dial resolves", async () => {
|
|
847
1435
|
const gate = createDialGate();
|
|
848
1436
|
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
@@ -912,6 +1500,7 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
912
1500
|
() => countFrames(frames, "utterance_end") === 1,
|
|
913
1501
|
"The utterance deferred under the seeded latch never released",
|
|
914
1502
|
);
|
|
1503
|
+
expect(countFrames(frames, "speech_started")).toBe(1);
|
|
915
1504
|
await waitFor(
|
|
916
1505
|
() => transcribers[0]?.stopped === true,
|
|
917
1506
|
"The utterance never finished releasing",
|
|
@@ -924,6 +1513,127 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
924
1513
|
await session.close("client_end");
|
|
925
1514
|
});
|
|
926
1515
|
|
|
1516
|
+
test.each([true, false])(
|
|
1517
|
+
"replays local onset when the dial falls back from Flux (turn-end %s)",
|
|
1518
|
+
async (flagOn) => {
|
|
1519
|
+
const gate = createDialGate();
|
|
1520
|
+
const { frames, session } = createHarness({
|
|
1521
|
+
providerId: "vellum",
|
|
1522
|
+
fluxConfig: flagOn ? FLUX_ON : FLUX_OFF,
|
|
1523
|
+
silenceThresholdMs: 10_000,
|
|
1524
|
+
resolveGate: gate.promise,
|
|
1525
|
+
});
|
|
1526
|
+
try {
|
|
1527
|
+
await session.start();
|
|
1528
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1529
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
1530
|
+
gate.open();
|
|
1531
|
+
await waitFor(() => countFrames(frames, "speech_started") === 1);
|
|
1532
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1533
|
+
expect(countFrames(frames, "speech_started")).toBe(1);
|
|
1534
|
+
} finally {
|
|
1535
|
+
gate.open();
|
|
1536
|
+
await session.close("client_end");
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
test.each([
|
|
1542
|
+
{ providerId: "vellum" as const, audio: LOUD_CHUNK, interrupts: false },
|
|
1543
|
+
{
|
|
1544
|
+
providerId: "vellum" as const,
|
|
1545
|
+
audio: SUSTAINED_LOUD_CHUNK,
|
|
1546
|
+
interrupts: true,
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
providerId: "deepgram-flux" as const,
|
|
1550
|
+
audio: LOUD_CHUNK,
|
|
1551
|
+
interrupts: true,
|
|
1552
|
+
},
|
|
1553
|
+
])(
|
|
1554
|
+
"speech released before the dial interrupts only after provider or guard confirmation: $providerId",
|
|
1555
|
+
async ({ providerId, audio, interrupts }) => {
|
|
1556
|
+
const gate = createDialGate();
|
|
1557
|
+
const abort = mock();
|
|
1558
|
+
const { frames, session, transcribers } = createHarness({
|
|
1559
|
+
providerId,
|
|
1560
|
+
fluxConfig: FLUX_OFF,
|
|
1561
|
+
resolveGate: gate.promise,
|
|
1562
|
+
startVoiceTurn: async () => ({ turnId: "greeting-turn", abort }),
|
|
1563
|
+
});
|
|
1564
|
+
try {
|
|
1565
|
+
await session.start();
|
|
1566
|
+
await session.handleClientFrame({
|
|
1567
|
+
type: "text",
|
|
1568
|
+
text: "Say hello.",
|
|
1569
|
+
hidden: true,
|
|
1570
|
+
});
|
|
1571
|
+
await waitFor(() => countFrames(frames, "thinking") === 1);
|
|
1572
|
+
await session.handleBinaryAudio(audio);
|
|
1573
|
+
await waitFor(() => countFrames(frames, "utterance_end") === 1);
|
|
1574
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
1575
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1576
|
+
|
|
1577
|
+
gate.open();
|
|
1578
|
+
await gate.promise;
|
|
1579
|
+
const transcriber = transcribers[0]!;
|
|
1580
|
+
if (providerId === "deepgram-flux") {
|
|
1581
|
+
transcriber.onAudio = () => {
|
|
1582
|
+
transcriber.startOfTurn(0);
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
await waitFor(() => transcriber.stopped);
|
|
1586
|
+
await flushAsyncCallbacks();
|
|
1587
|
+
expect(countFrames(frames, "speech_started")).toBe(interrupts ? 1 : 0);
|
|
1588
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(interrupts ? 1 : 0);
|
|
1589
|
+
expect(abort).toHaveBeenCalledTimes(interrupts ? 1 : 0);
|
|
1590
|
+
} finally {
|
|
1591
|
+
gate.open();
|
|
1592
|
+
await session.close("client_end");
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
test("a fully parked follow-up retains its guard when the next dial falls back", async () => {
|
|
1598
|
+
const abort = mock();
|
|
1599
|
+
const options = {
|
|
1600
|
+
providerId: "deepgram-flux" as SttProviderId,
|
|
1601
|
+
fluxConfig: FLUX_ON,
|
|
1602
|
+
startVoiceTurn: async () => ({ turnId: "reply-turn", abort }),
|
|
1603
|
+
};
|
|
1604
|
+
const { frames, session, transcribers } = createHarness(options);
|
|
1605
|
+
try {
|
|
1606
|
+
await session.start();
|
|
1607
|
+
await waitFor(() => transcribers.length === 1);
|
|
1608
|
+
const first = transcribers[0]!;
|
|
1609
|
+
first.holdStopEvents = true;
|
|
1610
|
+
first.pendingFlushText = "tell me a story";
|
|
1611
|
+
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
1612
|
+
first.startOfTurn(0);
|
|
1613
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
1614
|
+
await waitFor(() => first.stopped);
|
|
1615
|
+
|
|
1616
|
+
// The first input is released but cannot dispatch until its final arrives.
|
|
1617
|
+
await session.handleBinaryAudio(SUSTAINED_LOUD_CHUNK);
|
|
1618
|
+
await sleep(PAST_SILENCE_BOUNDARY_MS);
|
|
1619
|
+
options.providerId = "vellum";
|
|
1620
|
+
first.flushStopEvents();
|
|
1621
|
+
await waitFor(() => countFrames(frames, "thinking") === 1);
|
|
1622
|
+
expect(countFrames(frames, "turn_cancelled")).toBe(0);
|
|
1623
|
+
|
|
1624
|
+
// Idle input arms the parked follow-up without any further speech.
|
|
1625
|
+
await session.handleBinaryAudio(pcm(0));
|
|
1626
|
+
await waitFor(() => transcribers.length === 2);
|
|
1627
|
+
await waitFor(() => countFrames(frames, "turn_cancelled") === 1);
|
|
1628
|
+
expect(abort).toHaveBeenCalledTimes(1);
|
|
1629
|
+
expect(transcribers[1]!.received).toContainEqual(
|
|
1630
|
+
Buffer.from(SUSTAINED_LOUD_CHUNK),
|
|
1631
|
+
);
|
|
1632
|
+
} finally {
|
|
1633
|
+
await session.close("client_end");
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
|
|
927
1637
|
test("seeds the latch from the live-voice role, not the global provider", async () => {
|
|
928
1638
|
// The configuration roles exist for: live voice on flux while the global
|
|
929
1639
|
// stays on a family that owns no turn boundary. Seeding from the global
|
|
@@ -950,6 +1660,7 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
950
1660
|
|
|
951
1661
|
// Deferred under the seeded latch: Flux owns this boundary.
|
|
952
1662
|
expect(countFrames(frames, "utterance_end")).toBe(0);
|
|
1663
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
953
1664
|
expect(turnCalls).toHaveLength(0);
|
|
954
1665
|
|
|
955
1666
|
gate.open();
|
|
@@ -966,7 +1677,7 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
966
1677
|
await session.close("client_end");
|
|
967
1678
|
});
|
|
968
1679
|
|
|
969
|
-
test("
|
|
1680
|
+
test("seeds only provider starts when provider end-of-turn is disabled", async () => {
|
|
970
1681
|
const gate = createDialGate();
|
|
971
1682
|
const { frames, session, transcribers, turnCalls } = createHarness({
|
|
972
1683
|
// Turn-end explicitly off while config still selects the flux family.
|
|
@@ -979,12 +1690,13 @@ describe("LiveVoiceSession Flux end-of-turn during the STT dial", () => {
|
|
|
979
1690
|
await session.start();
|
|
980
1691
|
await session.handleBinaryAudio(LOUD_CHUNK);
|
|
981
1692
|
|
|
982
|
-
//
|
|
1693
|
+
// The local silence boundary releases, but local onset cannot interrupt.
|
|
983
1694
|
await waitFor(
|
|
984
1695
|
() => countFrames(frames, "utterance_end") === 1,
|
|
985
1696
|
"The flag-off silence boundary stopped releasing during the dial",
|
|
986
1697
|
);
|
|
987
1698
|
expect(turnCalls).toHaveLength(0);
|
|
1699
|
+
expect(countFrames(frames, "speech_started")).toBe(0);
|
|
988
1700
|
|
|
989
1701
|
gate.open();
|
|
990
1702
|
await waitFor(() => transcribers.length > 0);
|