@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
|
@@ -798,12 +798,125 @@ export function replaceInDocument(
|
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// Append idempotency
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
|
|
805
|
+
/** A blank-line-delimited block of markdown, located in its source string. */
|
|
806
|
+
interface MarkdownBlock {
|
|
807
|
+
/** Whitespace-trimmed block text, the unit duplicate detection compares. */
|
|
808
|
+
text: string;
|
|
809
|
+
/** Offset of the block's first character in the source string. */
|
|
810
|
+
start: number;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Split markdown into blank-line-delimited blocks, dropping empty ones but
|
|
815
|
+
* keeping each surviving block's offset so a suffix of the source can be
|
|
816
|
+
* recovered verbatim.
|
|
817
|
+
*/
|
|
818
|
+
function splitIntoBlocks(content: string): MarkdownBlock[] {
|
|
819
|
+
const blocks: MarkdownBlock[] = [];
|
|
820
|
+
const separator = /\n[^\S\n]*\n/g;
|
|
821
|
+
let cursor = 0;
|
|
822
|
+
let match: RegExpExecArray | null;
|
|
823
|
+
const push = (start: number, end: number): void => {
|
|
824
|
+
const text = content.slice(start, end).trim();
|
|
825
|
+
if (text.length > 0) {
|
|
826
|
+
blocks.push({ text, start });
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
while ((match = separator.exec(content)) !== null) {
|
|
830
|
+
push(cursor, match.index);
|
|
831
|
+
cursor = match.index + match[0].length;
|
|
832
|
+
}
|
|
833
|
+
push(cursor, content.length);
|
|
834
|
+
return blocks;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* An append has to repeat at least this many characters of the document's tail
|
|
839
|
+
* before the repetition is treated as an accident.
|
|
840
|
+
*
|
|
841
|
+
* The floor is what keeps legitimate repetition intact. A document can honestly
|
|
842
|
+
* repeat a heading, a refrain, a table row, or a bare "TODO" back to back, and
|
|
843
|
+
* those are all short. Only a substantial restatement (an opening paragraph
|
|
844
|
+
* passed to both `document_create` and the first append) clears the bar.
|
|
845
|
+
*/
|
|
846
|
+
const MIN_DUPLICATE_APPEND_CHARS = 40;
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Drop a leading run of blocks from `markdown` that exactly repeats the tail of
|
|
850
|
+
* `existing`, so an append that restates content already committed does not
|
|
851
|
+
* write it a second time.
|
|
852
|
+
*
|
|
853
|
+
* The guard is deliberately narrow: the repeated run must be an exact,
|
|
854
|
+
* block-aligned match, it must sit at the very start of the append and the very
|
|
855
|
+
* end of the document, and it must be at least
|
|
856
|
+
* {@link MIN_DUPLICATE_APPEND_CHARS} long. Anything else, including the same
|
|
857
|
+
* text reappearing further down the append, is passed through untouched.
|
|
858
|
+
*/
|
|
859
|
+
function stripDuplicateLeadingBlocks(
|
|
860
|
+
existing: string,
|
|
861
|
+
markdown: string,
|
|
862
|
+
): string {
|
|
863
|
+
const existingBlocks = splitIntoBlocks(existing);
|
|
864
|
+
const incomingBlocks = splitIntoBlocks(markdown);
|
|
865
|
+
const maxOverlap = Math.min(existingBlocks.length, incomingBlocks.length);
|
|
866
|
+
|
|
867
|
+
let overlap = 0;
|
|
868
|
+
for (let candidate = maxOverlap; candidate >= 1; candidate--) {
|
|
869
|
+
const offset = existingBlocks.length - candidate;
|
|
870
|
+
let matches = true;
|
|
871
|
+
for (let i = 0; i < candidate; i++) {
|
|
872
|
+
if (existingBlocks[offset + i].text !== incomingBlocks[i].text) {
|
|
873
|
+
matches = false;
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (matches) {
|
|
878
|
+
overlap = candidate;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (overlap === 0) {
|
|
883
|
+
return markdown;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const duplicatedChars = incomingBlocks
|
|
887
|
+
.slice(0, overlap)
|
|
888
|
+
.reduce((total, block) => total + block.text.length, 0);
|
|
889
|
+
if (duplicatedChars < MIN_DUPLICATE_APPEND_CHARS) {
|
|
890
|
+
return markdown;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (overlap === incomingBlocks.length) {
|
|
894
|
+
return "";
|
|
895
|
+
}
|
|
896
|
+
return markdown
|
|
897
|
+
.slice(incomingBlocks[overlap].start)
|
|
898
|
+
.replace(/^(?:[^\S\n]*\n)+/, "");
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Outcome of a successful {@link updateDocumentContent} call. */
|
|
902
|
+
export interface DocumentContentUpdated {
|
|
903
|
+
success: true;
|
|
904
|
+
/**
|
|
905
|
+
* The markdown that actually landed. Equals the submitted markdown except
|
|
906
|
+
* when an append's leading blocks duplicated the document's tail, in which
|
|
907
|
+
* case it is the remainder. Clients render this so their view matches the row.
|
|
908
|
+
*/
|
|
909
|
+
appliedMarkdown: string;
|
|
910
|
+
/** True when a duplicated leading run was dropped from an append. */
|
|
911
|
+
duplicateLeadingContentSkipped: boolean;
|
|
912
|
+
}
|
|
913
|
+
|
|
801
914
|
/** Update persisted document content (append or replace). */
|
|
802
915
|
export function updateDocumentContent(
|
|
803
916
|
surfaceId: string,
|
|
804
917
|
markdown: string,
|
|
805
918
|
mode: string,
|
|
806
|
-
):
|
|
919
|
+
): DocumentContentUpdated | { success: false; error: string } {
|
|
807
920
|
try {
|
|
808
921
|
const existing = rawGet<{ content: string }>(
|
|
809
922
|
"documents:updateDocumentContent:get",
|
|
@@ -814,9 +927,19 @@ export function updateDocumentContent(
|
|
|
814
927
|
log.info({ surfaceId }, "No persisted document to update");
|
|
815
928
|
return { success: false, error: "Document not found" };
|
|
816
929
|
}
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
930
|
+
const appending = mode === "append";
|
|
931
|
+
const appliedMarkdown = appending
|
|
932
|
+
? stripDuplicateLeadingBlocks(existing.content, markdown)
|
|
933
|
+
: markdown;
|
|
934
|
+
const duplicateLeadingContentSkipped =
|
|
935
|
+
appending && appliedMarkdown !== markdown;
|
|
936
|
+
const sep =
|
|
937
|
+
appending && existing.content.length > 0 && appliedMarkdown.length > 0
|
|
938
|
+
? "\n\n"
|
|
939
|
+
: "";
|
|
940
|
+
const newContent = appending
|
|
941
|
+
? existing.content + sep + appliedMarkdown
|
|
942
|
+
: appliedMarkdown;
|
|
820
943
|
writeThroughToWorkspaceFile(surfaceId, newContent);
|
|
821
944
|
const wordCount = countWords(newContent);
|
|
822
945
|
rawRun(
|
|
@@ -827,8 +950,18 @@ export function updateDocumentContent(
|
|
|
827
950
|
Date.now(),
|
|
828
951
|
surfaceId,
|
|
829
952
|
);
|
|
953
|
+
if (duplicateLeadingContentSkipped) {
|
|
954
|
+
log.info(
|
|
955
|
+
{ surfaceId, skippedChars: markdown.length - appliedMarkdown.length },
|
|
956
|
+
"Skipped append content duplicating the document tail",
|
|
957
|
+
);
|
|
958
|
+
}
|
|
830
959
|
log.info({ surfaceId, mode }, "Updated document content");
|
|
831
|
-
return {
|
|
960
|
+
return {
|
|
961
|
+
success: true,
|
|
962
|
+
appliedMarkdown,
|
|
963
|
+
duplicateLeadingContentSkipped,
|
|
964
|
+
};
|
|
832
965
|
} catch (error) {
|
|
833
966
|
log.error({ err: error, surfaceId }, "Document content update error");
|
|
834
967
|
return {
|
package/src/hooks/hook-loader.ts
CHANGED
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
* registry, so the next read re-resolves fresh.
|
|
40
40
|
*
|
|
41
41
|
* Plugin *discovery* (which plugin directories exist, in what order) lives in
|
|
42
|
-
* `../plugins/mtime-cache.ts`;
|
|
43
|
-
*
|
|
44
|
-
* this module lets it sit below the plugin cache with no import cycle.
|
|
42
|
+
* `../plugins/mtime-cache.ts`; hook lookup in `./registry.ts` passes the
|
|
43
|
+
* discovered names into {@link collectUserHookEntries}. Keeping discovery out
|
|
44
|
+
* of this module lets it sit below the plugin cache with no import cycle.
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
import {
|
package/src/hooks/registry.ts
CHANGED
|
@@ -6,20 +6,22 @@
|
|
|
6
6
|
* hook-chain ordering.
|
|
7
7
|
*
|
|
8
8
|
* User-land hooks (from the filesystem) are owned by
|
|
9
|
-
* {@link ./hook-loader.ts} and surfaced through
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* {@link ./hook-loader.ts} and surfaced through {@link getUserHooksFor}.
|
|
10
|
+
* This module owns the in-process hooks that default plugins register at
|
|
11
|
+
* boot, plus the user-land lookup that walks discovered plugin names from
|
|
12
|
+
* the plugin cache.
|
|
12
13
|
*
|
|
13
14
|
* {@link getHooksFor} combines both sources: in-process hooks from this
|
|
14
15
|
* registry (filtered by `isPluginDisabled` at read time) and user-land hooks
|
|
15
16
|
* from the plugin cache. The read-time filtering is what makes `assistant
|
|
16
|
-
* plugins disable default-*` take effect immediately in a running assistant
|
|
17
|
-
*
|
|
17
|
+
* plugins disable default-*` take effect immediately in a running assistant:
|
|
18
|
+
* the hooks stay registered but are filtered out on the next turn.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
21
|
-
import {
|
|
22
|
+
import { getDiscoveredUserPluginNames } from "../plugins/mtime-cache.js";
|
|
22
23
|
import type { HookEntry, HookFunction } from "../plugins/types.js";
|
|
24
|
+
import { collectUserHookEntries } from "./hook-loader.js";
|
|
23
25
|
|
|
24
26
|
// ─── Internal state ──────────────────────────────────────────────────────────
|
|
25
27
|
|
|
@@ -75,6 +77,48 @@ export function unregisterPluginHooks(pluginName: string): void {
|
|
|
75
77
|
|
|
76
78
|
// ─── Queries ─────────────────────────────────────────────────────────────────
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Get all hooks for a given event name from user plugins and standalone
|
|
82
|
+
* workspace hooks, from the hook loader's in-memory cache. Plugin hooks run
|
|
83
|
+
* in install-date order, the workspace hook runs last.
|
|
84
|
+
*
|
|
85
|
+
* This is a pure cache read: it never scans disk, activates a plugin, or
|
|
86
|
+
* runs `init`. Activation happens only at boot (`populateCacheAtBoot`)
|
|
87
|
+
* and through the imperative install/uninstall poke
|
|
88
|
+
* (`reconcilePluginSourcesNow`), both main-daemon paths, so a
|
|
89
|
+
* sidecar process that dispatches hooks (a worker running conversation
|
|
90
|
+
* turns) can never bring a plugin up in its own process.
|
|
91
|
+
*
|
|
92
|
+
* `effectiveEnabledPlugins` carries the per-chat plugin scope: when non-null,
|
|
93
|
+
* user plugins outside the set are skipped (standalone workspace hooks always
|
|
94
|
+
* run). `null`/omitted means no per-chat restriction.
|
|
95
|
+
*/
|
|
96
|
+
export async function getUserHookEntriesFor<TCtx = unknown>(
|
|
97
|
+
hookName: string,
|
|
98
|
+
effectiveEnabledPlugins?: Set<string> | null,
|
|
99
|
+
): Promise<HookEntry<TCtx>[]> {
|
|
100
|
+
return collectUserHookEntries<TCtx>(
|
|
101
|
+
hookName,
|
|
102
|
+
getDiscoveredUserPluginNames(),
|
|
103
|
+
effectiveEnabledPlugins,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* {@link getUserHookEntriesFor} without owner attribution. Returns just the
|
|
109
|
+
* hook functions in the same order.
|
|
110
|
+
*/
|
|
111
|
+
export async function getUserHooksFor<TCtx = unknown>(
|
|
112
|
+
hookName: string,
|
|
113
|
+
effectiveEnabledPlugins?: Set<string> | null,
|
|
114
|
+
): Promise<HookFunction<TCtx>[]> {
|
|
115
|
+
const entries = await getUserHookEntriesFor<TCtx>(
|
|
116
|
+
hookName,
|
|
117
|
+
effectiveEnabledPlugins,
|
|
118
|
+
);
|
|
119
|
+
return entries.map((e) => e.fn);
|
|
120
|
+
}
|
|
121
|
+
|
|
78
122
|
/**
|
|
79
123
|
* Collect every registered hook for the given name, in registration order.
|
|
80
124
|
* Plugins that don't declare a hook for `name` are skipped. Used by the
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the shared OAuth callback URL resolver.
|
|
3
|
+
*
|
|
4
|
+
* The property that matters is stability: every caller and every attempt
|
|
5
|
+
* must get the same redirect URI. Dynamic client registration and Client
|
|
6
|
+
* ID Metadata Documents both pin `redirect_uris` when the client is
|
|
7
|
+
* registered, and the authorization server matches the value exactly on
|
|
8
|
+
* each authorization request, so a URI that varies per attempt makes both
|
|
9
|
+
* mechanisms unusable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
const resolveCallbackUrl = mock(
|
|
15
|
+
async () => "https://ingress.example/webhooks/oauth/callback",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
mock.module("../platform-callback-registration.js", () => ({
|
|
19
|
+
resolveCallbackUrl,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module("../public-ingress-urls.js", () => ({
|
|
23
|
+
getOAuthCallbackUrl: () => "https://direct.example/webhooks/oauth/callback",
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module("../../config/loader.js", () => ({
|
|
27
|
+
loadConfig: () => ({}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const { OAUTH_CALLBACK_PATH, resolveOauthCallbackUrl } =
|
|
31
|
+
await import("../oauth-callback-url.js");
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
resolveCallbackUrl.mockClear();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("resolveOauthCallbackUrl", () => {
|
|
38
|
+
test("returns whatever the shared resolver decides", async () => {
|
|
39
|
+
await expect(resolveOauthCallbackUrl()).resolves.toEqual(
|
|
40
|
+
"https://ingress.example/webhooks/oauth/callback",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("registers the shared callback path, not a per-caller one", async () => {
|
|
45
|
+
await resolveOauthCallbackUrl();
|
|
46
|
+
const [, callbackPath] = resolveCallbackUrl.mock.calls[0] as unknown[];
|
|
47
|
+
expect(callbackPath).toEqual(OAUTH_CALLBACK_PATH);
|
|
48
|
+
expect(OAUTH_CALLBACK_PATH).toEqual("webhooks/oauth/callback");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("every call resolves to the same URL, path, and registration type", async () => {
|
|
52
|
+
// The URI a client registers and the URI it later authorizes with are
|
|
53
|
+
// the same string, which is what an exact-match redirect_uri check
|
|
54
|
+
// needs. Nothing about the caller can vary it: the function takes no
|
|
55
|
+
// arguments.
|
|
56
|
+
const first = await resolveOauthCallbackUrl();
|
|
57
|
+
const second = await resolveOauthCallbackUrl();
|
|
58
|
+
expect(first).toEqual(second);
|
|
59
|
+
|
|
60
|
+
const [, pathA, typeA] = resolveCallbackUrl.mock.calls[0] as unknown[];
|
|
61
|
+
const [, pathB, typeB] = resolveCallbackUrl.mock.calls[1] as unknown[];
|
|
62
|
+
expect(pathA).toEqual(pathB);
|
|
63
|
+
expect(typeA).toEqual(typeB);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("registers one route type so the platform holds a single admin row", async () => {
|
|
67
|
+
await resolveOauthCallbackUrl();
|
|
68
|
+
const [, , type] = resolveCallbackUrl.mock.calls[0] as unknown[];
|
|
69
|
+
expect(type).toEqual("oauth");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("propagates the resolver's failure rather than inventing a URL", async () => {
|
|
73
|
+
// No ingress and no platform connection means there is no URL that
|
|
74
|
+
// would work. Returning a plausible one produces an authorization
|
|
75
|
+
// request whose callback silently never arrives.
|
|
76
|
+
resolveCallbackUrl.mockImplementationOnce(async () => {
|
|
77
|
+
throw new Error("No public base URL configured.");
|
|
78
|
+
});
|
|
79
|
+
await expect(resolveOauthCallbackUrl()).rejects.toThrow(
|
|
80
|
+
/No public base URL/,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public URL resolution for the assistant's shared OAuth callback route.
|
|
3
|
+
*
|
|
4
|
+
* Every browser-based OAuth flow in the daemon redirects back to one path,
|
|
5
|
+
* `webhooks/oauth/callback`, which the gateway serves and forwards to
|
|
6
|
+
* `POST /v1/internal/oauth/callback`. The runtime matches the arriving
|
|
7
|
+
* `state` against `security/oauth-callback-registry.ts` and resolves the
|
|
8
|
+
* waiting flow, so one route multiplexes every concurrent handshake and no
|
|
9
|
+
* caller needs a listener of its own.
|
|
10
|
+
*
|
|
11
|
+
* Which base URL is correct depends on how the assistant is reachable: a
|
|
12
|
+
* platform pod and a platform-connected assistant are served through a
|
|
13
|
+
* managed callback route, a self-hosted deployment through its configured
|
|
14
|
+
* public ingress. `resolveCallbackUrl` owns that decision, and this is the
|
|
15
|
+
* OAuth-shaped entry point to it, mirroring `plugin-api/webhook-url.ts`
|
|
16
|
+
* for plugin ingress.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { loadConfig } from "../config/loader.js";
|
|
20
|
+
import { resolveCallbackUrl } from "./platform-callback-registration.js";
|
|
21
|
+
import { getOAuthCallbackUrl } from "./public-ingress-urls.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Path the gateway serves the shared OAuth callback on.
|
|
25
|
+
*
|
|
26
|
+
* Fixed rather than caller-supplied. Every flow is demultiplexed by OAuth
|
|
27
|
+
* `state`, so a per-caller path would buy nothing and would need its own
|
|
28
|
+
* gateway route and its own platform registration.
|
|
29
|
+
*/
|
|
30
|
+
export const OAUTH_CALLBACK_PATH = "webhooks/oauth/callback";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registration type recorded with the platform.
|
|
34
|
+
*
|
|
35
|
+
* The platform keys a callback route by `(path, type)`, and there is one
|
|
36
|
+
* OAuth route, so one type describes it. Registering the same path under
|
|
37
|
+
* several types would produce duplicate admin rows pointing at the same
|
|
38
|
+
* URL.
|
|
39
|
+
*/
|
|
40
|
+
const OAUTH_REGISTRATION_TYPE = "oauth";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the redirect URI an authorization server should send the user
|
|
44
|
+
* back to.
|
|
45
|
+
*
|
|
46
|
+
* Takes no arguments on purpose: every caller shares one route, so there
|
|
47
|
+
* is nothing to vary. That is what makes the URI stable across callers and
|
|
48
|
+
* across attempts, which is what an exact-match `redirect_uri` check needs.
|
|
49
|
+
*
|
|
50
|
+
* @throws when no public ingress is configured and the assistant is not
|
|
51
|
+
* connected to the platform. There is no URL that would work in that
|
|
52
|
+
* case, and returning a plausible one produces an authorization request
|
|
53
|
+
* whose callback silently never arrives.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolveOauthCallbackUrl(): Promise<string> {
|
|
56
|
+
return resolveCallbackUrl(
|
|
57
|
+
() => getOAuthCallbackUrl(loadConfig()),
|
|
58
|
+
OAUTH_CALLBACK_PATH,
|
|
59
|
+
OAUTH_REGISTRATION_TYPE,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { sanitizeForTts } from "../../calls/tts-text-sanitizer.js";
|
|
4
3
|
import type {
|
|
5
4
|
VoiceTurnCallbacks,
|
|
6
5
|
VoiceTurnOptions,
|
|
7
6
|
} from "../../calls/voice-session-bridge.js";
|
|
8
|
-
import type { LiveVoiceFrontModelConfig } from "../../config/schemas/live-voice.js";
|
|
9
7
|
import type {
|
|
10
8
|
StreamingTranscriber,
|
|
11
9
|
SttStreamServerEvent,
|
|
12
10
|
} from "../../stt/types.js";
|
|
13
|
-
import type { VoiceFrontDecider } from "../front-decision.js";
|
|
14
11
|
import {
|
|
15
12
|
LiveVoiceSession,
|
|
16
13
|
type LiveVoiceTtsStreamer,
|
|
@@ -96,8 +93,6 @@ function createSessionHarness(
|
|
|
96
93
|
createTurnId?: () => string;
|
|
97
94
|
emitMetrics?: boolean;
|
|
98
95
|
streamTtsAudio?: LiveVoiceTtsStreamer;
|
|
99
|
-
frontModelConfig?: Partial<LiveVoiceFrontModelConfig>;
|
|
100
|
-
frontDecider?: VoiceFrontDecider;
|
|
101
96
|
} = {},
|
|
102
97
|
) {
|
|
103
98
|
const transcriber =
|
|
@@ -119,10 +114,6 @@ function createSessionHarness(
|
|
|
119
114
|
...(options.streamTtsAudio
|
|
120
115
|
? { streamTtsAudio: options.streamTtsAudio }
|
|
121
116
|
: {}),
|
|
122
|
-
...(options.frontModelConfig
|
|
123
|
-
? { frontModelConfig: options.frontModelConfig }
|
|
124
|
-
: {}),
|
|
125
|
-
...(options.frontDecider ? { frontDecider: options.frontDecider } : {}),
|
|
126
117
|
});
|
|
127
118
|
|
|
128
119
|
return { frames, session, startVoiceTurn, transcriber };
|
|
@@ -468,7 +459,7 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
468
459
|
});
|
|
469
460
|
});
|
|
470
461
|
|
|
471
|
-
test("
|
|
462
|
+
test("publishes tool activity while the turn is running", async () => {
|
|
472
463
|
let callbacks: VoiceTurnCallbacks | undefined;
|
|
473
464
|
const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
|
|
474
465
|
callbacks = options.callbacks;
|
|
@@ -488,12 +479,6 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
488
479
|
callbacks?.tool_use_start?.("some_tool");
|
|
489
480
|
await waitForFrameCount(frames, 4);
|
|
490
481
|
|
|
491
|
-
const activeTurn = (
|
|
492
|
-
session as unknown as {
|
|
493
|
-
activeAssistantTurn: { toolUseStarted: boolean } | null;
|
|
494
|
-
}
|
|
495
|
-
).activeAssistantTurn;
|
|
496
|
-
expect(activeTurn?.toolUseStarted).toBe(true);
|
|
497
482
|
// One activity frame, so a silent stretch of tool work is visible on the
|
|
498
483
|
// surfaces that show the session. An unrecognized tool still gets a line.
|
|
499
484
|
expect(frames.at(-1)).toMatchObject({
|
|
@@ -560,90 +545,6 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
560
545
|
});
|
|
561
546
|
});
|
|
562
547
|
|
|
563
|
-
describe("LiveVoiceSession tool-use spoken ack", () => {
|
|
564
|
-
const ACK_TIMEOUT_MS = 40;
|
|
565
|
-
// Every ack is front-model-phrased; the generated text passes through the
|
|
566
|
-
// same TTS sanitizer as regular segments. The stub varies its phrasing by
|
|
567
|
-
// trigger so the two ack kinds stay distinguishable in assertions.
|
|
568
|
-
const TOOL_ACK = "Let me look that up.";
|
|
569
|
-
const FIRST_DELTA_ACK = "Sure — one moment.";
|
|
570
|
-
const EXPECTED_TOOL_ACK = sanitizeForTts(TOOL_ACK).trim();
|
|
571
|
-
const EXPECTED_FIRST_DELTA_ACK = sanitizeForTts(FIRST_DELTA_ACK).trim();
|
|
572
|
-
|
|
573
|
-
const ackDecider: VoiceFrontDecider = {
|
|
574
|
-
generateAckText: async (input) =>
|
|
575
|
-
input.toolName ? TOOL_ACK : FIRST_DELTA_ACK,
|
|
576
|
-
generateProgressText: async () => null,
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
function createAckHarness() {
|
|
580
|
-
const { startVoiceTurn, getCallbacks } = createCapturingTurnStarter();
|
|
581
|
-
const { streamTtsAudio, ttsTexts } = createRecordingTtsStreamer();
|
|
582
|
-
const harness = createSessionHarness({
|
|
583
|
-
startVoiceTurn,
|
|
584
|
-
streamTtsAudio,
|
|
585
|
-
frontModelConfig: { ackFirstDeltaTimeoutMs: ACK_TIMEOUT_MS },
|
|
586
|
-
frontDecider: ackDecider,
|
|
587
|
-
});
|
|
588
|
-
return { ...harness, getCallbacks, ttsTexts };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
test("tool_use_start before any delta speaks an immediate tool ack and cancels the timer", async () => {
|
|
592
|
-
const { frames, session, getCallbacks, ttsTexts } = createAckHarness();
|
|
593
|
-
|
|
594
|
-
await startReleasedTurn(session, getCallbacks);
|
|
595
|
-
getCallbacks()?.tool_use_start?.("web_search");
|
|
596
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
597
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
|
|
598
|
-
|
|
599
|
-
// The pending first-delta timer was cancelled: letting its budget elapse
|
|
600
|
-
// speaks no second ack.
|
|
601
|
-
await new Promise((resolve) => setTimeout(resolve, ACK_TIMEOUT_MS + 40));
|
|
602
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK]);
|
|
603
|
-
|
|
604
|
-
// The ack is audio-only — no caption frame carries it.
|
|
605
|
-
expect(frames.some((frame) => frame.type === "assistant_text_delta")).toBe(
|
|
606
|
-
false,
|
|
607
|
-
);
|
|
608
|
-
|
|
609
|
-
emitTextDelta(getCallbacks, "Hello there.");
|
|
610
|
-
emitMessageComplete(getCallbacks);
|
|
611
|
-
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
612
|
-
expect(ttsTexts).toEqual([EXPECTED_TOOL_ACK, "Hello there."]);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
test("tool_use_start after the first delta speaks no ack", async () => {
|
|
616
|
-
const { frames, session, getCallbacks, ttsTexts } = createAckHarness();
|
|
617
|
-
|
|
618
|
-
await startReleasedTurn(session, getCallbacks);
|
|
619
|
-
emitTextDelta(getCallbacks, "Hello there.");
|
|
620
|
-
getCallbacks()?.tool_use_start?.("web_search");
|
|
621
|
-
emitMessageComplete(getCallbacks);
|
|
622
|
-
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
623
|
-
await new Promise((resolve) => setTimeout(resolve, ACK_TIMEOUT_MS + 40));
|
|
624
|
-
|
|
625
|
-
expect(ttsTexts).toEqual(["Hello there."]);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
test("tool_use_start after a first-delta ack already spoke adds no second ack", async () => {
|
|
629
|
-
const { frames, session, getCallbacks, ttsTexts } = createAckHarness();
|
|
630
|
-
|
|
631
|
-
await startReleasedTurn(session, getCallbacks);
|
|
632
|
-
// Let the slow-first-delta timer speak the turn's one ack first.
|
|
633
|
-
await waitFor(() => ttsTexts.length === 1);
|
|
634
|
-
expect(ttsTexts).toEqual([EXPECTED_FIRST_DELTA_ACK]);
|
|
635
|
-
|
|
636
|
-
getCallbacks()?.tool_use_start?.("web_search");
|
|
637
|
-
await flushAsyncCallbacks();
|
|
638
|
-
expect(ttsTexts).toEqual([EXPECTED_FIRST_DELTA_ACK]);
|
|
639
|
-
|
|
640
|
-
emitTextDelta(getCallbacks, "Hello there.");
|
|
641
|
-
emitMessageComplete(getCallbacks);
|
|
642
|
-
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
643
|
-
expect(ttsTexts).toEqual([EXPECTED_FIRST_DELTA_ACK, "Hello there."]);
|
|
644
|
-
});
|
|
645
|
-
});
|
|
646
|
-
|
|
647
548
|
describe("LiveVoiceSession room reveal", () => {
|
|
648
549
|
function createMarkerHarness() {
|
|
649
550
|
const { startVoiceTurn, getCallbacks } = createCapturingTurnStarter();
|
|
@@ -651,9 +552,6 @@ describe("LiveVoiceSession room reveal", () => {
|
|
|
651
552
|
const harness = createSessionHarness({
|
|
652
553
|
startVoiceTurn,
|
|
653
554
|
streamTtsAudio,
|
|
654
|
-
// Deltas are emitted immediately in these runs; a long ack budget keeps
|
|
655
|
-
// filler phrases out of the recorded TTS text.
|
|
656
|
-
frontModelConfig: { ackFirstDeltaTimeoutMs: 10_000 },
|
|
657
555
|
});
|
|
658
556
|
return { ...harness, getCallbacks, ttsTexts };
|
|
659
557
|
}
|
|
@@ -689,7 +587,6 @@ describe("LiveVoiceSession room reveal", () => {
|
|
|
689
587
|
const harness = createSessionHarness({
|
|
690
588
|
startVoiceTurn,
|
|
691
589
|
streamTtsAudio,
|
|
692
|
-
frontModelConfig: { ackFirstDeltaTimeoutMs: 10_000 },
|
|
693
590
|
});
|
|
694
591
|
return {
|
|
695
592
|
...harness,
|
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
VoiceTurnCallbacks,
|
|
5
5
|
VoiceTurnOptions,
|
|
6
6
|
} from "../../calls/voice-session-bridge.js";
|
|
7
|
-
import { VOICE_NO_SETUP_FLOWS_RULE } from "../../calls/voice-session-bridge.js";
|
|
8
7
|
import type {
|
|
9
8
|
StreamingTranscriber,
|
|
10
9
|
SttStreamServerEvent,
|
|
@@ -254,14 +253,14 @@ describe("LiveVoiceSession archive and metrics events", () => {
|
|
|
254
253
|
userMessageInterface: "macos",
|
|
255
254
|
assistantMessageInterface: "macos",
|
|
256
255
|
// Pins the full production control prompt for the FRONT-DOOR leg (the
|
|
257
|
-
// first leg of every routed turn): the speech-first rule
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
256
|
+
// first leg of every routed turn): the speech-first rule, and nothing
|
|
257
|
+
// else. This leg is toolless, so both the screen-reveal and setup-flow
|
|
258
|
+
// teachings are deliberately absent. It has nothing to show and no
|
|
259
|
+
// connection to run, and its capability digest already routes anything
|
|
260
|
+
// needing a tool to the escalated leg (see
|
|
261
|
+
// live-voice-triage-escalate.test.ts).
|
|
262
262
|
voiceControlPrompt:
|
|
263
|
-
"You are speaking in a local live voice session. Keep replies brief and conversational. Speech is the main channel: say the answer, and do not narrate a surface instead of answering. You can also put something on screen when it genuinely helps (a form, a list to pick from, a progress card for long work); the call overlay minimizes by itself once you finish speaking, so the user sees it without doing anything. Never tell the user you cannot show them something. Reply in the language the caller is speaking; if they switch languages, switch with them. "
|
|
264
|
-
VOICE_NO_SETUP_FLOWS_RULE,
|
|
263
|
+
"You are speaking in a local live voice session. Keep replies brief and conversational. Speech is the main channel: say the answer, and do not narrate a surface instead of answering. You can also put something on screen when it genuinely helps (a form, a list to pick from, a progress card for long work); the call overlay minimizes by itself once you finish speaking, so the user sees it without doing anything. Never tell the user you cannot show them something. Reply in the language the caller is speaking; if they switch languages, switch with them. ",
|
|
265
264
|
});
|
|
266
265
|
callbacks?.assistant_text_delta?.(makeTextDelta("Hello there."));
|
|
267
266
|
callbacks?.message_complete?.(makeMessageComplete());
|