@vellumai/assistant 0.11.6 → 0.11.7-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 +31 -19
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +3 -3
- package/docs/flux-turn-detection-spike.md +8 -7
- package/docs/runbook-trusted-contacts.md +14 -0
- package/docs/stt-provider-onboarding.md +8 -4
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
- package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
- package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
- package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
- package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
- package/node_modules/@vellumai/environments/src/shell.ts +65 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/openapi.yaml +419 -55
- package/package.json +4 -1
- package/scripts/sync-llm-catalog.ts +6 -0
- package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
- package/src/__tests__/approval-card-edit.test.ts +153 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/avatar-identity-sync.test.ts +5 -1
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-policy.test.ts +1 -3
- package/src/__tests__/channel-readiness-discord.test.ts +9 -70
- package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
- package/src/__tests__/config-loader-backfill.test.ts +69 -0
- package/src/__tests__/config-schema.test.ts +2 -3
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +44 -0
- package/src/__tests__/conversation-evictor.test.ts +19 -0
- package/src/__tests__/conversation-fork-referential.test.ts +73 -63
- package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +0 -218
- package/src/__tests__/credential-broker.test.ts +43 -420
- package/src/__tests__/credential-record-write-through.test.ts +78 -0
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -9
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/delete-propagation.test.ts +92 -2
- package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
- package/src/__tests__/discord-channel-config.test.ts +237 -0
- package/src/__tests__/edit-propagation.test.ts +42 -2
- package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
- package/src/__tests__/image-conversion.test.ts +38 -4
- package/src/__tests__/install-skill-routing.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver.test.ts +74 -0
- package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
- package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
- package/src/__tests__/managed-profile-guard.test.ts +28 -0
- package/src/__tests__/managed-speech-defaults.test.ts +196 -1
- package/src/__tests__/mcp-cli.test.ts +23 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
- package/src/__tests__/media-stream-stt-session.test.ts +2 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-discord-adapter.test.ts +162 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/platform.test.ts +19 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/require-fresh-approval.test.ts +5 -2
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/schedule-retry.test.ts +183 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
- package/src/__tests__/skill-load-tool.test.ts +34 -0
- package/src/__tests__/skill-tool-factory.test.ts +6 -1
- package/src/__tests__/skill-tool-manifest.test.ts +20 -0
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/subagent-manager-notify.test.ts +38 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +146 -2
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/tool-audit.test.ts +111 -40
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
- package/src/__tests__/ui-shape-teaching.test.ts +1 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
- package/src/__tests__/voice-config-update.test.ts +43 -0
- package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
- package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
- package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
- package/src/agent/loop.ts +31 -2
- package/src/approvals/guardian-card-withdrawal.ts +22 -0
- package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
- package/src/avatar/avatar-manifest.ts +48 -100
- package/src/avatar/avatar-store.ts +7 -7
- package/src/avatar/ensure-raster.ts +146 -0
- package/src/avatar/resvg-lazy.test.ts +0 -1
- package/src/avatar/traits-png-sync.ts +9 -10
- package/src/background-wake/wake-intent-hooks.test.ts +0 -1
- package/src/backup/__tests__/paths.test.ts +6 -2
- package/src/backup/paths.ts +6 -4
- package/src/bundler/app-compiler.ts +2 -2
- package/src/bundler/compiler-tools.test.ts +36 -0
- package/src/bundler/compiler-tools.ts +54 -18
- package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
- package/src/calls/media-stream-stt-session.ts +2 -1
- package/src/calls/telephony-synthesis-language.ts +13 -5
- package/src/calls/voice-session-bridge.ts +19 -4
- package/src/channels/config.ts +23 -12
- package/src/cli/AGENTS.md +3 -1
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/contacts.help.ts +16 -4
- package/src/cli/commands/contacts.ts +5 -1
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-providers.ts +18 -53
- package/src/cli/commands/mcp.help.ts +6 -5
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +11 -5
- package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
- package/src/config/default-profile-catalog.ts +195 -10
- package/src/config/default-profile-names.ts +41 -0
- package/src/config/feature-flag-registry.json +34 -2
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +25 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +160 -10
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
- package/src/config/schemas/__tests__/stt.test.ts +94 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/channels.ts +32 -0
- package/src/config/schemas/live-voice.ts +13 -4
- package/src/config/schemas/llm.ts +263 -15
- package/src/config/schemas/mcp.ts +10 -2
- package/src/config/schemas/memory-retrospective.ts +1 -19
- package/src/config/schemas/services.ts +24 -0
- package/src/config/schemas/stt.ts +128 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +21 -0
- package/src/contacts/notify-contacts-changed.ts +5 -2
- package/src/context/compactor.ts +74 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
- package/src/daemon/config-watcher.ts +2 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
- package/src/daemon/conversation-agent-loop.ts +86 -10
- package/src/daemon/conversation-error.ts +25 -0
- package/src/daemon/conversation-evictor.ts +2 -4
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-store.ts +8 -1
- package/src/daemon/conversation-tool-setup.ts +37 -0
- package/src/daemon/conversation.ts +0 -4
- package/src/daemon/handlers/channel-config-result.ts +30 -0
- package/src/daemon/handlers/config-discord-channel.ts +301 -0
- package/src/daemon/handlers/config-ingress.ts +9 -0
- package/src/daemon/handlers/config-slack-channel.ts +13 -15
- package/src/daemon/handlers/config-telegram.ts +2 -5
- package/src/daemon/handlers/conversations.ts +0 -2
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +3 -1
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/tool-setup-types.ts +5 -3
- package/src/daemon/trust-context-types.test.ts +33 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
- package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
- package/src/live-voice/__tests__/protocol.test.ts +224 -0
- package/src/live-voice/live-voice-credential-preflight.ts +5 -1
- package/src/live-voice/live-voice-session-manager.ts +39 -2
- package/src/live-voice/live-voice-session.ts +224 -9
- package/src/live-voice/protocol.ts +187 -1
- package/src/messaging/providers/discord/api.ts +2 -1
- package/src/messaging/providers/discord/withdraw.ts +62 -0
- package/src/messaging/read-provider-metadata.ts +32 -0
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/db-integrity-sample.ts +1 -0
- package/src/monitoring/file-descriptors.ts +39 -6
- package/src/monitoring/process-memory.ts +37 -5
- package/src/monitoring/resource-sampler.ts +25 -8
- package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
- package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
- package/src/notifications/__tests__/connected-channels.test.ts +12 -0
- package/src/notifications/__tests__/copy-composer.test.ts +83 -0
- package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
- package/src/notifications/activity-failed-dedupe.ts +95 -0
- package/src/notifications/adapters/discord.ts +132 -0
- package/src/notifications/adapters/shared.ts +16 -0
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +5 -8
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/background-failure-signal.ts +116 -0
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/copy-composer.ts +41 -8
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/destination-resolver.ts +45 -5
- package/src/notifications/emit-signal.ts +19 -5
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/notifications/types.ts +2 -8
- package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
- package/src/oauth/connect-orchestrator.ts +16 -0
- package/src/oauth/seed-providers.ts +262 -0
- package/src/permissions/checker.test.ts +21 -0
- package/src/permissions/checker.ts +6 -0
- package/src/permissions/gateway-threshold-reader.test.ts +184 -0
- package/src/permissions/gateway-threshold-reader.ts +144 -10
- package/src/permissions/prompter.ts +0 -6
- package/src/permissions/types.ts +5 -4
- package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
- package/src/persistence/bulk-write-gate.ts +9 -10
- package/src/persistence/conversation-crud.ts +88 -134
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/embeddings/embedding-local.ts +14 -85
- package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
- package/src/persistence/lifecycle-events-store.test.ts +50 -0
- package/src/persistence/lifecycle-events-store.ts +75 -5
- package/src/platform/platform-patch-queue.test.ts +368 -0
- package/src/platform/platform-patch-queue.ts +190 -0
- package/src/platform/sync-avatar.test.ts +527 -0
- package/src/platform/sync-avatar.ts +209 -0
- package/src/platform/sync-identity.test.ts +75 -0
- package/src/platform/sync-identity.ts +17 -110
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugin-api/transcription-session.ts +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +4 -0
- package/src/plugins/defaults/image-recovery/recover.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
- package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
- package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +55 -0
- package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
- package/src/providers/connection-resolution.ts +2 -17
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
- package/src/providers/inference/adapter-factory.ts +246 -3
- package/src/providers/inference/auth.ts +15 -14
- package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
- package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
- package/src/providers/inference/resolve-auth.ts +1 -1
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +100 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/api-error-normalization.ts +13 -0
- package/src/providers/openai/chat-completions-provider.ts +84 -0
- package/src/providers/opencode/client.test.ts +150 -0
- package/src/providers/opencode/client.ts +61 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/provider-secret-catalog.ts +3 -2
- package/src/providers/retry.ts +956 -68
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +191 -3
- package/src/providers/speech-to-text/resolve.ts +132 -24
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +39 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/providers/vellum/client.ts +29 -0
- package/src/providers/vellum-model-routing.test.ts +2 -0
- package/src/providers/vellum-model-routing.ts +3 -3
- package/src/runtime/AGENTS.md +1 -2
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/background-job-runner.ts +68 -50
- package/src/runtime/channel-readiness-service.ts +0 -39
- package/src/runtime/http-server.ts +12 -4
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
- package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
- package/src/runtime/routes/approval-routes.ts +0 -12
- package/src/runtime/routes/avatar-routes.ts +17 -30
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
- package/src/runtime/routes/contact-prompt-routes.ts +65 -6
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +98 -10
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +12 -9
- package/src/runtime/routes/inbound-message-handler.ts +47 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
- package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profiles-routes.ts +12 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +2 -2
- package/src/runtime/routes/integrations/discord.ts +119 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/mcp-auth-routes.ts +12 -6
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/oauth-pending-flows.ts +136 -0
- package/src/runtime/routes/secret-routes.ts +7 -0
- package/src/runtime/routes/stt-routes.ts +38 -6
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +9 -5
- package/src/runtime/sync/resource-sync-events.ts +2 -0
- package/src/runtime/trust-verdict-consumer.ts +3 -3
- package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/schedule-recovery.ts +0 -1
- package/src/schedule/scheduler.ts +89 -51
- package/src/schedule/worker.ts +57 -0
- package/src/security/__tests__/openai-device-auth.test.ts +355 -0
- package/src/security/ces-rpc-record-backend.ts +123 -0
- package/src/security/oauth2.ts +7 -0
- package/src/security/openai-device-auth.ts +364 -0
- package/src/security/secure-keys.ts +15 -0
- package/src/skills/available-skills.ts +10 -1
- package/src/skills/catalog-install.ts +19 -1
- package/src/skills/inline-command-runner.ts +6 -2
- package/src/skills/platform-compatibility.test.ts +49 -0
- package/src/skills/platform-compatibility.ts +65 -0
- package/src/skills/tool-manifest.ts +20 -0
- package/src/stt/__tests__/roles.test.ts +152 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/roles.ts +180 -0
- package/src/stt/stt-stream-session.ts +3 -2
- package/src/stt/types.ts +33 -3
- package/src/subagent/manager.ts +12 -0
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
- package/src/telemetry/live-voice-funnel.ts +12 -2
- package/src/telemetry/telemetry-wire.generated.ts +6 -0
- package/src/telemetry/tool-audit.ts +83 -30
- package/src/telemetry/tool-executed-events-store.test.ts +0 -1
- package/src/telemetry/turn-outcome.ts +75 -4
- package/src/tools/client-os.ts +115 -0
- package/src/tools/computer-use/definitions.ts +11 -7
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
- package/src/tools/credentials/broker-types.ts +0 -71
- package/src/tools/credentials/broker.ts +6 -201
- package/src/tools/credentials/metadata-store.ts +65 -16
- package/src/tools/credentials/store.ts +2 -0
- package/src/tools/executor.ts +2 -8
- package/src/tools/host-shell.test.ts +75 -0
- package/src/tools/host-shell.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/mcp/mcp-tool-factory.ts +44 -9
- package/src/tools/permission-checker.ts +73 -21
- package/src/tools/policy-context.ts +1 -0
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/find-similar-skills.ts +10 -2
- package/src/tools/skills/load.ts +41 -6
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
- package/src/tools/terminal/safe-env.ts +39 -9
- package/src/tools/terminal/shell.ts +12 -31
- package/src/tools/tool-approval-handler.ts +64 -10
- package/src/tools/tool-types.ts +10 -0
- package/src/tools/types.ts +19 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
- package/src/tools/workflows/manage-workflows.ts +2 -2
- package/src/tools/workflows/run-workflow.test.ts +2 -1
- package/src/util/__tests__/file-use.test.ts +61 -0
- package/src/util/__tests__/process-table.test.ts +66 -0
- package/src/util/__tests__/process-tree.test.ts +46 -0
- package/src/util/errors.ts +1 -0
- package/src/util/file-use.ts +91 -0
- package/src/util/host-process.test.ts +17 -0
- package/src/util/host-process.ts +75 -0
- package/src/util/image-conversion.ts +39 -7
- package/src/util/platform.ts +32 -13
- package/src/util/process-table.ts +249 -0
- package/src/util/process-tree.ts +85 -129
- package/src/util/provider-error-patterns.ts +20 -0
- package/src/util/spawn.ts +9 -2
- package/src/watch/__tests__/watch-retro.test.ts +48 -1
- package/src/watch/watch-retro.ts +46 -4
- package/src/workspace/git-service.ts +4 -14
- package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
- package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
- package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
- package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/permission-types.test.ts +0 -17
- package/src/channels/gateway-discord-admission.ts +0 -32
- package/src/persistence/fork-message-copy.ts +0 -239
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
- package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
- package/src/security/oauth2-device-code.ts +0 -312
|
@@ -35,6 +35,8 @@ export interface ThresholdReaderMockState {
|
|
|
35
35
|
executionContext?: string;
|
|
36
36
|
cellQuery?: Record<string, unknown>;
|
|
37
37
|
}>;
|
|
38
|
+
/** Resolved by `getContactAutoApproveThreshold`. */
|
|
39
|
+
contactThreshold: string | null;
|
|
38
40
|
/** How many times the cell was looked up. */
|
|
39
41
|
cellLookups: number;
|
|
40
42
|
}
|
|
@@ -45,6 +47,7 @@ export const thresholdReaderMock: ThresholdReaderMockState = {
|
|
|
45
47
|
cell: { ok: true, resolved: null },
|
|
46
48
|
roomDefault: "low",
|
|
47
49
|
thresholdReads: [],
|
|
50
|
+
contactThreshold: null,
|
|
48
51
|
cellLookups: 0,
|
|
49
52
|
};
|
|
50
53
|
|
|
@@ -58,6 +61,7 @@ export function resetThresholdReaderMock(): void {
|
|
|
58
61
|
thresholdReaderMock.cell = { ok: true, resolved: null };
|
|
59
62
|
thresholdReaderMock.roomDefault = "low";
|
|
60
63
|
thresholdReaderMock.thresholdReads.length = 0;
|
|
64
|
+
thresholdReaderMock.contactThreshold = null;
|
|
61
65
|
thresholdReaderMock.cellLookups = 0;
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -81,6 +85,8 @@ export function installThresholdReaderMock(): void {
|
|
|
81
85
|
return thresholdReaderMock.threshold;
|
|
82
86
|
},
|
|
83
87
|
refreshAutoApproveThreshold: async () => thresholdReaderMock.refreshed,
|
|
88
|
+
getContactAutoApproveThreshold: async () =>
|
|
89
|
+
thresholdReaderMock.contactThreshold,
|
|
84
90
|
resolveChannelPermissionCell: async () => {
|
|
85
91
|
thresholdReaderMock.cellLookups += 1;
|
|
86
92
|
return thresholdReaderMock.cell;
|
|
@@ -14,6 +14,11 @@ mock.module("../messaging/providers/slack/withdraw.js", () => ({
|
|
|
14
14
|
withdrawSlackApprovalCard,
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
|
+
const withdrawDiscordApprovalCard = mock(async (_params: unknown) => undefined);
|
|
18
|
+
mock.module("../messaging/providers/discord/withdraw.js", () => ({
|
|
19
|
+
withdrawDiscordApprovalCard,
|
|
20
|
+
}));
|
|
21
|
+
|
|
17
22
|
const withdrawTelegramApprovalCard = mock(
|
|
18
23
|
async (_params: Record<string, unknown>) => {},
|
|
19
24
|
);
|
|
@@ -68,6 +73,7 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
68
73
|
markSurfaceCompleted.mockClear();
|
|
69
74
|
withdrawSlackApprovalCard.mockClear();
|
|
70
75
|
withdrawTelegramApprovalCard.mockClear();
|
|
76
|
+
withdrawDiscordApprovalCard.mockClear();
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
test("withdraws + broadcasts the in-app card when the decision came from another surface", async () => {
|
|
@@ -232,6 +238,30 @@ describe("withdrawGuardianRequestCards", () => {
|
|
|
232
238
|
});
|
|
233
239
|
});
|
|
234
240
|
|
|
241
|
+
test("withdraws the Discord card by rewriting it to the outcome", async () => {
|
|
242
|
+
const req = makeRequest({ sourceChannel: "discord" });
|
|
243
|
+
bridgeState.seedDelivery({
|
|
244
|
+
requestId: req.id,
|
|
245
|
+
destinationChannel: "discord",
|
|
246
|
+
destinationChatId: "111222333444555666",
|
|
247
|
+
destinationMessageId: "2001",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await withdrawGuardianRequestCards({
|
|
251
|
+
request: req,
|
|
252
|
+
status: "approved",
|
|
253
|
+
originChannel: "vellum",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(withdrawTelegramApprovalCard).not.toHaveBeenCalled();
|
|
257
|
+
expect(withdrawDiscordApprovalCard).toHaveBeenCalledTimes(1);
|
|
258
|
+
expect(withdrawDiscordApprovalCard).toHaveBeenCalledWith({
|
|
259
|
+
guardianUserId: "111222333444555666",
|
|
260
|
+
messageId: "2001",
|
|
261
|
+
status: "approved",
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
235
265
|
test("suppresses the Telegram status reply only when the origin flow replies to the guardian", async () => {
|
|
236
266
|
const req = makeRequest({ sourceChannel: "telegram" });
|
|
237
267
|
bridgeState.seedDelivery({
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the shared
|
|
2
|
+
* Tests for the shared cross-platform image converter.
|
|
3
3
|
*
|
|
4
4
|
* Pure logic (ftyp sniffing, filename rewriting, passthrough behavior) runs
|
|
5
|
-
* everywhere;
|
|
6
|
-
* gated on darwin.
|
|
5
|
+
* everywhere; macOS-specific HEIC fixture generation is gated on Darwin.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { createHash } from "node:crypto";
|
|
@@ -151,6 +150,41 @@ describe("isCompleteJpeg", () => {
|
|
|
151
150
|
});
|
|
152
151
|
});
|
|
153
152
|
|
|
153
|
+
describe("convertImageToJpeg", () => {
|
|
154
|
+
test("converts PNG input with the cross-platform encoder", async () => {
|
|
155
|
+
const converted = await convertImageToJpeg(PNG_1PX_BYTES, {
|
|
156
|
+
resizeToPx: { width: 2, height: 2 },
|
|
157
|
+
quality: 82,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(converted).not.toBeNull();
|
|
161
|
+
expect(isCompleteJpeg(converted!)).toBe(true);
|
|
162
|
+
expect(hasValidJpegStructure(converted!)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("applies EXIF orientation before encoding", async () => {
|
|
166
|
+
const { default: sharp } = await import("sharp");
|
|
167
|
+
const source = await sharp({
|
|
168
|
+
create: {
|
|
169
|
+
width: 2,
|
|
170
|
+
height: 1,
|
|
171
|
+
channels: 3,
|
|
172
|
+
background: { r: 255, g: 0, b: 0 },
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
.jpeg()
|
|
176
|
+
.withMetadata({ orientation: 6 })
|
|
177
|
+
.toBuffer();
|
|
178
|
+
|
|
179
|
+
const converted = await convertImageToJpeg(source, { quality: 83 });
|
|
180
|
+
expect(converted).not.toBeNull();
|
|
181
|
+
const metadata = await sharp(converted!).metadata();
|
|
182
|
+
expect(metadata.width).toBe(1);
|
|
183
|
+
expect(metadata.height).toBe(2);
|
|
184
|
+
expect(metadata.orientation).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
154
188
|
// Mirrors the converter's cache-key derivation so the test can plant a
|
|
155
189
|
// poisoned entry at the exact path a conversion will consult.
|
|
156
190
|
function cacheKeyFor(bytes: Uint8Array, quality: number): string {
|
|
@@ -256,7 +290,7 @@ describe("normalizeImageBytes passthrough", () => {
|
|
|
256
290
|
});
|
|
257
291
|
|
|
258
292
|
test("HEIF header with undecodable payload passes through", async () => {
|
|
259
|
-
//
|
|
293
|
+
// The malformed container cannot be decoded, so the bytes are kept.
|
|
260
294
|
const fake = fakeHeifHeaderBytes();
|
|
261
295
|
const result = await normalizeImageBytes("image/heic", fake);
|
|
262
296
|
expect(result.converted).toBe(false);
|
|
@@ -13,6 +13,7 @@ const mockCatalogSkills = mock(
|
|
|
13
13
|
description: string;
|
|
14
14
|
source: string;
|
|
15
15
|
directoryPath?: string;
|
|
16
|
+
platforms?: Array<"macos" | "windows" | "linux">;
|
|
16
17
|
}> => [],
|
|
17
18
|
);
|
|
18
19
|
const mockClawhubInstall = mock(
|
|
@@ -110,6 +111,7 @@ mock.module("../providers/provider-send-message.js", () => ({
|
|
|
110
111
|
userMessage: () => ({}),
|
|
111
112
|
}));
|
|
112
113
|
mock.module("../skills/catalog-cache.js", () => ({
|
|
114
|
+
getCachedCatalogSync: () => [],
|
|
113
115
|
getCatalog: mockGetCatalog,
|
|
114
116
|
}));
|
|
115
117
|
mock.module("../skills/catalog-install.js", () => ({
|
|
@@ -382,6 +384,29 @@ Body.
|
|
|
382
384
|
expect(mockClawhubInstall).not.toHaveBeenCalled();
|
|
383
385
|
});
|
|
384
386
|
|
|
387
|
+
test("rejects bundled skills that do not support the host platform", async () => {
|
|
388
|
+
const incompatiblePlatform =
|
|
389
|
+
process.platform === "darwin" ? "windows" : "macos";
|
|
390
|
+
mockCatalogSkills.mockReturnValue([
|
|
391
|
+
{
|
|
392
|
+
id: "platform-skill",
|
|
393
|
+
displayName: "Platform Skill",
|
|
394
|
+
description: "A platform-specific skill",
|
|
395
|
+
source: "bundled",
|
|
396
|
+
directoryPath: "/tmp/test-bundled-skills/platform-skill",
|
|
397
|
+
platforms: [incompatiblePlatform],
|
|
398
|
+
},
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
const result = await installSkill({ slug: "platform-skill" });
|
|
402
|
+
|
|
403
|
+
expect(result.success).toBe(false);
|
|
404
|
+
if (!result.success) {
|
|
405
|
+
expect(result.error).toContain("unavailable on this operating system");
|
|
406
|
+
}
|
|
407
|
+
expect(mockEnsureSkillEntry).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
385
410
|
test("skills.sh install failure propagates error", async () => {
|
|
386
411
|
mockInstallExternalSkill.mockRejectedValue(
|
|
387
412
|
new Error("Skill not found in repo"),
|
|
@@ -67,6 +67,7 @@ interface ClientCatalogModel {
|
|
|
67
67
|
cacheWritePer1mTokens?: number;
|
|
68
68
|
}>;
|
|
69
69
|
};
|
|
70
|
+
featureFlag?: string;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
interface ClientCatalogEntry {
|
|
@@ -79,6 +80,7 @@ interface ClientCatalogEntry {
|
|
|
79
80
|
apiKeyPlaceholder?: string;
|
|
80
81
|
credentialsGuide?: ClientCatalogCredentialsGuide;
|
|
81
82
|
supportsPlatformAuth?: boolean;
|
|
83
|
+
featureFlag?: string;
|
|
82
84
|
defaultModel: string;
|
|
83
85
|
models: ClientCatalogModel[];
|
|
84
86
|
}
|
|
@@ -133,6 +135,7 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
133
135
|
expect(clientEntry.supportsPlatformAuth).toBe(
|
|
134
136
|
daemonEntry.supportsPlatformAuth,
|
|
135
137
|
);
|
|
138
|
+
expect(clientEntry.featureFlag).toBe(daemonEntry.featureFlag);
|
|
136
139
|
expect(clientEntry.credentialsGuide).toEqual(
|
|
137
140
|
daemonEntry.credentialsGuide,
|
|
138
141
|
);
|
|
@@ -199,6 +202,7 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
199
202
|
expect(clientModel.supportsVision).toBe(daemonModel.supportsVision);
|
|
200
203
|
expect(clientModel.supportsToolUse).toBe(daemonModel.supportsToolUse);
|
|
201
204
|
expect(clientModel.pricing).toEqual(daemonModel.pricing);
|
|
205
|
+
expect(clientModel.featureFlag).toBe(daemonModel.featureFlag);
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
208
|
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveCallSiteConfigWithProfile,
|
|
10
10
|
resolveDefaultProfileKey,
|
|
11
11
|
resolveEffectiveProfileKey,
|
|
12
|
+
resolveSingleRouteProfileKey,
|
|
12
13
|
} from "../config/llm-resolver.js";
|
|
13
14
|
import { type LLMCallSite, LLMSchema } from "../config/schemas/llm.js";
|
|
14
15
|
import { resolveModelIntent } from "../providers/model-intents.js";
|
|
@@ -303,6 +304,26 @@ describe("resolveCallSiteConfig", () => {
|
|
|
303
304
|
expect(resolved.effort).toBe("high");
|
|
304
305
|
});
|
|
305
306
|
|
|
307
|
+
test("a winning profile's fallbackProfile metadata never leaks into the resolved config", () => {
|
|
308
|
+
const llm = LLMSchema.parse({
|
|
309
|
+
defaultProvider: { provider: "vellum" },
|
|
310
|
+
profiles: {
|
|
311
|
+
"quality-optimized": {
|
|
312
|
+
source: "managed",
|
|
313
|
+
fallbackProfile: "quality-optimized-backup",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
callSites: {
|
|
317
|
+
memoryExtraction: { profile: "quality-optimized" },
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
const resolved = resolveCallSiteConfig("memoryExtraction", llm);
|
|
321
|
+
expect(resolved.model).toBe("gpt-5.6-sol");
|
|
322
|
+
// `fallbackProfile` is ProfileEntry-only metadata, absent from
|
|
323
|
+
// `LLMConfigBase`; the resolved config must not carry the key at all.
|
|
324
|
+
expect(Object.hasOwn(resolved, "fallbackProfile")).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
|
|
306
327
|
test("topP defaults to null when no profile or override sets it", () => {
|
|
307
328
|
const llm = LLMSchema.parse({});
|
|
308
329
|
const resolved = resolveCallSiteConfig("mainAgent", llm);
|
|
@@ -869,6 +890,59 @@ describe("mix profiles", () => {
|
|
|
869
890
|
);
|
|
870
891
|
});
|
|
871
892
|
|
|
893
|
+
test("resolveSingleRouteProfileKey names standard winners and refuses mixes", () => {
|
|
894
|
+
// The mix's arm is picked per conversation and can differ from any
|
|
895
|
+
// post-hoc recomputation, so the outer mix key must never be used as a
|
|
896
|
+
// single-route attribution scope.
|
|
897
|
+
expect(resolveSingleRouteProfileKey("mainAgent", mixLlm)).toBeUndefined();
|
|
898
|
+
expect(
|
|
899
|
+
resolveSingleRouteProfileKey("mainAgent", mixLlm, {
|
|
900
|
+
overrideProfile: "a",
|
|
901
|
+
}),
|
|
902
|
+
).toBe("a");
|
|
903
|
+
// An unusable override falls through to the active mix, which still
|
|
904
|
+
// refuses to name a route.
|
|
905
|
+
expect(
|
|
906
|
+
resolveSingleRouteProfileKey("mainAgent", mixLlm, {
|
|
907
|
+
overrideProfile: "no-such-profile",
|
|
908
|
+
}),
|
|
909
|
+
).toBeUndefined();
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test("resolveSingleRouteProfileKey honors the dispatch resolvability predicate", () => {
|
|
913
|
+
// A pin whose provider dispatch cannot resolve (e.g. a force-deleted
|
|
914
|
+
// connection) must not be claimed as a scope: selection falls through to
|
|
915
|
+
// the active mix exactly as dispatch does, which then refuses to name a
|
|
916
|
+
// route. Without the predicate the stale pin still wins the chain, which
|
|
917
|
+
// is why scope callers must pass dispatch's predicate.
|
|
918
|
+
const staleLlm = LLMSchema.parse({
|
|
919
|
+
profiles: {
|
|
920
|
+
a: { provider: "anthropic", model: "model-a" },
|
|
921
|
+
b: { provider: "anthropic", model: "model-b" },
|
|
922
|
+
stale: { provider: "openai", model: "model-s" },
|
|
923
|
+
ab: {
|
|
924
|
+
mix: [
|
|
925
|
+
{ profile: "a", weight: 1 },
|
|
926
|
+
{ profile: "b", weight: 1 },
|
|
927
|
+
],
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
activeProfile: "ab",
|
|
931
|
+
});
|
|
932
|
+
const openaiForceDeleted = (provider: string) => provider !== "openai";
|
|
933
|
+
expect(
|
|
934
|
+
resolveSingleRouteProfileKey("mainAgent", staleLlm, {
|
|
935
|
+
overrideProfile: "stale",
|
|
936
|
+
isResolvableProvider: openaiForceDeleted,
|
|
937
|
+
}),
|
|
938
|
+
).toBeUndefined();
|
|
939
|
+
expect(
|
|
940
|
+
resolveSingleRouteProfileKey("mainAgent", staleLlm, {
|
|
941
|
+
overrideProfile: "stale",
|
|
942
|
+
}),
|
|
943
|
+
).toBe("stale");
|
|
944
|
+
});
|
|
945
|
+
|
|
872
946
|
test("all dereference spots in a turn agree for the same seed", () => {
|
|
873
947
|
// mainAgent (mix as activeProfile) and a non-main call site resolving the
|
|
874
948
|
// same mix as its call-site profile must pick the same arm when given the
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BACKUP_PROFILE_KEYS,
|
|
7
|
+
DEFAULT_PROFILE_KEYS,
|
|
8
|
+
FALLBACK_PROFILE_BY_KEY,
|
|
9
|
+
} from "../config/default-profile-names.js";
|
|
10
|
+
import { LLMSchema } from "../config/schemas/llm.js";
|
|
11
|
+
|
|
12
|
+
describe("LLMSchema fallbackProfile", () => {
|
|
13
|
+
test.each([...DEFAULT_PROFILE_KEYS])(
|
|
14
|
+
"accepts the code-owned managed pointer for %s",
|
|
15
|
+
(profileName) => {
|
|
16
|
+
const result = LLMSchema.safeParse({
|
|
17
|
+
defaultProvider: { provider: "vellum" },
|
|
18
|
+
profiles: {
|
|
19
|
+
[profileName]: {
|
|
20
|
+
source: "managed",
|
|
21
|
+
fallbackProfile: FALLBACK_PROFILE_BY_KEY[profileName],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
expect(result.success).toBe(true);
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
test("rejects a user-authored fallbackProfile even when its target exists", () => {
|
|
30
|
+
const result = LLMSchema.safeParse({
|
|
31
|
+
profiles: {
|
|
32
|
+
primary: { fallbackProfile: "backup" },
|
|
33
|
+
backup: { speed: "fast" },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
expect(result.success).toBe(false);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
const issue = result.error.issues.find((i) =>
|
|
39
|
+
i.message.includes("Automatic fallbacks are code-owned"),
|
|
40
|
+
);
|
|
41
|
+
expect(issue?.path).toEqual(["profiles", "primary", "fallbackProfile"]);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("rejects a fallbackProfile on a user-owned default-profile shadow", () => {
|
|
46
|
+
const profileName = DEFAULT_PROFILE_KEYS[0];
|
|
47
|
+
const result = LLMSchema.safeParse({
|
|
48
|
+
profiles: {
|
|
49
|
+
[profileName]: {
|
|
50
|
+
source: "user",
|
|
51
|
+
fallbackProfile: FALLBACK_PROFILE_BY_KEY[profileName],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
expect(result.success).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("rejects a managed default pointing anywhere except its code-owned backup", () => {
|
|
59
|
+
const profileName = DEFAULT_PROFILE_KEYS[0];
|
|
60
|
+
const result = LLMSchema.safeParse({
|
|
61
|
+
profiles: {
|
|
62
|
+
[profileName]: {
|
|
63
|
+
source: "managed",
|
|
64
|
+
fallbackProfile: "custom-backup",
|
|
65
|
+
},
|
|
66
|
+
"custom-backup": { speed: "fast" },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
expect(result.success).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("empty-string fallbackProfile is rejected at field level", () => {
|
|
73
|
+
const result = LLMSchema.safeParse({
|
|
74
|
+
profiles: {
|
|
75
|
+
primary: { fallbackProfile: "" },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(result.success).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("profiles without fallbackProfile still parse (back-compat)", () => {
|
|
82
|
+
const result = LLMSchema.safeParse({
|
|
83
|
+
profiles: {
|
|
84
|
+
fast: { speed: "fast", effort: "low" },
|
|
85
|
+
thorough: { effort: "high", maxTokens: 128000 },
|
|
86
|
+
},
|
|
87
|
+
callSites: {
|
|
88
|
+
mainAgent: { profile: "thorough" },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
expect(result.success).toBe(true);
|
|
92
|
+
if (result.success) {
|
|
93
|
+
expect(result.data.profiles["fast"]?.fallbackProfile).toBeUndefined();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a code-owned fallback pointer is rejected on a non-managed column", () => {
|
|
98
|
+
const profileName = DEFAULT_PROFILE_KEYS[0];
|
|
99
|
+
for (const provider of ["anthropic", "chatgpt"] as const) {
|
|
100
|
+
const result = LLMSchema.safeParse({
|
|
101
|
+
defaultProvider: { provider },
|
|
102
|
+
profiles: {
|
|
103
|
+
[profileName]: {
|
|
104
|
+
source: "managed",
|
|
105
|
+
fallbackProfile: FALLBACK_PROFILE_BY_KEY[profileName],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(result.success).toBe(false);
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
const issue = result.error.issues.find((i) =>
|
|
112
|
+
i.path.join(".").endsWith("fallbackProfile"),
|
|
113
|
+
);
|
|
114
|
+
expect(issue?.message).toContain("managed backup profile");
|
|
115
|
+
expect(issue?.path).toEqual([
|
|
116
|
+
"profiles",
|
|
117
|
+
profileName,
|
|
118
|
+
"fallbackProfile",
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("z.toJSONSchema still generates for LLMSchema (config docs/routes)", () => {
|
|
125
|
+
const json = z.toJSONSchema(LLMSchema, {
|
|
126
|
+
unrepresentable: "any",
|
|
127
|
+
io: "input",
|
|
128
|
+
});
|
|
129
|
+
expect(json).toMatchObject({
|
|
130
|
+
properties: {
|
|
131
|
+
profiles: {
|
|
132
|
+
additionalProperties: {
|
|
133
|
+
properties: { fallbackProfile: { readOnly: true } },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("LLMSchema backup profile references vs llm.defaultProvider", () => {
|
|
142
|
+
const backupKey = BACKUP_PROFILE_KEYS[0];
|
|
143
|
+
const managed = { provider: "vellum" } as const;
|
|
144
|
+
const nonManaged = [{ provider: "anthropic" }, { provider: "chatgpt" }];
|
|
145
|
+
|
|
146
|
+
test("activeProfile naming a backup parses on the managed column", () => {
|
|
147
|
+
const result = LLMSchema.safeParse({
|
|
148
|
+
defaultProvider: managed,
|
|
149
|
+
activeProfile: backupKey,
|
|
150
|
+
});
|
|
151
|
+
expect(result.success).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("activeProfile naming a backup is rejected on a non-managed column", () => {
|
|
155
|
+
for (const defaultProvider of nonManaged) {
|
|
156
|
+
const result = LLMSchema.safeParse({
|
|
157
|
+
defaultProvider,
|
|
158
|
+
activeProfile: backupKey,
|
|
159
|
+
});
|
|
160
|
+
expect(result.success).toBe(false);
|
|
161
|
+
if (!result.success) {
|
|
162
|
+
const issue = result.error.issues.find(
|
|
163
|
+
(i) => i.path.join(".") === "activeProfile",
|
|
164
|
+
);
|
|
165
|
+
expect(issue?.message).toContain("managed backup profile");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("advisorProfile naming a backup follows the same split", () => {
|
|
171
|
+
expect(
|
|
172
|
+
LLMSchema.safeParse({
|
|
173
|
+
defaultProvider: managed,
|
|
174
|
+
advisorProfile: backupKey,
|
|
175
|
+
}).success,
|
|
176
|
+
).toBe(true);
|
|
177
|
+
for (const defaultProvider of nonManaged) {
|
|
178
|
+
const result = LLMSchema.safeParse({
|
|
179
|
+
defaultProvider,
|
|
180
|
+
advisorProfile: backupKey,
|
|
181
|
+
});
|
|
182
|
+
expect(result.success).toBe(false);
|
|
183
|
+
if (!result.success) {
|
|
184
|
+
expect(
|
|
185
|
+
result.error.issues.some(
|
|
186
|
+
(i) => i.path.join(".") === "advisorProfile",
|
|
187
|
+
),
|
|
188
|
+
).toBe(true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("a call-site pin on a backup follows the same split", () => {
|
|
194
|
+
expect(
|
|
195
|
+
LLMSchema.safeParse({
|
|
196
|
+
defaultProvider: managed,
|
|
197
|
+
callSites: { recall: { profile: backupKey } },
|
|
198
|
+
}).success,
|
|
199
|
+
).toBe(true);
|
|
200
|
+
for (const defaultProvider of nonManaged) {
|
|
201
|
+
const result = LLMSchema.safeParse({
|
|
202
|
+
defaultProvider,
|
|
203
|
+
callSites: { recall: { profile: backupKey } },
|
|
204
|
+
});
|
|
205
|
+
expect(result.success).toBe(false);
|
|
206
|
+
if (!result.success) {
|
|
207
|
+
const issue = result.error.issues.find(
|
|
208
|
+
(i) => i.path.join(".") === "callSites.recall.profile",
|
|
209
|
+
);
|
|
210
|
+
expect(issue?.message).toContain("managed backup profile");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("a mix arm naming a backup follows the same split", () => {
|
|
216
|
+
const mixConfig = (defaultProvider: { provider: string }) => ({
|
|
217
|
+
defaultProvider,
|
|
218
|
+
profiles: {
|
|
219
|
+
armA: { speed: "fast" },
|
|
220
|
+
blend: {
|
|
221
|
+
mix: [
|
|
222
|
+
{ profile: backupKey, weight: 1 },
|
|
223
|
+
{ profile: "armA", weight: 1 },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
expect(LLMSchema.safeParse(mixConfig(managed)).success).toBe(true);
|
|
229
|
+
for (const defaultProvider of nonManaged) {
|
|
230
|
+
expect(LLMSchema.safeParse(mixConfig(defaultProvider)).success).toBe(
|
|
231
|
+
false,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("an absent or malformed defaultProvider reads as the managed column", () => {
|
|
237
|
+
// `DefaultProviderField` catches an invalid value to `undefined`, and an
|
|
238
|
+
// install predating the field is managed by definition, so both keep
|
|
239
|
+
// their backups referenceable rather than losing a valid selection.
|
|
240
|
+
for (const defaultProvider of [undefined, { provider: 42 }, "vellum"]) {
|
|
241
|
+
const result = LLMSchema.safeParse({
|
|
242
|
+
defaultProvider,
|
|
243
|
+
activeProfile: backupKey,
|
|
244
|
+
});
|
|
245
|
+
expect(result.success).toBe(true);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("the default profile keys stay valid on every column", () => {
|
|
250
|
+
for (const defaultProvider of [managed, ...nonManaged]) {
|
|
251
|
+
for (const key of DEFAULT_PROFILE_KEYS) {
|
|
252
|
+
expect(
|
|
253
|
+
LLMSchema.safeParse({ defaultProvider, activeProfile: key }).success,
|
|
254
|
+
).toBe(true);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// A managed install can persist a thin `{ source: "managed" }` stub for a
|
|
260
|
+
// backup key: `normalizeManagedProfileWrites` reduces a `config get` ->
|
|
261
|
+
// `config set` echo of the effective profile list to exactly that. The stub
|
|
262
|
+
// holds no body of its own, so it must not carry a reference past the
|
|
263
|
+
// provider gate once the install moves off the managed column.
|
|
264
|
+
const stub = { source: "managed" } as const;
|
|
265
|
+
|
|
266
|
+
test("a managed stub does not keep a backup reference alive off the managed column", () => {
|
|
267
|
+
for (const defaultProvider of nonManaged) {
|
|
268
|
+
const result = LLMSchema.safeParse({
|
|
269
|
+
defaultProvider,
|
|
270
|
+
profiles: { [backupKey]: stub, primary: { effort: "high" } },
|
|
271
|
+
activeProfile: backupKey,
|
|
272
|
+
});
|
|
273
|
+
expect(result.success).toBe(false);
|
|
274
|
+
if (!result.success) {
|
|
275
|
+
const issue = result.error.issues.find(
|
|
276
|
+
(i) => i.path.join(".") === "activeProfile",
|
|
277
|
+
);
|
|
278
|
+
expect(issue?.message).toContain("managed backup profile");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("a managed stub does not keep a call-site pin or mix arm alive either", () => {
|
|
284
|
+
for (const defaultProvider of nonManaged) {
|
|
285
|
+
const callSitePin = LLMSchema.safeParse({
|
|
286
|
+
defaultProvider,
|
|
287
|
+
profiles: { [backupKey]: stub },
|
|
288
|
+
callSites: { recall: { profile: backupKey } },
|
|
289
|
+
});
|
|
290
|
+
expect(callSitePin.success).toBe(false);
|
|
291
|
+
|
|
292
|
+
const mixArm = LLMSchema.safeParse({
|
|
293
|
+
defaultProvider,
|
|
294
|
+
profiles: {
|
|
295
|
+
[backupKey]: stub,
|
|
296
|
+
armA: { speed: "fast" },
|
|
297
|
+
blend: {
|
|
298
|
+
mix: [
|
|
299
|
+
{ profile: backupKey, weight: 1 },
|
|
300
|
+
{ profile: "armA", weight: 1 },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
expect(mixArm.success).toBe(false);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("a managed stub does not keep a fallbackProfile pointer alive either", () => {
|
|
310
|
+
for (const defaultProvider of nonManaged) {
|
|
311
|
+
const result = LLMSchema.safeParse({
|
|
312
|
+
defaultProvider,
|
|
313
|
+
profiles: {
|
|
314
|
+
[backupKey]: stub,
|
|
315
|
+
primary: { fallbackProfile: backupKey },
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
expect(result.success).toBe(false);
|
|
319
|
+
if (!result.success) {
|
|
320
|
+
const issue = result.error.issues.find(
|
|
321
|
+
(i) => i.path.join(".") === "profiles.primary.fallbackProfile",
|
|
322
|
+
);
|
|
323
|
+
expect(issue?.message).toContain("Automatic fallbacks are code-owned");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("a managed stub keeps its references valid on the managed column", () => {
|
|
329
|
+
// The stub stands for a body that does resolve here, so nothing about it
|
|
330
|
+
// makes the reference worse than the unmaterialized case.
|
|
331
|
+
const result = LLMSchema.safeParse({
|
|
332
|
+
defaultProvider: managed,
|
|
333
|
+
profiles: {
|
|
334
|
+
[backupKey]: stub,
|
|
335
|
+
},
|
|
336
|
+
activeProfile: backupKey,
|
|
337
|
+
callSites: { recall: { profile: backupKey } },
|
|
338
|
+
});
|
|
339
|
+
expect(result.success).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("a user-owned entry under a backup name cannot enable custom fallback", () => {
|
|
343
|
+
for (const defaultProvider of [managed, ...nonManaged]) {
|
|
344
|
+
const result = LLMSchema.safeParse({
|
|
345
|
+
defaultProvider,
|
|
346
|
+
profiles: {
|
|
347
|
+
[backupKey]: {
|
|
348
|
+
source: "user",
|
|
349
|
+
provider: "anthropic",
|
|
350
|
+
model: "claude-opus-4-7",
|
|
351
|
+
},
|
|
352
|
+
primary: { fallbackProfile: backupKey },
|
|
353
|
+
},
|
|
354
|
+
activeProfile: backupKey,
|
|
355
|
+
});
|
|
356
|
+
expect(result.success).toBe(false);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|