@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321

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.
Files changed (209) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +12 -12
  3. package/docker/mousepad/90-vellum.gschema.override +4 -0
  4. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  5. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  6. package/openapi.yaml +97 -4
  7. package/package.json +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +4 -2
  9. package/src/__tests__/call-routes-http.test.ts +4 -2
  10. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  11. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  13. package/src/__tests__/conversation-pairing.test.ts +10 -0
  14. package/src/__tests__/conversation-resurface-done.test.ts +289 -0
  15. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  16. package/src/__tests__/gemini-provider.test.ts +31 -0
  17. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  18. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  19. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  20. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  21. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  22. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  23. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  24. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  25. package/src/__tests__/pricing.test.ts +33 -0
  26. package/src/__tests__/provider-error-scenarios.test.ts +46 -0
  27. package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
  28. package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
  29. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  30. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  31. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
  32. package/src/__tests__/send-endpoint-busy.test.ts +4 -2
  33. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  34. package/src/__tests__/skill-load-tool.test.ts +42 -11
  35. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  36. package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
  37. package/src/__tests__/validate-input.test.ts +73 -0
  38. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  39. package/src/api/ask-question-tool.ts +50 -0
  40. package/src/api/events/tool-result.ts +11 -0
  41. package/src/api/index.ts +7 -0
  42. package/src/api/requests/dictation.ts +2 -2
  43. package/src/api/responses/home.ts +27 -0
  44. package/src/avatar/notification-avatar.test.ts +42 -34
  45. package/src/avatar/notification-avatar.ts +1 -0
  46. package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
  47. package/src/calls/voice-escalation-judge.ts +6 -5
  48. package/src/calls/voice-screen-annotation.ts +44 -0
  49. package/src/calls/voice-session-bridge.ts +22 -0
  50. package/src/cli/commands/__tests__/plugins.test.ts +29 -1
  51. package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
  52. package/src/cli/commands/platform/index.help.ts +2 -1
  53. package/src/cli/commands/platform/index.ts +8 -2
  54. package/src/cli/commands/plugins.ts +16 -0
  55. package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
  56. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
  57. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
  58. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  59. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
  60. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
  61. package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
  62. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
  63. package/src/cli/lib/bundled-marketplace.json +4 -4
  64. package/src/cli/lib/inspect-plugin.ts +5 -0
  65. package/src/cli/lib/plugin-catalog-cache.ts +41 -4
  66. package/src/cli/lib/plugin-catalog-local.ts +9 -0
  67. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  68. package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
  69. package/src/cli/lib/plugin-details.ts +10 -7
  70. package/src/cli/lib/search-plugins.ts +2 -0
  71. package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
  72. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  73. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  74. package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
  75. package/src/config/call-site-defaults.ts +17 -11
  76. package/src/config/default-profile-catalog.ts +118 -19
  77. package/src/config/default-profile-names.ts +69 -0
  78. package/src/config/feature-flag-registry.json +25 -9
  79. package/src/config/inference-profile-validation.ts +7 -6
  80. package/src/config/llm-resolver.ts +16 -0
  81. package/src/config/profile-text-generation.test.ts +9 -0
  82. package/src/config/schemas/call-site-catalog.ts +8 -1
  83. package/src/config/schemas/llm.ts +70 -9
  84. package/src/config/sidebar-done-gate.ts +27 -0
  85. package/src/config/sync-gated-profiles.ts +96 -44
  86. package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
  87. package/src/daemon/auto-profile-router.ts +220 -0
  88. package/src/daemon/conversation-agent-loop.ts +77 -4
  89. package/src/daemon/conversation-slash.ts +2 -2
  90. package/src/daemon/conversation-turn-finalize.ts +12 -0
  91. package/src/daemon/dictation-text-processing.ts +4 -4
  92. package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
  93. package/src/daemon/handlers/config-channels.ts +3 -1
  94. package/src/daemon/handlers/conversation-history.ts +11 -1
  95. package/src/daemon/handlers/conversations.ts +118 -10
  96. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  97. package/src/daemon/pkb-reminder-builder.ts +1 -1
  98. package/src/desktop/desktop-browser-endpoint.ts +1 -0
  99. package/src/home/feed-types.ts +2 -0
  100. package/src/inbound/platform-callback-registration.ts +19 -6
  101. package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
  102. package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
  103. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  104. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  105. package/src/live-voice/continuation-judge.ts +1 -1
  106. package/src/live-voice/live-voice-session.ts +7 -6
  107. package/src/live-voice/protocol.ts +16 -5
  108. package/src/messaging/providers/gmail/client.ts +4 -0
  109. package/src/notifications/README.md +11 -3
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
  111. package/src/notifications/conversation-pairing.ts +23 -8
  112. package/src/notifications/home-feed-mirror.ts +39 -0
  113. package/src/notifications/home-feed-side-effect.ts +46 -19
  114. package/src/oauth/AGENTS.md +7 -1
  115. package/src/permissions/question-prompter.ts +14 -4
  116. package/src/persistence/conversation-crud.ts +114 -3
  117. package/src/persistence/conversation-queries.ts +48 -3
  118. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
  119. package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
  120. package/src/persistence/embeddings/embedding-backend.ts +23 -41
  121. package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
  122. package/src/persistence/embeddings/embedding-gemini.ts +3 -1
  123. package/src/persistence/embeddings/embedding-local.ts +72 -17
  124. package/src/persistence/embeddings/embedding-types.ts +0 -19
  125. package/src/persistence/jobs-store.ts +229 -0
  126. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  127. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  128. package/src/persistence/steps.ts +12 -0
  129. package/src/platform/sync-avatar.test.ts +29 -0
  130. package/src/plugin-api/model-profiles.ts +5 -3
  131. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  132. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
  133. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  134. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  135. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  136. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  137. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  138. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
  139. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  140. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  141. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  142. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  143. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  144. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  145. package/src/providers/anthropic/client.ts +2 -2
  146. package/src/providers/gemini/client.ts +17 -1
  147. package/src/providers/inference/auth.ts +25 -0
  148. package/src/providers/jev/ask.test.ts +66 -0
  149. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
  150. package/src/providers/malformed-tool-call.ts +55 -0
  151. package/src/providers/model-catalog.ts +257 -4
  152. package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
  153. package/src/providers/openai/chat-completions-provider.ts +30 -0
  154. package/src/providers/openai/codex-models.ts +2 -0
  155. package/src/providers/platform-proxy/constants.ts +5 -0
  156. package/src/providers/retry.ts +68 -36
  157. package/src/providers/search-provider-catalog.ts +10 -0
  158. package/src/providers/vellum-model-routing.test.ts +1 -0
  159. package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
  160. package/src/runtime/__tests__/question-resolution.test.ts +32 -0
  161. package/src/runtime/agent-wake.ts +9 -2
  162. package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
  163. package/src/runtime/auth/middleware.ts +15 -11
  164. package/src/runtime/middleware/auth.ts +7 -1
  165. package/src/runtime/pending-interactions.ts +8 -8
  166. package/src/runtime/question-resolution.ts +54 -8
  167. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
  168. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  169. package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
  170. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  171. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  172. package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
  173. package/src/runtime/routes/conversation-list-routes.ts +16 -14
  174. package/src/runtime/routes/conversation-query-routes.ts +4 -0
  175. package/src/runtime/routes/conversation-routes.ts +4 -2
  176. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  177. package/src/runtime/routes/global-search-routes.ts +22 -7
  178. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  179. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  180. package/src/runtime/routes/platform-routes.ts +6 -1
  181. package/src/runtime/routes/plugins-routes.ts +5 -2
  182. package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
  183. package/src/runtime/sync/resource-sync-events.ts +11 -0
  184. package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
  185. package/src/runtime/sync/worker-daemon-notify.ts +44 -0
  186. package/src/runtime/verification-outbound-actions.ts +2 -2
  187. package/src/skills/validate-input.ts +75 -11
  188. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  189. package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
  190. package/src/tools/ask-question/ask-question-tool.ts +12 -1
  191. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  192. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  193. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  194. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  195. package/src/tools/network/web-fetch.ts +30 -9
  196. package/src/tools/network/web-search.ts +235 -4
  197. package/src/tools/skills/execute.ts +1 -1
  198. package/src/tools/skills/format-input-schema.ts +15 -0
  199. package/src/tools/skills/load.ts +15 -43
  200. package/src/tools/skills/scaffold-managed.ts +86 -71
  201. package/src/tools/skills/skill-tool-factory.ts +2 -1
  202. package/src/tools/workflows/manage-workflows.ts +4 -2
  203. package/src/util/worker-pipe.test.ts +146 -0
  204. package/src/util/worker-pipe.ts +49 -0
  205. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  206. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  207. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
