@vellumai/assistant 0.11.3 → 0.11.4-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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -15,7 +15,15 @@ import { getDb } from "../persistence/db-connection.js";
|
|
|
15
15
|
import { acpSessionHistory } from "../persistence/schema/index.js";
|
|
16
16
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
17
17
|
import { getLogger } from "../util/logger.js";
|
|
18
|
+
import { markAcpConnectCardRaised } from "./acp-connect-card-state.js";
|
|
18
19
|
import { AcpAgentProcess } from "./agent-process.js";
|
|
20
|
+
import {
|
|
21
|
+
ACP_AUTH_RECOVERY_GUIDANCE,
|
|
22
|
+
ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
23
|
+
CLAUDE_ACP_COMMAND,
|
|
24
|
+
isAcpAuthRequired,
|
|
25
|
+
isClaudeAuthFailureMessage,
|
|
26
|
+
} from "./auth-required.js";
|
|
19
27
|
import { resolveAgentWithAutoInstall } from "./auto-install.js";
|
|
20
28
|
import { VellumAcpClientHandler } from "./client-handler.js";
|
|
21
29
|
import { deriveFailureError } from "./failure-error.js";
|
|
@@ -26,6 +34,30 @@ import type { AcpAgentConfig, AcpSessionState } from "./types.js";
|
|
|
26
34
|
|
|
27
35
|
const log = getLogger("acp:session-manager");
|
|
28
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The `authCode` for a run that failed on its Claude credential, or undefined
|
|
39
|
+
* for any other failure. Adapter-gated first: the signals are Claude-specific,
|
|
40
|
+
* and the marker promises a repair only the Connect Claude flow can perform.
|
|
41
|
+
* Checks both auth-failure shapes (see `auth-required.ts`) against both the
|
|
42
|
+
* raw rejection and the derived failure message, since `deriveFailureError`
|
|
43
|
+
* may replace one with the other.
|
|
44
|
+
*/
|
|
45
|
+
function claudeAuthRequiredCode(
|
|
46
|
+
err: unknown,
|
|
47
|
+
failureMessage: string,
|
|
48
|
+
entry: { command: string },
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (entry.command !== CLAUDE_ACP_COMMAND) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
54
|
+
return isAcpAuthRequired(err) ||
|
|
55
|
+
isClaudeAuthFailureMessage(rawMessage) ||
|
|
56
|
+
isClaudeAuthFailureMessage(failureMessage)
|
|
57
|
+
? ACP_CLAUDE_AUTH_REQUIRED_CODE
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
29
61
|
/**
|
|
30
62
|
* The manager's "unknown session id" error. Thrown whenever an operation
|
|
31
63
|
* references an acpSessionId with no in-memory entry (and, for resume, no
|
|
@@ -1093,6 +1125,11 @@ export class AcpSessionManager {
|
|
|
1093
1125
|
err.message,
|
|
1094
1126
|
current.process.stderrSince(stderrMark),
|
|
1095
1127
|
);
|
|
1128
|
+
const errorCode = claudeAuthRequiredCode(
|
|
1129
|
+
err,
|
|
1130
|
+
failureMessage,
|
|
1131
|
+
current,
|
|
1132
|
+
);
|
|
1096
1133
|
if (current.state.status !== "cancelled") {
|
|
1097
1134
|
current.state.status = "failed";
|
|
1098
1135
|
current.state.completedAt = Date.now();
|
|
@@ -1100,14 +1137,40 @@ export class AcpSessionManager {
|
|
|
1100
1137
|
}
|
|
1101
1138
|
current.currentPrompt = null;
|
|
1102
1139
|
log.error(
|
|
1103
|
-
{ acpSessionId, error: err.message, failureMessage },
|
|
1140
|
+
{ acpSessionId, error: err.message, failureMessage, errorCode },
|
|
1104
1141
|
"ACP prompt failed",
|
|
1105
1142
|
);
|
|
1143
|
+
// `acp_session_error` keeps its pre-existing shape so older packaged
|
|
1144
|
+
// clients still parse it; the recovery signal rides its own event
|
|
1145
|
+
// below (see `api/events/acp-auth-required.ts`).
|
|
1106
1146
|
current.sendToVellum({
|
|
1107
1147
|
type: "acp_session_error",
|
|
1108
1148
|
acpSessionId,
|
|
1109
1149
|
error: failureMessage,
|
|
1110
1150
|
});
|
|
1151
|
+
// The recovery surface (event, model guidance, prompt-dedup marker)
|
|
1152
|
+
// is raised as a unit, and only when a spawning tool call exists to
|
|
1153
|
+
// anchor the card AND the user did not already stop the run. A
|
|
1154
|
+
// cancelled run renders no card (the client guards it), so marking
|
|
1155
|
+
// the never-cleared prompt-dedup registry here would suppress the
|
|
1156
|
+
// secure token prompt against a card that never appears.
|
|
1157
|
+
const recoveryAnchor =
|
|
1158
|
+
errorCode !== undefined && current.state.status !== "cancelled"
|
|
1159
|
+
? current.parentToolUseId
|
|
1160
|
+
: undefined;
|
|
1161
|
+
if (errorCode !== undefined && recoveryAnchor !== undefined) {
|
|
1162
|
+
current.sendToVellum({
|
|
1163
|
+
type: "acp_auth_required",
|
|
1164
|
+
acpSessionId,
|
|
1165
|
+
authCode: errorCode,
|
|
1166
|
+
agent: current.state.agentId,
|
|
1167
|
+
parentToolUseId: recoveryAnchor,
|
|
1168
|
+
});
|
|
1169
|
+
// Same registry as the missing-token spawn path, so the
|
|
1170
|
+
// credential-prompt route redirects a redundant secure prompt at
|
|
1171
|
+
// the card instead of stacking a second one.
|
|
1172
|
+
markAcpConnectCardRaised(current.parentConversationId);
|
|
1173
|
+
}
|
|
1111
1174
|
|
|
1112
1175
|
// Persist the terminal row before teardown clears the buffer.
|
|
1113
1176
|
this.persistTerminal(acpSessionId, current);
|
|
@@ -1123,7 +1186,10 @@ export class AcpSessionManager {
|
|
|
1123
1186
|
if (current.state.status !== "cancelled") {
|
|
1124
1187
|
this.notifyParent(
|
|
1125
1188
|
current,
|
|
1126
|
-
`[ACP agent "${current.state.agentId}" failed]\n\n${failureMessage}
|
|
1189
|
+
`[ACP agent "${current.state.agentId}" failed]\n\n${failureMessage}` +
|
|
1190
|
+
(recoveryAnchor !== undefined
|
|
1191
|
+
? `\n\n${ACP_AUTH_RECOVERY_GUIDANCE}`
|
|
1192
|
+
: ""),
|
|
1127
1193
|
);
|
|
1128
1194
|
}
|
|
1129
1195
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `acp_auth_required` SSE event.
|
|
3
|
+
*
|
|
4
|
+
* Server to client notification that an ACP run failed because the agent
|
|
5
|
+
* rejected its stored credential and the client can offer a re-authentication
|
|
6
|
+
* affordance. Always emitted immediately AFTER the `acp_session_error` that
|
|
7
|
+
* carries the human-readable failure.
|
|
8
|
+
*
|
|
9
|
+
* It is a separate event rather than a field on `acp_session_error` because
|
|
10
|
+
* every event schema here is `.strict()` and clients fall back to an inert
|
|
11
|
+
* `unknown` event when parsing fails: a new FIELD makes an older packaged
|
|
12
|
+
* client (iOS/macOS bundle the web app) reject the whole error event and
|
|
13
|
+
* render the run as still active, while an unknown event TYPE is simply
|
|
14
|
+
* ignored. Additive signals on existing events need their own event type for
|
|
15
|
+
* the same reason.
|
|
16
|
+
*
|
|
17
|
+
* Canonical wire-contract source. Re-exported to external consumers via
|
|
18
|
+
* `@vellumai/assistant-api` (the `api/index.ts` barrel).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The code for a `claude-agent-acp` run whose Claude credential needs to be
|
|
25
|
+
* reconnected: the `authCode` on this event for post-spawn failures, and the
|
|
26
|
+
* `errorCode` on the failed `acp_spawn` tool result for pre-spawn ones.
|
|
27
|
+
* Single source for both sides of the wire: the daemon re-exports it via
|
|
28
|
+
* `acp/auth-required.ts`, the web client via
|
|
29
|
+
* `domains/chat/utils/acp-connect.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export const ACP_CLAUDE_AUTH_REQUIRED_CODE = "acp_claude_auth_required";
|
|
32
|
+
|
|
33
|
+
export const AcpAuthRequiredEventSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
type: z.literal("acp_auth_required"),
|
|
36
|
+
acpSessionId: z.string(),
|
|
37
|
+
/**
|
|
38
|
+
* Machine-readable classification of what kind of re-authentication is
|
|
39
|
+
* needed, so clients branch on a stable value rather than on the failure
|
|
40
|
+
* text (which varies by adapter and failure mode). Currently only
|
|
41
|
+
* {@link ACP_CLAUDE_AUTH_REQUIRED_CODE}.
|
|
42
|
+
*/
|
|
43
|
+
authCode: z.string(),
|
|
44
|
+
/** Agent id the run was spawned with, for display. */
|
|
45
|
+
agent: z.string(),
|
|
46
|
+
/**
|
|
47
|
+
* Tool-use id of the `acp_spawn` call that started this run, so the
|
|
48
|
+
* client anchors the inline affordance to the right transcript row.
|
|
49
|
+
* Absent when the run was not started by a tool call.
|
|
50
|
+
*/
|
|
51
|
+
parentToolUseId: z.string().optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
|
|
55
|
+
export type AcpAuthRequiredEvent = z.infer<typeof AcpAuthRequiredEventSchema>;
|
package/src/api/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import { AcpAuthRequiredEventSchema } from "./events/acp-auth-required.js";
|
|
3
4
|
import { AcpSessionCompletedEventSchema } from "./events/acp-session-completed.js";
|
|
4
5
|
import { AcpSessionErrorEventSchema } from "./events/acp-session-error.js";
|
|
5
6
|
import { AcpSessionSpawnedEventSchema } from "./events/acp-session-spawned.js";
|
|
@@ -149,6 +150,11 @@ export {
|
|
|
149
150
|
SSE_REPLAY_RING_COUNT_LIMIT,
|
|
150
151
|
} from "./constants/sse-replay.js";
|
|
151
152
|
export { DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC } from "./constants/tool-execution.js";
|
|
153
|
+
export {
|
|
154
|
+
ACP_CLAUDE_AUTH_REQUIRED_CODE,
|
|
155
|
+
type AcpAuthRequiredEvent,
|
|
156
|
+
AcpAuthRequiredEventSchema,
|
|
157
|
+
} from "./events/acp-auth-required.js";
|
|
152
158
|
export {
|
|
153
159
|
type AcpSessionCompletedEvent,
|
|
154
160
|
AcpSessionCompletedEventSchema,
|
|
@@ -895,6 +901,7 @@ export {
|
|
|
895
901
|
* migration recipe.
|
|
896
902
|
*/
|
|
897
903
|
export const AssistantEventSchema = z.discriminatedUnion("type", [
|
|
904
|
+
AcpAuthRequiredEventSchema,
|
|
898
905
|
AcpSessionCompletedEventSchema,
|
|
899
906
|
AcpSessionErrorEventSchema,
|
|
900
907
|
AcpSessionSpawnedEventSchema,
|
package/src/api/surfaces.ts
CHANGED
|
@@ -591,6 +591,7 @@ export const SURFACE_TYPES = [
|
|
|
591
591
|
"skill_card",
|
|
592
592
|
"call_summary",
|
|
593
593
|
"visual",
|
|
594
|
+
"voice_picker",
|
|
594
595
|
] as const;
|
|
595
596
|
|
|
596
597
|
export const SurfaceTypeSchema = z.enum(SURFACE_TYPES);
|
|
@@ -662,9 +663,10 @@ export type SurfaceData =
|
|
|
662
663
|
* and serves verbatim but whose shape it does not model, which is why they
|
|
663
664
|
* are absent from the renderable `SurfaceData` union: `channel_setup` (a
|
|
664
665
|
* side-effect command forwarded to the setup panel), `task_preferences` (a
|
|
665
|
-
* fixed grid that reads no data),
|
|
666
|
-
*
|
|
667
|
-
*
|
|
666
|
+
* fixed grid that reads no data), `voice_picker` (a settings card that reads
|
|
667
|
+
* its own config), and `skill_card` / `call_summary` (cards the daemon appends
|
|
668
|
+
* to history directly, the memory retrospective and a call summary, whose
|
|
669
|
+
* data shape is owned by their client renderers).
|
|
668
670
|
*/
|
|
669
671
|
export interface SurfaceDataByType {
|
|
670
672
|
card: CardSurfaceData;
|
|
@@ -684,6 +686,7 @@ export interface SurfaceDataByType {
|
|
|
684
686
|
skill_card: Record<string, unknown>;
|
|
685
687
|
call_summary: Record<string, unknown>;
|
|
686
688
|
visual: VisualSurfaceData;
|
|
689
|
+
voice_picker: Record<string, unknown>;
|
|
687
690
|
}
|
|
688
691
|
|
|
689
692
|
/** Any surface `data` payload, including the opaque (non-renderable) types. */
|
|
@@ -717,6 +720,7 @@ export const SURFACE_DATA_SCHEMAS: {
|
|
|
717
720
|
skill_card: z.record(z.string(), z.unknown()),
|
|
718
721
|
call_summary: z.record(z.string(), z.unknown()),
|
|
719
722
|
visual: VisualSurfaceDataSchema,
|
|
723
|
+
voice_picker: z.record(z.string(), z.unknown()),
|
|
720
724
|
};
|
|
721
725
|
|
|
722
726
|
/**
|
package/src/apps/app-store.ts
CHANGED
|
@@ -1422,6 +1422,9 @@ export function backfillAppConversationIds(): void {
|
|
|
1422
1422
|
}
|
|
1423
1423
|
|
|
1424
1424
|
try {
|
|
1425
|
+
// Unfinalized rows cannot match: their content is a { ref } pointer,
|
|
1426
|
+
// never inline surface JSON, so this scan sees finalized content only by
|
|
1427
|
+
// shape. No completeness predicate needed.
|
|
1425
1428
|
const rows = rawAll<{ conversation_id: string; content: string }>(
|
|
1426
1429
|
"apps:backfillConversationIds",
|
|
1427
1430
|
`SELECT conversation_id, content FROM messages WHERE content LIKE '%"type":"ui_surface"%'`,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { mkdir
|
|
9
|
+
import { mkdir } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { ensureBun } from "../util/bun-runtime.js";
|
|
@@ -24,8 +24,8 @@ export const ALLOWED_PACKAGES: readonly string[] = [
|
|
|
24
24
|
"clsx",
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
+
/** Bounds a runaway install; the allowlist above bounds what may be installed. */
|
|
27
28
|
const INSTALL_TIMEOUT_MS = 10_000;
|
|
28
|
-
const MAX_PACKAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
29
29
|
|
|
30
30
|
/** In-flight install promises keyed by package name, to deduplicate concurrent requests. */
|
|
31
31
|
const inflight = new Map<string, Promise<string | null>>();
|
|
@@ -148,37 +148,9 @@ async function installPackage(
|
|
|
148
148
|
return null;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
// Enforce max size
|
|
152
|
-
if (existsSync(pkgDir)) {
|
|
153
|
-
const size = await dirSize(pkgDir);
|
|
154
|
-
if (size > MAX_PACKAGE_SIZE_BYTES) {
|
|
155
|
-
log.warn({ pkg, size }, "Package exceeds size limit, removing");
|
|
156
|
-
const { rm } = await import("node:fs/promises");
|
|
157
|
-
await rm(pkgDir, { recursive: true, force: true });
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
151
|
return existsSync(pkgDir) ? nodeModulesDir : null;
|
|
163
152
|
} catch (err) {
|
|
164
153
|
log.warn({ pkg, err }, "Package resolution failed");
|
|
165
154
|
return null;
|
|
166
155
|
}
|
|
167
156
|
}
|
|
168
|
-
|
|
169
|
-
/** Recursively sum file sizes under a directory. */
|
|
170
|
-
async function dirSize(dir: string): Promise<number> {
|
|
171
|
-
const { readdir } = await import("node:fs/promises");
|
|
172
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
173
|
-
let total = 0;
|
|
174
|
-
for (const entry of entries) {
|
|
175
|
-
const full = join(dir, entry.name);
|
|
176
|
-
if (entry.isDirectory()) {
|
|
177
|
-
total += await dirSize(full);
|
|
178
|
-
} else {
|
|
179
|
-
const s = await stat(full);
|
|
180
|
-
total += s.size;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return total;
|
|
184
|
-
}
|
|
@@ -73,7 +73,11 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
73
73
|
import { setConfig } from "../../__tests__/helpers/set-config.js";
|
|
74
74
|
import { ABORT_WATCHDOG_MS } from "../../daemon/abort-watchdog.js";
|
|
75
75
|
import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
|
|
76
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
CALL_OPENING_MARKER,
|
|
78
|
+
ESCALATE_VERDICT_TOKEN,
|
|
79
|
+
HOLD_VERDICT_TOKEN,
|
|
80
|
+
} from "../voice-control-protocol.js";
|
|
77
81
|
import {
|
|
78
82
|
cutFrontDoorContentAtVerdict,
|
|
79
83
|
preSpeechLanguageRuleFragment,
|
|
@@ -1701,6 +1705,174 @@ describe("cutFrontDoorContentAtVerdict", () => {
|
|
|
1701
1705
|
});
|
|
1702
1706
|
});
|
|
1703
1707
|
|
|
1708
|
+
/**
|
|
1709
|
+
* The front-door leg's raw stream is a control plane: its leading tokens are
|
|
1710
|
+
* the routing verdict, not speech. The hub broadcast (web / passive devices)
|
|
1711
|
+
* must never carry those tokens, and must still carry every word of real
|
|
1712
|
+
* assistant content: an over-broad filter would leave the shared transcript
|
|
1713
|
+
* silent or truncated, which is worse than the leak it fixes.
|
|
1714
|
+
*/
|
|
1715
|
+
describe("front-door hub stream gate", () => {
|
|
1716
|
+
beforeEach(resetCrudLog);
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Conversation whose scripted agent loop announces a reserved row, streams
|
|
1720
|
+
* `deltas` in order, then ends the leg with `finalEvent`.
|
|
1721
|
+
*/
|
|
1722
|
+
function makeStreamingConversation(
|
|
1723
|
+
deltas: string[],
|
|
1724
|
+
finalEvent:
|
|
1725
|
+
| "message_complete"
|
|
1726
|
+
| "generation_cancelled" = "message_complete",
|
|
1727
|
+
): void {
|
|
1728
|
+
const fake = makeFakeConversation({ processing: false });
|
|
1729
|
+
fake.conversation.runAgentLoop = async (...args: unknown[]) => {
|
|
1730
|
+
const { onEvent } = args[2] as { onEvent: (msg: unknown) => void };
|
|
1731
|
+
onEvent({
|
|
1732
|
+
type: "assistant_turn_start",
|
|
1733
|
+
messageId: "assistant-row-1",
|
|
1734
|
+
conversationId: "conv-voice-bridge-test",
|
|
1735
|
+
});
|
|
1736
|
+
for (const text of deltas) {
|
|
1737
|
+
onEvent({
|
|
1738
|
+
type: "assistant_text_delta",
|
|
1739
|
+
text,
|
|
1740
|
+
messageId: "assistant-row-1",
|
|
1741
|
+
conversationId: "conv-voice-bridge-test",
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
onEvent({
|
|
1745
|
+
type: finalEvent,
|
|
1746
|
+
...(finalEvent === "message_complete"
|
|
1747
|
+
? { messageId: "assistant-row-1" }
|
|
1748
|
+
: {}),
|
|
1749
|
+
conversationId: "conv-voice-bridge-test",
|
|
1750
|
+
});
|
|
1751
|
+
};
|
|
1752
|
+
fakeConversation = fake.conversation;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** The text of every `assistant_text_delta` `turn` publishes to the hub. */
|
|
1756
|
+
async function collectBroadcastText(
|
|
1757
|
+
turn: () => Promise<unknown>,
|
|
1758
|
+
): Promise<string[]> {
|
|
1759
|
+
const texts: string[] = [];
|
|
1760
|
+
const subscription = assistantEventHub.subscribe({
|
|
1761
|
+
type: "process",
|
|
1762
|
+
filter: { conversationId: "conv-voice-bridge-test" },
|
|
1763
|
+
callback: (event) => {
|
|
1764
|
+
const msg = event.message as { type: string; text?: string };
|
|
1765
|
+
if (msg.type === "assistant_text_delta") {
|
|
1766
|
+
texts.push(msg.text ?? "");
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
});
|
|
1770
|
+
try {
|
|
1771
|
+
await turn();
|
|
1772
|
+
// The hub publishes off a promise chain, so every broadcast costs a
|
|
1773
|
+
// few microtask hops before it reaches a subscriber.
|
|
1774
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1775
|
+
await flushMicrotasks();
|
|
1776
|
+
} finally {
|
|
1777
|
+
subscription.dispose();
|
|
1778
|
+
}
|
|
1779
|
+
return texts;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
test("an escalating leg broadcasts only the capped bridge, never the verdict token", async () => {
|
|
1783
|
+
makeStreamingConversation([
|
|
1784
|
+
"[1]",
|
|
1785
|
+
" Let me check your calendar.",
|
|
1786
|
+
" weak answer past the cap",
|
|
1787
|
+
]);
|
|
1788
|
+
|
|
1789
|
+
const texts = await collectBroadcastText(() =>
|
|
1790
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
1791
|
+
);
|
|
1792
|
+
|
|
1793
|
+
expect(texts).toEqual(["Let me check your calendar."]);
|
|
1794
|
+
expect(texts.join("")).not.toContain(ESCALATE_VERDICT_TOKEN);
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
test("a front-door answer reaches the hub in full", async () => {
|
|
1798
|
+
makeStreamingConversation(["It is ", "Tuesday", ", and it is sunny."]);
|
|
1799
|
+
|
|
1800
|
+
const texts = await collectBroadcastText(() =>
|
|
1801
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
1802
|
+
);
|
|
1803
|
+
|
|
1804
|
+
expect(texts.join("")).toBe("It is Tuesday, and it is sunny.");
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
test("an answer that merely opens with a bracket is released in full", async () => {
|
|
1808
|
+
// "[" alone could still become the escalate token, so the gate holds it;
|
|
1809
|
+
// the next delta disproves the token and the whole prefix must come out.
|
|
1810
|
+
makeStreamingConversation(["[", "A] is the option to pick."]);
|
|
1811
|
+
|
|
1812
|
+
const texts = await collectBroadcastText(() =>
|
|
1813
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
1814
|
+
);
|
|
1815
|
+
|
|
1816
|
+
expect(texts.join("")).toBe("[A] is the option to pick.");
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
test("a hold verdict broadcasts nothing", async () => {
|
|
1820
|
+
makeStreamingConversation([HOLD_VERDICT_TOKEN]);
|
|
1821
|
+
|
|
1822
|
+
const texts = await collectBroadcastText(() =>
|
|
1823
|
+
startVoiceTurn({
|
|
1824
|
+
...makeTurnOptions(),
|
|
1825
|
+
routingLeg: "front-door",
|
|
1826
|
+
unifiedVerdict: true,
|
|
1827
|
+
}),
|
|
1828
|
+
);
|
|
1829
|
+
|
|
1830
|
+
expect(texts).toEqual([]);
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
test("a leg that completes mid-bridge broadcasts what it handed off with", async () => {
|
|
1834
|
+
makeStreamingConversation(["[1] Let me check"]);
|
|
1835
|
+
|
|
1836
|
+
const texts = await collectBroadcastText(() =>
|
|
1837
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
1838
|
+
);
|
|
1839
|
+
|
|
1840
|
+
expect(texts).toEqual(["Let me check"]);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
test("a leg cancelled mid-bridge never hands off, so it broadcasts nothing", async () => {
|
|
1844
|
+
makeStreamingConversation(["[1] Let me check"], "generation_cancelled");
|
|
1845
|
+
|
|
1846
|
+
const texts = await collectBroadcastText(() =>
|
|
1847
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
1848
|
+
);
|
|
1849
|
+
|
|
1850
|
+
expect(texts).toEqual([]);
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
test("the escalated continuation streams to the hub untouched", async () => {
|
|
1854
|
+
// The leg that produces the real answer is never gated: its deltas are
|
|
1855
|
+
// assistant speech from the first token.
|
|
1856
|
+
makeStreamingConversation(["Your next meeting ", "is at four."]);
|
|
1857
|
+
|
|
1858
|
+
const texts = await collectBroadcastText(() =>
|
|
1859
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "escalated" }),
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
expect(texts).toEqual(["Your next meeting ", "is at four."]);
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
test("an un-routed voice leg streams to the hub untouched", async () => {
|
|
1866
|
+
makeStreamingConversation(["[1] not a verdict here"]);
|
|
1867
|
+
|
|
1868
|
+
const texts = await collectBroadcastText(() =>
|
|
1869
|
+
startVoiceTurn(makeTurnOptions()),
|
|
1870
|
+
);
|
|
1871
|
+
|
|
1872
|
+
expect(texts).toEqual(["[1] not a verdict here"]);
|
|
1873
|
+
});
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1704
1876
|
describe("transcript hygiene (teardown pass)", () => {
|
|
1705
1877
|
beforeEach(resetCrudLog);
|
|
1706
1878
|
|
|
@@ -4,6 +4,7 @@ import { DEEPGRAM_MULTI_LANGUAGE_CODES } from "../../providers/speech-to-text/de
|
|
|
4
4
|
import {
|
|
5
5
|
capEscalationBridge,
|
|
6
6
|
classifyFrontDoorLeading,
|
|
7
|
+
createFrontDoorStreamGate,
|
|
7
8
|
ESCALATE_VERDICT_TOKEN,
|
|
8
9
|
escalatedContinuationRule,
|
|
9
10
|
ESCALATION_CONTINUATION_CONTENT,
|
|
@@ -439,3 +440,96 @@ describe("live-voice escalation orchestration (end-to-end — TODO)", () => {
|
|
|
439
440
|
() => {},
|
|
440
441
|
);
|
|
441
442
|
});
|
|
443
|
+
|
|
444
|
+
describe("createFrontDoorStreamGate", () => {
|
|
445
|
+
/** Feed `deltas` through a gate and return what it released, in order. */
|
|
446
|
+
function release(
|
|
447
|
+
deltas: string[],
|
|
448
|
+
opts?: { holdEnabled?: boolean; finish?: boolean },
|
|
449
|
+
): string[] {
|
|
450
|
+
const gate = createFrontDoorStreamGate(opts?.holdEnabled === true);
|
|
451
|
+
const out = deltas
|
|
452
|
+
.map((delta) => gate.push(delta))
|
|
453
|
+
.filter((chunk) => chunk.length > 0);
|
|
454
|
+
if (opts?.finish !== false) {
|
|
455
|
+
const trailing = gate.finish();
|
|
456
|
+
if (trailing.length > 0) {
|
|
457
|
+
out.push(trailing);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
test("an answer passes through delta by delta", () => {
|
|
464
|
+
expect(release(["It is ", "Tuesday."])).toEqual(["It is ", "Tuesday."]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("leading text held while the verdict was pending is not lost", () => {
|
|
468
|
+
expect(release(["[", "A] is the option."])).toEqual(["[A] is the option."]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("an escalation releases the capped bridge and nothing else", () => {
|
|
472
|
+
expect(
|
|
473
|
+
release([
|
|
474
|
+
ESCALATE_VERDICT_TOKEN,
|
|
475
|
+
" Let me check your calendar.",
|
|
476
|
+
" rambling past the cap",
|
|
477
|
+
]),
|
|
478
|
+
).toEqual(["Let me check your calendar."]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("a verdict token split across deltas still never escapes", () => {
|
|
482
|
+
expect(release(["[", "1", "] One moment.", " more"])).toEqual([
|
|
483
|
+
"One moment.",
|
|
484
|
+
]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("a too-short bridge releases nothing, matching the audio-only fallback", () => {
|
|
488
|
+
// Under MIN_SPOKEN_BRIDGE_CHARS the session discards the model's bridge
|
|
489
|
+
// and plays a canned fallback with no caption and no persisted row, so
|
|
490
|
+
// releasing the capped text would show words the caller never heard.
|
|
491
|
+
expect(release([ESCALATE_VERDICT_TOKEN, " ok"])).toEqual([]);
|
|
492
|
+
expect(release([ESCALATE_VERDICT_TOKEN, " ."])).toEqual([]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("a too-short bridge releases nothing via finish either", () => {
|
|
496
|
+
const gate = createFrontDoorStreamGate(false);
|
|
497
|
+
expect(gate.push(`${ESCALATE_VERDICT_TOKEN} ok`)).toBe("");
|
|
498
|
+
expect(gate.finish()).toBe("");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("a bridge at the spoken threshold is still released", () => {
|
|
502
|
+
// The boundary belongs to the spoken side: the session speaks anything
|
|
503
|
+
// that is not BELOW the threshold, so the gate must release it too.
|
|
504
|
+
expect(release([ESCALATE_VERDICT_TOKEN, " Sure."])).toEqual(["Sure."]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("a bridge with no terminator is released by finish", () => {
|
|
508
|
+
const gate = createFrontDoorStreamGate(false);
|
|
509
|
+
expect(gate.push("[1] Let me check")).toBe("");
|
|
510
|
+
expect(gate.finish()).toBe("Let me check");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("a bridge is dropped when the leg never finishes (cancellation)", () => {
|
|
514
|
+
expect(release(["[1] Let me check"], { finish: false })).toEqual([]);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("a hold verdict releases nothing", () => {
|
|
518
|
+
expect(release([HOLD_VERDICT_TOKEN], { holdEnabled: true })).toEqual([]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("the hold token is ordinary text on a leg that was never taught it", () => {
|
|
522
|
+
// A non-speculative leg's prompt has no hold branch, so its output must
|
|
523
|
+
// not be swallowed by a token it could only have parroted.
|
|
524
|
+
expect(release([`${HOLD_VERDICT_TOKEN} is the index.`])).toEqual([
|
|
525
|
+
`${HOLD_VERDICT_TOKEN} is the index.`,
|
|
526
|
+
]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("nothing more escapes after the bridge is released", () => {
|
|
530
|
+
const gate = createFrontDoorStreamGate(false);
|
|
531
|
+
expect(gate.push("[1] One moment.")).toBe("One moment.");
|
|
532
|
+
expect(gate.push(" and a leaked continuation")).toBe("");
|
|
533
|
+
expect(gate.finish()).toBe("");
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -21,6 +21,7 @@ import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
|
21
21
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
22
22
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
23
23
|
import { getCatalogProvider } from "../tts/provider-catalog.js";
|
|
24
|
+
import { createReasoningTagFilter } from "../tts/reasoning-tag-filter.js";
|
|
24
25
|
import { extractSpeakableSegments } from "../tts/speakable-segments.js";
|
|
25
26
|
import {
|
|
26
27
|
type AudioStoreSink,
|
|
@@ -728,6 +729,12 @@ export class CallController {
|
|
|
728
729
|
// could be the start of a control marker.
|
|
729
730
|
let ttsBuffer = "";
|
|
730
731
|
let fullResponseText = "";
|
|
732
|
+
// Reasoning models can inline <think> spans in the content stream when a
|
|
733
|
+
// profile has not opted into parseThinkTags. Neither the spoken path nor
|
|
734
|
+
// the post-turn consumers of fullResponseText (transcripts,
|
|
735
|
+
// assistant_spoke, END_CALL/ASK_GUARDIAN detection) may see them: both
|
|
736
|
+
// are fed only filtered text.
|
|
737
|
+
const reasoningFilter = createReasoningTagFilter();
|
|
731
738
|
|
|
732
739
|
// Synthesized path: text is split at speakable boundaries as it streams
|
|
733
740
|
// and each segment is synthesized while the LLM keeps generating. The
|
|
@@ -930,8 +937,13 @@ export class CallController {
|
|
|
930
937
|
if (!this.isCurrentRun(runVersion)) {
|
|
931
938
|
return;
|
|
932
939
|
}
|
|
933
|
-
|
|
934
|
-
|
|
940
|
+
// One filter feeds both consumers: the spoken stream and the text
|
|
941
|
+
// used for transcripts, assistant_spoke, and END_CALL/ASK_GUARDIAN
|
|
942
|
+
// marker detection. A control marker inside a reasoning span must
|
|
943
|
+
// never trigger a real action the caller did not hear.
|
|
944
|
+
const speakable = reasoningFilter.push(text);
|
|
945
|
+
fullResponseText += speakable;
|
|
946
|
+
ttsBuffer += speakable;
|
|
935
947
|
ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
|
|
936
948
|
flushSafeText();
|
|
937
949
|
};
|
|
@@ -1010,7 +1022,11 @@ export class CallController {
|
|
|
1010
1022
|
return fullResponseText;
|
|
1011
1023
|
}
|
|
1012
1024
|
|
|
1013
|
-
// Final sweep:
|
|
1025
|
+
// Final sweep: release any held-back partial tag to both consumers,
|
|
1026
|
+
// then strip any remaining control markers from the buffer.
|
|
1027
|
+
const filterTail = reasoningFilter.flush();
|
|
1028
|
+
fullResponseText += filterTail;
|
|
1029
|
+
ttsBuffer += filterTail;
|
|
1014
1030
|
ttsBuffer = stripInternalSpeechMarkers(ttsBuffer);
|
|
1015
1031
|
if (ttsBuffer.length > 0) {
|
|
1016
1032
|
emitSafeChunk(ttsBuffer);
|
|
@@ -1393,7 +1393,6 @@ export class CallSetupFlow {
|
|
|
1393
1393
|
|
|
1394
1394
|
try {
|
|
1395
1395
|
const accessResult = await ndeps.notifyGuardianOfAccessRequest({
|
|
1396
|
-
canonicalAssistantId: accessRequest.assistantId,
|
|
1397
1396
|
sourceChannel: "phone",
|
|
1398
1397
|
conversationExternalId: accessRequest.fromNumber,
|
|
1399
1398
|
actorExternalId: accessRequest.fromNumber,
|
|
@@ -538,6 +538,17 @@ export class MediaStreamSttSession {
|
|
|
538
538
|
switch (event.type) {
|
|
539
539
|
case "partial":
|
|
540
540
|
return;
|
|
541
|
+
case "finalized":
|
|
542
|
+
return;
|
|
543
|
+
case "turn-start":
|
|
544
|
+
case "eager-turn-end":
|
|
545
|
+
case "turn-resumed":
|
|
546
|
+
case "turn-end":
|
|
547
|
+
// The local VAD and the provider's boundary finals own turn
|
|
548
|
+
// taking. Listed case-by-case rather than under `default` so the
|
|
549
|
+
// exhaustiveness check flags this site when turn detection is
|
|
550
|
+
// wired in.
|
|
551
|
+
return;
|
|
541
552
|
case "final": {
|
|
542
553
|
const durationMs = Math.round(this.utteranceAudioMs);
|
|
543
554
|
this.utteranceAudioMs = 0;
|
|
@@ -570,6 +581,10 @@ export class MediaStreamSttSession {
|
|
|
570
581
|
this.enterBatchMode();
|
|
571
582
|
}
|
|
572
583
|
return;
|
|
584
|
+
default: {
|
|
585
|
+
const _exhaustive: never = event;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
573
588
|
}
|
|
574
589
|
}
|
|
575
590
|
|