@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
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
stripInternalSpeechMarkers,
|
|
56
56
|
} from "./voice-control-protocol.js";
|
|
57
57
|
import {
|
|
58
|
+
createFrontDoorStreamGate,
|
|
58
59
|
escalatedContinuationRule,
|
|
59
60
|
ESCALATION_CONTINUATION_CONTENT,
|
|
60
61
|
frontDoorCapabilityDigest,
|
|
@@ -407,18 +408,6 @@ export interface VoiceTurnHandle {
|
|
|
407
408
|
* on a live phone call" framing (the session system prompt already
|
|
408
409
|
* provides assistant identity) and guardian context (injected separately).
|
|
409
410
|
*/
|
|
410
|
-
/**
|
|
411
|
-
* Steering shared by every voice channel. A sign-in flow opens a browser
|
|
412
|
-
* window mid-call that the caller may be unable to see or complete, whether it
|
|
413
|
-
* is reached through a ui-surface tool or through shell and CLI tools (e.g.
|
|
414
|
-
* `assistant oauth connect`). Tell the model to speak the limitation and defer
|
|
415
|
-
* the flow to text chat instead.
|
|
416
|
-
*
|
|
417
|
-
* This outlives the ui-surface restriction it was written alongside: a
|
|
418
|
-
* live-voice call can now show surfaces, but a browser window handing control
|
|
419
|
-
* to a third party mid-call is a different problem, and one a minimized room
|
|
420
|
-
* does not solve.
|
|
421
|
-
*/
|
|
422
411
|
/**
|
|
423
412
|
* How long a live-voice call waits on an approval before deciding it itself.
|
|
424
413
|
*
|
|
@@ -429,8 +418,23 @@ export interface VoiceTurnHandle {
|
|
|
429
418
|
*/
|
|
430
419
|
const VOICE_APPROVAL_TIMEOUT_MS = 45_000;
|
|
431
420
|
|
|
432
|
-
|
|
433
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Telephony-only steering. A sign-in flow opens a browser window on a screen
|
|
423
|
+
* the caller does not have in front of them, whether it is reached through a
|
|
424
|
+
* ui-surface tool or through shell and CLI tools (e.g. `assistant oauth
|
|
425
|
+
* connect`). Tell the model to speak the limitation and defer the flow to text
|
|
426
|
+
* chat instead.
|
|
427
|
+
*
|
|
428
|
+
* Scoped to the phone because the screen is what decides it. A phone call has
|
|
429
|
+
* no screen, so the `open_url` signal a CLI tool can reach lands somewhere the
|
|
430
|
+
* caller will never see, and that signal bus carries no capability or
|
|
431
|
+
* conversation context, so this rule is the only thing standing in front of it
|
|
432
|
+
* here. A live-voice call is the opposite case: the user is holding the screen,
|
|
433
|
+
* and the room minimizes itself to hand it back (see
|
|
434
|
+
* LIVE_VOICE_SETUP_FLOW_TEACHING).
|
|
435
|
+
*/
|
|
436
|
+
const PHONE_NO_SETUP_FLOWS_RULE =
|
|
437
|
+
"Never start account connections, OAuth or sign-in flows, or any other action that opens a browser window or needs the user's screen during this call, not even through shell or CLI tools. If the task needs one, say so briefly and offer to finish it in text chat after the call.";
|
|
434
438
|
|
|
435
439
|
/**
|
|
436
440
|
* The pre-speech tail of the speak-the-caller's-language rule. A monolingual
|
|
@@ -551,7 +555,7 @@ function buildVoiceCallControlPrompt(opts: {
|
|
|
551
555
|
'10. Do not make up information. If you are unsure, use [ASK_GUARDIAN: your question] to consult your guardian. For tool permission requests, use [ASK_GUARDIAN_APPROVAL: {"question":"...","toolName":"...","input":{...}}].',
|
|
552
556
|
`11. Your text is sent directly to a text-to-speech engine. Never use markdown formatting (asterisks, headers, backticks, links) or emojis in your spoken responses. Write plain conversational text only. Protocol markers like ${opts.isCallerGuardian ? "[END_CALL]" : "[ASK_GUARDIAN: ...] and [END_CALL]"} are not spoken text and should still be used normally.`,
|
|
553
557
|
`12. Speak the caller's language: reply in the language of the caller's most recent actual speech, and follow them if they switch languages mid-call. Synthetic user turns (parenthetical markers like the call-connected and verification-completed notices) are not caller speech and never set the language. Before the caller has spoken, such as on the opening greeting turn, ${preSpeechLanguageRuleFragment(config.services.stt.language, config.services.stt.provider)}.`,
|
|
554
|
-
`13. ${
|
|
558
|
+
`13. ${PHONE_NO_SETUP_FLOWS_RULE}`,
|
|
555
559
|
);
|
|
556
560
|
|
|
557
561
|
// Triage-and-escalate routing rules. The front-door leg decides and may
|
|
@@ -1447,6 +1451,34 @@ export async function startVoiceTurn(
|
|
|
1447
1451
|
// Set by the handle's discard(): the whole leg must leave no trace.
|
|
1448
1452
|
let discarded = false;
|
|
1449
1453
|
|
|
1454
|
+
// Verdict-first gate on the hub broadcast. A front-door leg's raw stream
|
|
1455
|
+
// carries its routing verdict, so hub subscribers (web, passive devices)
|
|
1456
|
+
// read it through the gate and see only the text the caller heard. Every
|
|
1457
|
+
// other leg, including the escalated continuation that answers for real,
|
|
1458
|
+
// broadcasts its deltas untouched.
|
|
1459
|
+
const frontDoorStreamGate =
|
|
1460
|
+
opts.routingLeg === "front-door"
|
|
1461
|
+
? createFrontDoorStreamGate(opts.unifiedVerdict === true)
|
|
1462
|
+
: null;
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Broadcast one agent-loop event to hub subscribers, holding a front-door
|
|
1466
|
+
* leg's control-plane text back at the boundary rather than emitting it and
|
|
1467
|
+
* repairing the transcript afterwards. Text released by the gate travels as
|
|
1468
|
+
* an ordinary delta on the leg's own reserved row, so a client that renders
|
|
1469
|
+
* the stream lands on the same text the teardown hygiene pass persists.
|
|
1470
|
+
*/
|
|
1471
|
+
const broadcastLegEvent = (msg: AssistantEvent): void => {
|
|
1472
|
+
if (frontDoorStreamGate === null || msg.type !== "assistant_text_delta") {
|
|
1473
|
+
broadcastMessage(msg);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const released = frontDoorStreamGate.push(msg.text);
|
|
1477
|
+
if (released.length > 0) {
|
|
1478
|
+
broadcastMessage({ ...msg, text: released });
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1450
1482
|
/**
|
|
1451
1483
|
* Teardown transcript hygiene. Runs after the agent loop has fully
|
|
1452
1484
|
* settled — including the stranded-content fold that finalizes an aborted
|
|
@@ -1474,6 +1506,12 @@ export async function startVoiceTurn(
|
|
|
1474
1506
|
* the escalated leg — blocked on this turn's teardown — snapshots it, so
|
|
1475
1507
|
* the quality model never sees the marker text either. Best-effort: a
|
|
1476
1508
|
* hiccup here must not escalate into a turn-level failure.
|
|
1509
|
+
*
|
|
1510
|
+
* This pass owns the PERSISTED row, which the agent loop writes from the
|
|
1511
|
+
* model's raw output regardless of what was broadcast. The live hub stream
|
|
1512
|
+
* is gated separately by `broadcastLegEvent`, so the refetch this pass
|
|
1513
|
+
* publishes confirms text a subscriber already holds instead of correcting
|
|
1514
|
+
* it.
|
|
1477
1515
|
*/
|
|
1478
1516
|
const finalizeVoiceLegTranscript = async (): Promise<void> => {
|
|
1479
1517
|
if (reservedAssistantRowId == null) {
|
|
@@ -1617,7 +1655,24 @@ export async function startVoiceTurn(
|
|
|
1617
1655
|
} else if (msg.type === "conversation_error") {
|
|
1618
1656
|
lastError = msg.userMessage;
|
|
1619
1657
|
}
|
|
1620
|
-
|
|
1658
|
+
if (frontDoorStreamGate !== null && msg.type === "message_complete") {
|
|
1659
|
+
// A leg that completed mid-bridge (a holding phrase with no
|
|
1660
|
+
// sentence terminator) still hands off and speaks what arrived,
|
|
1661
|
+
// so release it ahead of the completion frame. A cancelled leg
|
|
1662
|
+
// never hands off, and correspondingly never flushes.
|
|
1663
|
+
const trailing = frontDoorStreamGate.finish();
|
|
1664
|
+
if (trailing.length > 0) {
|
|
1665
|
+
broadcastMessage({
|
|
1666
|
+
type: "assistant_text_delta",
|
|
1667
|
+
text: trailing,
|
|
1668
|
+
...(reservedAssistantRowId !== null
|
|
1669
|
+
? { messageId: reservedAssistantRowId }
|
|
1670
|
+
: {}),
|
|
1671
|
+
conversationId: opts.conversationId,
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
broadcastLegEvent(msg);
|
|
1621
1676
|
|
|
1622
1677
|
// Forward voice-relevant events to the real-time event sink
|
|
1623
1678
|
if (msg.type === "assistant_text_delta") {
|
|
@@ -221,6 +221,108 @@ export function classifyFrontDoorLeading(
|
|
|
221
221
|
return "answer";
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Verdict-first gate over a front-door leg's delta stream: given the leg's
|
|
226
|
+
* raw deltas in order, it releases only the text the caller actually hears.
|
|
227
|
+
* A front-door leg's raw stream is a control plane, not assistant speech,
|
|
228
|
+
* until its leading tokens classify, so anything downstream of the model
|
|
229
|
+
* that shows text to a person reads the stream through this gate.
|
|
230
|
+
*
|
|
231
|
+
* `push` returns the text released by that delta (empty while the gate is
|
|
232
|
+
* holding). `finish` is called when the leg completes normally and releases
|
|
233
|
+
* a bridge that stopped short of a sentence terminator. A leg that is
|
|
234
|
+
* cancelled instead spoke nothing past what `push` already released, so it
|
|
235
|
+
* simply never calls `finish`.
|
|
236
|
+
*/
|
|
237
|
+
export interface FrontDoorStreamGate {
|
|
238
|
+
push(deltaText: string): string;
|
|
239
|
+
finish(): string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build a {@link FrontDoorStreamGate}. `holdEnabled` mirrors
|
|
244
|
+
* {@link classifyFrontDoorLeading}: true only for speculative (unified
|
|
245
|
+
* front-door) legs, whose decision rule is the only one that teaches the
|
|
246
|
+
* hold token.
|
|
247
|
+
*
|
|
248
|
+
* The three verdicts release differently, matching what the caller hears:
|
|
249
|
+
*
|
|
250
|
+
* - `hold`: the leg is discarded and its row deleted, so nothing is ever
|
|
251
|
+
* released.
|
|
252
|
+
* - `escalate`: the only spoken text is the capped holding phrase, released
|
|
253
|
+
* in one piece once the bridge is complete (exactly what
|
|
254
|
+
* {@link capEscalationBridge} yields, so the released text, the audio, and
|
|
255
|
+
* the persisted row agree). The verdict token and anything streamed past
|
|
256
|
+
* the cap are dropped. A bridge shorter than
|
|
257
|
+
* {@link MIN_SPOKEN_BRIDGE_CHARS} releases nothing at all: the session
|
|
258
|
+
* substitutes an audio-only canned fallback for it, so there is no
|
|
259
|
+
* displayed text for the gate to agree with.
|
|
260
|
+
* - `answer`: the leg's output IS the reply, so every delta passes through,
|
|
261
|
+
* including the leading text held back while the verdict was pending.
|
|
262
|
+
*/
|
|
263
|
+
export function createFrontDoorStreamGate(
|
|
264
|
+
holdEnabled: boolean,
|
|
265
|
+
): FrontDoorStreamGate {
|
|
266
|
+
let raw = "";
|
|
267
|
+
let stage: "deciding" | "answer" | "bridging" | "done" = "deciding";
|
|
268
|
+
let bridgeRaw = "";
|
|
269
|
+
let releasedChars = 0;
|
|
270
|
+
|
|
271
|
+
const releaseBridge = (): string => {
|
|
272
|
+
stage = "done";
|
|
273
|
+
const capped = capEscalationBridge(bridgeRaw);
|
|
274
|
+
// Below the spoken threshold the session throws the model's bridge away
|
|
275
|
+
// and plays a canned fallback that is audio-only, deleting the row rather
|
|
276
|
+
// than persisting a phrase the model never really produced (see
|
|
277
|
+
// `usesFallbackBridge` in `live-voice-session.ts`). Releasing the capped
|
|
278
|
+
// text here would put words on a subscriber's screen that the caller never
|
|
279
|
+
// heard, which is the same spoken/displayed divergence this gate exists to
|
|
280
|
+
// prevent.
|
|
281
|
+
return capped.length < MIN_SPOKEN_BRIDGE_CHARS ? "" : capped;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
push(deltaText: string): string {
|
|
286
|
+
raw += deltaText;
|
|
287
|
+
if (stage === "done") {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
if (stage === "bridging") {
|
|
291
|
+
bridgeRaw += deltaText;
|
|
292
|
+
return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
|
|
293
|
+
}
|
|
294
|
+
if (stage === "deciding") {
|
|
295
|
+
const verdict = classifyFrontDoorLeading(raw.trimStart(), holdEnabled);
|
|
296
|
+
if (verdict === "pending") {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
if (verdict === "hold") {
|
|
300
|
+
stage = "done";
|
|
301
|
+
return "";
|
|
302
|
+
}
|
|
303
|
+
if (verdict === "escalate") {
|
|
304
|
+
stage = "bridging";
|
|
305
|
+
bridgeRaw = raw.trimStart().slice(ESCALATE_VERDICT_TOKEN.length);
|
|
306
|
+
return isEscalationBridgeComplete(bridgeRaw) ? releaseBridge() : "";
|
|
307
|
+
}
|
|
308
|
+
stage = "answer";
|
|
309
|
+
}
|
|
310
|
+
// Answer stage: release everything not yet released, which on the
|
|
311
|
+
// transition includes the leading text the pending verdict held.
|
|
312
|
+
const chunk = raw.slice(releasedChars);
|
|
313
|
+
releasedChars = raw.length;
|
|
314
|
+
return chunk;
|
|
315
|
+
},
|
|
316
|
+
finish(): string {
|
|
317
|
+
if (stage === "bridging") {
|
|
318
|
+
return releaseBridge();
|
|
319
|
+
}
|
|
320
|
+
stage = "done";
|
|
321
|
+
return "";
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
224
326
|
/**
|
|
225
327
|
* The escalated leg runs as its own voice turn. Rather than re-persist the
|
|
226
328
|
* caller's utterance (it is already in history from the front-door leg), the
|
|
@@ -273,8 +375,8 @@ export function frontDoorDecisionRule(opts?: {
|
|
|
273
375
|
// Hold requires positive evidence of an unfinished sentence, never
|
|
274
376
|
// mere uncertainty, because the two mistakes cost differently. A
|
|
275
377
|
// false hold is silent: the verdict, the extension window, and the
|
|
276
|
-
// replay dispatch all elapse
|
|
277
|
-
//
|
|
378
|
+
// replay dispatch all elapse before the turn commits, roughly
|
|
379
|
+
// tripling felt latency. A false
|
|
278
380
|
// release only answers a beat early, which barge-in absorbs.
|
|
279
381
|
`- If the caller's words are visibly unfinished (a trailing conjunction, a dangling clause, a list still being dictated) output ONLY ${HOLD_VERDICT_TOKEN} and stop, no other text. Judge the words themselves: a complete question or statement means they are done, even when it is short or leans on earlier context ("What do you think?", "Why?", "And then?"). Callers may speak any language: those examples are English exemplars only, and completeness is judged by the grammar of the language being spoken. In verb-final languages such as Hindi, Japanese, or Korean the sentence-final verb usually marks completion, so a missing final verb is the unfinished signal, not a missing conjunction. Never hold merely because more could follow.`,
|
|
280
382
|
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channels that installed plugins bring.
|
|
3
|
+
*
|
|
4
|
+
* `getWorkspacePluginsDir` is mocked to a scratch directory so these read real
|
|
5
|
+
* files off disk without touching the machine's workspace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
const workspacePluginsDir = mkdtempSync(join(tmpdir(), "plugin-channels-"));
|
|
14
|
+
|
|
15
|
+
const realPlatform = await import("../../util/platform.js");
|
|
16
|
+
|
|
17
|
+
mock.module("../../util/platform.js", () => ({
|
|
18
|
+
...realPlatform,
|
|
19
|
+
getWorkspacePluginsDir: () => workspacePluginsDir,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { discoverPluginChannels } = await import(
|
|
23
|
+
"../plugin-channel-declarations.js"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
interface PluginOptions {
|
|
27
|
+
/** Contents of `channels/ingress.json`; omit for a plugin with no ingress. */
|
|
28
|
+
ingress?: string;
|
|
29
|
+
manifest?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const INGRESS = JSON.stringify({
|
|
33
|
+
routes: [{ path: "events", kind: "http", description: "inbound" }],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function writePlugin(name: string, options: PluginOptions = {}): string {
|
|
37
|
+
const dir = join(workspacePluginsDir, name);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
writeFileSync(
|
|
40
|
+
join(dir, "package.json"),
|
|
41
|
+
JSON.stringify({ name, ...options.manifest }),
|
|
42
|
+
);
|
|
43
|
+
if (options.ingress !== undefined) {
|
|
44
|
+
mkdirSync(join(dir, "channels"), { recursive: true });
|
|
45
|
+
writeFileSync(join(dir, "channels", "ingress.json"), options.ingress);
|
|
46
|
+
}
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
rmSync(workspacePluginsDir, { recursive: true, force: true });
|
|
52
|
+
mkdirSync(workspacePluginsDir, { recursive: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterAll(() => {
|
|
56
|
+
rmSync(workspacePluginsDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("discoverPluginChannels", () => {
|
|
60
|
+
test("a plugin is a channel because it declares ingress", async () => {
|
|
61
|
+
writePlugin("courier", {
|
|
62
|
+
ingress: INGRESS,
|
|
63
|
+
manifest: {
|
|
64
|
+
displayName: "Courier",
|
|
65
|
+
description: "Reach the assistant by carrier pigeon.",
|
|
66
|
+
icon: "send",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(await discoverPluginChannels()).toEqual([
|
|
71
|
+
{
|
|
72
|
+
id: "courier",
|
|
73
|
+
source: "plugin:courier",
|
|
74
|
+
label: "Courier",
|
|
75
|
+
subtitle: "Reach the assistant by carrier pigeon.",
|
|
76
|
+
icon: "send",
|
|
77
|
+
supportsVerification: false,
|
|
78
|
+
setupMessages: {
|
|
79
|
+
guardian: "I want to set up Courier. Can you help me?",
|
|
80
|
+
contact:
|
|
81
|
+
"I'd like to reach you on Courier. Can you help me get set up?",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("ignores a plugin that declares no ingress", async () => {
|
|
88
|
+
// Presentation alone does not make a channel: reaching the assistant from
|
|
89
|
+
// outside is what one is, and ingress is where that is declared.
|
|
90
|
+
writePlugin("notes", { manifest: { displayName: "Notes", icon: "send" } });
|
|
91
|
+
|
|
92
|
+
expect(await discoverPluginChannels()).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("surfaces a channel whose ingress the gateway would reject", async () => {
|
|
96
|
+
// Validation belongs to the gateway, which owns the schema. A plugin with
|
|
97
|
+
// a broken declaration is a channel with broken ingress, and saying so
|
|
98
|
+
// beats dropping it off the page a guardian would look at.
|
|
99
|
+
writePlugin("courier", {
|
|
100
|
+
ingress: "{ not json",
|
|
101
|
+
manifest: { displayName: "Courier" },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect((await discoverPluginChannels())[0]?.label).toBe("Courier");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("skips a disabled plugin", async () => {
|
|
108
|
+
// Same source of truth the loader uses for hooks, tools and routes: a
|
|
109
|
+
// disabled plugin would otherwise offer a setup flow that cannot run.
|
|
110
|
+
const dir = writePlugin("courier", { ingress: INGRESS });
|
|
111
|
+
writeFileSync(join(dir, ".disabled"), "");
|
|
112
|
+
|
|
113
|
+
expect(await discoverPluginChannels()).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("refuses to let a plugin take a built-in channel's id", async () => {
|
|
117
|
+
// Two rows sharing an id would be ambiguous to any client keying on one,
|
|
118
|
+
// and letting the plugin win would let it impersonate a built-in.
|
|
119
|
+
writePlugin("slack", {
|
|
120
|
+
ingress: INGRESS,
|
|
121
|
+
manifest: { displayName: "Slack" },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(await discoverPluginChannels()).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("titles a plugin that names no display name", async () => {
|
|
128
|
+
// Presentation is best-effort, so a bare manifest costs a nicer title and
|
|
129
|
+
// never the row itself.
|
|
130
|
+
writePlugin("meeting-bot", { ingress: INGRESS });
|
|
131
|
+
|
|
132
|
+
expect(await discoverPluginChannels()).toMatchObject([
|
|
133
|
+
{
|
|
134
|
+
id: "meeting-bot",
|
|
135
|
+
source: "plugin:meeting-bot",
|
|
136
|
+
label: "Meeting Bot",
|
|
137
|
+
subtitle: "Provided by the Meeting Bot plugin",
|
|
138
|
+
icon: "message-square",
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("still lists a channel whose manifest cannot be read", async () => {
|
|
144
|
+
// The directory is the plugin's identity, so an unparseable manifest
|
|
145
|
+
// costs its presentation and not its existence.
|
|
146
|
+
const dir = writePlugin("courier", { ingress: INGRESS });
|
|
147
|
+
writeFileSync(join(dir, "package.json"), "{ not json");
|
|
148
|
+
|
|
149
|
+
expect((await discoverPluginChannels())[0]?.label).toBe("Courier");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("never claims a plugin channel supports verification", async () => {
|
|
153
|
+
// There is no client-side verification flow for one, so clients render it
|
|
154
|
+
// display-only rather than pre-warming a status that cannot arrive.
|
|
155
|
+
writePlugin("courier", { ingress: INGRESS });
|
|
156
|
+
|
|
157
|
+
expect((await discoverPluginChannels())[0]?.supportsVerification).toBe(
|
|
158
|
+
false,
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|
package/src/channels/config.ts
CHANGED
|
@@ -86,6 +86,19 @@ const CHANNEL_POLICIES = {
|
|
|
86
86
|
conversationStrategy: "continue_existing_conversation",
|
|
87
87
|
},
|
|
88
88
|
},
|
|
89
|
+
plugin: {
|
|
90
|
+
notification: {
|
|
91
|
+
// Every plugin-brought channel shares this row, so the answer has to
|
|
92
|
+
// hold for all of them. A reply to an inbound plugin message routes by
|
|
93
|
+
// `replyCallbackUrl` and does not read this flag; proactive notification
|
|
94
|
+
// does, and it needs a guardian binding plus a destination resolver to
|
|
95
|
+
// reach. Neither exists per-plugin, and `NotificationChannel` is derived
|
|
96
|
+
// from this flag, so enabling it would let the decision engine pick a
|
|
97
|
+
// channel that resolves to nothing for whichever plugin it landed on.
|
|
98
|
+
deliveryEnabled: false,
|
|
99
|
+
conversationStrategy: "continue_existing_conversation",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
89
102
|
} as const satisfies Record<ChannelId, ChannelNotificationPolicy>;
|
|
90
103
|
|
|
91
104
|
export type ChannelPolicies = typeof CHANNEL_POLICIES;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channels that installed plugins bring.
|
|
3
|
+
*
|
|
4
|
+
* A plugin is a channel because it declares ingress: `channels/ingress.json`
|
|
5
|
+
* is the list of routes the outside world may reach it on, and reaching the
|
|
6
|
+
* assistant from outside is what being a channel means. There is no second
|
|
7
|
+
* file saying so, and nothing a plugin can set to claim the status without
|
|
8
|
+
* declaring the reach that constitutes it.
|
|
9
|
+
*
|
|
10
|
+
* What the gateway does with that file is a separate matter, and stays the
|
|
11
|
+
* gateway's: validating the routes, digesting them, holding them behind a
|
|
12
|
+
* guardian's approval. This reads its presence and nothing else, so a
|
|
13
|
+
* declaration the gateway rejects still surfaces here. That is the honest
|
|
14
|
+
* report, the plugin is a channel and its ingress is broken, and it keeps a
|
|
15
|
+
* schema this module does not own from deciding what a settings page lists.
|
|
16
|
+
*
|
|
17
|
+
* Presentation comes from the plugin's own manifest, where a plugin's title,
|
|
18
|
+
* description and icon already belong. All three are optional and none gate
|
|
19
|
+
* anything: a plugin with ingress and a bare `package.json` still appears,
|
|
20
|
+
* titled from its directory.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { statSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
|
|
26
|
+
import { isPluginDisabled } from "../plugins/disabled-state.js";
|
|
27
|
+
import { parsePluginPresentation } from "../plugins/external-plugin-loader.js";
|
|
28
|
+
import { listInstalledPluginDirs } from "../plugins/installed-plugin-dirs.js";
|
|
29
|
+
import { type AvailableChannel, isChannelId } from "./types.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The declaration that makes a plugin a channel. Owned by the gateway, which
|
|
33
|
+
* parses it; named here only to test for it.
|
|
34
|
+
*/
|
|
35
|
+
export const PLUGIN_INGRESS_MANIFEST_RELPATH = "channels/ingress.json";
|
|
36
|
+
|
|
37
|
+
/** Icon for a plugin naming none. The generic "a message arrives here" glyph. */
|
|
38
|
+
const FALLBACK_ICON = "message-square";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Title for a plugin with no `displayName`.
|
|
42
|
+
*
|
|
43
|
+
* Derived rather than defaulted to the raw directory name so `meeting-bot`
|
|
44
|
+
* reads as "Meeting Bot" beside "Slack" and "Telegram". A plugin whose casing
|
|
45
|
+
* matters sets `displayName` and this never runs.
|
|
46
|
+
*/
|
|
47
|
+
function titleFromDirectory(name: string): string {
|
|
48
|
+
return name
|
|
49
|
+
.split(/[-_]/)
|
|
50
|
+
.filter((part) => part.length > 0)
|
|
51
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
52
|
+
.join(" ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True when the plugin declares ingress routes. */
|
|
56
|
+
function declaresIngress(pluginDir: string): boolean {
|
|
57
|
+
return (
|
|
58
|
+
statSync(join(pluginDir, PLUGIN_INGRESS_MANIFEST_RELPATH), {
|
|
59
|
+
throwIfNoEntry: false,
|
|
60
|
+
})?.isFile() === true
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every channel brought by an installed, enabled plugin.
|
|
66
|
+
*
|
|
67
|
+
* A plugin whose directory name is one of the assistant's own channels is
|
|
68
|
+
* skipped: two rows sharing an id would be ambiguous to any client keying on
|
|
69
|
+
* one, and the resolution that lets a plugin win would let it impersonate a
|
|
70
|
+
* built-in channel. The assistant's keep the name.
|
|
71
|
+
*
|
|
72
|
+
* Disabled plugins are skipped too, matching the source of truth the loader
|
|
73
|
+
* uses for hooks, tools and routes: a disabled plugin holds no ingress either,
|
|
74
|
+
* and one that reappeared here would offer a setup flow that cannot run.
|
|
75
|
+
*
|
|
76
|
+
* Order follows the plugins directory, and is stable for a stable install set.
|
|
77
|
+
*/
|
|
78
|
+
export async function discoverPluginChannels(): Promise<AvailableChannel[]> {
|
|
79
|
+
const channels: AvailableChannel[] = [];
|
|
80
|
+
|
|
81
|
+
for (const { name, dir } of listInstalledPluginDirs()) {
|
|
82
|
+
if (isPluginDisabled(name) || isChannelId(name) || !declaresIngress(dir)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const presentation = await parsePluginPresentation(dir);
|
|
86
|
+
const label = presentation?.displayName ?? titleFromDirectory(name);
|
|
87
|
+
channels.push({
|
|
88
|
+
id: name,
|
|
89
|
+
source: `plugin:${name}`,
|
|
90
|
+
label,
|
|
91
|
+
subtitle: presentation?.description ?? `Provided by the ${label} plugin`,
|
|
92
|
+
icon: presentation?.icon ?? FALLBACK_ICON,
|
|
93
|
+
// No client-side verification flow exists for a plugin channel, so
|
|
94
|
+
// clients render it display-only and never pre-warm a status for it.
|
|
95
|
+
supportsVerification: false,
|
|
96
|
+
// Openers for a setup conversation, which is what this field is for.
|
|
97
|
+
// Deliberately not the verification copy the built-ins carry: there is
|
|
98
|
+
// no identity to verify here, and inventing that wording would send
|
|
99
|
+
// someone down a flow that does not exist.
|
|
100
|
+
setupMessages: {
|
|
101
|
+
guardian: `I want to set up ${label}. Can you help me?`,
|
|
102
|
+
contact: `I'd like to reach you on ${label}. Can you help me get set up?`,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return channels;
|
|
108
|
+
}
|
package/src/channels/types.ts
CHANGED
|
@@ -51,6 +51,32 @@ export interface ChannelInfo {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Source that contributes a channel, in the `<kind>[:<id>]` form the tool
|
|
56
|
+
* catalog and process tree already use: `default` for one the assistant ships,
|
|
57
|
+
* `plugin:<name>` for one an installed plugin brings.
|
|
58
|
+
*
|
|
59
|
+
* Composed from the plugin's install directory rather than from anything it
|
|
60
|
+
* declares, so a manifest cannot claim to be another source or pass itself off
|
|
61
|
+
* as built in.
|
|
62
|
+
*/
|
|
63
|
+
export type ChannelSource = "default" | `plugin:${string}`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A channel as `/v1/channels/available` reports it: the display metadata plus
|
|
67
|
+
* where it came from.
|
|
68
|
+
*
|
|
69
|
+
* `id` widens to a string here because a plugin channel's id is its plugin
|
|
70
|
+
* name, which is not a member of the closed {@link ChannelId} union. The union
|
|
71
|
+
* still governs everything the assistant routes, verifies and applies
|
|
72
|
+
* admission policy to; `source` is what tells a client which kind it is
|
|
73
|
+
* holding, so nothing has to infer it from the id.
|
|
74
|
+
*/
|
|
75
|
+
export interface AvailableChannel extends Omit<ChannelInfo, "id"> {
|
|
76
|
+
id: string;
|
|
77
|
+
source: ChannelSource;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
/**
|
|
55
81
|
* Per-channel display metadata for the channels the gateway can currently
|
|
56
82
|
* surface to clients. Add an entry here when surfacing a new channel via
|
|
@@ -155,6 +181,10 @@ export const INTERFACE_IDS = [
|
|
|
155
181
|
// device/service callbacks). Non-interactive — permission prompts route
|
|
156
182
|
// through the guardian system, not an interactive client — and non-host-proxy.
|
|
157
183
|
"route",
|
|
184
|
+
// Turns a plugin-brought channel delivered through the gateway. One id for
|
|
185
|
+
// every plugin, matching the `plugin` channel; which plugin it was is in
|
|
186
|
+
// `sourceMetadata.plugin`.
|
|
187
|
+
"plugin",
|
|
158
188
|
] as const;
|
|
159
189
|
|
|
160
190
|
export type InterfaceId = (typeof INTERFACE_IDS)[number];
|
package/src/cli/AGENTS.md
CHANGED
|
@@ -123,10 +123,13 @@ registerFooCommand(program);
|
|
|
123
123
|
_synchronously_ at command registration (so it can't be a lazy
|
|
124
124
|
`import()`) may stay hoisted behind a scoped
|
|
125
125
|
`// eslint-disable-next-line cli/no-daemon-internals` with a comment
|
|
126
|
-
explaining why. The
|
|
126
|
+
explaining why. The current cases are `browser/operation-meta`, in
|
|
127
127
|
`commands/browser.ts` (it drives synchronous subcommand generation and
|
|
128
128
|
deliberately pulls no Playwright graph) and `commands/browser.help.ts`
|
|
129
|
-
(which derives the declarative help from the same contract)
|
|
129
|
+
(which derives the declarative help from the same contract); and
|
|
130
|
+
`providers/speech-to-text/provider-catalog`, in `commands/stt.help.ts`
|
|
131
|
+
(a literal map behind type-only imports, which the help text reads to
|
|
132
|
+
name the providers that serve the batch transcription boundary).
|
|
130
133
|
|
|
131
134
|
2. **Lazy-import daemon functionality inside the action.** Running daemon
|
|
132
135
|
logic in-process is encouraged where it avoids an IPC round-trip (which
|
|
@@ -293,8 +293,8 @@ encrypted vault with the specified metadata.
|
|
|
293
293
|
Requires the assistant to be running with at least one connected client.
|
|
294
294
|
|
|
295
295
|
This command BLOCKS until the user answers the prompt, so it returns only once
|
|
296
|
-
the prompt has already closed. Tell the user what to paste BEFORE running it
|
|
297
|
-
|
|
296
|
+
the prompt has already closed. Tell the user what to paste BEFORE running it.
|
|
297
|
+
Anything said afterwards reaches them after the input is gone, and pointing
|
|
298
298
|
them at a closed prompt makes them re-submit a value that was already received.
|
|
299
299
|
The --label and --description text renders inside the prompt itself, so put the
|
|
300
300
|
instructions there rather than in a follow-up message.
|
|
@@ -7,7 +7,8 @@ export const mcpHelp: CliCommandHelp = {
|
|
|
7
7
|
description: "Manage MCP (Model Context Protocol) servers",
|
|
8
8
|
helpText: `
|
|
9
9
|
MCP servers extend the assistant's capabilities with external tools. Servers
|
|
10
|
-
are configured in the assistant's config.json under the mcp.servers key
|
|
10
|
+
are configured in the assistant's config.json under the mcp.servers key, or
|
|
11
|
+
declared by an installed plugin in its root mcp.json. Each
|
|
11
12
|
server uses one of three transport types:
|
|
12
13
|
|
|
13
14
|
stdio Local process communicating over stdin/stdout
|
|
@@ -28,13 +29,21 @@ Examples:
|
|
|
28
29
|
description: "List configured MCP servers and their status",
|
|
29
30
|
options: [{ flags: "--json", description: "Output as JSON" }],
|
|
30
31
|
helpText: `
|
|
31
|
-
Shows each
|
|
32
|
+
Shows each MCP server with its current status and configuration. Servers come
|
|
33
|
+
from two places: the mcp.servers key in config.json, and the root mcp.json of
|
|
34
|
+
any installed plugin that declares one.
|
|
32
35
|
|
|
33
|
-
Name The server identifier
|
|
34
|
-
Status Health check result:
|
|
36
|
+
Name The server identifier
|
|
37
|
+
Status Health check result for workspace servers:
|
|
35
38
|
✓ Connected and responding
|
|
36
39
|
✗ Error or disabled
|
|
37
40
|
! Needs authentication (OAuth required)
|
|
41
|
+
Plugin-declared servers report "declared" instead. They are
|
|
42
|
+
listed but not connected by the assistant, and they are not
|
|
43
|
+
health-checked, so no stored credential can reach a URL a
|
|
44
|
+
plugin chose.
|
|
45
|
+
Source Shown only for plugin-declared servers, naming the plugin.
|
|
46
|
+
Servers from config.json print no Source line.
|
|
38
47
|
Transport stdio, sse, or streamable-http
|
|
39
48
|
URL/Command The server URL (sse/streamable-http) or command (stdio)
|
|
40
49
|
Risk Default risk level: low, medium, or high
|