@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
|
@@ -825,6 +825,38 @@ describe("always-candidate frontmatter parsing", () => {
|
|
|
825
825
|
.map(({ id, chars }) => `${id}: ${chars}`);
|
|
826
826
|
expect(overrun).toEqual([]);
|
|
827
827
|
});
|
|
828
|
+
|
|
829
|
+
test("the bundled acp skill's card fits the default budget", async () => {
|
|
830
|
+
// `acp` owns Claude Code and Codex setup, sign-in, and delegation, and the
|
|
831
|
+
// terms that route those turns to it live in this card. The card is
|
|
832
|
+
// hard-truncated at the default budget, so an overrun drops the trailing
|
|
833
|
+
// hints and the whole avoid-when list from what the selector sees.
|
|
834
|
+
const { DEFAULT_CARD_CHARS, buildSkillContent } =
|
|
835
|
+
await import("../plugins/defaults/memory/substrate/skill-content.js");
|
|
836
|
+
const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
|
|
837
|
+
expect(acp).toBeDefined();
|
|
838
|
+
|
|
839
|
+
// A card that overran is cut to exactly the budget, so staying under it is
|
|
840
|
+
// what proves nothing was dropped. `avoid-when` is appended last and is
|
|
841
|
+
// therefore the first content a truncation eats.
|
|
842
|
+
const card = buildSkillContent(acp!);
|
|
843
|
+
expect(card.length).toBeLessThan(DEFAULT_CARD_CHARS);
|
|
844
|
+
expect(card).toContain("Avoid when:");
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("the bundled acp card carries its setup and sign-in vocabulary", async () => {
|
|
848
|
+
// A "configure Claude Code" or "let's do the auth" turn only reaches this
|
|
849
|
+
// skill if the card actually says it covers setup and authentication;
|
|
850
|
+
// delegation-only wording routes those turns to a terminal skill instead.
|
|
851
|
+
const { buildSkillContent } =
|
|
852
|
+
await import("../plugins/defaults/memory/substrate/skill-content.js");
|
|
853
|
+
const acp = loadSkillCatalog().find((skill) => skill.id === "acp");
|
|
854
|
+
const card = buildSkillContent(acp!).toLowerCase();
|
|
855
|
+
|
|
856
|
+
for (const term of ["set up", "install", "authenticate", "connect"]) {
|
|
857
|
+
expect(card).toContain(term);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
828
860
|
});
|
|
829
861
|
|
|
830
862
|
describe("bundled skill categories", () => {
|
|
@@ -184,8 +184,11 @@ import {
|
|
|
184
184
|
clearConversations,
|
|
185
185
|
setConversation,
|
|
186
186
|
} from "../daemon/conversation-registry.js";
|
|
187
|
+
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
187
188
|
import { CallSiteRoutingProvider } from "../providers/call-site-routing.js";
|
|
188
189
|
import { SubagentManager } from "../subagent/manager.js";
|
|
190
|
+
import { mockAuthContext } from "./helpers/mock-actor-context.js";
|
|
191
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
189
192
|
|
|
190
193
|
// Seed the workspace `llm` config per-case. The real loader schema-merges the
|
|
191
194
|
// raw fragment over defaults exactly as the code path reads it.
|
|
@@ -304,26 +307,29 @@ describe("SubagentManager — provider call-site routing", () => {
|
|
|
304
307
|
test("copies parent guardian and auth context into spawned conversation", async () => {
|
|
305
308
|
setLlmConfig(makeLlmFixture());
|
|
306
309
|
|
|
307
|
-
const parentTrustContext = {
|
|
310
|
+
const parentTrustContext: TrustContext = {
|
|
308
311
|
sourceChannel: "vellum",
|
|
309
312
|
trustClass: "guardian",
|
|
310
313
|
guardianPrincipalId: "guardian-1",
|
|
311
314
|
guardianExternalUserId: "guardian-1",
|
|
312
315
|
};
|
|
313
|
-
const parentAuthContext = {
|
|
316
|
+
const parentAuthContext = mockAuthContext({
|
|
314
317
|
subject: "local:self:parent-perms",
|
|
315
318
|
actorPrincipalId: "guardian-1",
|
|
316
|
-
};
|
|
319
|
+
});
|
|
317
320
|
|
|
318
321
|
capturedConversations.length = 0;
|
|
319
322
|
clearConversations();
|
|
320
323
|
const manager = new SubagentManager();
|
|
321
|
-
setConversation(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
setConversation(
|
|
325
|
+
"parent-perms",
|
|
326
|
+
asConversation({
|
|
327
|
+
trustContext: parentTrustContext,
|
|
328
|
+
getAuthContext: () => parentAuthContext,
|
|
329
|
+
assistantId: "self",
|
|
330
|
+
getCurrentSystemPrompt: () => "parent system",
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
327
333
|
|
|
328
334
|
await manager.spawn(
|
|
329
335
|
{
|
|
@@ -354,11 +360,14 @@ describe("SubagentManager — provider call-site routing", () => {
|
|
|
354
360
|
capturedConversations.length = 0;
|
|
355
361
|
clearConversations();
|
|
356
362
|
const manager = new SubagentManager();
|
|
357
|
-
setConversation(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
363
|
+
setConversation(
|
|
364
|
+
"parent-scoped",
|
|
365
|
+
asConversation({
|
|
366
|
+
enabledPlugins: parentScope,
|
|
367
|
+
getAuthContext: () => undefined,
|
|
368
|
+
getCurrentSystemPrompt: () => "parent system",
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
362
371
|
|
|
363
372
|
await manager.spawn(
|
|
364
373
|
{
|
|
@@ -383,11 +392,14 @@ describe("SubagentManager — provider call-site routing", () => {
|
|
|
383
392
|
capturedConversations.length = 0;
|
|
384
393
|
clearConversations();
|
|
385
394
|
const manager = new SubagentManager();
|
|
386
|
-
setConversation(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
395
|
+
setConversation(
|
|
396
|
+
"parent-unscoped",
|
|
397
|
+
asConversation({
|
|
398
|
+
enabledPlugins: null,
|
|
399
|
+
getAuthContext: () => undefined,
|
|
400
|
+
getCurrentSystemPrompt: () => "parent system",
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
391
403
|
|
|
392
404
|
await manager.spawn(
|
|
393
405
|
{
|
|
@@ -207,6 +207,7 @@ import {
|
|
|
207
207
|
setConversation,
|
|
208
208
|
} from "../daemon/conversation-registry.js";
|
|
209
209
|
import { SubagentAbortedError, SubagentManager } from "../subagent/manager.js";
|
|
210
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
210
211
|
|
|
211
212
|
function makeConfig(overrides: Record<string, unknown> = {}) {
|
|
212
213
|
return {
|
|
@@ -229,16 +230,19 @@ function registerFakeParent(parentConversationId: string): {
|
|
|
229
230
|
enqueuedCount: () => number;
|
|
230
231
|
} {
|
|
231
232
|
let enqueued = 0;
|
|
232
|
-
setConversation(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
233
|
+
setConversation(
|
|
234
|
+
parentConversationId,
|
|
235
|
+
asConversation({
|
|
236
|
+
// Accessors read by setUpSubagent when copying trust/auth context.
|
|
237
|
+
trustContext: undefined,
|
|
238
|
+
getAuthContext: () => undefined,
|
|
239
|
+
assistantId: undefined,
|
|
240
|
+
enqueueMessage: () => {
|
|
241
|
+
enqueued += 1;
|
|
242
|
+
return { rejected: false, queued: true, requestId: "req-fake" };
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
242
246
|
return { enqueuedCount: () => enqueued };
|
|
243
247
|
}
|
|
244
248
|
|
|
@@ -331,3 +331,46 @@ describe("queryUnreportedTurnEvents", () => {
|
|
|
331
331
|
expect(() => stampTurnOutcome("no-such-message", "failed")).not.toThrow();
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
|
+
|
|
335
|
+
describe("unfinalized rows and turn accounting", () => {
|
|
336
|
+
test("an in-flight grouped tool-result row is not a real user turn", async () => {
|
|
337
|
+
const conv = createConversation({ conversationType: "standard" });
|
|
338
|
+
const first = await insertUserMessageAt(conv.id, "real question", 1000);
|
|
339
|
+
// The grouped tool-result row: user-role, reserved unfinalized while
|
|
340
|
+
// tool output streams, content a { ref } that does not contain the
|
|
341
|
+
// tool_result text the content exclusions match. Without the finalized
|
|
342
|
+
// predicate it counts as a phantom user turn until the fold.
|
|
343
|
+
const inflight = await insertUserMessageAt(
|
|
344
|
+
conv.id,
|
|
345
|
+
JSON.stringify({ ref: "conversations/conv/inflight/x.jsonl" }),
|
|
346
|
+
2000,
|
|
347
|
+
);
|
|
348
|
+
getDb().run(`UPDATE messages SET finalized = 0 WHERE id = '${inflight}'`);
|
|
349
|
+
const second = await insertUserMessageAt(conv.id, "second question", 3000);
|
|
350
|
+
|
|
351
|
+
const events = queryUnreportedTurnEvents(0, undefined, 100);
|
|
352
|
+
const ids = events
|
|
353
|
+
.filter((e) => e.conversationId === conv.id)
|
|
354
|
+
.map((e) => e.id);
|
|
355
|
+
|
|
356
|
+
expect(ids).toEqual([first, second]);
|
|
357
|
+
const byId = Object.fromEntries(events.map((e) => [e.id, e]));
|
|
358
|
+
expect(byId[first].turnIndex).toBe(1);
|
|
359
|
+
expect(byId[second].turnIndex).toBe(2);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("a persisted empty-display user row still counts as a turn", async () => {
|
|
363
|
+
const conv = createConversation({ conversationType: "standard" });
|
|
364
|
+
const real = await insertUserMessageAt(conv.id, "real question", 1000);
|
|
365
|
+
// A hidden send persists its user row with [] content (displayContent "")
|
|
366
|
+
// and still runs a turn, so [] rows must stay in turn accounting.
|
|
367
|
+
const hidden = await insertUserMessageAt(conv.id, "[]", 2000);
|
|
368
|
+
|
|
369
|
+
const events = queryUnreportedTurnEvents(0, undefined, 100);
|
|
370
|
+
const ids = events
|
|
371
|
+
.filter((e) => e.conversationId === conv.id)
|
|
372
|
+
.map((e) => e.id);
|
|
373
|
+
|
|
374
|
+
expect(ids).toEqual([real, hidden]);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -5,6 +5,7 @@ import { uiShowTool } from "../tools/ui-surface/definitions.js";
|
|
|
5
5
|
import {
|
|
6
6
|
SURFACE_SHAPE_DOCS,
|
|
7
7
|
SURFACE_TYPE_NAMES,
|
|
8
|
+
UI_SHOW_TYPE_DOCS,
|
|
8
9
|
uiShowTeachingError,
|
|
9
10
|
} from "../tools/ui-surface/surface-shape-docs.js";
|
|
10
11
|
|
|
@@ -290,6 +291,12 @@ describe("ui_show displayable payloads proxy through", () => {
|
|
|
290
291
|
surfaceType: "channel_setup",
|
|
291
292
|
input: { surface_type: "channel_setup", data: { channel: "telegram" } },
|
|
292
293
|
},
|
|
294
|
+
{
|
|
295
|
+
// The picker reads its own voice and catalog, so an empty payload is
|
|
296
|
+
// the whole contract and there is no missing-content state to teach.
|
|
297
|
+
surfaceType: "voice_picker",
|
|
298
|
+
input: { surface_type: "voice_picker", data: {} },
|
|
299
|
+
},
|
|
293
300
|
];
|
|
294
301
|
|
|
295
302
|
for (const { surfaceType, input } of cases) {
|
|
@@ -347,4 +354,30 @@ describe("uiShowTeachingError", () => {
|
|
|
347
354
|
expect(uiShowTool.description).toContain(name);
|
|
348
355
|
}
|
|
349
356
|
});
|
|
357
|
+
|
|
358
|
+
test("voice_picker is a cold type that accepts an empty payload", () => {
|
|
359
|
+
expect(
|
|
360
|
+
uiShowTeachingError({ surface_type: "voice_picker", data: {} }),
|
|
361
|
+
).toBeNull();
|
|
362
|
+
expect(UI_SHOW_TYPE_DOCS).toContain(
|
|
363
|
+
`voice_picker (${SURFACE_SHAPE_DOCS.voice_picker!.purpose})`,
|
|
364
|
+
);
|
|
365
|
+
expect(UI_SHOW_TYPE_DOCS).not.toContain("- voice_picker:");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("voice_picker steering reaches the model through the cold index", () => {
|
|
369
|
+
// Asserted on the built description rather than the doc entry: a cold
|
|
370
|
+
// type's `shape` reaches the model only through a teaching error, and an
|
|
371
|
+
// empty payload has no missing-content state to raise one, so steering
|
|
372
|
+
// parked there would be dead.
|
|
373
|
+
for (const directive of [
|
|
374
|
+
"change, hear, or pick a voice",
|
|
375
|
+
"rather than describing voices in prose",
|
|
376
|
+
"attach no actions and never set await_action",
|
|
377
|
+
"during a live voice call answer in speech",
|
|
378
|
+
]) {
|
|
379
|
+
expect(UI_SHOW_TYPE_DOCS).toContain(directive);
|
|
380
|
+
expect(uiShowTool.description).toContain(directive);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
350
383
|
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `voice_picker` ui_show surface's "never blocks a turn"
|
|
3
|
+
* invariant.
|
|
4
|
+
*
|
|
5
|
+
* The card has no terminal action: it settles when the user picks a voice,
|
|
6
|
+
* with no button to click. `actions` and `await_action` are generic ui_show
|
|
7
|
+
* params though, so the model can attach them to any surface, and the web
|
|
8
|
+
* client latches `awaiting_user_input` on the presence of actions alone with
|
|
9
|
+
* nothing to clear it. The daemon resolver therefore strips both, and a
|
|
10
|
+
* pending picker never holds the one-interactive-surface lock.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import { UISurfaceShowEventSchema } from "../api/events/ui-surface-show.js";
|
|
16
|
+
import type { AssistantEvent } from "../api/index.js";
|
|
17
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
18
|
+
import {
|
|
19
|
+
createSurfaceMutex,
|
|
20
|
+
surfaceProxyResolver,
|
|
21
|
+
} from "../daemon/conversation-surfaces.js";
|
|
22
|
+
import type { SurfaceType } from "../daemon/message-protocol.js";
|
|
23
|
+
import { INTERACTIVE_SURFACE_TYPES } from "../daemon/message-protocol.js";
|
|
24
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
25
|
+
|
|
26
|
+
function makeContext(sent: AssistantEvent[] = []): Conversation {
|
|
27
|
+
return asConversation({
|
|
28
|
+
conversationId: "session-1",
|
|
29
|
+
sendToClient: (msg) => sent.push(msg),
|
|
30
|
+
pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
|
|
31
|
+
lastSurfaceAction: new Map<
|
|
32
|
+
string,
|
|
33
|
+
{ actionId: string; data?: Record<string, unknown> }
|
|
34
|
+
>(),
|
|
35
|
+
surfaceState: new Map(),
|
|
36
|
+
surfaceUndoStacks: new Map<string, string[]>(),
|
|
37
|
+
accumulatedSurfaceState: new Map<string, Record<string, unknown>>(),
|
|
38
|
+
surfaceActionRequestIds: new Set<string>(),
|
|
39
|
+
currentTurnSurfaces: [],
|
|
40
|
+
isProcessing: () => false,
|
|
41
|
+
enqueueMessage: () => ({ queued: false, requestId: "req-1" }),
|
|
42
|
+
getQueueDepth: () => 0,
|
|
43
|
+
processMessage: async () => "ok",
|
|
44
|
+
withSurface: createSurfaceMutex(),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showEvent(sent: AssistantEvent[]) {
|
|
49
|
+
return UISurfaceShowEventSchema.parse(
|
|
50
|
+
sent.find((msg) => msg.type === "ui_surface_show"),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("voice_picker never blocks a turn", () => {
|
|
55
|
+
test("shows with an empty payload and awaits nothing", async () => {
|
|
56
|
+
const sent: AssistantEvent[] = [];
|
|
57
|
+
const ctx = makeContext(sent);
|
|
58
|
+
|
|
59
|
+
const result = await surfaceProxyResolver(ctx, "ui_show", {
|
|
60
|
+
surface_type: "voice_picker",
|
|
61
|
+
data: {},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.isError).toBe(false);
|
|
65
|
+
expect(result.yieldToUser).toBeUndefined();
|
|
66
|
+
|
|
67
|
+
const parsed = showEvent(sent);
|
|
68
|
+
expect(parsed.surfaceType).toBe("voice_picker");
|
|
69
|
+
expect(parsed.actions).toBeUndefined();
|
|
70
|
+
expect(ctx.pendingSurfaceActions.has(parsed.surfaceId)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("strips actions the model attached", async () => {
|
|
74
|
+
const sent: AssistantEvent[] = [];
|
|
75
|
+
const ctx = makeContext(sent);
|
|
76
|
+
|
|
77
|
+
const result = await surfaceProxyResolver(ctx, "ui_show", {
|
|
78
|
+
surface_type: "voice_picker",
|
|
79
|
+
data: {},
|
|
80
|
+
actions: [{ id: "done", label: "Done", style: "primary" }],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.isError).toBe(false);
|
|
84
|
+
expect(result.yieldToUser).toBeUndefined();
|
|
85
|
+
|
|
86
|
+
const parsed = showEvent(sent);
|
|
87
|
+
expect(parsed.actions).toBeUndefined();
|
|
88
|
+
// The persisted copy must match the emitted one, or a history reseed would
|
|
89
|
+
// resurrect the button the client blocks on.
|
|
90
|
+
expect(ctx.currentTurnSurfaces[0]?.actions).toBeUndefined();
|
|
91
|
+
expect(ctx.pendingSurfaceActions.has(parsed.surfaceId)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("ignores an explicit await_action", async () => {
|
|
95
|
+
const sent: AssistantEvent[] = [];
|
|
96
|
+
const ctx = makeContext(sent);
|
|
97
|
+
|
|
98
|
+
const result = await surfaceProxyResolver(ctx, "ui_show", {
|
|
99
|
+
surface_type: "voice_picker",
|
|
100
|
+
data: {},
|
|
101
|
+
await_action: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.isError).toBe(false);
|
|
105
|
+
expect(result.yieldToUser).toBeUndefined();
|
|
106
|
+
expect(result.content).not.toContain("awaiting_user_action");
|
|
107
|
+
expect(ctx.pendingSurfaceActions.size).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("a pending picker does not hold the one-interactive-surface lock", async () => {
|
|
111
|
+
const ctx = makeContext();
|
|
112
|
+
ctx.pendingSurfaceActions.set("surface-picker", {
|
|
113
|
+
surfaceType: "voice_picker",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = await surfaceProxyResolver(ctx, "ui_show", {
|
|
117
|
+
surface_type: "choice",
|
|
118
|
+
data: { options: [{ id: "a", title: "Option A" }] },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.isError).toBe(false);
|
|
122
|
+
expect(result.content).not.toContain("already awaiting user input");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("voice_picker is not an interactive surface type", () => {
|
|
126
|
+
expect(INTERACTIVE_SURFACE_TYPES).not.toContain("voice_picker");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -23,9 +23,9 @@ import { tmpdir } from "node:os";
|
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
25
25
|
|
|
26
|
+
import { getUserHooksFor } from "../hooks/registry.js";
|
|
26
27
|
import {
|
|
27
28
|
getCachedUserTools,
|
|
28
|
-
getUserHooksFor,
|
|
29
29
|
resetPluginCacheForTests,
|
|
30
30
|
} from "../plugins/mtime-cache.js";
|
|
31
31
|
import { loadUserPlugins } from "../plugins/user-loader.js";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { registerDefaultPluginInjectors } from "../plugins/defaults/index.js";
|
|
26
26
|
import type { Message } from "../providers/types.js";
|
|
27
27
|
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
28
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
28
29
|
|
|
29
30
|
// The injector chain is registered by the daemon bootstrap in production; do
|
|
30
31
|
// the same here so `applyRuntimeInjections` walks a non-empty chain.
|
|
@@ -151,15 +152,21 @@ describe("visible_app injection", () => {
|
|
|
151
152
|
];
|
|
152
153
|
|
|
153
154
|
function seedConversation(currentTurnVisibleAppId: string | undefined): void {
|
|
154
|
-
setConversation(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
setConversation(
|
|
156
|
+
CONVERSATION_ID,
|
|
157
|
+
asConversation({
|
|
158
|
+
conversationId: CONVERSATION_ID,
|
|
159
|
+
workingDir: "/sandbox",
|
|
160
|
+
workspaceTopLevelContext: "",
|
|
161
|
+
workspaceTopLevelDirty: false,
|
|
162
|
+
surfaceState: new Map(),
|
|
163
|
+
currentTurnVisibleAppId,
|
|
164
|
+
currentTurnTemporalSnapshot: {
|
|
165
|
+
clientTimezone: null,
|
|
166
|
+
timeSinceLastMessage: null,
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
afterEach(() => {
|
|
@@ -276,6 +276,46 @@ describe("voice_config_update — tts_voice_id (managed/vellum active)", () => {
|
|
|
276
276
|
expect(result.content).not.toContain("broadcast");
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
+
test("hints the inline picker for the next managed voice change", async () => {
|
|
280
|
+
makeVellumActive();
|
|
281
|
+
|
|
282
|
+
const result = await run(
|
|
283
|
+
{ setting: "tts_voice_id", value: "aura-2-zeus-en" },
|
|
284
|
+
makeContext(),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(result.isError).toBe(false);
|
|
288
|
+
expect(result.content).toStartWith("Vellum managed voice updated to");
|
|
289
|
+
expect(result.content).toContain(
|
|
290
|
+
'ui_show { surface_type: "voice_picker", data: {} }',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("does not hint the picker when the voice is not managed", async () => {
|
|
295
|
+
writeConfig({ services: { tts: { provider: "elevenlabs" } } });
|
|
296
|
+
invalidateConfigCache();
|
|
297
|
+
|
|
298
|
+
const result = await run(
|
|
299
|
+
{ setting: "tts_voice_id", value: "abc123" },
|
|
300
|
+
makeContext(),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(result.isError).toBe(false);
|
|
304
|
+
expect(result.content).not.toContain("voice_picker");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("does not hint the picker for a non-voice setting", async () => {
|
|
308
|
+
makeVellumActive();
|
|
309
|
+
|
|
310
|
+
const result = await run(
|
|
311
|
+
{ setting: "conversation_timeout", value: 30 },
|
|
312
|
+
makeContext(),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
expect(result.isError).toBe(false);
|
|
316
|
+
expect(result.content).not.toContain("voice_picker");
|
|
317
|
+
});
|
|
318
|
+
|
|
279
319
|
test("rejects a value with characters invalid for any voice id", async () => {
|
|
280
320
|
makeVellumActive();
|
|
281
321
|
|
|
@@ -18,10 +18,18 @@ import { Glob } from "bun";
|
|
|
18
18
|
* - start the PID-file identity guard — the PID file is a worker's sole
|
|
19
19
|
* tracking handle, so a worker the file stops naming can never be
|
|
20
20
|
* stopped externally and must evict itself.
|
|
21
|
+
*
|
|
22
|
+
* The subset that hosts real agent conversations
|
|
23
|
+
* ({@link CONVERSATION_RUNNING_ENTRYPOINTS}) must additionally register the
|
|
24
|
+
* default plugins' hooks and injectors, since hook dispatch is process-global
|
|
25
|
+
* and a worker that skips it runs conversations with no plugin behavior at all
|
|
26
|
+
* (no image captioning or vision-rejection recovery, no tool-result
|
|
27
|
+
* truncation, no runtime injections).
|
|
21
28
|
*/
|
|
22
29
|
|
|
23
30
|
const DISABLE_CALL = "disableStreamSeqStamping()";
|
|
24
31
|
const PID_GUARD_CALL = "startWorkerPidFileGuard(";
|
|
32
|
+
const PLUGIN_SURFACE_CALL = "registerWorkerPluginSurface(";
|
|
25
33
|
|
|
26
34
|
/** Shape of a shutdown that schedules `process.exit` instead of calling it. */
|
|
27
35
|
const DEFERRED_EXIT = ".finally(() => process.exit";
|
|
@@ -44,6 +52,16 @@ const WORK_START_MARKERS: Record<string, string> = {
|
|
|
44
52
|
"routes/worker.ts": 'server.on("connection"',
|
|
45
53
|
};
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The entrypoints whose work wakes real agent conversations. `monitoring` (a
|
|
57
|
+
* resource sampler) and `routes` (an HTTP route host) run no conversations, so
|
|
58
|
+
* they carry no plugin surface.
|
|
59
|
+
*/
|
|
60
|
+
const CONVERSATION_RUNNING_ENTRYPOINTS: ReadonlySet<string> = new Set([
|
|
61
|
+
"schedule/worker.ts",
|
|
62
|
+
"plugins/defaults/memory/worker.ts",
|
|
63
|
+
]);
|
|
64
|
+
|
|
47
65
|
function findWorkerEntrypoints(): string[] {
|
|
48
66
|
const srcRoot = join(process.cwd(), "src");
|
|
49
67
|
const glob = new Glob("**/worker.ts");
|
|
@@ -137,4 +155,40 @@ describe("worker entrypoint guards", () => {
|
|
|
137
155
|
"worker superseded at startup before it runs orphaned work.",
|
|
138
156
|
).toEqual([]);
|
|
139
157
|
});
|
|
158
|
+
|
|
159
|
+
test("the conversation-running entrypoints are all discovered workers", () => {
|
|
160
|
+
const discovered = new Set(findWorkerEntrypoints());
|
|
161
|
+
const unknown = [...CONVERSATION_RUNNING_ENTRYPOINTS].filter(
|
|
162
|
+
(file) => !discovered.has(file),
|
|
163
|
+
);
|
|
164
|
+
expect(
|
|
165
|
+
unknown,
|
|
166
|
+
"CONVERSATION_RUNNING_ENTRYPOINTS names an entrypoint the glob does " +
|
|
167
|
+
"not find — the worker moved or was renamed.",
|
|
168
|
+
).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("every conversation-running entrypoint registers the default plugin surface before starting work", () => {
|
|
172
|
+
const offenders = [...CONVERSATION_RUNNING_ENTRYPOINTS]
|
|
173
|
+
.sort()
|
|
174
|
+
.filter((file) => {
|
|
175
|
+
const source = readFileSync(join(process.cwd(), "src", file), "utf8");
|
|
176
|
+
const marker = WORK_START_MARKERS[file];
|
|
177
|
+
if (marker == null) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
const registerAt = source.indexOf(PLUGIN_SURFACE_CALL);
|
|
181
|
+
const workAt = source.indexOf(marker);
|
|
182
|
+
return registerAt < 0 || workAt < 0 || registerAt > workAt;
|
|
183
|
+
});
|
|
184
|
+
expect(
|
|
185
|
+
offenders,
|
|
186
|
+
`A worker that wakes agent conversations must call ` +
|
|
187
|
+
`${PLUGIN_SURFACE_CALL}) before its work-start call (see ` +
|
|
188
|
+
"WORK_START_MARKERS). Hook dispatch is process-global, so without it " +
|
|
189
|
+
"the conversations this process runs get no default plugin behavior: " +
|
|
190
|
+
"no vision-rejection recovery, no tool-result truncation, no runtime " +
|
|
191
|
+
"injections.",
|
|
192
|
+
).toEqual([]);
|
|
193
|
+
});
|
|
140
194
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link registerWorkerPluginSurface}, the default-plugin surface
|
|
3
|
+
* that sidecar worker processes install at startup.
|
|
4
|
+
*
|
|
5
|
+
* A worker that wakes real agent conversations shares the daemon's
|
|
6
|
+
* process-global hook and injector registries, so registering the defaults is
|
|
7
|
+
* what gives those conversations the same behavior as daemon conversations.
|
|
8
|
+
* Registration must populate both tables and stay idempotent, and it must do so
|
|
9
|
+
* without running any plugin `init` hook (the memory plugin's init starts the
|
|
10
|
+
* memory jobs worker process itself).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import { getHookEntriesFor } from "../hooks/registry.js";
|
|
16
|
+
import {
|
|
17
|
+
clearInjectorRegistry,
|
|
18
|
+
getRegisteredInjectors,
|
|
19
|
+
} from "../plugins/injector-registry.js";
|
|
20
|
+
import { resetPluginRegistryForTests } from "../plugins/registry.js";
|
|
21
|
+
import { registerWorkerPluginSurface } from "../plugins/worker-plugin-surface.js";
|
|
22
|
+
|
|
23
|
+
async function hookOwnersFor(name: string): Promise<string[]> {
|
|
24
|
+
const entries = await getHookEntriesFor(name);
|
|
25
|
+
return entries.map((entry) => entry.owner.id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("registerWorkerPluginSurface", () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
resetPluginRegistryForTests();
|
|
31
|
+
clearInjectorRegistry();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("serves the default plugins' hooks from the process-global registry", async () => {
|
|
35
|
+
expect(await hookOwnersFor("post-model-call")).not.toContain(
|
|
36
|
+
"default-image-fallback",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
registerWorkerPluginSurface();
|
|
40
|
+
|
|
41
|
+
// The vision-rejection recovery that a worker without this surface never
|
|
42
|
+
// ran, plus an oversized-tool-result guard on the other hook surface.
|
|
43
|
+
expect(await hookOwnersFor("post-model-call")).toContain(
|
|
44
|
+
"default-image-fallback",
|
|
45
|
+
);
|
|
46
|
+
expect(await hookOwnersFor("post-tool-use")).toContain(
|
|
47
|
+
"default-tool-result-truncate",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("registers the injector-only defaults' runtime injectors", () => {
|
|
52
|
+
expect(
|
|
53
|
+
getRegisteredInjectors().some((i) => i.name === "unified-turn-context"),
|
|
54
|
+
).toBe(false);
|
|
55
|
+
|
|
56
|
+
registerWorkerPluginSurface();
|
|
57
|
+
|
|
58
|
+
// `default-turn-context` contributes no hooks, so its injections only reach
|
|
59
|
+
// a worker's conversations through the injector registry.
|
|
60
|
+
expect(
|
|
61
|
+
getRegisteredInjectors().some((i) => i.name === "unified-turn-context"),
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("is idempotent: a second call neither throws nor duplicates", async () => {
|
|
66
|
+
registerWorkerPluginSurface();
|
|
67
|
+
const hooksAfterFirst = await hookOwnersFor("post-model-call");
|
|
68
|
+
const injectorsAfterFirst = getRegisteredInjectors().map((i) => i.name);
|
|
69
|
+
|
|
70
|
+
expect(() => registerWorkerPluginSurface()).not.toThrow();
|
|
71
|
+
|
|
72
|
+
expect(await hookOwnersFor("post-model-call")).toEqual(hooksAfterFirst);
|
|
73
|
+
expect(getRegisteredInjectors().map((i) => i.name)).toEqual(
|
|
74
|
+
injectorsAfterFirst,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|