@vellumai/assistant 0.12.3 → 0.12.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/openapi.yaml +9 -3
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/calls/__tests__/typesafe-noul.test.ts +66 -0
- package/src/calls/typesafe-noul.ts +8 -2
- package/src/calls/voice-escalation-judge.ts +5 -4
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/config/__tests__/default-profile-catalog.test.ts +33 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/call-site-defaults.ts +11 -11
- package/src/config/default-profile-catalog.ts +88 -18
- package/src/config/default-profile-names.ts +35 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +1 -1
- package/src/config/schemas/llm.ts +54 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +157 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +23 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +100 -0
- package/src/util/worker-pipe.ts +44 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -15,12 +15,14 @@ import {
|
|
|
15
15
|
type DefaultProfileKey,
|
|
16
16
|
type DefaultProfileProvider,
|
|
17
17
|
FALLBACK_PROFILE_BY_KEY,
|
|
18
|
-
isBackupProfileKey,
|
|
19
18
|
isDefaultProfileKey,
|
|
20
19
|
isDefaultProfileProvider,
|
|
20
|
+
isManagedOnlyProfileKey,
|
|
21
|
+
JEV_MANAGED_PROFILE_KEY,
|
|
21
22
|
OS_BETA_PROFILE_KEY,
|
|
22
23
|
} from "./default-profile-names.js";
|
|
23
24
|
import { resolveDefaultConnectionName } from "./default-provider-resolution.js";
|
|
25
|
+
import { profileSupportsTextGeneration } from "./profile-text-generation.js";
|
|
24
26
|
import {
|
|
25
27
|
backupProfilesResolveUnderDefaultProvider,
|
|
26
28
|
DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS,
|
|
@@ -385,18 +387,39 @@ export const PROFILE_IMPLS: Record<
|
|
|
385
387
|
Record<DefaultProfileProvider, DefaultProfileTemplate>
|
|
386
388
|
>;
|
|
387
389
|
|
|
390
|
+
/**
|
|
391
|
+
* The managed Jev profile: TypeSafe's System One decision model through the
|
|
392
|
+
* platform proxy. Managed column only, because the platform is the only
|
|
393
|
+
* route the catalog offers for it here, and never a conversation profile,
|
|
394
|
+
* because `jev-latest` returns structured answers rather than chat text
|
|
395
|
+
* (`supportsText: false` on the `typesafe` catalog entry). Its budget is
|
|
396
|
+
* TypeSafe's published request limit rather than the default context window.
|
|
397
|
+
*/
|
|
398
|
+
export const JEV_MANAGED_PROFILE_TEMPLATE: DefaultProfileTemplate = {
|
|
399
|
+
model: "jev-latest",
|
|
400
|
+
provider: "vellum",
|
|
401
|
+
source: "managed",
|
|
402
|
+
label: "Jev",
|
|
403
|
+
description:
|
|
404
|
+
"TypeSafe decision model for yes/no verdicts, for the voice judges and memory selection",
|
|
405
|
+
maxTokens: 4096,
|
|
406
|
+
thinking: { enabled: false, streamThinking: false },
|
|
407
|
+
contextWindow: { maxInputTokens: 32_000 },
|
|
408
|
+
};
|
|
409
|
+
|
|
388
410
|
/**
|
|
389
411
|
* Managed profiles, i.e. the `vellum` column keyed by profile name, plus the
|
|
390
|
-
* managed backup profiles. Backups come after the
|
|
391
|
-
* places them after the primaries in the seeded
|
|
392
|
-
* inserts missing managed keys in this record's
|
|
393
|
-
* user-facing defaults only: an internal profile is
|
|
394
|
-
* listed or ordered.
|
|
412
|
+
* managed backup profiles and the managed Jev profile. Backups come after the
|
|
413
|
+
* primaries, which is what places them after the primaries in the seeded
|
|
414
|
+
* `profileOrder`: the seeder inserts missing managed keys in this record's
|
|
415
|
+
* order. Keyed by the user-facing defaults only: an internal profile is
|
|
416
|
+
* code-resolved and never listed or ordered.
|
|
395
417
|
*/
|
|
396
418
|
export const MANAGED_PROFILE_TEMPLATES: Record<string, DefaultProfileTemplate> =
|
|
397
419
|
Object.fromEntries([
|
|
398
420
|
...DEFAULT_PROFILE_KEYS.map((key) => [key, PROFILE_IMPLS[key].vellum]),
|
|
399
421
|
...BACKUP_PROFILE_KEYS.map((key) => [key, BACKUP_PROFILE_IMPLS[key]]),
|
|
422
|
+
[JEV_MANAGED_PROFILE_KEY, JEV_MANAGED_PROFILE_TEMPLATE],
|
|
400
423
|
]);
|
|
401
424
|
|
|
402
425
|
/**
|
|
@@ -484,6 +507,9 @@ export const OS_BETA_PROFILE_TEMPLATE: DefaultProfileTemplate = {
|
|
|
484
507
|
export const CODE_OWNED_PROFILE_NAMES = new Set<string>([
|
|
485
508
|
"latency-optimized",
|
|
486
509
|
...BACKUP_PROFILE_KEYS,
|
|
510
|
+
// A shadow could repoint the judges at a chat model, which answers a
|
|
511
|
+
// yes/no question with prose the verdict parser cannot read.
|
|
512
|
+
JEV_MANAGED_PROFILE_KEY,
|
|
487
513
|
]);
|
|
488
514
|
|
|
489
515
|
// All managed profiles, including the flag-gated os-beta, are invariant:
|
|
@@ -494,6 +520,7 @@ export const CODE_OWNED_PROFILE_NAMES = new Set<string>([
|
|
|
494
520
|
export const INVARIANT_PROFILE_NAMES = new Set<string>([
|
|
495
521
|
...DEFAULT_PROFILE_KEYS,
|
|
496
522
|
...BACKUP_PROFILE_KEYS,
|
|
523
|
+
JEV_MANAGED_PROFILE_KEY,
|
|
497
524
|
OS_BETA_PROFILE_KEY,
|
|
498
525
|
]);
|
|
499
526
|
|
|
@@ -507,6 +534,7 @@ export const INVARIANT_PROFILE_NAMES = new Set<string>([
|
|
|
507
534
|
export const MANAGED_PROFILE_NAMES = new Set<string>([
|
|
508
535
|
...DEFAULT_PROFILE_KEYS,
|
|
509
536
|
...BACKUP_PROFILE_KEYS,
|
|
537
|
+
JEV_MANAGED_PROFILE_KEY,
|
|
510
538
|
OS_BETA_PROFILE_KEY,
|
|
511
539
|
]);
|
|
512
540
|
|
|
@@ -590,6 +618,19 @@ for (const key of BACKUP_PROFILE_KEYS) {
|
|
|
590
618
|
}
|
|
591
619
|
}
|
|
592
620
|
|
|
621
|
+
// The managed Jev profile pins a model the platform proxy must front, and the
|
|
622
|
+
// judges depend on it being the non-text TypeSafe model rather than a chat
|
|
623
|
+
// model that happens to share the id.
|
|
624
|
+
if (
|
|
625
|
+
getManagedUpstream(JEV_MANAGED_PROFILE_TEMPLATE.model ?? "") !== "typesafe"
|
|
626
|
+
) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`JEV_MANAGED_PROFILE_TEMPLATE references model "${JEV_MANAGED_PROFILE_TEMPLATE.model ?? ""}" ` +
|
|
629
|
+
`which the managed route does not serve through typesafe. ` +
|
|
630
|
+
`Update model-catalog.ts, platform-proxy/constants.ts, or default-profile-catalog.ts.`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
593
634
|
// Provider choices without a named column materialize from the shared BYOK
|
|
594
635
|
// templates; verify each one's resolved model lands in the catalog.
|
|
595
636
|
for (const provider of DEFAULT_PROVIDER_CHOICES) {
|
|
@@ -621,6 +662,10 @@ function buildDefaultProfileEntries(): Record<string, ProfileEntry> {
|
|
|
621
662
|
const impl = BACKUP_PROFILE_IMPLS[key];
|
|
622
663
|
entries[key] = materializeProfile(impl, impl.provider);
|
|
623
664
|
}
|
|
665
|
+
entries[JEV_MANAGED_PROFILE_KEY] = materializeProfile(
|
|
666
|
+
JEV_MANAGED_PROFILE_TEMPLATE,
|
|
667
|
+
JEV_MANAGED_PROFILE_TEMPLATE.provider,
|
|
668
|
+
);
|
|
624
669
|
entries[OS_BETA_PROFILE_KEY] = materializeProfile(
|
|
625
670
|
OS_BETA_PROFILE_TEMPLATE,
|
|
626
671
|
OS_BETA_PROFILE_TEMPLATE.provider,
|
|
@@ -697,12 +742,13 @@ function resolveAgainstBody(
|
|
|
697
742
|
if (body == null) {
|
|
698
743
|
// A managed stub is the workspace's slot for a code-owned profile, never
|
|
699
744
|
// a profile of its own, so with no body behind it there is nothing to
|
|
700
|
-
// resolve. Only a
|
|
701
|
-
// default keys always have a body), and it
|
|
702
|
-
//
|
|
703
|
-
// profile with no provider or model and let a
|
|
704
|
-
// valid, which is precisely what `LLMSchema` rejects
|
|
705
|
-
|
|
745
|
+
// resolve. Only a managed-only name (a backup, the Jev profile) reaches
|
|
746
|
+
// this branch with a stub (the default keys always have a body), and it
|
|
747
|
+
// reaches it on the columns where those profiles do not exist. Resolving
|
|
748
|
+
// to the stub would list a profile with no provider or model and let a
|
|
749
|
+
// reference to it look valid, which is precisely what `LLMSchema` rejects
|
|
750
|
+
// on those columns.
|
|
751
|
+
if (isManagedOnlyProfileKey(name) && workspace?.source === "managed") {
|
|
706
752
|
return undefined;
|
|
707
753
|
}
|
|
708
754
|
return workspace;
|
|
@@ -787,14 +833,14 @@ function defaultProfileBodyForProvider(
|
|
|
787
833
|
defaultProvider: DefaultProviderConfig | null,
|
|
788
834
|
): ProfileEntry | undefined {
|
|
789
835
|
if (!isDefaultProfileKey(name)) {
|
|
790
|
-
// Backup profiles
|
|
791
|
-
// under a BYOK or chatgpt default provider the
|
|
792
|
-
// `fallbackProfile` pointers, and
|
|
793
|
-
//
|
|
836
|
+
// Backup profiles and the managed Jev profile exist on the managed
|
|
837
|
+
// (`vellum`) column only: under a BYOK or chatgpt default provider the
|
|
838
|
+
// primaries carry no `fallbackProfile` pointers, and neither may
|
|
839
|
+
// materialize, since the install may hold no credential for their
|
|
794
840
|
// provider. A null defaultProvider predates `llm.defaultProvider` and
|
|
795
|
-
// resolves to the managed column, so it keeps
|
|
841
|
+
// resolves to the managed column, so it keeps them.
|
|
796
842
|
if (
|
|
797
|
-
|
|
843
|
+
isManagedOnlyProfileKey(name) &&
|
|
798
844
|
!backupProfilesResolveUnderDefaultProvider(defaultProvider)
|
|
799
845
|
) {
|
|
800
846
|
return undefined;
|
|
@@ -923,3 +969,27 @@ export function getUserSelectableProfilesForProvider(
|
|
|
923
969
|
}
|
|
924
970
|
return selectable;
|
|
925
971
|
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* The profiles a conversation can run on: the user-selectable view minus any
|
|
975
|
+
* profile whose model returns structured answers rather than chat text (the
|
|
976
|
+
* managed Jev profile). Every surface that picks or validates a conversation
|
|
977
|
+
* profile (the `/model` command, a per-conversation pin, the plugin and
|
|
978
|
+
* workflow listings) reads this view; the call-site override pickers keep
|
|
979
|
+
* the wider selectable view, since the verdict sites are what Jev is for.
|
|
980
|
+
*/
|
|
981
|
+
export function getConversationProfilesForProvider(
|
|
982
|
+
workspaceProfiles: Record<string, ProfileEntry> | undefined,
|
|
983
|
+
defaultProvider: DefaultProviderConfig | null,
|
|
984
|
+
): Record<string, ProfileEntry> {
|
|
985
|
+
const selectable = getUserSelectableProfilesForProvider(
|
|
986
|
+
workspaceProfiles,
|
|
987
|
+
defaultProvider,
|
|
988
|
+
);
|
|
989
|
+
for (const [name, entry] of Object.entries(selectable)) {
|
|
990
|
+
if (!profileSupportsTextGeneration(entry, selectable)) {
|
|
991
|
+
delete selectable[name];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return selectable;
|
|
995
|
+
}
|
|
@@ -67,6 +67,41 @@ 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
|
+
* The call sites that consume a structured verdict and so may be pinned to
|
|
82
|
+
* the Jev profile. Every other site expects chat text or a tool call, which
|
|
83
|
+
* the decision model never produces.
|
|
84
|
+
*/
|
|
85
|
+
export const JEV_MANAGED_PROFILE_CALL_SITES = [
|
|
86
|
+
"voiceEscalationJudge",
|
|
87
|
+
"voiceContinuationJudge",
|
|
88
|
+
"memoryV3SelectL2",
|
|
89
|
+
] as const;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Keys whose profile exists on the managed (`vellum`) column only: the
|
|
93
|
+
* backups and the Jev profile. Under a BYOK or ChatGPT default provider
|
|
94
|
+
* these names have no body to resolve to.
|
|
95
|
+
*/
|
|
96
|
+
export const MANAGED_ONLY_PROFILE_KEYS = [
|
|
97
|
+
...BACKUP_PROFILE_KEYS,
|
|
98
|
+
JEV_MANAGED_PROFILE_KEY,
|
|
99
|
+
] as const;
|
|
100
|
+
|
|
101
|
+
export function isManagedOnlyProfileKey(value: string): boolean {
|
|
102
|
+
return (MANAGED_ONLY_PROFILE_KEYS as readonly string[]).includes(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
70
105
|
/**
|
|
71
106
|
* Flag-gated default profile: only available while the `os-beta` feature
|
|
72
107
|
* flag has reconciled it into the workspace (see `sync-gated-profiles.ts`).
|
|
@@ -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",
|
|
@@ -559,6 +551,14 @@
|
|
|
559
551
|
"label": "Assistant Inbox",
|
|
560
552
|
"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.",
|
|
561
553
|
"defaultEnabled": false
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"id": "sidebar-done",
|
|
557
|
+
"scope": "both",
|
|
558
|
+
"key": "sidebar-done",
|
|
559
|
+
"label": "Sidebar Done",
|
|
560
|
+
"description": "Turns archiving a chat into marking it done: the sidebar row carries a Done check instead of the overflow menu, an Old 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
|
+
"defaultEnabled": false
|
|
562
562
|
}
|
|
563
563
|
]
|
|
564
564
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
10
|
-
*
|
|
11
|
-
* routes remain available to automatic
|
|
12
|
-
*
|
|
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 =
|
|
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.
|
|
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: {
|
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
parseVellumModel,
|
|
15
15
|
} from "../../providers/vellum-model-routing.js";
|
|
16
16
|
import {
|
|
17
|
-
BACKUP_PROFILE_KEYS,
|
|
18
17
|
DEFAULT_PROFILE_KEYS,
|
|
19
18
|
DEFAULT_PROFILE_PROVIDERS,
|
|
20
19
|
FALLBACK_PROFILE_BY_KEY,
|
|
21
|
-
isBackupProfileKey,
|
|
22
20
|
isDefaultProfileKey,
|
|
21
|
+
isManagedOnlyProfileKey,
|
|
22
|
+
JEV_MANAGED_PROFILE_CALL_SITES,
|
|
23
|
+
JEV_MANAGED_PROFILE_KEY,
|
|
24
|
+
MANAGED_ONLY_PROFILE_KEYS,
|
|
23
25
|
} from "../default-profile-names.js";
|
|
24
26
|
import { InputModalitiesSchema } from "../input-modalities.js";
|
|
25
27
|
|
|
@@ -766,10 +768,12 @@ function unresolvableProfileReason(
|
|
|
766
768
|
name: string,
|
|
767
769
|
backupsResolve: boolean,
|
|
768
770
|
): string {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
771
|
+
if (backupsResolve || !isManagedOnlyProfileKey(name)) {
|
|
772
|
+
return "is not defined in llm.profiles";
|
|
773
|
+
}
|
|
774
|
+
return name === JEV_MANAGED_PROFILE_KEY
|
|
775
|
+
? "is the managed Jev profile, which resolves only while llm.defaultProvider is the managed provider"
|
|
776
|
+
: "is a managed backup profile, which resolves only while llm.defaultProvider is the managed provider";
|
|
773
777
|
}
|
|
774
778
|
|
|
775
779
|
/**
|
|
@@ -800,7 +804,7 @@ function referenceableProfileKeys(
|
|
|
800
804
|
): string[] {
|
|
801
805
|
return Object.entries(profiles ?? {})
|
|
802
806
|
.filter(([name, value]) => {
|
|
803
|
-
if (backupsResolve || !
|
|
807
|
+
if (backupsResolve || !isManagedOnlyProfileKey(name)) {
|
|
804
808
|
return true;
|
|
805
809
|
}
|
|
806
810
|
const entry =
|
|
@@ -854,7 +858,7 @@ export function collectFallbackProfileIssues(
|
|
|
854
858
|
const profileNames = new Set([
|
|
855
859
|
...referenceableProfileKeys(profiles, backupsResolve),
|
|
856
860
|
...DEFAULT_PROFILE_KEYS,
|
|
857
|
-
...(backupsResolve ?
|
|
861
|
+
...(backupsResolve ? MANAGED_ONLY_PROFILE_KEYS : []),
|
|
858
862
|
]);
|
|
859
863
|
const mixProfileNames = new Set(
|
|
860
864
|
entries
|
|
@@ -1004,6 +1008,8 @@ export const LLMSchema = z
|
|
|
1004
1008
|
// they join the set only under a managed `llm.defaultProvider`: on a BYOK
|
|
1005
1009
|
// or ChatGPT default provider they have no body to resolve to, and
|
|
1006
1010
|
// keeping the reference would strand a selection the picker cannot show.
|
|
1011
|
+
// The managed Jev profile (`JEV_MANAGED_PROFILE_KEY`) is scoped the same
|
|
1012
|
+
// way, as a pin target for the judge call sites.
|
|
1007
1013
|
// The flag-gated `os-beta` is excluded: it resolves only while a
|
|
1008
1014
|
// workspace entry exists, so a reference to it is valid only when that
|
|
1009
1015
|
// entry is present in `config.profiles`. A backup name materialized as a
|
|
@@ -1018,9 +1024,23 @@ export const LLMSchema = z
|
|
|
1018
1024
|
backupsResolve,
|
|
1019
1025
|
),
|
|
1020
1026
|
...DEFAULT_PROFILE_KEYS,
|
|
1021
|
-
...(backupsResolve ?
|
|
1027
|
+
...(backupsResolve ? MANAGED_ONLY_PROFILE_KEYS : []),
|
|
1022
1028
|
]);
|
|
1023
1029
|
for (const [siteId, siteConfig] of Object.entries(config.callSites ?? {})) {
|
|
1030
|
+
// The Jev profile answers with structured verdicts, so only the sites
|
|
1031
|
+
// built to read one may pin it; a text-producing site would receive
|
|
1032
|
+
// output it cannot use. (Mix arms are covered by the profile-level
|
|
1033
|
+
// mix validation below, which bars the profile from any mix.)
|
|
1034
|
+
if (
|
|
1035
|
+
siteConfig?.profile === JEV_MANAGED_PROFILE_KEY &&
|
|
1036
|
+
!(JEV_MANAGED_PROFILE_CALL_SITES as readonly string[]).includes(siteId)
|
|
1037
|
+
) {
|
|
1038
|
+
ctx.addIssue({
|
|
1039
|
+
code: "custom",
|
|
1040
|
+
path: ["callSites", siteId, "profile"],
|
|
1041
|
+
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(", ")}`,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1024
1044
|
if (siteConfig?.profile == null) {
|
|
1025
1045
|
continue;
|
|
1026
1046
|
}
|
|
@@ -1052,6 +1072,20 @@ export const LLMSchema = z
|
|
|
1052
1072
|
message: `Profile "${config.advisorProfile}" referenced by llm.advisorProfile ${unresolvableProfileReason(config.advisorProfile, backupsResolve)}`,
|
|
1053
1073
|
});
|
|
1054
1074
|
}
|
|
1075
|
+
// The managed Jev profile is a valid reference target for call-site pins
|
|
1076
|
+
// only. Its model returns structured verdicts rather than chat text, so
|
|
1077
|
+
// the conversation positions reject it here as well as at the write
|
|
1078
|
+
// routes, which keeps a raw config write from handing the main agent or
|
|
1079
|
+
// the advisor a model that cannot answer in prose.
|
|
1080
|
+
for (const field of ["activeProfile", "advisorProfile"] as const) {
|
|
1081
|
+
if (config[field] === JEV_MANAGED_PROFILE_KEY) {
|
|
1082
|
+
ctx.addIssue({
|
|
1083
|
+
code: "custom",
|
|
1084
|
+
path: [field],
|
|
1085
|
+
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`,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1055
1089
|
|
|
1056
1090
|
// --- Mix profile validation --------------------------------------------
|
|
1057
1091
|
// Config keys a mix profile must NOT also set (a mix only references other
|
|
@@ -1105,6 +1139,17 @@ export const LLMSchema = z
|
|
|
1105
1139
|
});
|
|
1106
1140
|
continue;
|
|
1107
1141
|
}
|
|
1142
|
+
// A mix is a conversation profile, so an arm that returns structured
|
|
1143
|
+
// verdicts rather than text would hand a chat turn to the decision
|
|
1144
|
+
// model whenever the seeded pick lands on it.
|
|
1145
|
+
if (arm.profile === JEV_MANAGED_PROFILE_KEY) {
|
|
1146
|
+
ctx.addIssue({
|
|
1147
|
+
code: "custom",
|
|
1148
|
+
path: ["profiles", name, "mix", index, "profile"],
|
|
1149
|
+
message: `Mix profile "${name}" references "${JEV_MANAGED_PROFILE_KEY}", which returns structured answers rather than chat text and cannot be a mix constituent.`,
|
|
1150
|
+
});
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1108
1153
|
// (b) No nesting — a mix arm must be a standard (non-mix) profile.
|
|
1109
1154
|
if (mixProfileNames.has(arm.profile)) {
|
|
1110
1155
|
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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InterfaceId } from "../channels/types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getConversationProfilesForProvider } from "../config/default-profile-catalog.js";
|
|
3
3
|
import { resolveEffectiveContextWindow } from "../config/llm-context-resolution.js";
|
|
4
4
|
import { resolveCallSiteConfig } from "../config/llm-resolver.js";
|
|
5
5
|
import {
|
|
@@ -148,7 +148,7 @@ async function resolveModelCommand(
|
|
|
148
148
|
parse: ModelCommandParse,
|
|
149
149
|
): Promise<SlashResolution> {
|
|
150
150
|
const config = getConfig();
|
|
151
|
-
const profiles =
|
|
151
|
+
const profiles = getConversationProfilesForProvider(
|
|
152
152
|
config.llm.profiles,
|
|
153
153
|
config.llm.defaultProvider ?? null,
|
|
154
154
|
);
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
getConversation,
|
|
41
41
|
getMessageById,
|
|
42
42
|
parseMessageMetadata,
|
|
43
|
+
resurfaceArchivedConversation,
|
|
43
44
|
} from "../persistence/conversation-crud.js";
|
|
44
45
|
import { getResolvedConversationDirPath } from "../persistence/conversation-directories.js";
|
|
45
46
|
import { syncMessageToDisk } from "../persistence/conversation-disk-view.js";
|
|
@@ -150,6 +151,17 @@ export function buildDeferredFinalizeEffect(params: {
|
|
|
150
151
|
"Failed to project assistant message for attention tracking (non-fatal)",
|
|
151
152
|
);
|
|
152
153
|
}
|
|
154
|
+
// The row this turn reserved was empty at insert, so this is where its
|
|
155
|
+
// content becomes readable and a Done conversation earns its way back to
|
|
156
|
+
// the list. A run that never got this far produced nothing to read.
|
|
157
|
+
try {
|
|
158
|
+
resurfaceArchivedConversation(conversationId, finalizedRow.createdAt);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
rlog.warn(
|
|
161
|
+
{ err, conversationId, messageId: assistantMessageId },
|
|
162
|
+
"Failed to resurface Done conversation after finalize (non-fatal)",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
153
165
|
};
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure text-processing functions for dictation profiles.
|
|
3
3
|
*
|
|
4
|
-
* - expandSnippets:
|
|
5
|
-
* - applyDictionary:
|
|
4
|
+
* - expandSnippets: dictation mode only
|
|
5
|
+
* - applyDictionary: dictation and selected-text edits (including fallback paths)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
@@ -33,7 +33,7 @@ function wrapWordBoundary(
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Expand snippet triggers in text
|
|
36
|
+
* Expand user-configured snippet triggers in dictated text.
|
|
37
37
|
*
|
|
38
38
|
* - Only enabled snippets are considered
|
|
39
39
|
* - Sorted by trigger length descending (longest match wins)
|
|
@@ -80,7 +80,7 @@ export function expandSnippets(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Apply dictionary
|
|
83
|
+
* Apply user-configured dictionary replacements to dictated or edited text.
|
|
84
84
|
*
|
|
85
85
|
* - Sorted by spoken length descending
|
|
86
86
|
* - Respects wholeWord (default true) and caseSensitive (default false) per entry
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `includeArchived` option on the agent-facing conversation
|
|
3
|
+
* history search.
|
|
4
|
+
*
|
|
5
|
+
* The `"*"` wildcard takes its own branch (FTS reads `*` as a literal), so it
|
|
6
|
+
* has to honor the option through the list read rather than through
|
|
7
|
+
* `searchConversations`, which has its own coverage in
|
|
8
|
+
* `persistence/__tests__/conversation-queries-search.test.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import { createConversation } from "../../../persistence/conversation-crud.js";
|
|
14
|
+
import { getDb } from "../../../persistence/db-connection.js";
|
|
15
|
+
import { initializeDb } from "../../../persistence/db-init.js";
|
|
16
|
+
import { rawRun } from "../../../persistence/raw-query.js";
|
|
17
|
+
import { conversations } from "../../../persistence/schema/index.js";
|
|
18
|
+
import { performConversationSearch } from "../conversation-history.js";
|
|
19
|
+
|
|
20
|
+
await initializeDb();
|
|
21
|
+
|
|
22
|
+
function seedArchived(title: string): string {
|
|
23
|
+
const conv = createConversation({ title });
|
|
24
|
+
rawRun(
|
|
25
|
+
"test:archiveConversation",
|
|
26
|
+
"UPDATE conversations SET archived_at = ? WHERE id = ?",
|
|
27
|
+
Date.now(),
|
|
28
|
+
conv.id,
|
|
29
|
+
);
|
|
30
|
+
return conv.id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("performConversationSearch · wildcard listing", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
getDb().delete(conversations).run();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("omits archived conversations by default", async () => {
|
|
39
|
+
createConversation("live-1");
|
|
40
|
+
seedArchived("archived-1");
|
|
41
|
+
|
|
42
|
+
const results = await performConversationSearch({ query: "*" });
|
|
43
|
+
|
|
44
|
+
expect(results.map((r) => r.conversationTitle)).toEqual(["live-1"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("includeArchived reaches archived conversations", async () => {
|
|
48
|
+
createConversation("live-1");
|
|
49
|
+
seedArchived("archived-1");
|
|
50
|
+
|
|
51
|
+
const results = await performConversationSearch({
|
|
52
|
+
query: "*",
|
|
53
|
+
includeArchived: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(results.map((r) => r.conversationTitle).sort()).toEqual([
|
|
57
|
+
"archived-1",
|
|
58
|
+
"live-1",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -156,7 +156,9 @@ export async function createInboundChallenge(
|
|
|
156
156
|
success: false,
|
|
157
157
|
error: "already_bound",
|
|
158
158
|
message:
|
|
159
|
-
|
|
159
|
+
resolvedChannel === "phone"
|
|
160
|
+
? "A guardian is already bound for this channel. On phone, verify through an outbound call with rebind: true. To a new number, it replaces the bound number once the code is entered. To the bound number, it confirms that number again and leaves the binding as it is. An inbound challenge cannot be completed by phone: a call collects a six-digit code, which an inbound challenge does not issue."
|
|
161
|
+
: "A guardian is already bound for this channel. To verify a different account, revoke the existing binding first. Set rebind: true only to verify the same account again.",
|
|
160
162
|
channel: resolvedChannel,
|
|
161
163
|
};
|
|
162
164
|
}
|