@@ -67,6 +67,61 @@ export function isBackupProfileKey(value: string): value is BackupProfileKey {
67
67
  return (BACKUP_PROFILE_KEYS as readonly string[]).includes(value);
68
68
  }
69
69
 
70
+ /**
71
+ * Managed profile for the TypeSafe decision model (Jev). Deliberately NOT
72
+ * part of `DEFAULT_PROFILE_KEYS`: it has no BYOK column, because the
73
+ * platform serves Jev only through the managed route, and its model returns
74
+ * structured answers rather than chat text, so it can never be the active
75
+ * or conversation profile. It exists as a pin target for the call sites
76
+ * that consume a yes/no verdict (the voice judges, the memory selector).
77
+ */
78
+ export const JEV_MANAGED_PROFILE_KEY = "jev-managed";
79
+
80
+ /**
81
+ * Managed profile that picks one of the default profiles per message: the
82
+ * agent loop asks Jev which default fits the user's latest message and runs
83
+ * the turn on that profile (`daemon/auto-profile-router.ts`). Its own body
84
+ * is the managed Balanced body, so any path that dispatches the name
85
+ * directly, or a turn on which the router is unavailable, runs Balanced.
86
+ * Managed column only, because the router depends on the managed Jev route,
87
+ * and flag-gated: it exists only while the `auto-profile` feature flag has
88
+ * reconciled it into the workspace (see `sync-gated-profiles.ts`).
89
+ * Deliberately NOT part of `DEFAULT_PROFILE_KEYS`: it is a selection over
90
+ * those keys, not a fifth intent.
91
+ */
92
+ export const AUTO_PROFILE_KEY = "auto";
93
+ export const AUTO_PROFILE_FEATURE_FLAG_KEY = "auto-profile";
94
+
95
+ /** The call site the automatic profile router asks Jev through. */
96
+ export const AUTO_PROFILE_ROUTER_CALL_SITE = "autoProfileRouter";
97
+
98
+ /**
99
+ * The call sites that consume a structured verdict and so may be pinned to
100
+ * the Jev profile. Every other site expects chat text or a tool call, which
101
+ * the decision model never produces.
102
+ */
103
+ export const JEV_MANAGED_PROFILE_CALL_SITES = [
104
+ "voiceEscalationJudge",
105
+ "voiceContinuationJudge",
106
+ "memoryV3SelectL2",
107
+ AUTO_PROFILE_ROUTER_CALL_SITE,
108
+ ] as const;
109
+
110
+ /**
111
+ * Keys whose profile exists on the managed (`vellum`) column only: the
112
+ * backups, the Jev profile, and the Auto profile. Under a BYOK or ChatGPT
113
+ * default provider these names have no body to resolve to.
114
+ */
115
+ export const MANAGED_ONLY_PROFILE_KEYS = [
116
+ ...BACKUP_PROFILE_KEYS,
117
+ JEV_MANAGED_PROFILE_KEY,
118
+ AUTO_PROFILE_KEY,
119
+ ] as const;
120
+
121
+ export function isManagedOnlyProfileKey(value: string): boolean {
122
+ return (MANAGED_ONLY_PROFILE_KEYS as readonly string[]).includes(value);
123
+ }
124
+
70
125
  /**
71
126
  * Flag-gated default profile: only available while the `os-beta` feature
72
127
  * flag has reconciled it into the workspace (see `sync-gated-profiles.ts`).
@@ -76,6 +131,20 @@ export function isBackupProfileKey(value: string): value is BackupProfileKey {
76
131
  */
