@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
|
@@ -8,17 +8,17 @@ import {
|
|
|
8
8
|
test,
|
|
9
9
|
} from "bun:test";
|
|
10
10
|
|
|
11
|
-
mock.module("
|
|
11
|
+
mock.module("../../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
|
|
12
12
|
|
|
13
13
|
// Guardian requests/deliveries are created through the gateway client; serve
|
|
14
14
|
// that surface from the in-memory sim so dispatch never opens IPC.
|
|
15
15
|
import {
|
|
16
16
|
bridgeState,
|
|
17
17
|
gatewayGuardianRequestsStoreBridge,
|
|
18
|
-
} from "
|
|
18
|
+
} from "../../__tests__/helpers/gateway-guardian-requests-store-bridge.js";
|
|
19
19
|
|
|
20
20
|
mock.module(
|
|
21
|
-
"
|
|
21
|
+
"../../channels/gateway-guardian-requests.js",
|
|
22
22
|
() => gatewayGuardianRequestsStoreBridge,
|
|
23
23
|
);
|
|
24
24
|
|
|
@@ -36,7 +36,7 @@ mock.module(
|
|
|
36
36
|
let mockResolvableProviderKeys: ((service: string) => string | null) | null =
|
|
37
37
|
null;
|
|
38
38
|
|
|
39
|
-
mock.module("
|
|
39
|
+
mock.module("../../security/secure-keys.js", () => ({
|
|
40
40
|
getSecureKeyAsync: async () => null,
|
|
41
41
|
getSecureKey: () => null,
|
|
42
42
|
getProviderKeyAsync: async (service: string) =>
|
|
@@ -45,7 +45,19 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
45
45
|
: "test-key",
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// Managed-speech defaulting reaches the platform client and the credential
|
|
49
|
+
// store. The controller tests drive the bridge mock on a microtask budget, so
|
|
50
|
+
// the configured providers stand as the effective ones here.
|
|
51
|
+
mock.module("../../config/managed-speech-defaults.js", () => ({
|
|
52
|
+
resolveEffectiveSpeechProviders: async (config?: {
|
|
53
|
+
services: { stt: { provider: string }; tts: { provider: string } };
|
|
54
|
+
}) => {
|
|
55
|
+
const services = (config ?? loadConfig()).services;
|
|
56
|
+
return { stt: services.stt.provider, tts: services.tts.provider };
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
mock.module("../../security/credential-key.js", () => ({
|
|
49
61
|
credentialKey: (...args: string[]) => args.join("/"),
|
|
50
62
|
}));
|
|
51
63
|
|
|
@@ -56,7 +68,10 @@ let mockSilenceTimeoutMs = 30_000;
|
|
|
56
68
|
let mockEndCallListenWindowMs = 0;
|
|
57
69
|
let mockEndCallDrainMaxWaitMs = 15_000;
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
import * as realCallConstants from "../call-constants.js";
|
|
72
|
+
|
|
73
|
+
mock.module("../call-constants.js", () => ({
|
|
74
|
+
...realCallConstants,
|
|
60
75
|
getMaxCallDurationMs: () => 12 * 60 * 1000,
|
|
61
76
|
getUserConsultationTimeoutMs: () => mockConsultationTimeoutMs,
|
|
62
77
|
getSilenceTimeoutMs: () => mockSilenceTimeoutMs,
|
|
@@ -110,7 +125,7 @@ let mockStartVoiceTurn: Mock<any>;
|
|
|
110
125
|
|
|
111
126
|
// ── Notification pipeline mock (prevent async handle leaks from fire-and-forget dispatches) ──
|
|
112
127
|
|
|
113
|
-
mock.module("
|
|
128
|
+
mock.module("../../notifications/emit-signal.js", () => ({
|
|
114
129
|
emitNotificationSignal: async () => ({
|
|
115
130
|
signalId: "mock-signal",
|
|
116
131
|
deduplicated: false,
|
|
@@ -122,7 +137,7 @@ mock.module("../notifications/emit-signal.js", () => ({
|
|
|
122
137
|
|
|
123
138
|
// Guardian principalId is resolved from the gateway binding reader. Mirror the
|
|
124
139
|
// vellum binding seeded by resetTables so guardian dispatch can resolve it.
|
|
125
|
-
mock.module("
|
|
140
|
+
mock.module("../../contacts/guardian-delivery-reader.js", () => ({
|
|
126
141
|
getGuardianDelivery: async () => [
|
|
127
142
|
{
|
|
128
143
|
channelType: "vellum",
|
|
@@ -139,7 +154,7 @@ mock.module("../contacts/guardian-delivery-reader.js", () => ({
|
|
|
139
154
|
anyGuardian: (list: unknown[]) => list[0],
|
|
140
155
|
}));
|
|
141
156
|
|
|
142
|
-
mock.module("../
|
|
157
|
+
mock.module("../voice-session-bridge.js", () => {
|
|
143
158
|
mockStartVoiceTurn = mock(createMockVoiceTurn(["Hello", " there"]));
|
|
144
159
|
return {
|
|
145
160
|
startVoiceTurn: (...args: unknown[]) => mockStartVoiceTurn(...args),
|
|
@@ -155,8 +170,8 @@ mock.module("../calls/voice-session-bridge.js", () => {
|
|
|
155
170
|
import {
|
|
156
171
|
_resetTtsProviderOverridesForTests,
|
|
157
172
|
_setTtsProviderForTests,
|
|
158
|
-
} from "
|
|
159
|
-
import type { TtsProvider } from "
|
|
173
|
+
} from "../../tts/provider-catalog.js";
|
|
174
|
+
import type { TtsProvider } from "../../tts/types.js";
|
|
160
175
|
|
|
161
176
|
function registerTestTtsProviders(): void {
|
|
162
177
|
_resetTtsProviderOverridesForTests();
|
|
@@ -206,26 +221,33 @@ registerTestTtsProviders();
|
|
|
206
221
|
|
|
207
222
|
// ── Import source modules after all mocks are registered ────────────
|
|
208
223
|
|
|
209
|
-
import {
|
|
210
|
-
import {
|
|
224
|
+
import { resetDbForTesting } from "../../__tests__/db-test-helpers.js";
|
|
225
|
+
import { createGuardianBinding } from "../../__tests__/helpers/create-guardian-binding.js";
|
|
226
|
+
import { setConfig } from "../../__tests__/helpers/set-config.js";
|
|
227
|
+
import { loadConfig } from "../../config/loader.js";
|
|
228
|
+
import type { VoiceProgressConfig } from "../../config/schemas/voice.js";
|
|
229
|
+
import { getMessages } from "../../persistence/conversation-crud.js";
|
|
230
|
+
import { getDb } from "../../persistence/db-connection.js";
|
|
231
|
+
import { initializeDb } from "../../persistence/db-init.js";
|
|
232
|
+
import { resetTestTables } from "../../persistence/raw-query.js";
|
|
233
|
+
import { conversations } from "../../persistence/schema/index.js";
|
|
234
|
+
import { CallController } from "../call-controller.js";
|
|
235
|
+
import { getCallController } from "../call-state.js";
|
|
211
236
|
import {
|
|
212
237
|
createCallSession,
|
|
213
238
|
getCallEvents,
|
|
214
239
|
getCallSession,
|
|
215
240
|
getPendingQuestion,
|
|
216
241
|
updateCallSession,
|
|
217
|
-
} from "../
|
|
218
|
-
import type { CallTransport } from "../
|
|
219
|
-
import {
|
|
220
|
-
import {
|
|
221
|
-
import {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
import { resetDbForTesting } from "./db-test-helpers.js";
|
|
227
|
-
import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
|
|
228
|
-
import { setConfig } from "./helpers/set-config.js";
|
|
242
|
+
} from "../call-store.js";
|
|
243
|
+
import type { CallTransport } from "../call-transport.js";
|
|
244
|
+
import type { VoiceProgressNarrator } from "../progress-narration.js";
|
|
245
|
+
import { resolveCallTtsProvider } from "../resolve-call-tts-provider.js";
|
|
246
|
+
import {
|
|
247
|
+
ESCALATION_CONTINUATION_CONTENT,
|
|
248
|
+
FALLBACK_ESCALATION_BRIDGE,
|
|
249
|
+
FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE,
|
|
250
|
+
} from "../voice-triage-escalate.js";
|
|
229
251
|
|
|
230
252
|
// Disable memory so persisted call messages skip background indexing, and
|
|
231
253
|
// seed the ingress base URL used for synthesized-audio play URLs.
|
|
@@ -245,6 +267,8 @@ afterAll(() => {
|
|
|
245
267
|
|
|
246
268
|
interface MockTransport extends CallTransport {
|
|
247
269
|
sentTokens: Array<{ token: string; last: boolean }>;
|
|
270
|
+
/** Tokens sent with the system-copy flag (no caller-language hint). */
|
|
271
|
+
systemCopyTokens: string[];
|
|
248
272
|
sentPlayUrls: string[];
|
|
249
273
|
endCalled: boolean;
|
|
250
274
|
endReason: string | undefined;
|
|
@@ -254,6 +278,7 @@ interface MockTransport extends CallTransport {
|
|
|
254
278
|
function createMockTransport(): MockTransport {
|
|
255
279
|
const state = {
|
|
256
280
|
sentTokens: [] as Array<{ token: string; last: boolean }>,
|
|
281
|
+
systemCopyTokens: [] as string[],
|
|
257
282
|
sentPlayUrls: [] as string[],
|
|
258
283
|
_endCalled: false,
|
|
259
284
|
_endReason: undefined as string | undefined,
|
|
@@ -264,6 +289,9 @@ function createMockTransport(): MockTransport {
|
|
|
264
289
|
get sentTokens() {
|
|
265
290
|
return state.sentTokens;
|
|
266
291
|
},
|
|
292
|
+
get systemCopyTokens() {
|
|
293
|
+
return state.systemCopyTokens;
|
|
294
|
+
},
|
|
267
295
|
get sentPlayUrls() {
|
|
268
296
|
return state.sentPlayUrls;
|
|
269
297
|
},
|
|
@@ -276,8 +304,15 @@ function createMockTransport(): MockTransport {
|
|
|
276
304
|
get cancelPendingSpeechCount() {
|
|
277
305
|
return state._cancelPendingSpeechCount;
|
|
278
306
|
},
|
|
279
|
-
sendTextToken(
|
|
307
|
+
sendTextToken(
|
|
308
|
+
token: string,
|
|
309
|
+
last: boolean,
|
|
310
|
+
opts?: { systemCopy?: boolean },
|
|
311
|
+
) {
|
|
280
312
|
state.sentTokens.push({ token, last });
|
|
313
|
+
if (opts?.systemCopy === true) {
|
|
314
|
+
state.systemCopyTokens.push(token);
|
|
315
|
+
}
|
|
281
316
|
},
|
|
282
317
|
sendPlayUrl(url: string) {
|
|
283
318
|
state.sentPlayUrls.push(url);
|
|
@@ -372,13 +407,16 @@ function setupController(
|
|
|
372
407
|
task?: string,
|
|
373
408
|
opts?: {
|
|
374
409
|
assistantId?: string;
|
|
375
|
-
trustContext?: import("
|
|
410
|
+
trustContext?: import("../../daemon/trust-context-types.js").TrustContext;
|
|
376
411
|
/** Simulate the media-stream transport's PCM requirement. */
|
|
377
412
|
requiresPcmAudio?: boolean;
|
|
378
413
|
/** Simulate a transport that gates teardown on playback drain. */
|
|
379
414
|
awaitPlaybackDrained?: () => Promise<void>;
|
|
380
415
|
/** Synthesis-language resolver, as the media-stream server wires it. */
|
|
381
416
|
resolveSynthesisLanguage?: () => string | undefined;
|
|
417
|
+
/** Progress narration seam; an explicit null keeps the call silent. */
|
|
418
|
+
progressNarrator?: VoiceProgressNarrator | null;
|
|
419
|
+
progressConfig?: VoiceProgressConfig;
|
|
382
420
|
},
|
|
383
421
|
) {
|
|
384
422
|
ensureConversation("conv-ctrl-test");
|
|
@@ -403,6 +441,11 @@ function setupController(
|
|
|
403
441
|
assistantId: opts?.assistantId,
|
|
404
442
|
trustContext: opts?.trustContext,
|
|
405
443
|
resolveSynthesisLanguage: opts?.resolveSynthesisLanguage,
|
|
444
|
+
// Tests that do not exercise narration keep the default narrator out of
|
|
445
|
+
// the way: a null narrator never generates or speaks.
|
|
446
|
+
progressNarrator:
|
|
447
|
+
opts?.progressNarrator === undefined ? null : opts.progressNarrator,
|
|
448
|
+
progressConfig: opts?.progressConfig,
|
|
406
449
|
});
|
|
407
450
|
return { session, relay: transport, controller };
|
|
408
451
|
}
|
|
@@ -506,35 +549,6 @@ describe("call-controller", () => {
|
|
|
506
549
|
controller.destroy();
|
|
507
550
|
});
|
|
508
551
|
|
|
509
|
-
test("handleCallerUtterance: includes speaker context in voice turn content", async () => {
|
|
510
|
-
mockStartVoiceTurn.mockImplementation(
|
|
511
|
-
async (opts: {
|
|
512
|
-
content: string;
|
|
513
|
-
onTextDelta: (t: string) => void;
|
|
514
|
-
onComplete: () => void;
|
|
515
|
-
}) => {
|
|
516
|
-
expect(opts.content).toContain(
|
|
517
|
-
'[SPEAKER id="speaker-1" label="Aaron" source="provider" confidence="0.91"]',
|
|
518
|
-
);
|
|
519
|
-
expect(opts.content).toContain("Can you summarize this meeting?");
|
|
520
|
-
opts.onTextDelta("Sure, here is a summary.");
|
|
521
|
-
opts.onComplete();
|
|
522
|
-
return { turnId: "run-1", abort: () => {} };
|
|
523
|
-
},
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
const { controller } = setupController();
|
|
527
|
-
|
|
528
|
-
await controller.handleCallerUtterance("Can you summarize this meeting?", {
|
|
529
|
-
speakerId: "speaker-1",
|
|
530
|
-
speakerLabel: "Aaron",
|
|
531
|
-
speakerConfidence: 0.91,
|
|
532
|
-
source: "provider",
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
controller.destroy();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
552
|
test("startInitialGreeting: sends CALL_OPENING content and strips control marker from speech", async () => {
|
|
539
553
|
let turnCount = 0;
|
|
540
554
|
mockStartVoiceTurn.mockImplementation(
|
|
@@ -2287,7 +2301,7 @@ describe("call-controller", () => {
|
|
|
2287
2301
|
|
|
2288
2302
|
// Compute expected digest independently using the same utility
|
|
2289
2303
|
const { computeToolApprovalDigest } =
|
|
2290
|
-
await import("
|
|
2304
|
+
await import("../../security/tool-approval-digest.js");
|
|
2291
2305
|
const expectedDigest = computeToolApprovalDigest("send_email", {
|
|
2292
2306
|
subject: "Hello",
|
|
2293
2307
|
to: "bob@example.com",
|
|
@@ -3087,7 +3101,7 @@ describe("call-controller", () => {
|
|
|
3087
3101
|
controller.destroy();
|
|
3088
3102
|
});
|
|
3089
3103
|
|
|
3090
|
-
test("synthesized provider: stays 'processing' during synthesis latency
|
|
3104
|
+
test("synthesized provider: stays 'processing' during synthesis latency until the play URL is sent", async () => {
|
|
3091
3105
|
const cfg = loadConfig();
|
|
3092
3106
|
cfg.services.tts.provider = "fish-audio";
|
|
3093
3107
|
cfg.services.tts.providers["fish-audio"].referenceId = "fish-ref-123";
|
|
@@ -3128,11 +3142,11 @@ describe("call-controller", () => {
|
|
|
3128
3142
|
await new Promise((r) => setTimeout(r, 20));
|
|
3129
3143
|
|
|
3130
3144
|
// No audio has reached the caller yet (play URL not sent), so the controller
|
|
3131
|
-
// must stay in `processing
|
|
3132
|
-
//
|
|
3145
|
+
// must stay in `processing`: the state tells the transport nothing is
|
|
3146
|
+
// audible, and only sustained caller speech (the transport's guard) may
|
|
3147
|
+
// cut the inaudible turn off. See JARVIS-1232.
|
|
3133
3148
|
expect(relay.sentPlayUrls.length).toBe(0);
|
|
3134
3149
|
expect(controller.getState()).toBe("processing");
|
|
3135
|
-
expect(controller.handleBargeIn()).toBe(false);
|
|
3136
3150
|
|
|
3137
3151
|
// Release audio → play URL sent → turn finishes and returns to idle.
|
|
3138
3152
|
releaseChunk?.();
|
|
@@ -3404,7 +3418,7 @@ describe("call-controller", () => {
|
|
|
3404
3418
|
function registerFishAudioSegmentRecorder(opts?: {
|
|
3405
3419
|
onSynthesizeStream?: (
|
|
3406
3420
|
text: string,
|
|
3407
|
-
request: import("
|
|
3421
|
+
request: import("../../tts/types.js").TtsSynthesisRequest,
|
|
3408
3422
|
onChunk: (chunk: Buffer) => void,
|
|
3409
3423
|
) => Promise<void>;
|
|
3410
3424
|
}): { synthesizedTexts: string[] } {
|
|
@@ -4220,6 +4234,415 @@ describe("call-controller", () => {
|
|
|
4220
4234
|
});
|
|
4221
4235
|
});
|
|
4222
4236
|
|
|
4237
|
+
// ── Triage-and-escalate routing ─────────────────────────────────────
|
|
4238
|
+
|
|
4239
|
+
describe("triage-and-escalate routing", () => {
|
|
4240
|
+
interface LegOpts {
|
|
4241
|
+
content: string;
|
|
4242
|
+
routingLeg?: string;
|
|
4243
|
+
spokenEscalationBridge?: string;
|
|
4244
|
+
launchedAtMs?: number;
|
|
4245
|
+
voiceTelemetry?: { sessionId: string; entry?: string };
|
|
4246
|
+
callbacks?: {
|
|
4247
|
+
persisted_user_message_id?: (id: string) => void;
|
|
4248
|
+
persisted_assistant_message_id?: (id: string) => void;
|
|
4249
|
+
tool_use_start?: (
|
|
4250
|
+
toolName: string,
|
|
4251
|
+
detail?: { toolUseId?: string },
|
|
4252
|
+
) => void;
|
|
4253
|
+
tool_result?: (event: {
|
|
4254
|
+
toolName: string;
|
|
4255
|
+
toolUseId?: string;
|
|
4256
|
+
isError?: boolean;
|
|
4257
|
+
resultPreview: string;
|
|
4258
|
+
}) => void;
|
|
4259
|
+
};
|
|
4260
|
+
onTextDelta: (t: string) => void;
|
|
4261
|
+
onComplete: () => void;
|
|
4262
|
+
signal?: AbortSignal;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
/**
|
|
4266
|
+
* Script the bridge one leg per call: the front-door leg's tokens first,
|
|
4267
|
+
* then the escalated leg's. Records every call's options and which legs
|
|
4268
|
+
* the controller aborted.
|
|
4269
|
+
*/
|
|
4270
|
+
function scriptLegs(
|
|
4271
|
+
legs: string[][],
|
|
4272
|
+
beforeComplete?: (opts: LegOpts, index: number) => void,
|
|
4273
|
+
) {
|
|
4274
|
+
const calls: LegOpts[] = [];
|
|
4275
|
+
const aborted: number[] = [];
|
|
4276
|
+
mockStartVoiceTurn.mockImplementation(async (opts: LegOpts) => {
|
|
4277
|
+
const index = calls.length;
|
|
4278
|
+
calls.push(opts);
|
|
4279
|
+
for (const token of legs[index] ?? []) {
|
|
4280
|
+
opts.onTextDelta(token);
|
|
4281
|
+
}
|
|
4282
|
+
beforeComplete?.(opts, index);
|
|
4283
|
+
opts.onComplete();
|
|
4284
|
+
return { turnId: `run-${index}`, abort: () => aborted.push(index) };
|
|
4285
|
+
});
|
|
4286
|
+
return { calls, aborted };
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
function spokenText(relay: MockTransport): string {
|
|
4290
|
+
return relay.sentTokens
|
|
4291
|
+
.filter((t) => !t.last)
|
|
4292
|
+
.map((t) => t.token)
|
|
4293
|
+
.join("");
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
function assistantSpokePayload(sessionId: string): Record<string, unknown> {
|
|
4297
|
+
const spoke = getCallEvents(sessionId).filter(
|
|
4298
|
+
(e) => e.eventType === "assistant_spoke",
|
|
4299
|
+
);
|
|
4300
|
+
expect(spoke.length).toBe(1);
|
|
4301
|
+
return JSON.parse(spoke[0].payloadJson) as Record<string, unknown>;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
test("a front-door answer runs one leg and is spoken verbatim", async () => {
|
|
4305
|
+
const { calls, aborted } = scriptLegs([["Hello", " there."]]);
|
|
4306
|
+
const { session, relay, controller } = setupController();
|
|
4307
|
+
|
|
4308
|
+
await controller.handleCallerUtterance("Hi");
|
|
4309
|
+
|
|
4310
|
+
expect(calls.length).toBe(1);
|
|
4311
|
+
expect(calls[0].routingLeg).toBe("front-door");
|
|
4312
|
+
expect(calls[0].content).toBe("Hi");
|
|
4313
|
+
expect(calls[0].spokenEscalationBridge).toBeUndefined();
|
|
4314
|
+
expect(aborted).toEqual([]);
|
|
4315
|
+
expect(spokenText(relay)).toContain("Hello there.");
|
|
4316
|
+
expect(assistantSpokePayload(session.id).text).toBe("Hello there.");
|
|
4317
|
+
|
|
4318
|
+
controller.destroy();
|
|
4319
|
+
});
|
|
4320
|
+
|
|
4321
|
+
test("an escalate verdict speaks the holding phrase, aborts the front-door leg, and continues on the escalated leg", async () => {
|
|
4322
|
+
const { calls, aborted } = scriptLegs([
|
|
4323
|
+
// The verdict token may arrive split across deltas; the leading
|
|
4324
|
+
// "[" is held until it classifies.
|
|
4325
|
+
["[", "1] Let me check", " that.", " Ignored past the cap."],
|
|
4326
|
+
["The answer is 42."],
|
|
4327
|
+
]);
|
|
4328
|
+
const { session, relay, controller } = setupController();
|
|
4329
|
+
|
|
4330
|
+
await controller.handleCallerUtterance("What is six times seven?");
|
|
4331
|
+
|
|
4332
|
+
expect(calls.length).toBe(2);
|
|
4333
|
+
expect(calls[0].routingLeg).toBe("front-door");
|
|
4334
|
+
expect(calls[1].routingLeg).toBe("escalated");
|
|
4335
|
+
expect(calls[1].content).toBe(ESCALATION_CONTINUATION_CONTENT);
|
|
4336
|
+
expect(calls[1].spokenEscalationBridge).toBe("Let me check that.");
|
|
4337
|
+
expect(aborted).toEqual([0]);
|
|
4338
|
+
|
|
4339
|
+
const spoken = spokenText(relay);
|
|
4340
|
+
expect(spoken).toContain("Let me check that.");
|
|
4341
|
+
expect(spoken).toContain("The answer is 42.");
|
|
4342
|
+
expect(spoken.indexOf("Let me check that.")).toBeLessThan(
|
|
4343
|
+
spoken.indexOf("The answer is 42."),
|
|
4344
|
+
);
|
|
4345
|
+
expect(spoken).not.toContain("[1]");
|
|
4346
|
+
expect(spoken).not.toContain("Ignored past the cap.");
|
|
4347
|
+
expect(assistantSpokePayload(session.id).text).toBe(
|
|
4348
|
+
"Let me check that. The answer is 42.",
|
|
4349
|
+
);
|
|
4350
|
+
// One end-of-turn signal for the whole turn, after the escalated leg.
|
|
4351
|
+
expect(relay.sentTokens.filter((t) => t.last).length).toBe(1);
|
|
4352
|
+
|
|
4353
|
+
controller.destroy();
|
|
4354
|
+
});
|
|
4355
|
+
|
|
4356
|
+
test("a bare escalate verdict speaks the canned bridge, audio-only", async () => {
|
|
4357
|
+
const { calls } = scriptLegs([["[1]"], ["Forty-two."]]);
|
|
4358
|
+
const { session, relay, controller } = setupController();
|
|
4359
|
+
|
|
4360
|
+
await controller.handleCallerUtterance("What is six times seven?");
|
|
4361
|
+
|
|
4362
|
+
expect(calls.length).toBe(2);
|
|
4363
|
+
expect(calls[1].spokenEscalationBridge).toBe(FALLBACK_ESCALATION_BRIDGE);
|
|
4364
|
+
expect(spokenText(relay)).toContain(FALLBACK_ESCALATION_BRIDGE);
|
|
4365
|
+
// The canned bridge is not the model's speech: the turn's text (and the
|
|
4366
|
+
// transcript row the bridge keeps) carry only the escalated answer.
|
|
4367
|
+
expect(assistantSpokePayload(session.id).text).toBe("Forty-two.");
|
|
4368
|
+
|
|
4369
|
+
controller.destroy();
|
|
4370
|
+
});
|
|
4371
|
+
|
|
4372
|
+
test("a leg cancelled mid-bridge never spawns the escalated leg", async () => {
|
|
4373
|
+
const calls: LegOpts[] = [];
|
|
4374
|
+
let releaseLeg: () => void = () => {};
|
|
4375
|
+
mockStartVoiceTurn.mockImplementation(async (opts: LegOpts) => {
|
|
4376
|
+
calls.push(opts);
|
|
4377
|
+
opts.onTextDelta("[1] Let me");
|
|
4378
|
+
await new Promise<void>((resolve) => {
|
|
4379
|
+
releaseLeg = resolve;
|
|
4380
|
+
opts.signal?.addEventListener("abort", () => resolve());
|
|
4381
|
+
});
|
|
4382
|
+
// The bridge reports a cancelled leg's completion too.
|
|
4383
|
+
opts.onComplete();
|
|
4384
|
+
return { turnId: "run-cancelled", abort: () => releaseLeg() };
|
|
4385
|
+
});
|
|
4386
|
+
const { relay, controller } = setupController();
|
|
4387
|
+
|
|
4388
|
+
const turnPromise = controller.handleCallerUtterance("Hi");
|
|
4389
|
+
for (let i = 0; i < 10; i++) {
|
|
4390
|
+
await Promise.resolve();
|
|
4391
|
+
}
|
|
4392
|
+
expect(calls.length).toBe(1);
|
|
4393
|
+
|
|
4394
|
+
controller.handleInterrupt();
|
|
4395
|
+
await turnPromise;
|
|
4396
|
+
|
|
4397
|
+
expect(calls.length).toBe(1);
|
|
4398
|
+
expect(spokenText(relay)).toBe("");
|
|
4399
|
+
|
|
4400
|
+
controller.destroy();
|
|
4401
|
+
});
|
|
4402
|
+
|
|
4403
|
+
test("passes the launch time, phone telemetry, and callbacks to the bridge", async () => {
|
|
4404
|
+
const before = Date.now();
|
|
4405
|
+
const { calls } = scriptLegs([["Done."]], (opts) => {
|
|
4406
|
+
opts.callbacks?.persisted_user_message_id?.("user-row-1");
|
|
4407
|
+
opts.callbacks?.tool_use_start?.("web_search", { toolUseId: "tu-1" });
|
|
4408
|
+
opts.callbacks?.tool_result?.({
|
|
4409
|
+
toolName: "web_search",
|
|
4410
|
+
toolUseId: "tu-1",
|
|
4411
|
+
isError: false,
|
|
4412
|
+
resultPreview: "three results",
|
|
4413
|
+
});
|
|
4414
|
+
opts.callbacks?.persisted_assistant_message_id?.("assistant-row-1");
|
|
4415
|
+
});
|
|
4416
|
+
const { session, controller } = setupController();
|
|
4417
|
+
|
|
4418
|
+
await controller.handleCallerUtterance("Look that up");
|
|
4419
|
+
|
|
4420
|
+
expect(calls.length).toBe(1);
|
|
4421
|
+
expect(calls[0].launchedAtMs).toBeGreaterThanOrEqual(before);
|
|
4422
|
+
expect(calls[0].launchedAtMs).toBeLessThanOrEqual(Date.now());
|
|
4423
|
+
expect(calls[0].voiceTelemetry).toEqual({
|
|
4424
|
+
sessionId: session.id,
|
|
4425
|
+
entry: "phone_inbound",
|
|
4426
|
+
});
|
|
4427
|
+
|
|
4428
|
+
const events = getCallEvents(session.id);
|
|
4429
|
+
const started = events.filter((e) => e.eventType === "tool_use_started");
|
|
4430
|
+
expect(started.length).toBe(1);
|
|
4431
|
+
expect(JSON.parse(started[0].payloadJson)).toEqual({
|
|
4432
|
+
toolName: "web_search",
|
|
4433
|
+
toolUseId: "tu-1",
|
|
4434
|
+
});
|
|
4435
|
+
const completed = events.filter(
|
|
4436
|
+
(e) => e.eventType === "tool_use_completed",
|
|
4437
|
+
);
|
|
4438
|
+
expect(completed.length).toBe(1);
|
|
4439
|
+
expect(JSON.parse(completed[0].payloadJson)).toEqual({
|
|
4440
|
+
toolName: "web_search",
|
|
4441
|
+
toolUseId: "tu-1",
|
|
4442
|
+
isError: false,
|
|
4443
|
+
resultPreview: "three results",
|
|
4444
|
+
});
|
|
4445
|
+
expect(assistantSpokePayload(session.id)).toEqual({
|
|
4446
|
+
text: "Done.",
|
|
4447
|
+
userMessageId: "user-row-1",
|
|
4448
|
+
assistantMessageId: "assistant-row-1",
|
|
4449
|
+
});
|
|
4450
|
+
|
|
4451
|
+
controller.destroy();
|
|
4452
|
+
});
|
|
4453
|
+
|
|
4454
|
+
test("the escalated leg's hidden continuation row is not the caller's utterance", async () => {
|
|
4455
|
+
const { calls } = scriptLegs(
|
|
4456
|
+
[["[1] One moment."], ["Here you go."]],
|
|
4457
|
+
(opts, index) => {
|
|
4458
|
+
opts.callbacks?.persisted_user_message_id?.(`user-row-${index}`);
|
|
4459
|
+
opts.callbacks?.persisted_assistant_message_id?.(
|
|
4460
|
+
`assistant-row-${index}`,
|
|
4461
|
+
);
|
|
4462
|
+
},
|
|
4463
|
+
);
|
|
4464
|
+
const { session, controller } = setupController();
|
|
4465
|
+
|
|
4466
|
+
await controller.handleCallerUtterance("Hi");
|
|
4467
|
+
|
|
4468
|
+
expect(calls.length).toBe(2);
|
|
4469
|
+
expect(assistantSpokePayload(session.id)).toEqual({
|
|
4470
|
+
text: "One moment. Here you go.",
|
|
4471
|
+
userMessageId: "user-row-0",
|
|
4472
|
+
assistantMessageId: "assistant-row-1",
|
|
4473
|
+
});
|
|
4474
|
+
|
|
4475
|
+
controller.destroy();
|
|
4476
|
+
});
|
|
4477
|
+
|
|
4478
|
+
test("native route: a canned bridge in a language the table lacks goes out as system copy", async () => {
|
|
4479
|
+
scriptLegs([["[1]"], ["Forty-two."]]);
|
|
4480
|
+
const { relay, controller } = setupController(undefined, {
|
|
4481
|
+
resolveSynthesisLanguage: () => "ko",
|
|
4482
|
+
});
|
|
4483
|
+
|
|
4484
|
+
await controller.handleCallerUtterance("What is six times seven?");
|
|
4485
|
+
|
|
4486
|
+
// The English fallback must not ride the caller's Korean hint.
|
|
4487
|
+
expect(relay.systemCopyTokens).toEqual([
|
|
4488
|
+
`${FALLBACK_ESCALATION_BRIDGE} `,
|
|
4489
|
+
]);
|
|
4490
|
+
expect(spokenText(relay)).toContain("Forty-two.");
|
|
4491
|
+
|
|
4492
|
+
controller.destroy();
|
|
4493
|
+
});
|
|
4494
|
+
|
|
4495
|
+
test("native route: a localized canned bridge rides the caller's language like model text", async () => {
|
|
4496
|
+
scriptLegs([["[1]"], ["Cuarenta y dos."]]);
|
|
4497
|
+
const { relay, controller } = setupController(undefined, {
|
|
4498
|
+
resolveSynthesisLanguage: () => "es",
|
|
4499
|
+
});
|
|
4500
|
+
|
|
4501
|
+
await controller.handleCallerUtterance("¿Cuánto es seis por siete?");
|
|
4502
|
+
|
|
4503
|
+
expect(relay.systemCopyTokens).toEqual([]);
|
|
4504
|
+
expect(spokenText(relay)).toContain(
|
|
4505
|
+
FALLBACK_ESCALATION_BRIDGE_BY_LANGUAGE.es,
|
|
4506
|
+
);
|
|
4507
|
+
|
|
4508
|
+
controller.destroy();
|
|
4509
|
+
});
|
|
4510
|
+
|
|
4511
|
+
test("synthesized route: the canned bridge carries its own language hint, the answer the caller's", async () => {
|
|
4512
|
+
const requests: Array<{ text: string; language: string | undefined }> =
|
|
4513
|
+
[];
|
|
4514
|
+
registerFishAudioSegmentRecorder({
|
|
4515
|
+
onSynthesizeStream: async (text, request) => {
|
|
4516
|
+
requests.push({ text, language: request.language });
|
|
4517
|
+
},
|
|
4518
|
+
});
|
|
4519
|
+
scriptLegs([["[1]"], ["Forty-two."]]);
|
|
4520
|
+
const { controller } = setupController(undefined, {
|
|
4521
|
+
resolveSynthesisLanguage: () => "ko",
|
|
4522
|
+
});
|
|
4523
|
+
|
|
4524
|
+
await controller.handleCallerUtterance("What is six times seven?");
|
|
4525
|
+
|
|
4526
|
+
expect(requests).toEqual([
|
|
4527
|
+
{ text: FALLBACK_ESCALATION_BRIDGE, language: "en" },
|
|
4528
|
+
{ text: "Forty-two.", language: "ko" },
|
|
4529
|
+
]);
|
|
4530
|
+
|
|
4531
|
+
controller.destroy();
|
|
4532
|
+
});
|
|
4533
|
+
|
|
4534
|
+
test("progress narration speaks into the escalated leg's tool loop, audio-only", async () => {
|
|
4535
|
+
const narrationConfig: VoiceProgressConfig = {
|
|
4536
|
+
enabled: true,
|
|
4537
|
+
opsThreshold: 1,
|
|
4538
|
+
idleIntervalMs: 60_000,
|
|
4539
|
+
maxSilenceMs: 60_000,
|
|
4540
|
+
longOpMs: 15_000,
|
|
4541
|
+
minGapMs: 10,
|
|
4542
|
+
generationTimeoutMs: 1_500,
|
|
4543
|
+
};
|
|
4544
|
+
const narrator: VoiceProgressNarrator = {
|
|
4545
|
+
generateProgressText: async (input) => {
|
|
4546
|
+
expect(input.transcriptSoFar).toBe("What is on my calendar?");
|
|
4547
|
+
expect(input.currentOp?.toolName).toBe("calendar_list");
|
|
4548
|
+
return "Pulling up your calendar now.";
|
|
4549
|
+
},
|
|
4550
|
+
};
|
|
4551
|
+
const { session, relay, controller } = setupController(undefined, {
|
|
4552
|
+
progressNarrator: narrator,
|
|
4553
|
+
progressConfig: narrationConfig,
|
|
4554
|
+
});
|
|
4555
|
+
const calls: LegOpts[] = [];
|
|
4556
|
+
mockStartVoiceTurn.mockImplementation(async (opts: LegOpts) => {
|
|
4557
|
+
const index = calls.length;
|
|
4558
|
+
calls.push(opts);
|
|
4559
|
+
if (index === 0) {
|
|
4560
|
+
opts.onTextDelta("[1] One moment.");
|
|
4561
|
+
opts.onComplete();
|
|
4562
|
+
return { turnId: "run-front", abort: () => {} };
|
|
4563
|
+
}
|
|
4564
|
+
// The escalated leg runs a tool; narration must land while it runs.
|
|
4565
|
+
// The bridge just spoken is a floor holder, so the tool starts after
|
|
4566
|
+
// the (test-short) minimum gap.
|
|
4567
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
4568
|
+
opts.callbacks?.tool_use_start?.("calendar_list", { toolUseId: "c1" });
|
|
4569
|
+
await pollUntil(() =>
|
|
4570
|
+
relay.sentTokens.some((t) =>
|
|
4571
|
+
t.token.includes("Pulling up your calendar now."),
|
|
4572
|
+
),
|
|
4573
|
+
);
|
|
4574
|
+
opts.callbacks?.tool_result?.({
|
|
4575
|
+
toolName: "calendar_list",
|
|
4576
|
+
toolUseId: "c1",
|
|
4577
|
+
resultPreview: "two events",
|
|
4578
|
+
});
|
|
4579
|
+
opts.onTextDelta("You have two events today.");
|
|
4580
|
+
opts.onComplete();
|
|
4581
|
+
return { turnId: "run-escalated", abort: () => {} };
|
|
4582
|
+
});
|
|
4583
|
+
|
|
4584
|
+
await controller.handleCallerUtterance("What is on my calendar?");
|
|
4585
|
+
|
|
4586
|
+
const spoken = spokenText(relay);
|
|
4587
|
+
expect(spoken.indexOf("One moment.")).toBeLessThan(
|
|
4588
|
+
spoken.indexOf("Pulling up your calendar now."),
|
|
4589
|
+
);
|
|
4590
|
+
expect(spoken.indexOf("Pulling up your calendar now.")).toBeLessThan(
|
|
4591
|
+
spoken.indexOf("You have two events today."),
|
|
4592
|
+
);
|
|
4593
|
+
// Narration is audio-only: the turn's recorded text is the model's.
|
|
4594
|
+
expect(assistantSpokePayload(session.id).text).toBe(
|
|
4595
|
+
"One moment. You have two events today.",
|
|
4596
|
+
);
|
|
4597
|
+
|
|
4598
|
+
controller.destroy();
|
|
4599
|
+
});
|
|
4600
|
+
|
|
4601
|
+
test("no narration without a narrator", async () => {
|
|
4602
|
+
const { relay, controller } = setupController(undefined, {
|
|
4603
|
+
progressNarrator: null,
|
|
4604
|
+
progressConfig: {
|
|
4605
|
+
enabled: true,
|
|
4606
|
+
opsThreshold: 1,
|
|
4607
|
+
idleIntervalMs: 60_000,
|
|
4608
|
+
maxSilenceMs: 60_000,
|
|
4609
|
+
longOpMs: 15_000,
|
|
4610
|
+
minGapMs: 10,
|
|
4611
|
+
generationTimeoutMs: 1_500,
|
|
4612
|
+
},
|
|
4613
|
+
});
|
|
4614
|
+
scriptLegs([["Done."]], (opts) => {
|
|
4615
|
+
opts.callbacks?.tool_use_start?.("web_search", { toolUseId: "t1" });
|
|
4616
|
+
opts.callbacks?.tool_result?.({
|
|
4617
|
+
toolName: "web_search",
|
|
4618
|
+
toolUseId: "t1",
|
|
4619
|
+
resultPreview: "ok",
|
|
4620
|
+
});
|
|
4621
|
+
});
|
|
4622
|
+
|
|
4623
|
+
await controller.handleCallerUtterance("Look it up");
|
|
4624
|
+
|
|
4625
|
+
expect(spokenText(relay)).toBe("Done.");
|
|
4626
|
+
|
|
4627
|
+
controller.destroy();
|
|
4628
|
+
});
|
|
4629
|
+
|
|
4630
|
+
test("an outbound call reports the outbound entry", async () => {
|
|
4631
|
+
const { calls } = scriptLegs([["Hello, this is Ava."]]);
|
|
4632
|
+
const { session, controller } = setupController("Book a table for two");
|
|
4633
|
+
|
|
4634
|
+
await controller.startInitialGreeting();
|
|
4635
|
+
|
|
4636
|
+
expect(calls.length).toBe(1);
|
|
4637
|
+
expect(calls[0].voiceTelemetry).toEqual({
|
|
4638
|
+
sessionId: session.id,
|
|
4639
|
+
entry: "phone_outbound",
|
|
4640
|
+
});
|
|
4641
|
+
|
|
4642
|
+
controller.destroy();
|
|
4643
|
+
});
|
|
4644
|
+
});
|
|
4645
|
+
|
|
4223
4646
|
// ── handleBargeIn ───────────────────────────────────────────────────
|
|
4224
4647
|
|
|
4225
4648
|
describe("handleBargeIn", () => {
|
|
@@ -4242,10 +4665,11 @@ describe("call-controller", () => {
|
|
|
4242
4665
|
controller.destroy();
|
|
4243
4666
|
});
|
|
4244
4667
|
|
|
4245
|
-
test("handleBargeIn
|
|
4668
|
+
test("handleBargeIn accepts a barge-in while still processing and aborts the silent turn", async () => {
|
|
4246
4669
|
// Simulate a turn stuck waiting for the processing lock: no tokens
|
|
4247
|
-
// emitted, no completion. The controller
|
|
4248
|
-
//
|
|
4670
|
+
// emitted, no completion. The controller stays in `processing`, and a
|
|
4671
|
+
// sustained caller barge-in (the transport's guard has already
|
|
4672
|
+
// vouched for it) cuts the thinking turn off before it speaks.
|
|
4249
4673
|
mockStartVoiceTurn.mockImplementation(
|
|
4250
4674
|
async (opts: {
|
|
4251
4675
|
onTextDelta: (t: string) => void;
|
|
@@ -4270,18 +4694,17 @@ describe("call-controller", () => {
|
|
|
4270
4694
|
|
|
4271
4695
|
const onAccepted = mock(() => {});
|
|
4272
4696
|
const bargeResult = controller.handleBargeIn(onAccepted);
|
|
4273
|
-
expect(bargeResult).toBe(
|
|
4274
|
-
expect(onAccepted).
|
|
4275
|
-
//
|
|
4276
|
-
expect(controller.getState()).toBe("
|
|
4697
|
+
expect(bargeResult).toBe(true);
|
|
4698
|
+
expect(onAccepted).toHaveBeenCalledTimes(1);
|
|
4699
|
+
// The turn is aborted; nothing was spoken, so no end-of-turn token.
|
|
4700
|
+
expect(controller.getState()).toBe("idle");
|
|
4277
4701
|
const endTokens = relay.sentTokens.filter(
|
|
4278
4702
|
(t) => t.last === true && t.token === "",
|
|
4279
4703
|
);
|
|
4280
4704
|
expect(endTokens.length).toBe(0);
|
|
4281
4705
|
|
|
4282
|
-
// Cleanup: abort the pending turn
|
|
4283
|
-
controller.destroy();
|
|
4284
4706
|
await turnPromise.catch(() => {});
|
|
4707
|
+
controller.destroy();
|
|
4285
4708
|
});
|
|
4286
4709
|
|
|
4287
4710
|
test("stays in processing until first token, then flips to speaking and barge-in is accepted", async () => {
|
|
@@ -4323,9 +4746,7 @@ describe("call-controller", () => {
|
|
|
4323
4746
|
await Promise.resolve();
|
|
4324
4747
|
}
|
|
4325
4748
|
|
|
4326
|
-
// Before any token: processing
|
|
4327
|
-
expect(controller.getState()).toBe("processing");
|
|
4328
|
-
expect(controller.handleBargeIn()).toBe(false);
|
|
4749
|
+
// Before any token: processing (no audio out yet).
|
|
4329
4750
|
expect(controller.getState()).toBe("processing");
|
|
4330
4751
|
|
|
4331
4752
|
// Release the first token → controller flips to speaking.
|
|
@@ -4343,7 +4764,7 @@ describe("call-controller", () => {
|
|
|
4343
4764
|
await turnPromise.catch(() => {});
|
|
4344
4765
|
});
|
|
4345
4766
|
|
|
4346
|
-
test("buffering transport (media-stream): streamed tokens do not flip to speaking; barge-in
|
|
4767
|
+
test("buffering transport (media-stream): streamed tokens do not flip to speaking; a barge-in still cuts the thinking turn off", async () => {
|
|
4347
4768
|
// Simulates the media-stream transport: sendTextToken(.., false)
|
|
4348
4769
|
// only buffers; audio starts later. The controller must stay in
|
|
4349
4770
|
// `processing` until the transport's audio-start signal fires, so
|
|
@@ -4387,17 +4808,17 @@ describe("call-controller", () => {
|
|
|
4387
4808
|
expect(controller.getState()).toBe("processing");
|
|
4388
4809
|
expect(audioStartCallback).not.toBeNull();
|
|
4389
4810
|
|
|
4390
|
-
//
|
|
4391
|
-
|
|
4392
|
-
expect(controller.
|
|
4811
|
+
// Sustained caller speech mid-generation: the buffered, unspoken turn
|
|
4812
|
+
// is cut off like a spoken one would be, and its text is discarded.
|
|
4813
|
+
expect(controller.handleBargeIn()).toBe(true);
|
|
4814
|
+
expect(controller.getState()).toBe("idle");
|
|
4393
4815
|
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
await turnPromise;
|
|
4816
|
+
await turnPromise.catch(() => {});
|
|
4817
|
+
// No end-of-turn token: nothing had been spoken.
|
|
4397
4818
|
const endTokens = relay.sentTokens.filter(
|
|
4398
4819
|
(t) => t.last === true && t.token === "",
|
|
4399
4820
|
);
|
|
4400
|
-
expect(endTokens.length).toBe(
|
|
4821
|
+
expect(endTokens.length).toBe(0);
|
|
4401
4822
|
|
|
4402
4823
|
controller.destroy();
|
|
4403
4824
|
});
|