@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
|
@@ -32,22 +32,29 @@ mock.module("../../daemon/conversation-store.js", () => ({
|
|
|
32
32
|
getOrCreateConversation: async () => fakeConversation,
|
|
33
33
|
}));
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
// The escalation judge's verdict for front-door legs, scripted per test.
|
|
36
|
+
let judgeEscalationCalls: Array<{ utterance: string }> = [];
|
|
37
|
+
let judgeEscalationVerdict = false;
|
|
38
|
+
// When set, the judge's verdict waits on this before resolving.
|
|
39
|
+
let judgeEscalationGate: Promise<void> | null = null;
|
|
40
|
+
mock.module("../voice-escalation-judge.js", () => ({
|
|
41
|
+
judgeEscalation: async (args: { utterance: string }) => {
|
|
42
|
+
judgeEscalationCalls.push({ utterance: args.utterance });
|
|
43
|
+
if (judgeEscalationGate) {
|
|
44
|
+
await judgeEscalationGate;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
escalate: judgeEscalationVerdict,
|
|
48
|
+
outcome: judgeEscalationVerdict ? "escalate" : "clear",
|
|
49
|
+
latencyMs: 0,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const unresolvableProviderNames = new Set<string>();
|
|
55
|
+
mock.module("../../providers/provider-resolvability.js", () => ({
|
|
56
|
+
dispatchProviderResolvable: (provider: string) =>
|
|
57
|
+
!unresolvableProviderNames.has(provider),
|
|
51
58
|
}));
|
|
52
59
|
|
|
53
60
|
// Attachment hydration for the parked-camera-frame path. Only `att-frame-*`
|
|
@@ -137,6 +144,7 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
137
144
|
}));
|
|
138
145
|
|
|
139
146
|
import { setConfig } from "../../__tests__/helpers/set-config.js";
|
|
147
|
+
import type { PreparedModelCall } from "../../agent/loop.js";
|
|
140
148
|
import { selectWinningProfile } from "../../config/llm-resolver.js";
|
|
141
149
|
import { getConfig } from "../../config/loader.js";
|
|
142
150
|
import { ABORT_WATCHDOG_MS } from "../../daemon/abort-watchdog.js";
|
|
@@ -205,6 +213,7 @@ interface FakeConversation {
|
|
|
205
213
|
opts?: { decisionContext?: string },
|
|
206
214
|
) => void;
|
|
207
215
|
runAgentLoop: (...args: unknown[]) => Promise<void>;
|
|
216
|
+
warmPromptCache: (options?: Record<string, unknown>) => Promise<void>;
|
|
208
217
|
getMessages: () => Array<{ role: string; content: unknown[] }>;
|
|
209
218
|
abort: (reason?: unknown) => void;
|
|
210
219
|
loadFromDb: () => Promise<void>;
|
|
@@ -304,6 +313,7 @@ function makeFakeConversation(opts: {
|
|
|
304
313
|
confirmationDecisions.push({ requestId, decision });
|
|
305
314
|
},
|
|
306
315
|
runAgentLoop: () => (opts.runAgentLoop ?? (async () => {}))(),
|
|
316
|
+
warmPromptCache: async () => {},
|
|
307
317
|
getMessages: () => opts.messages ?? [],
|
|
308
318
|
abort: () => {},
|
|
309
319
|
loadFromDb: async () => {
|
|
@@ -721,9 +731,142 @@ describe("startVoiceTurn camera-frame attachments", () => {
|
|
|
721
731
|
voiceSessionTurn: true,
|
|
722
732
|
});
|
|
723
733
|
});
|
|
734
|
+
|
|
735
|
+
test("claims a captured camera source before voice user persistence", async () => {
|
|
736
|
+
const events: string[] = [];
|
|
737
|
+
const fake = makeFakeConversation({
|
|
738
|
+
processing: false,
|
|
739
|
+
events,
|
|
740
|
+
onPersist: () => events.push("persisted"),
|
|
741
|
+
});
|
|
742
|
+
const claims: Array<{ turnId: string; sourceId: string }> = [];
|
|
743
|
+
Object.assign(fake.conversation, {
|
|
744
|
+
modeSessions: {
|
|
745
|
+
claimTurn: (
|
|
746
|
+
turnId: string,
|
|
747
|
+
source: { sourceId: string; id: string; mode: string },
|
|
748
|
+
) => {
|
|
749
|
+
claims.push({ turnId, sourceId: source.sourceId });
|
|
750
|
+
events.push("claimed");
|
|
751
|
+
return { id: source.id, mode: source.mode };
|
|
752
|
+
},
|
|
753
|
+
releaseTurn: () => {},
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
fakeConversation = fake.conversation;
|
|
757
|
+
|
|
758
|
+
await startVoiceTurn({
|
|
759
|
+
...makeTurnOptions(),
|
|
760
|
+
content: "what is this",
|
|
761
|
+
modeSessionSource: {
|
|
762
|
+
sourceId: "live-voice-camera:voice-1",
|
|
763
|
+
generation: 7,
|
|
764
|
+
activation: 1,
|
|
765
|
+
id: "session-camera",
|
|
766
|
+
mode: "live_vision",
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
expect(claims).toHaveLength(1);
|
|
771
|
+
expect(claims[0]?.sourceId).toBe("live-voice-camera:voice-1");
|
|
772
|
+
expect(events.indexOf("claimed")).toBeLessThan(events.indexOf("persist"));
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("uses preaccepted camera ownership without reclaiming a retired source", async () => {
|
|
776
|
+
let releaseIdle!: () => void;
|
|
777
|
+
const idle = new Promise<void>((resolve) => {
|
|
778
|
+
releaseIdle = resolve;
|
|
779
|
+
});
|
|
780
|
+
let reachedAdmission!: () => void;
|
|
781
|
+
const admissionStarted = new Promise<void>((resolve) => {
|
|
782
|
+
reachedAdmission = resolve;
|
|
783
|
+
});
|
|
784
|
+
const fake = makeFakeConversation({
|
|
785
|
+
processing: true,
|
|
786
|
+
waitForIdle: async () => {
|
|
787
|
+
reachedAdmission();
|
|
788
|
+
await idle;
|
|
789
|
+
fake.setProcessingFlag(false);
|
|
790
|
+
return true;
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
const claims: string[] = [];
|
|
794
|
+
const owner = { id: "session-camera", mode: "live_vision" as const };
|
|
795
|
+
Object.assign(fake.conversation, {
|
|
796
|
+
modeSessions: {
|
|
797
|
+
claimTurn: (turnId: string) => {
|
|
798
|
+
claims.push(turnId);
|
|
799
|
+
return undefined;
|
|
800
|
+
},
|
|
801
|
+
getTurnOwner: (turnId: string) =>
|
|
802
|
+
turnId === "preaccepted-request" ? owner : undefined,
|
|
803
|
+
releaseTurn: () => {},
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
fakeConversation = fake.conversation;
|
|
807
|
+
|
|
808
|
+
const starting = startVoiceTurn({
|
|
809
|
+
...makeTurnOptions(),
|
|
810
|
+
content: "what is this",
|
|
811
|
+
preacceptedModeSession: {
|
|
812
|
+
requestId: "preaccepted-request",
|
|
813
|
+
source: {
|
|
814
|
+
sourceId: "live-voice-camera:voice-1",
|
|
815
|
+
generation: 7,
|
|
816
|
+
activation: 1,
|
|
817
|
+
...owner,
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
await admissionStarted;
|
|
822
|
+
|
|
823
|
+
releaseIdle();
|
|
824
|
+
await starting;
|
|
825
|
+
|
|
826
|
+
expect(claims).toEqual([]);
|
|
827
|
+
expect(fake.lastPersistOpts()?.requestId).toBe("preaccepted-request");
|
|
828
|
+
});
|
|
724
829
|
});
|
|
725
830
|
|
|
726
831
|
describe("startVoiceTurn hiddenSyntheticPrompt", () => {
|
|
832
|
+
test("task announcements retain metadata and cron attribution without a user echo", async () => {
|
|
833
|
+
const fake = makeFakeConversation({ processing: false });
|
|
834
|
+
fakeConversation = fake.conversation;
|
|
835
|
+
const loopOptions: unknown[] = [];
|
|
836
|
+
fake.conversation.runAgentLoop = async (...args: unknown[]) => {
|
|
837
|
+
loopOptions.push(args[2]);
|
|
838
|
+
};
|
|
839
|
+
const metadata = {
|
|
840
|
+
subagentNotification: {
|
|
841
|
+
subagentId: "task-1",
|
|
842
|
+
label: "Compare options",
|
|
843
|
+
status: "completed",
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
const echoes = await collectUserMessageEchoes(async () => {
|
|
847
|
+
await startVoiceTurn({
|
|
848
|
+
...makeTurnOptions(),
|
|
849
|
+
content: "Comparison completed",
|
|
850
|
+
hiddenSyntheticPrompt: true,
|
|
851
|
+
subagentNotification: {
|
|
852
|
+
taskId: "task-1",
|
|
853
|
+
message: "Comparison completed",
|
|
854
|
+
metadata,
|
|
855
|
+
cronRunId: "run-123",
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
expect(fake.lastPersistOpts()?.metadata).toMatchObject({
|
|
860
|
+
...metadata,
|
|
861
|
+
hidden: true,
|
|
862
|
+
scripted: true,
|
|
863
|
+
voiceSessionTurn: true,
|
|
864
|
+
});
|
|
865
|
+
expect(loopOptions).toEqual([
|
|
866
|
+
expect.objectContaining({ cronRunId: "run-123" }),
|
|
867
|
+
]);
|
|
868
|
+
expect(echoes).toEqual([]);
|
|
869
|
+
});
|
|
727
870
|
// A caller whose internal instruction is composed per call carries no
|
|
728
871
|
// sentinel for the content comparisons to recognize, so it declares itself.
|
|
729
872
|
const SYNTHETIC_CONTENT =
|
|
@@ -831,6 +974,22 @@ describe("startVoiceTurn triage-and-escalate control prompt", () => {
|
|
|
831
974
|
expect(installed()).toContain(frontDoorDecisionRule({ includeHold: true }));
|
|
832
975
|
});
|
|
833
976
|
|
|
977
|
+
test("a hidden look follow-up routes the original request", async () => {
|
|
978
|
+
const installed = captureInstalledPrompt();
|
|
979
|
+
const callerUtterance = "Show me where to add a new page.";
|
|
980
|
+
const content = "(fresh view taken; answer from it now)";
|
|
981
|
+
await startVoiceTurn({
|
|
982
|
+
...makeTurnOptions(),
|
|
983
|
+
content,
|
|
984
|
+
routingUtterance: callerUtterance,
|
|
985
|
+
hiddenSyntheticPrompt: true,
|
|
986
|
+
voiceControlPrompt: LIVE_VOICE_PROMPT,
|
|
987
|
+
routingLeg: "front-door",
|
|
988
|
+
});
|
|
989
|
+
expect(installed()).toContain(frontDoorDecisionRule({ callerUtterance }));
|
|
990
|
+
expect(installed()).not.toContain(JSON.stringify(content));
|
|
991
|
+
});
|
|
992
|
+
|
|
834
993
|
test("appends the escalated continuation rule to a caller-supplied prompt", async () => {
|
|
835
994
|
const installed = captureInstalledPrompt();
|
|
836
995
|
await startVoiceTurn({
|
|
@@ -2374,6 +2533,51 @@ describe("front-door hub stream gate", () => {
|
|
|
2374
2533
|
expect(texts.join("")).toBe("It is Tuesday, and it is sunny.");
|
|
2375
2534
|
});
|
|
2376
2535
|
|
|
2536
|
+
test("an answer waits on the escalation judge before reaching the hub", async () => {
|
|
2537
|
+
let openGate!: () => void;
|
|
2538
|
+
judgeEscalationGate = new Promise<void>((resolve) => {
|
|
2539
|
+
openGate = resolve;
|
|
2540
|
+
});
|
|
2541
|
+
judgeEscalationVerdict = false;
|
|
2542
|
+
makeStreamingConversation(["Sure, ", "it's Tuesday."]);
|
|
2543
|
+
try {
|
|
2544
|
+
const whilePending = await collectBroadcastText(() =>
|
|
2545
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
2546
|
+
);
|
|
2547
|
+
expect(whilePending).toEqual([]);
|
|
2548
|
+
|
|
2549
|
+
const afterClear = await collectBroadcastText(async () => {
|
|
2550
|
+
openGate();
|
|
2551
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
2552
|
+
});
|
|
2553
|
+
expect(afterClear.join("")).toBe("Sure, it's Tuesday.");
|
|
2554
|
+
} finally {
|
|
2555
|
+
judgeEscalationGate = null;
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
|
|
2559
|
+
test("an overruled answer never reaches the hub", async () => {
|
|
2560
|
+
judgeEscalationVerdict = true;
|
|
2561
|
+
makeStreamingConversation(["Yeah okay, ", "I'll do it."]);
|
|
2562
|
+
|
|
2563
|
+
const texts = await collectBroadcastText(async () => {
|
|
2564
|
+
const handle = await startVoiceTurn({
|
|
2565
|
+
...makeTurnOptions(),
|
|
2566
|
+
routingLeg: "front-door",
|
|
2567
|
+
});
|
|
2568
|
+
// What the driver does with an escalate verdict on a held answer.
|
|
2569
|
+
void handle.escalationJudgement?.then((escalate) => {
|
|
2570
|
+
if (escalate) {
|
|
2571
|
+
handle.overrule?.();
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
expect(texts).toEqual([]);
|
|
2578
|
+
judgeEscalationVerdict = false;
|
|
2579
|
+
});
|
|
2580
|
+
|
|
2377
2581
|
test("an answer that merely opens with a bracket is released in full", async () => {
|
|
2378
2582
|
// "[" alone could still become the escalate token, so the gate holds it;
|
|
2379
2583
|
// the next delta disproves the token and the whole prefix must come out.
|
|
@@ -2520,6 +2724,60 @@ describe("transcript hygiene (teardown pass)", () => {
|
|
|
2520
2724
|
expect(events).toContain("loadFromDb");
|
|
2521
2725
|
});
|
|
2522
2726
|
|
|
2727
|
+
test("an answer the escalation judge overruled is deleted", async () => {
|
|
2728
|
+
const { events, releaseLoop } = makeReservedRowConversation({
|
|
2729
|
+
holdLoopOpen: true,
|
|
2730
|
+
});
|
|
2731
|
+
getMessageByIdImpl = () => makeRow("I'm adding it to the draft now.");
|
|
2732
|
+
|
|
2733
|
+
const handle = await startVoiceTurn({
|
|
2734
|
+
...makeTurnOptions(),
|
|
2735
|
+
routingLeg: "front-door",
|
|
2736
|
+
});
|
|
2737
|
+
await flushMicrotasks();
|
|
2738
|
+
handle.overrule?.();
|
|
2739
|
+
releaseLoop();
|
|
2740
|
+
await flushMicrotasks();
|
|
2741
|
+
|
|
2742
|
+
// Only the unheard answer goes: the user row stays for the escalated leg.
|
|
2743
|
+
expect(crudLog.deletes).toEqual(["assistant-row-1"]);
|
|
2744
|
+
expect(events).toContain("loadFromDb");
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
test("a verdict that lands after the model finished still deletes the overruled row", async () => {
|
|
2748
|
+
let openGate!: () => void;
|
|
2749
|
+
judgeEscalationGate = new Promise<void>((resolve) => {
|
|
2750
|
+
openGate = resolve;
|
|
2751
|
+
});
|
|
2752
|
+
judgeEscalationVerdict = true;
|
|
2753
|
+
const { events } = makeReservedRowConversation();
|
|
2754
|
+
getMessageByIdImpl = () => makeRow("Yeah okay, I'll do it.");
|
|
2755
|
+
try {
|
|
2756
|
+
const handle = await startVoiceTurn({
|
|
2757
|
+
...makeTurnOptions(),
|
|
2758
|
+
routingLeg: "front-door",
|
|
2759
|
+
});
|
|
2760
|
+
void handle.escalationJudgement?.then((escalate) => {
|
|
2761
|
+
if (escalate) {
|
|
2762
|
+
handle.overrule?.();
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
// The loop has finished; teardown hygiene waits on the verdict.
|
|
2766
|
+
await flushMicrotasks();
|
|
2767
|
+
expect(crudLog.deletes).toEqual([]);
|
|
2768
|
+
|
|
2769
|
+
openGate();
|
|
2770
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
2771
|
+
await flushMicrotasks();
|
|
2772
|
+
|
|
2773
|
+
expect(crudLog.deletes).toEqual(["assistant-row-1"]);
|
|
2774
|
+
expect(events).toContain("loadFromDb");
|
|
2775
|
+
} finally {
|
|
2776
|
+
judgeEscalationGate = null;
|
|
2777
|
+
judgeEscalationVerdict = false;
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2523
2781
|
test("a committed front-door answer (no verdict token) is left untouched", async () => {
|
|
2524
2782
|
const { events } = makeReservedRowConversation();
|
|
2525
2783
|
getMessageByIdImpl = () => makeRow("It is Tuesday.");
|
|
@@ -2756,11 +3014,7 @@ const PHOTO_HISTORY = [
|
|
|
2756
3014
|
},
|
|
2757
3015
|
];
|
|
2758
3016
|
|
|
2759
|
-
describe("startVoiceTurn
|
|
2760
|
-
beforeEach(() => {
|
|
2761
|
-
pinProfileSupportsVision = true;
|
|
2762
|
-
});
|
|
2763
|
-
|
|
3017
|
+
describe("startVoiceTurn with images in history", () => {
|
|
2764
3018
|
async function runOptionsFor(opts: {
|
|
2765
3019
|
messages?: Array<{ role: string; content: unknown[] }>;
|
|
2766
3020
|
turn?: Record<string, unknown>;
|
|
@@ -2778,42 +3032,16 @@ describe("startVoiceTurn image-bearing profile pin", () => {
|
|
|
2778
3032
|
return runOptions;
|
|
2779
3033
|
}
|
|
2780
3034
|
|
|
2781
|
-
test("
|
|
2782
|
-
|
|
3035
|
+
test("an image in history leaves the call-site profile in place", async () => {
|
|
3036
|
+
// A text-only model gets the image captioned by the image-fallback
|
|
3037
|
+
// plugin, the same as a typed turn, so no profile is pinned for it.
|
|
3038
|
+
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
2783
3039
|
|
|
3040
|
+
expect(runOptions.callSite).toBe("callAgent");
|
|
2784
3041
|
expect(runOptions.overrideProfile).toBeUndefined();
|
|
2785
3042
|
expect(runOptions.forceOverrideProfile).toBeUndefined();
|
|
2786
3043
|
});
|
|
2787
3044
|
|
|
2788
|
-
test("an image in history pins the image-capable profile", async () => {
|
|
2789
|
-
// `callAgent`'s balanced profile carries no guarantee that its model takes
|
|
2790
|
-
// an image, and a model that rejects one fails the whole leg.
|
|
2791
|
-
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
2792
|
-
|
|
2793
|
-
expect(runOptions.overrideProfile).toBe("latency-optimized");
|
|
2794
|
-
// callAgent is not `mainAgent`, so an unforced override would sit below
|
|
2795
|
-
// the call-site profile and never apply.
|
|
2796
|
-
expect(runOptions.forceOverrideProfile).toBe(true);
|
|
2797
|
-
});
|
|
2798
|
-
|
|
2799
|
-
test("an image nested in a tool result counts too", async () => {
|
|
2800
|
-
const runOptions = await runOptionsFor({
|
|
2801
|
-
messages: [
|
|
2802
|
-
{
|
|
2803
|
-
role: "user",
|
|
2804
|
-
content: [
|
|
2805
|
-
{
|
|
2806
|
-
type: "tool_result",
|
|
2807
|
-
contentBlocks: [{ type: "image", source: { data: "abc" } }],
|
|
2808
|
-
},
|
|
2809
|
-
],
|
|
2810
|
-
},
|
|
2811
|
-
],
|
|
2812
|
-
});
|
|
2813
|
-
|
|
2814
|
-
expect(runOptions.overrideProfile).toBe("latency-optimized");
|
|
2815
|
-
});
|
|
2816
|
-
|
|
2817
3045
|
test("a front-door leg is left alone — its own call site pins it", async () => {
|
|
2818
3046
|
const runOptions = await runOptionsFor({
|
|
2819
3047
|
messages: PHOTO_HISTORY,
|
|
@@ -2824,19 +3052,7 @@ describe("startVoiceTurn image-bearing profile pin", () => {
|
|
|
2824
3052
|
expect(runOptions.callSite).toBe("voiceFrontDoor");
|
|
2825
3053
|
});
|
|
2826
3054
|
|
|
2827
|
-
test("
|
|
2828
|
-
// Fireworks: `latency-optimized` resolves to a text-only model while
|
|
2829
|
-
// `balanced` is vision-capable, so pinning would break the very turn the
|
|
2830
|
-
// pin exists to save.
|
|
2831
|
-
pinProfileSupportsVision = false;
|
|
2832
|
-
|
|
2833
|
-
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
2834
|
-
|
|
2835
|
-
expect(runOptions.overrideProfile).toBeUndefined();
|
|
2836
|
-
expect(runOptions.forceOverrideProfile).toBeUndefined();
|
|
2837
|
-
});
|
|
2838
|
-
|
|
2839
|
-
test("an explicit routing pin wins over the image pin", async () => {
|
|
3055
|
+
test("an explicit routing pin still applies", async () => {
|
|
2840
3056
|
const runOptions = await runOptionsFor({
|
|
2841
3057
|
messages: PHOTO_HISTORY,
|
|
2842
3058
|
turn: { overrideProfile: "quality-optimized" },
|
|
@@ -2848,12 +3064,13 @@ describe("startVoiceTurn image-bearing profile pin", () => {
|
|
|
2848
3064
|
|
|
2849
3065
|
describe("startVoiceTurn escalated-leg profile pin", () => {
|
|
2850
3066
|
beforeEach(() => {
|
|
2851
|
-
|
|
2852
|
-
conversationProfileSupportsVision = true;
|
|
3067
|
+
unresolvableProviderNames.clear();
|
|
2853
3068
|
});
|
|
2854
3069
|
|
|
2855
3070
|
afterEach(() => {
|
|
3071
|
+
unresolvableProviderNames.clear();
|
|
2856
3072
|
setConfig("llm", {});
|
|
3073
|
+
setConfig("rateLimit", {});
|
|
2857
3074
|
});
|
|
2858
3075
|
|
|
2859
3076
|
async function runOptionsFor(opts: {
|
|
@@ -2885,10 +3102,31 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
|
|
|
2885
3102
|
return runOptions;
|
|
2886
3103
|
}
|
|
2887
3104
|
|
|
3105
|
+
function reportPreparedTarget(
|
|
3106
|
+
runOptions: Record<string, unknown>,
|
|
3107
|
+
overrideProfile: string,
|
|
3108
|
+
): void {
|
|
3109
|
+
const onPrepared = runOptions.onFirstModelCallPrepared as (prepared: {
|
|
3110
|
+
callSite: "mainAgent";
|
|
3111
|
+
overrideProfile: string;
|
|
3112
|
+
forceOverrideProfile: boolean;
|
|
3113
|
+
systemPrompt: string;
|
|
3114
|
+
tools: [];
|
|
3115
|
+
}) => void;
|
|
3116
|
+
onPrepared({
|
|
3117
|
+
callSite: "mainAgent",
|
|
3118
|
+
overrideProfile,
|
|
3119
|
+
forceOverrideProfile: true,
|
|
3120
|
+
systemPrompt: "prepared prompt",
|
|
3121
|
+
tools: [],
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2888
3125
|
test("with no chat-model selection the leg keeps the call-site profile", async () => {
|
|
2889
3126
|
const runOptions = await runOptionsFor({});
|
|
2890
3127
|
|
|
2891
3128
|
expect(runOptions.callSite).toBe("callAgent");
|
|
3129
|
+
expect(runOptions.inferenceCallSite).toBe("mainAgent");
|
|
2892
3130
|
expect(runOptions.overrideProfile).toBeUndefined();
|
|
2893
3131
|
expect(runOptions.forceOverrideProfile).toBeUndefined();
|
|
2894
3132
|
});
|
|
@@ -2902,8 +3140,168 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
|
|
|
2902
3140
|
const runOptions = await runOptionsFor({});
|
|
2903
3141
|
|
|
2904
3142
|
expect(runOptions.callSite).toBe("callAgent");
|
|
3143
|
+
expect(runOptions.inferenceCallSite).toBe("mainAgent");
|
|
2905
3144
|
expect(runOptions.overrideProfile).toBe("quality-optimized");
|
|
2906
3145
|
expect(runOptions.forceOverrideProfile).toBe(true);
|
|
3146
|
+
expect(runOptions.onFirstModelCallPrepared).toBeFunction();
|
|
3147
|
+
});
|
|
3148
|
+
|
|
3149
|
+
test("starts warming the finalized request without awaiting it", async () => {
|
|
3150
|
+
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
3151
|
+
const runOptions = await runOptionsFor({});
|
|
3152
|
+
const warmPromptCache = mock(() => new Promise<void>(() => {}));
|
|
3153
|
+
fakeConversation.warmPromptCache = warmPromptCache;
|
|
3154
|
+
const onFirstModelCallPrepared = runOptions.onFirstModelCallPrepared as (
|
|
3155
|
+
prepared: PreparedModelCall,
|
|
3156
|
+
) => void;
|
|
3157
|
+
const tools = [
|
|
3158
|
+
{
|
|
3159
|
+
name: "dynamic_tool",
|
|
3160
|
+
description: "Dynamic",
|
|
3161
|
+
input_schema: { type: "object" as const },
|
|
3162
|
+
},
|
|
3163
|
+
];
|
|
3164
|
+
const turnAbort = new AbortController();
|
|
3165
|
+
|
|
3166
|
+
expect(
|
|
3167
|
+
onFirstModelCallPrepared({
|
|
3168
|
+
callSite: "mainAgent",
|
|
3169
|
+
overrideProfile: "hook-selected-profile",
|
|
3170
|
+
forceOverrideProfile: true,
|
|
3171
|
+
signal: turnAbort.signal,
|
|
3172
|
+
systemPrompt: "hook-edited prompt",
|
|
3173
|
+
tools,
|
|
3174
|
+
}),
|
|
3175
|
+
).toBeUndefined();
|
|
3176
|
+
|
|
3177
|
+
expect(warmPromptCache).toHaveBeenCalledWith({
|
|
3178
|
+
callSite: "mainAgent",
|
|
3179
|
+
overrideProfile: "hook-selected-profile",
|
|
3180
|
+
forceOverrideProfile: true,
|
|
3181
|
+
signal: turnAbort.signal,
|
|
3182
|
+
systemPrompt: "hook-edited prompt",
|
|
3183
|
+
tools,
|
|
3184
|
+
});
|
|
3185
|
+
});
|
|
3186
|
+
|
|
3187
|
+
test("reports the conversation profile selected for the handoff", async () => {
|
|
3188
|
+
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
3189
|
+
const onEscalationTargetResolved = mock();
|
|
3190
|
+
|
|
3191
|
+
const runOptions = await runOptionsFor({
|
|
3192
|
+
turn: { onEscalationTargetResolved },
|
|
3193
|
+
});
|
|
3194
|
+
reportPreparedTarget(runOptions, "quality-optimized");
|
|
3195
|
+
|
|
3196
|
+
expect(onEscalationTargetResolved).toHaveBeenCalledWith({
|
|
3197
|
+
profile: "quality-optimized",
|
|
3198
|
+
source: "conversation",
|
|
3199
|
+
});
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
test("reports the concrete profile selected from a mix", async () => {
|
|
3203
|
+
setConfig("llm", {
|
|
3204
|
+
activeProfile: "voice-mix",
|
|
3205
|
+
profiles: {
|
|
3206
|
+
"voice-mix": {
|
|
3207
|
+
mix: [
|
|
3208
|
+
{ profile: "quality-optimized", weight: 1 },
|
|
3209
|
+
{ profile: "cost-optimized", weight: 1 },
|
|
3210
|
+
],
|
|
3211
|
+
},
|
|
3212
|
+
},
|
|
3213
|
+
});
|
|
3214
|
+
let selectedProfile: string | undefined;
|
|
3215
|
+
selectWinningProfile("mainAgent", getConfig().llm, {
|
|
3216
|
+
overrideProfile: "voice-mix",
|
|
3217
|
+
forceOverrideProfile: true,
|
|
3218
|
+
selectionSeed: "conv-voice-bridge-test",
|
|
3219
|
+
onMixSelected: ({ chosenProfile }) => {
|
|
3220
|
+
selectedProfile = chosenProfile;
|
|
3221
|
+
},
|
|
3222
|
+
});
|
|
3223
|
+
const onEscalationTargetResolved = mock();
|
|
3224
|
+
const runOptions = await runOptionsFor({
|
|
3225
|
+
turn: { onEscalationTargetResolved },
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3228
|
+
// The leg is pinned to the chosen arm itself (see the mix pin test).
|
|
3229
|
+
expect(runOptions.overrideProfile).toBe(selectedProfile);
|
|
3230
|
+
reportPreparedTarget(runOptions, runOptions.overrideProfile as string);
|
|
3231
|
+
|
|
3232
|
+
expect(selectedProfile).toBeDefined();
|
|
3233
|
+
expect(onEscalationTargetResolved).toHaveBeenCalledWith({
|
|
3234
|
+
profile: selectedProfile,
|
|
3235
|
+
source: "conversation",
|
|
3236
|
+
});
|
|
3237
|
+
});
|
|
3238
|
+
|
|
3239
|
+
test("warms the main-agent route when the conversation has no profile pin", async () => {
|
|
3240
|
+
const runOptions = await runOptionsFor({});
|
|
3241
|
+
const warmPromptCache = mock(async () => {});
|
|
3242
|
+
fakeConversation.warmPromptCache = warmPromptCache;
|
|
3243
|
+
const onFirstModelCallPrepared = runOptions.onFirstModelCallPrepared as (
|
|
3244
|
+
prepared: PreparedModelCall,
|
|
3245
|
+
) => void;
|
|
3246
|
+
onFirstModelCallPrepared({
|
|
3247
|
+
callSite: "mainAgent",
|
|
3248
|
+
forceOverrideProfile: false,
|
|
3249
|
+
systemPrompt: "system prompt",
|
|
3250
|
+
tools: [],
|
|
3251
|
+
});
|
|
3252
|
+
|
|
3253
|
+
expect(warmPromptCache).toHaveBeenCalledWith({
|
|
3254
|
+
callSite: "mainAgent",
|
|
3255
|
+
forceOverrideProfile: false,
|
|
3256
|
+
signal: undefined,
|
|
3257
|
+
systemPrompt: "system prompt",
|
|
3258
|
+
tools: [],
|
|
3259
|
+
});
|
|
3260
|
+
});
|
|
3261
|
+
|
|
3262
|
+
test("does not warm a route whose finalized policy disables caching", async () => {
|
|
3263
|
+
const runOptions = await runOptionsFor({});
|
|
3264
|
+
const warmPromptCache = mock(async () => {});
|
|
3265
|
+
fakeConversation.warmPromptCache = warmPromptCache;
|
|
3266
|
+
const onFirstModelCallPrepared = runOptions.onFirstModelCallPrepared as (
|
|
3267
|
+
prepared: PreparedModelCall,
|
|
3268
|
+
) => void;
|
|
3269
|
+
|
|
3270
|
+
onFirstModelCallPrepared({
|
|
3271
|
+
callSite: "mainAgent",
|
|
3272
|
+
forceOverrideProfile: false,
|
|
3273
|
+
disableCache: true,
|
|
3274
|
+
systemPrompt: "system prompt",
|
|
3275
|
+
tools: [],
|
|
3276
|
+
});
|
|
3277
|
+
|
|
3278
|
+
expect(warmPromptCache).not.toHaveBeenCalled();
|
|
3279
|
+
});
|
|
3280
|
+
|
|
3281
|
+
test("does not warm when requests are rate limited", async () => {
|
|
3282
|
+
setConfig("rateLimit", { maxRequestsPerMinute: 1 });
|
|
3283
|
+
const runOptions = await runOptionsFor({});
|
|
3284
|
+
const warmPromptCache = mock(async () => {});
|
|
3285
|
+
fakeConversation.warmPromptCache = warmPromptCache;
|
|
3286
|
+
|
|
3287
|
+
expect(runOptions.onFirstModelCallPrepared).toBeFunction();
|
|
3288
|
+
reportPreparedTarget(runOptions, "quality-optimized");
|
|
3289
|
+
expect(warmPromptCache).not.toHaveBeenCalled();
|
|
3290
|
+
});
|
|
3291
|
+
|
|
3292
|
+
test("reports a profile selected by final pre-model routing", async () => {
|
|
3293
|
+
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
3294
|
+
const onEscalationTargetResolved = mock();
|
|
3295
|
+
|
|
3296
|
+
const runOptions = await runOptionsFor({
|
|
3297
|
+
turn: { onEscalationTargetResolved },
|
|
3298
|
+
});
|
|
3299
|
+
reportPreparedTarget(runOptions, "cost-optimized");
|
|
3300
|
+
|
|
3301
|
+
expect(onEscalationTargetResolved).toHaveBeenCalledWith({
|
|
3302
|
+
profile: "cost-optimized",
|
|
3303
|
+
source: "pre_model_hook",
|
|
3304
|
+
});
|
|
2907
3305
|
});
|
|
2908
3306
|
|
|
2909
3307
|
test("the conversation's own pin wins over the workspace selection", async () => {
|
|
@@ -2942,34 +3340,47 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
|
|
|
2942
3340
|
expect(unrouted.overrideProfile).toBeUndefined();
|
|
2943
3341
|
});
|
|
2944
3342
|
|
|
2945
|
-
test("an image
|
|
3343
|
+
test("an image keeps a text-only conversation profile", async () => {
|
|
3344
|
+
// The escalated leg is the tool-capable brain of the call. Images reach a
|
|
3345
|
+
// text-only model as image-fallback captions, never by swapping the leg
|
|
3346
|
+
// onto the latency-class profile.
|
|
2946
3347
|
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
2947
3348
|
|
|
2948
3349
|
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
2949
3350
|
|
|
2950
3351
|
expect(runOptions.overrideProfile).toBe("quality-optimized");
|
|
2951
|
-
});
|
|
2952
|
-
|
|
2953
|
-
test("an image hands a text-only conversation profile to the image pin", async () => {
|
|
2954
|
-
// A model that rejects the image fails the whole leg, so the image pin
|
|
2955
|
-
// outranks the conversation's choice for this one turn.
|
|
2956
|
-
setConfig("llm", { activeProfile: "quality-optimized" });
|
|
2957
|
-
conversationProfileSupportsVision = false;
|
|
2958
|
-
|
|
2959
|
-
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
2960
|
-
|
|
2961
|
-
expect(runOptions.overrideProfile).toBe("latency-optimized");
|
|
2962
3352
|
expect(runOptions.forceOverrideProfile).toBe(true);
|
|
3353
|
+
expect(runOptions.inferenceCallSite).toBe("mainAgent");
|
|
2963
3354
|
});
|
|
2964
3355
|
|
|
2965
|
-
test("
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3356
|
+
test("a mix pins the arm serving this conversation, not the mix name", async () => {
|
|
3357
|
+
// The image-fallback check judges the pinned profile, and a mix reads as
|
|
3358
|
+
// vision-capable when any arm is. Pinning the chosen arm keeps a
|
|
3359
|
+
// text-only arm from receiving raw images.
|
|
3360
|
+
setConfig("llm", {
|
|
3361
|
+
activeProfile: "voice-mix",
|
|
3362
|
+
profiles: {
|
|
3363
|
+
"voice-mix": {
|
|
3364
|
+
mix: [
|
|
3365
|
+
{ profile: "quality-optimized", weight: 1 },
|
|
3366
|
+
{ profile: "cost-optimized", weight: 1 },
|
|
3367
|
+
],
|
|
3368
|
+
},
|
|
3369
|
+
},
|
|
3370
|
+
});
|
|
3371
|
+
let chosenArm: string | undefined;
|
|
3372
|
+
selectWinningProfile("mainAgent", getConfig().llm, {
|
|
3373
|
+
selectionSeed: "conv-voice-bridge-test",
|
|
3374
|
+
onMixSelected: ({ chosenProfile }) => {
|
|
3375
|
+
chosenArm = chosenProfile;
|
|
3376
|
+
},
|
|
3377
|
+
});
|
|
3378
|
+
expect(chosenArm).toBeDefined();
|
|
2969
3379
|
|
|
2970
3380
|
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
2971
3381
|
|
|
2972
|
-
expect(runOptions.overrideProfile).toBe(
|
|
3382
|
+
expect(runOptions.overrideProfile).toBe(chosenArm);
|
|
3383
|
+
expect(runOptions.forceOverrideProfile).toBe(true);
|
|
2973
3384
|
});
|
|
2974
3385
|
|
|
2975
3386
|
test("an explicit routing pin wins over the conversation profile", async () => {
|
|
@@ -2982,51 +3393,94 @@ describe("startVoiceTurn escalated-leg profile pin", () => {
|
|
|
2982
3393
|
expect(runOptions.overrideProfile).toBe("balanced");
|
|
2983
3394
|
});
|
|
2984
3395
|
|
|
2985
|
-
test("a mix
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
// image, so only that arm's capability decides whether the image pin
|
|
2989
|
-
// takes over. The pin itself stays the mix's own name, so dispatch lands
|
|
2990
|
-
// on the same arm.
|
|
2991
|
-
const llm = {
|
|
2992
|
-
activeProfile: "voice-mix",
|
|
3396
|
+
test("a mix with no resolvable arm falls back to the main-agent profile", async () => {
|
|
3397
|
+
setConfig("llm", {
|
|
3398
|
+
activeProfile: "stale-mix",
|
|
2993
3399
|
profiles: {
|
|
2994
|
-
"
|
|
3400
|
+
"stale-a": {
|
|
3401
|
+
provider: "deleted-connection-a",
|
|
3402
|
+
model: "model-a",
|
|
3403
|
+
},
|
|
3404
|
+
"stale-b": {
|
|
3405
|
+
provider: "deleted-connection-b",
|
|
3406
|
+
model: "model-b",
|
|
3407
|
+
},
|
|
3408
|
+
"stale-mix": {
|
|
2995
3409
|
mix: [
|
|
2996
|
-
{ profile: "
|
|
2997
|
-
{ profile: "
|
|
3410
|
+
{ profile: "stale-a", weight: 1 },
|
|
3411
|
+
{ profile: "stale-b", weight: 1 },
|
|
2998
3412
|
],
|
|
2999
3413
|
},
|
|
3000
3414
|
},
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
let chosenArm: string | undefined;
|
|
3004
|
-
selectWinningProfile("mainAgent", getConfig().llm, {
|
|
3005
|
-
selectionSeed: "conv-voice-bridge-test",
|
|
3006
|
-
onMixSelected: ({ chosenProfile }) => {
|
|
3007
|
-
chosenArm = chosenProfile;
|
|
3415
|
+
callSites: {
|
|
3416
|
+
mainAgent: { profile: "quality-optimized" },
|
|
3008
3417
|
},
|
|
3009
3418
|
});
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3419
|
+
unresolvableProviderNames.add("deleted-connection-a");
|
|
3420
|
+
unresolvableProviderNames.add("deleted-connection-b");
|
|
3421
|
+
const runOptions = await runOptionsFor({ messages: PHOTO_HISTORY });
|
|
3422
|
+
|
|
3423
|
+
expect(runOptions.overrideProfile).toBe("quality-optimized");
|
|
3424
|
+
});
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
describe("startVoiceTurn escalation judge", () => {
|
|
3428
|
+
beforeEach(() => {
|
|
3429
|
+
judgeEscalationCalls = [];
|
|
3430
|
+
judgeEscalationVerdict = false;
|
|
3431
|
+
fakeConversation = makeFakeConversation({ processing: false }).conversation;
|
|
3432
|
+
});
|
|
3433
|
+
|
|
3434
|
+
test("a front-door leg carries the judge's verdict and an overrule", async () => {
|
|
3435
|
+
judgeEscalationVerdict = true;
|
|
3436
|
+
|
|
3437
|
+
const handle = await startVoiceTurn({
|
|
3438
|
+
...makeTurnOptions(),
|
|
3439
|
+
content: "Text my mom I'm late.",
|
|
3440
|
+
routingLeg: "front-door",
|
|
3441
|
+
});
|
|
3442
|
+
|
|
3443
|
+
expect(judgeEscalationCalls).toEqual([
|
|
3444
|
+
{ utterance: "Text my mom I'm late." },
|
|
3445
|
+
]);
|
|
3446
|
+
expect(await handle.escalationJudgement).toBe(true);
|
|
3447
|
+
expect(typeof handle.overrule).toBe("function");
|
|
3448
|
+
});
|
|
3449
|
+
|
|
3450
|
+
test("escalated, unrouted, and synthetic legs are not judged", async () => {
|
|
3451
|
+
const escalated = await startVoiceTurn({
|
|
3452
|
+
...makeTurnOptions(),
|
|
3453
|
+
routingLeg: "escalated",
|
|
3454
|
+
});
|
|
3455
|
+
const unrouted = await startVoiceTurn(makeTurnOptions());
|
|
3456
|
+
const synthetic = await startVoiceTurn({
|
|
3457
|
+
...makeTurnOptions(),
|
|
3458
|
+
routingLeg: "front-door",
|
|
3459
|
+
hiddenSyntheticPrompt: true,
|
|
3460
|
+
});
|
|
3461
|
+
|
|
3462
|
+
expect(judgeEscalationCalls).toEqual([]);
|
|
3463
|
+
for (const handle of [escalated, unrouted, synthetic]) {
|
|
3464
|
+
expect(handle.escalationJudgement).toBeUndefined();
|
|
3465
|
+
expect(handle.overrule).toBeUndefined();
|
|
3030
3466
|
}
|
|
3031
3467
|
});
|
|
3468
|
+
|
|
3469
|
+
test.each([
|
|
3470
|
+
"What do you see on my screen?",
|
|
3471
|
+
"Show me where to add a new page.",
|
|
3472
|
+
])("a captured look bypasses the text-only judge: %s", async (request) => {
|
|
3473
|
+
judgeEscalationVerdict = true;
|
|
3474
|
+
const handle = await startVoiceTurn({
|
|
3475
|
+
...makeTurnOptions(),
|
|
3476
|
+
content: "(fresh view taken; answer from it now)",
|
|
3477
|
+
routingUtterance: request,
|
|
3478
|
+
routingLeg: "front-door",
|
|
3479
|
+
hiddenSyntheticPrompt: true,
|
|
3480
|
+
});
|
|
3481
|
+
|
|
3482
|
+
expect(judgeEscalationCalls).toEqual([]);
|
|
3483
|
+
expect(handle.escalationJudgement).toBeUndefined();
|
|
3484
|
+
expect(handle.overrule).toBeUndefined();
|
|
3485
|
+
});
|
|
3032
3486
|
});
|