77
132
  export const OS_BETA_PROFILE_KEY = "os-beta";
78
133
 
134
+ /**
135
+ * Managed profiles that resolve only while a workspace entry exists, because
136
+ * a feature flag reconciles that entry in and out. Never always-valid
137
+ * reference targets: a reference to one is valid only with the entry present.
138
+ */
139
+ export const FLAG_GATED_PROFILE_KEYS = [
140
+ OS_BETA_PROFILE_KEY,
141
+ AUTO_PROFILE_KEY,
142
+ ] as const;
143
+
144
+ export function isFlagGatedProfileKey(value: string): boolean {
145
+ return (FLAG_GATED_PROFILE_KEYS as readonly string[]).includes(value);
146
+ }
147
+
79
148
  /**
80
149
  * The named columns of the intent × provider matrix. `vellum` is the
81
150
  * platform-managed column (routed through the single `vellum` connection to
@@ -170,14 +170,6 @@
170
170
  "description": "Enable Anthropic fast mode for Opus models (4.6, 4.7, 4.8), delivering up to 2.5x higher output tokens per second at premium pricing",
171
171
  "defaultEnabled": false
172
172
  },
173
- {
174
- "id": "teleport",
175
- "scope": "client",
176
- "key": "teleport",
177
- "label": "Teleport",
178
- "description": "Enable the local-to-platform teleport UI in General settings. Platform-to-local teleport is GA and not gated by this flag",
179
- "defaultEnabled": false
180
- },
181
173
  {
182
174
  "id": "apple-container",
183
175
  "scope": "client",
@@ -346,6 +338,14 @@
346
338
  "description": "Show the Add Server action on the MCP settings page. The MCP page itself remains visible; this flag gates only creating new Model Context Protocol server connections.",
347
339
  "defaultEnabled": false
348
340
  },
341
+ {
342
+ "id": "mcp-catalog-qa-integrations",
343
+ "scope": "assistant",
344
+ "key": "mcp-catalog-qa-integrations",
345
+ "label": "MCP Catalog QA Integrations",
346
+ "description": "Show Gamma, Guru, Intercom, Ramp, Semrush, and Typeform in the installable plugin catalog while their OAuth flows are validated. When off, catalog search, detail, and install-by-name cannot resolve them. Existing installed connections remain available.",
347
+ "defaultEnabled": false
348
+ },
349
349
  {
350
350
  "id": "os-beta",
351
351
  "scope": "assistant",
@@ -354,6 +354,14 @@
354
354
  "description": "Enable the OS Beta model profile (MiniMax M3 / Together) in the assistant's model profile selection.",
355
355
  "defaultEnabled": false
356
356
  },
357
+ {
358
+ "id": "auto-profile",
359
+ "scope": "assistant",
360
+ "key": "auto-profile",
361
+ "label": "Auto Profile",
362
+ "description": "Enable the Auto model profile, which asks Jev which default profile should answer each message.",
363
+ "defaultEnabled": false
364
+ },
357
365
  {
358
366
  "id": "new-thread-suggestions",
359
367
  "scope": "client",
@@ -557,7 +565,15 @@
557
565
  "scope": "client",
558
566
  "key": "assistant-inbox",
559
567
  "label": "Assistant Inbox",
560
- "description": "Shows an Assistant Inbox entry above Preferences in the web sidebar and the /assistant/inbox page it opens: a mailbox for the assistant's managed vellum.me address (received and sent, with a reading pane), the inline email setup card for entitled orgs with no address yet, and an upgrade card for orgs without the managed_email entitlement. Off hides the entry and redirects the route to chat.",
568
+ "description": "Shows an Assistant Inbox entry above Preferences in the web sidebar and the /assistant/inbox page it opens: a mailbox for the assistant's managed vellum.me address (received and sent, with a reading pane), the inline email setup card for entitled orgs with no address yet, and an upgrade card for orgs without the managed_email entitlement. With it on, email setup happens only there: the billing wizard hands off to the inbox instead of its own domain step, the billing page's finish-setup nudge gives way to the inbox's rail entry, and the Channels page's Email section points there instead of registering an address itself. Off hides the entry, redirects the route to chat, and keeps the older setup surfaces.",
569
+ "defaultEnabled": false
570
+ },
571
+ {
572
+ "id": "sidebar-done",
573
+ "scope": "both",
574
+ "key": "sidebar-done",
575
+ "label": "Sidebar Done",
576
+ "description": "Turns archiving a chat into marking it done: the sidebar row carries a Done check instead of the overflow menu, an All chats page lists the full history, and done chats stay findable in search by default. Off keeps today's Archive wording, the overflow menu, and search that hides archived rows unless the query asks for them.",
561
577
  "defaultEnabled": false
562
578
  }
563
579
  ]
@@ -1,4 +1,4 @@
1
- import { getUserSelectableProfilesForProvider } from "./default-profile-catalog.js";
1
+ import { getConversationProfilesForProvider } from "./default-profile-catalog.js";
2
2
  import { getConfigReadOnly } from "./loader.js";
3
3
 
4
4
  /**
@@ -6,17 +6,18 @@ import { getConfigReadOnly } from "./loader.js";
6
6
  * (code-defined defaults + workspace `llm.profiles`). Returns a user-facing
7
7
  * error message when the key is empty or unknown, or `null` when valid.
8
8
  *
9
- * Resolved through the `llm.defaultProvider`-aware user-selectable view so
10
- * availability matches what a direct profile pin can choose. Managed backup
11
- * routes remain available to automatic fallback resolution but are not
12
- * reported as selectable profiles.
9
+ * Resolved through the `llm.defaultProvider`-aware conversation view, since
10
+ * every caller (schedules, subagent spawns, per-conversation pins) runs chat
11
+ * turns on the result. Managed backup routes remain available to automatic
12
+ * fallback resolution, and the managed Jev profile to the verdict call
13
+ * sites, but neither is reported as a selectable profile here.
13
14
  */
14
15
  export function validateInferenceProfileKey(profile: string): string | null {
15
16
  if (!profile.trim()) {
16
17
  return "inferenceProfile must be a non-empty string";
17
18
  }
18
19
  const llm = getConfigReadOnly().llm;
19
- const profiles = getUserSelectableProfilesForProvider(
20
+ const profiles = getConversationProfilesForProvider(
20
21
  llm?.profiles,
21
22
  llm?.defaultProvider ?? null,
22
23
  );
@@ -627,6 +627,22 @@ export function resolveDefaultProfileKey(
627
627
  return selectWinningProfile(callSite, llm, {}).profileName ?? undefined;
628
628
  }
629
629
 
630
+ /**
631
+ * Returns the code-owned profile tier for catalog display when that tier is
632
+ * available through the workspace's default provider. Workspace call-site
633
+ * pins do not affect this value. A managed-only tier on a BYOK install returns
634
+ * undefined so clients do not report a profile that cannot run there.
635
+ */
636
+ export function resolveShippedDefaultProfileKey(
637
+ callSite: LLMCallSite,
638
+ llm: z.infer<typeof LLMSchema>,
639
+ ): string | undefined {
640
+ const intent = CALL_SITE_DEFAULTS[callSite]?.profile ?? "balanced";
641
+ return usableDefaultIntent(intent, llm, {}, () => undefined) == null
642
+ ? undefined
643
+ : intent;
644
+ }
645
+
630
646
  /**
631
647
  * Returns the profile key that `resolveCallSiteConfig` treats as the winning
632
648
  * (highest-precedence) profile for a turn — the profile whose fragment supplies
@@ -15,6 +15,15 @@ describe("profileSupportsTextGeneration", () => {
15
15
  ).toBe(false);
16
16
  });
17
17
 
18
+ test("false for the managed Jev profile shape", () => {
19
+ expect(
20
+ profileSupportsTextGeneration(
21
+ { provider: "vellum", model: "jev-latest" },
22
+ {},
23
+ ),
24
+ ).toBe(false);
25
+ });
26
+
18
27
  test("true for a chat model and for unlisted custom ids", () => {
19
28
  expect(
20
29
  profileSupportsTextGeneration(
@@ -307,7 +307,7 @@ const CATALOG_RECORD: CatalogRecord = {
307
307
  id: "voiceEscalationJudge",
308
308
  displayName: "Voice Escalation Judge",
309
309
  description:
310
- "Second opinion on the voice front door's escalate decision. Runs only when pinned to a TypeSafe profile; a confident 'needs tools' verdict hands the turn to the escalated leg.",
310
+ "Second opinion on the voice front door's escalate decision. Uses managed Jev when Vellum-managed inference is available, or an explicitly configured TypeSafe profile; a confident 'needs tools' verdict hands the turn to the escalated leg.",
311
311
  domain: "agentLoop",
312
312
  },
313
313
  voiceContinuationJudge: {
@@ -317,6 +317,13 @@ const CATALOG_RECORD: CatalogRecord = {
317
317
  "Decides from the caller's interruption whether a barged-in request should keep running in the background. Runs only when pinned to a TypeSafe profile.",
318
318
  domain: "agentLoop",
319
319
  },
320
+ autoProfileRouter: {
321
+ id: "autoProfileRouter",
322
+ displayName: "Auto Profile Router",
323
+ description:
324
+ "Picks which default profile answers a message when the conversation runs on the Auto profile. Asks managed Jev one choice question over the latest message; when unavailable the turn runs on Balanced.",
325
+ domain: "agentLoop",
326
+ },
320
327
  voiceContinuationLabel: {
321
328
  id: "voiceContinuationLabel",
322
329
  displayName: "Voice Continuation Label",
@@ -14,15 +14,28 @@ import {
14
14
  parseVellumModel,
15
15
  } from "../../providers/vellum-model-routing.js";
16
16
  import {
17
- BACKUP_PROFILE_KEYS,
17
+ AUTO_PROFILE_KEY,
18
18
  DEFAULT_PROFILE_KEYS,
19
19
  DEFAULT_PROFILE_PROVIDERS,
20
20
  FALLBACK_PROFILE_BY_KEY,
21
- isBackupProfileKey,
22
21
  isDefaultProfileKey,
22
+ isFlagGatedProfileKey,
23
+ isManagedOnlyProfileKey,
24
+ JEV_MANAGED_PROFILE_CALL_SITES,
25
+ JEV_MANAGED_PROFILE_KEY,
26
+ MANAGED_ONLY_PROFILE_KEYS,
23
27
  } from "../default-profile-names.js";
24
28
  import { InputModalitiesSchema } from "../input-modalities.js";
25
29
 
30
+ /**
31
+ * Managed-only keys that are valid reference targets on the managed column
32
+ * whether or not a workspace stub exists. The flag-gated ones (`os-beta`,
33
+ * `auto`) are excluded: they resolve only while their reconciled stub is
34
+ * present, so they enter the reference set through `llm.profiles` alone.
35
+ */
36
+ const ALWAYS_RESOLVING_MANAGED_ONLY_PROFILE_KEYS =
37
+ MANAGED_ONLY_PROFILE_KEYS.filter((key) => !isFlagGatedProfileKey(key));
38
+
26
39
  /**
27
40
  * Unified LLM configuration schema.
28
41
  *
@@ -232,6 +245,7 @@ export const LLMCallSiteEnum = z.enum([
232
245
  "voiceEscalationJudge",
233
246
  "voiceContinuationJudge",
234
247
  "voiceContinuationLabel",
248
+ "autoProfileRouter",
235
249
  "trustRuleSuggestion",
236
250
  "homeGreeting",
237
251
  "homeSuggestedPrompts",
@@ -766,10 +780,16 @@ function unresolvableProfileReason(
766
780
  name: string,
767
781
  backupsResolve: boolean,
768
782
  ): string {
769
- return !backupsResolve &&
770
- (BACKUP_PROFILE_KEYS as readonly string[]).includes(name)
771
- ? "is a managed backup profile, which resolves only while llm.defaultProvider is the managed provider"
772
- : "is not defined in llm.profiles";
783
+ if (backupsResolve || !isManagedOnlyProfileKey(name)) {
784
+ return "is not defined in llm.profiles";
785
+ }
786
+ if (name === JEV_MANAGED_PROFILE_KEY) {
787
+ return "is the managed Jev profile, which resolves only while llm.defaultProvider is the managed provider";
788
+ }
789
+ if (name === AUTO_PROFILE_KEY) {
790
+ return "is the managed Auto profile, which resolves only while llm.defaultProvider is the managed provider";
791
+ }
792
+ return "is a managed backup profile, which resolves only while llm.defaultProvider is the managed provider";
773
793
  }
774
794
 
775
795
  /**
@@ -800,7 +820,7 @@ function referenceableProfileKeys(
800
820
  ): string[] {
801
821
  return Object.entries(profiles ?? {})
802
822
  .filter(([name, value]) => {
803
- if (backupsResolve || !isBackupProfileKey(name)) {
823
+ if (backupsResolve || !isManagedOnlyProfileKey(name)) {
804
824
  return true;
805
825
  }
806
826
  const entry =
@@ -854,7 +874,7 @@ export function collectFallbackProfileIssues(
854
874
  const profileNames = new Set([
855
875
  ...referenceableProfileKeys(profiles, backupsResolve),
856
876
  ...DEFAULT_PROFILE_KEYS,
857
- ...(backupsResolve ? BACKUP_PROFILE_KEYS : []),
877
+ ...(backupsResolve ? ALWAYS_RESOLVING_MANAGED_ONLY_PROFILE_KEYS : []),
858
878
  ]);
859
879
  const mixProfileNames = new Set(
860
880
  entries
@@ -1004,6 +1024,8 @@ export const LLMSchema = z
1004
1024
  // they join the set only under a managed `llm.defaultProvider`: on a BYOK
1005
1025
  // or ChatGPT default provider they have no body to resolve to, and
1006
1026
  // keeping the reference would strand a selection the picker cannot show.
1027
+ // The managed Jev profile (`JEV_MANAGED_PROFILE_KEY`) is scoped the same
1028
+ // way, as a pin target for the judge call sites.
1007
1029
  // The flag-gated `os-beta` is excluded: it resolves only while a
1008
1030
  // workspace entry exists, so a reference to it is valid only when that
1009
1031
  // entry is present in `config.profiles`. A backup name materialized as a
@@ -1018,9 +1040,23 @@ export const LLMSchema = z
1018
1040
  backupsResolve,
1019
1041
  ),
1020
1042
  ...DEFAULT_PROFILE_KEYS,
1021
- ...(backupsResolve ? BACKUP_PROFILE_KEYS : []),
1043
+ ...(backupsResolve ? ALWAYS_RESOLVING_MANAGED_ONLY_PROFILE_KEYS : []),
1022
1044
  ]);
1023
1045
  for (const [siteId, siteConfig] of Object.entries(config.callSites ?? {})) {
1046
+ // The Jev profile answers with structured verdicts, so only the sites
1047
+ // built to read one may pin it; a text-producing site would receive
1048
+ // output it cannot use. (Mix arms are covered by the profile-level
1049
+ // mix validation below, which bars the profile from any mix.)
1050
+ if (
1051
+ siteConfig?.profile === JEV_MANAGED_PROFILE_KEY &&
1052
+ !(JEV_MANAGED_PROFILE_CALL_SITES as readonly string[]).includes(siteId)
1053
+ ) {
1054
+ ctx.addIssue({
1055
+ code: "custom",
1056
+ path: ["callSites", siteId, "profile"],
1057
+ message: `Profile "${JEV_MANAGED_PROFILE_KEY}" returns structured answers rather than chat text, so it can only be pinned to ${JEV_MANAGED_PROFILE_CALL_SITES.join(", ")}`,
1058
+ });
1059
+ }
1024
1060
  if (siteConfig?.profile == null) {
1025
1061
  continue;
1026
1062
  }
@@ -1052,6 +1088,20 @@ export const LLMSchema = z
1052
1088
  message: `Profile "${config.advisorProfile}" referenced by llm.advisorProfile ${unresolvableProfileReason(config.advisorProfile, backupsResolve)}`,
1053
1089
  });
1054
1090
  }
1091
+ // The managed Jev profile is a valid reference target for call-site pins
1092
+ // only. Its model returns structured verdicts rather than chat text, so
1093
+ // the conversation positions reject it here as well as at the write
1094
+ // routes, which keeps a raw config write from handing the main agent or
1095
+ // the advisor a model that cannot answer in prose.
1096
+ for (const field of ["activeProfile", "advisorProfile"] as const) {
1097
+ if (config[field] === JEV_MANAGED_PROFILE_KEY) {
1098
+ ctx.addIssue({
1099
+ code: "custom",
1100
+ path: [field],
1101
+ message: `Profile "${JEV_MANAGED_PROFILE_KEY}" referenced by llm.${field} returns structured answers rather than chat text, so it cannot be the conversation or advisor profile`,
1102
+ });
1103
+ }
1104
+ }
1055
1105
 
1056
1106
  // --- Mix profile validation --------------------------------------------
1057
1107
  // Config keys a mix profile must NOT also set (a mix only references other
@@ -1105,6 +1155,17 @@ export const LLMSchema = z
1105
1155
  });
1106
1156
  continue;
1107
1157
  }
1158
+ // A mix is a conversation profile, so an arm that returns structured
1159
+ // verdicts rather than text would hand a chat turn to the decision
1160
+ // model whenever the seeded pick lands on it.
1161
+ if (arm.profile === JEV_MANAGED_PROFILE_KEY) {
1162
+ ctx.addIssue({
1163
+ code: "custom",
1164
+ path: ["profiles", name, "mix", index, "profile"],
1165
+ message: `Mix profile "${name}" references "${JEV_MANAGED_PROFILE_KEY}", which returns structured answers rather than chat text and cannot be a mix constituent.`,
1166
+ });
1167
+ continue;
1168
+ }
1108
1169
  // (b) No nesting — a mix arm must be a standard (non-mix) profile.
1109
1170
  if (mixProfileNames.has(arm.profile)) {
1110
1171
  ctx.addIssue({
@@ -0,0 +1,27 @@
1
+ import { isAssistantFeatureFlagEnabled } from "./assistant-feature-flags.js";
2
+ import type { AssistantConfig } from "./schema.js";
3
+
4
+ export const SIDEBAR_DONE_FLAG_KEY = "sidebar-done" as const;
5
+
6
+ /**
7
+ * Whether archiving a chat means marking it done.
8
+ *
9
+ * The flag is `both`-scoped: clients gate the Done affordances and the Old
10
+ * chats page on it, and the daemon gates the halves the feature needs. Done is
11
+ * `archived_at`, so no row changes shape in either state.
12
+ *
13
+ * On the daemon side there are two. The first is search reach: off, an
14
+ * archived conversation is hidden from search unless the query names it
15
+ * (`is:archived`); on, a done chat is findable by default and `is:unarchived`
16
+ * is the opt-out, because a chat the user marked done is filed, not deleted,
17
+ * and search is how they get back to it.
18
+ *
19
+ * The second is that a done chat keeps working. Off, an archived conversation
20
+ * rejects every wake and nothing but the unarchive route clears the column, so
21
+ * marking a chat done silently kills its schedule or its channel thread. On, a
22
+ * wake still runs on it, and a message the user reads landing in it brings the
23
+ * conversation back to the list.
24
+ */
25
+ export function isSidebarDoneEnabled(config?: AssistantConfig): boolean {
26
+ return isAssistantFeatureFlagEnabled(SIDEBAR_DONE_FLAG_KEY, config);
27
+ }
@@ -2,8 +2,16 @@ import { isDeepStrictEqual } from "node:util";
2
2
 
3
3
  import { getLogger } from "../util/logger.js";
4
4
  import { isAssistantFeatureFlagEnabled } from "./assistant-feature-flags.js";
5
- import { OS_BETA_PROFILE_TEMPLATE } from "./default-profile-catalog.js";
6
- import { OS_BETA_PROFILE_KEY } from "./default-profile-names.js";
5
+ import {
6
+ AUTO_PROFILE_TEMPLATE,
7
+ type DefaultProfileTemplate,
8
+ OS_BETA_PROFILE_TEMPLATE,
9
+ } from "./default-profile-catalog.js";
10
+ import {
11
+ AUTO_PROFILE_FEATURE_FLAG_KEY,
12
+ AUTO_PROFILE_KEY,
13
+ OS_BETA_PROFILE_KEY,
14
+ } from "./default-profile-names.js";
7
15
  import {
8
16
  getConfigReadOnly,
9
17
  invalidateConfigCache,
@@ -18,16 +26,56 @@ import {
18
26
 
19
27
  const log = getLogger("sync-gated-profiles");
20
28
 
29
+ interface GatedProfile {
30
+ key: string;
31
+ flag: string;
32
+ template: DefaultProfileTemplate;
33
+ /** Insert the key into a `profileOrder` that does not yet list it. */
34
+ place: (profileOrder: string[]) => void;
35
+ /**
36
+ * Whether a BYOK install first materializes the stub disabled. True for a
37
+ * profile whose managed route exists but is unusable until the user enables
38
+ * the managed connection; false for a managed-only profile, whose stub has
39
+ * no body on a BYOK column and so is never offered there anyway.
40
+ */
41
+ byokStartsDisabled: boolean;
42
+ }
43
+
44
+ const GATED_PROFILES: readonly GatedProfile[] = [
45
+ {
46
+ key: OS_BETA_PROFILE_KEY,
47
+ flag: OS_BETA_FEATURE_FLAG_KEY,
48
+ template: OS_BETA_PROFILE_TEMPLATE,
49
+ place: (profileOrder) => {
50
+ const balancedIndex = profileOrder.indexOf("balanced");
51
+ if (balancedIndex >= 0) {
52
+ profileOrder.splice(balancedIndex + 1, 0, OS_BETA_PROFILE_KEY);
53
+ } else {
54
+ profileOrder.push(OS_BETA_PROFILE_KEY);
55
+ }
56
+ },
57
+ byokStartsDisabled: true,
58
+ },
59
+ {
60
+ key: AUTO_PROFILE_KEY,
61
+ flag: AUTO_PROFILE_FEATURE_FLAG_KEY,
62
+ template: AUTO_PROFILE_TEMPLATE,
63
+ // Auto leads the picker.
64
+ place: (profileOrder) => profileOrder.unshift(AUTO_PROFILE_KEY),
65
+ byokStartsDisabled: false,
66
+ },
67
+ ];
68
+
21
69
  /**
22
70
  * Reconcile flag-gated managed profiles against the current feature-flag state.
23
71
  *
24
72
  * `seedInferenceProfiles()` runs synchronously at boot before feature flags are
25
- * available, so the OS Beta profile (MiniMax M3 / together-managed) is
26
- * materialized here once flags have loaded. When the `os-beta` flag is on, the
27
- * managed profile is created (ordered right after `balanced`); when it is off, a
28
- * previously managed entry is removed with `profileOrder` / `activeProfile` /
29
- * `advisorProfile` fallbacks. The reconcile is idempotent and never touches a
30
- * user-owned profile of the same name.
73
+ * available, so the gated profiles (OS Beta, Auto) are materialized here once
74
+ * flags have loaded. When a profile's flag is on, its managed stub is created
75
+ * and placed in `profileOrder`; when it is off, a previously managed entry is
76
+ * removed with `profileOrder` / `activeProfile` / `advisorProfile` / call-site
77
+ * fallbacks. The reconcile is idempotent and never touches a user-owned
78
+ * profile of the same name.
31
79
  *
32
80
  * Returns whether the on-disk config changed.
33
81
  */
@@ -49,44 +97,52 @@ export function reconcileFlagGatedProfiles(): boolean {
49
97
  : [];
50
98
  llm.profileOrder = profileOrder;
51
99
 
52
- // The resolver reads flag state from the gateway-populated override cache and
53
- // ignores the config argument; pass the read-only config for signature parity
54
- // without mutating disk before the reconcile decision is made.
55
- const enabled = isAssistantFeatureFlagEnabled(
56
- OS_BETA_FEATURE_FLAG_KEY,
57
- getConfigReadOnly(),
58
- );
59
-
60
100
  const isPlatform =
61
101
  process.env.IS_PLATFORM === "true" || process.env.IS_PLATFORM === "1";
62
102
  const isByokMode = !isPlatform;
63
103
 
64
- const previous = readObject(profiles[OS_BETA_PROFILE_KEY]);
104
+ let changed = false;
105
+ for (const gated of GATED_PROFILES) {
106
+ // The resolver reads flag state from the gateway-populated override cache
107
+ // and ignores the config argument; pass the read-only config for signature
108
+ // parity without mutating disk before the reconcile decision is made.
109
+ const enabled = isAssistantFeatureFlagEnabled(
110
+ gated.flag,
111
+ getConfigReadOnly(),
112
+ );
113
+
114
+ const previous = readObject(profiles[gated.key]);
65
115
 
66
- // Never clobber a user-owned profile that happens to be named `os-beta`. The
67
- // entry is ours to manage only when it is absent or already managed; a
68
- // user-sourced entry of the same name is left untouched on every path.
69
- const isOursToManage = previous == null || previous.source === "managed";
70
- if (!isOursToManage) {
71
- return false;
72
- }
116
+ // Never clobber a user-owned profile that happens to share the name. The
117
+ // entry is ours to manage only when it is absent or already managed; a
118
+ // user-sourced entry of the same name is left untouched on every path.
119
+ const isOursToManage = previous == null || previous.source === "managed";
120
+ if (!isOursToManage) {
121
+ continue;
122
+ }
73
123
 
74
- const changed = enabled
75
- ? enableProfile(profiles, profileOrder, previous, isByokMode)
76
- : disableProfile(llm, profiles, profileOrder, previous);
124
+ const order = llm.profileOrder as string[];
125
+ const profileChanged = enabled
126
+ ? enableProfile(gated, profiles, order, previous, isByokMode)
127
+ : disableProfile(gated.key, llm, profiles, order, previous);
128
+ if (profileChanged) {
129
+ changed = true;
130
+ log.info(
131
+ { profile: gated.key, enabled },
132
+ "Reconciled flag-gated profile",
133
+ );
134
+ }
135
+ }
77
136
 
78
137
  if (changed) {
79
138
  saveRawConfig(config);
80
139
  invalidateConfigCache();
81
- log.info(
82
- { profile: OS_BETA_PROFILE_KEY, enabled },
83
- "Reconciled flag-gated profile",
84
- );
85
140
  }
86
141
  return changed;
87
142
  }
88
143
 
89
144
  function enableProfile(
145
+ gated: GatedProfile,
90
146
  profiles: Record<string, Record<string, unknown>>,
91
147
  profileOrder: string[],
92
148
  previous: Record<string, unknown> | null,
@@ -100,13 +156,13 @@ function enableProfile(
100
156
 
101
157
  // BYOK installs create the stub disabled: the managed inference connection
102
158
  // backing this profile isn't usable until the user enables it, so a fresh
103
- // OS Beta entry starts disabled to avoid offering an unusable route. The
159
+ // entry starts disabled to avoid offering an unusable route. The
104
160
  // " (Managed)" label suffix disambiguates it from personal profiles in
105
161
  // pickers. A user's own overrides (preserved below) win on later
106
162
  // reconciles.
107
- if (isByokMode && !previous) {
163
+ if (gated.byokStartsDisabled && isByokMode && !previous) {
108
164
  next.status = "disabled";
109
- next.label = `${OS_BETA_PROFILE_TEMPLATE.label} (Managed)`;
165
+ next.label = `${gated.template.label} (Managed)`;
110
166
  }
111
167
 
112
168
  if (previous) {
@@ -124,17 +180,12 @@ function enableProfile(
124
180
 
125
181
  let changed = false;
126
182
  if (!previous || !isDeepStrictEqual(previous, next)) {
127
- profiles[OS_BETA_PROFILE_KEY] = next as ProfileEntry;
183
+ profiles[gated.key] = next as ProfileEntry;
128
184
  changed = true;
129
185
  }
130
186
 
131
- if (!profileOrder.includes(OS_BETA_PROFILE_KEY)) {
132
- const balancedIndex = profileOrder.indexOf("balanced");
133
- if (balancedIndex >= 0) {
134
- profileOrder.splice(balancedIndex + 1, 0, OS_BETA_PROFILE_KEY);
135
- } else {
136
- profileOrder.push(OS_BETA_PROFILE_KEY);
137
- }
187
+ if (!profileOrder.includes(gated.key)) {
188
+ gated.place(profileOrder);
138
189
  changed = true;
139
190
  }
140
191
 
@@ -146,6 +197,7 @@ function enableProfile(
146
197
  const MIX_MIN_ARMS = 2;
147
198
 
148
199
  function disableProfile(
200
+ key: string,
149
201
  llm: Record<string, unknown>,
150
202
  profiles: Record<string, Record<string, unknown>>,
151
203
  profileOrder: string[],
@@ -155,13 +207,13 @@ function disableProfile(
155
207
  return false;
156
208
  }
157
209
 
158
- delete profiles[OS_BETA_PROFILE_KEY];
210
+ delete profiles[key];
159
211
 
160
212
  // The removal closure: every name here is absent from `profiles` once the
161
213
  // closure settles, so the written config can never reference one. A mix that
162
214
  // loses arms below the >= 2 minimum is itself invalid, so it joins the set
163
215
  // and the loop runs to a fixpoint to resolve any references that cascade.
164
- const removed = new Set<string>([OS_BETA_PROFILE_KEY]);
216
+ const removed = new Set<string>([key]);
165
217
  let cascading = true;
166
218
  while (cascading) {
167
219
  cascading = false;