@vellumai/assistant 0.9.0 → 0.9.1-staging.1

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 (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -22,57 +22,6 @@ mock.module("../util/logger.js", () => ({
22
22
  }),
23
23
  }));
24
24
 
25
- const checkpointStore = new Map<string, string>();
26
-
27
- mock.module("../memory/checkpoints.js", () => ({
28
- getMemoryCheckpoint: (key: string) => checkpointStore.get(key) ?? null,
29
- setMemoryCheckpoint: (key: string, value: string) => {
30
- checkpointStore.set(key, value);
31
- },
32
- }));
33
-
34
- const getConfiguredProviderCalls: string[] = [];
35
- const mockProvider = { name: "mock-provider" };
36
-
37
- mock.module("../providers/provider-send-message.js", () => ({
38
- getConfiguredProvider: mock(async (callSite: string) => {
39
- getConfiguredProviderCalls.push(callSite);
40
- return mockProvider;
41
- }),
42
- }));
43
-
44
- type SidechainCall = {
45
- callSite?: string;
46
- content: string;
47
- maxTokens?: number;
48
- systemPrompt?: string;
49
- tools: unknown[];
50
- };
51
-
52
- type SidechainResult = {
53
- text: string;
54
- hadTextDeltas: false;
55
- response: { content: [] };
56
- };
57
-
58
- const sidechainCalls: SidechainCall[] = [];
59
- let sidechainText = "";
60
- let sidechainResultPromise: Promise<SidechainResult> | null = null;
61
-
62
- mock.module("../runtime/btw-sidechain.js", () => ({
63
- runBtwSidechain: mock(async (params: SidechainCall) => {
64
- sidechainCalls.push(params);
65
- if (sidechainResultPromise) {
66
- return sidechainResultPromise;
67
- }
68
- return {
69
- text: sidechainText,
70
- hadTextDeltas: false,
71
- response: { content: [] },
72
- };
73
- }),
74
- }));
75
-
76
25
  import {
77
26
  handleDetailedHealth,
78
27
  handleReadyz,
@@ -86,17 +35,6 @@ import {
86
35
  selectHatchedAtFromStats,
87
36
  } from "../workspace/hatched-date.js";
88
37
 
89
- function createDeferred<T>(): {
90
- promise: Promise<T>;
91
- resolve: (value: T) => void;
92
- } {
93
- let resolve!: (value: T) => void;
94
- const promise = new Promise<T>((res) => {
95
- resolve = res;
96
- });
97
- return { promise, resolve };
98
- }
99
-
100
38
  // ── Env helpers ─────────────────────────────────────────────────────────
101
39
 
102
40
  let savedEnv: Record<string, string | undefined>;
@@ -194,11 +132,6 @@ beforeEach(() => {
194
132
  rmSync(getHatchedSidecarPath(), { force: true });
195
133
  rmSync(join(getWorkspaceDir(), "IDENTITY.md"), { force: true });
196
134
  rmSync(join(getWorkspaceDir(), "SOUL.md"), { force: true });
197
- checkpointStore.clear();
198
- getConfiguredProviderCalls.length = 0;
199
- sidechainCalls.length = 0;
200
- sidechainText = "";
201
- sidechainResultPromise = null;
202
135
  });
203
136
 
204
137
  afterEach(() => {
@@ -564,125 +497,3 @@ describe("identity routes — createdAt selection", () => {
564
497
  expect(existsSync(getHatchedSidecarPath())).toBe(false);
565
498
  });
566
499
  });
567
-
568
- describe("identity routes — intro greetings", () => {
569
- test("returns fallback immediately, generates personalized greetings in the background, then reuses the cache", async () => {
570
- const workspaceDir = getWorkspaceDir();
571
- writeFileSync(
572
- join(workspaceDir, "IDENTITY.md"),
573
- [
574
- "# Identity",
575
- "",
576
- "- **Name:** Example Assistant",
577
- "- **Personality:** enjoys crisp, useful hellos",
578
- "",
579
- "Identity sentinel: chartreuse compass.",
580
- ].join("\n"),
581
- "utf-8",
582
- );
583
- writeFileSync(
584
- join(workspaceDir, "SOUL.md"),
585
- [
586
- "# Soul",
587
- "",
588
- "Soul sentinel: copper lighthouse.",
589
- "",
590
- "Keep greetings warm and specific.",
591
- ].join("\n"),
592
- "utf-8",
593
- );
594
- const deferredSidechain = createDeferred<SidechainResult>();
595
- sidechainResultPromise = deferredSidechain.promise;
596
-
597
- const route = ROUTES.find(
598
- (candidate) => candidate.operationId === "identity_intro",
599
- );
600
- expect(route).toBeDefined();
601
-
602
- const body = route!.handler({
603
- queryParams: { localHour: "8", localMinute: "15" },
604
- }) as {
605
- greetings: string[];
606
- text: string;
607
- source: string;
608
- refreshing: boolean;
609
- };
610
-
611
- expect(body).toEqual({
612
- greetings: [
613
- "What are we working on?",
614
- "I'm here whenever you need me.",
615
- "What's on your mind?",
616
- "Ready when you are.",
617
- ],
618
- text: "What are we working on?",
619
- source: "fallback",
620
- refreshing: true,
621
- });
622
- expect(getConfiguredProviderCalls).toEqual([]);
623
- expect(sidechainCalls).toEqual([]);
624
-
625
- await Promise.resolve();
626
- await Promise.resolve();
627
-
628
- expect(getConfiguredProviderCalls).toEqual(["emptyStateGreeting"]);
629
- expect(sidechainCalls).toHaveLength(1);
630
- expect(sidechainCalls[0]?.callSite).toBe("emptyStateGreeting");
631
- expect(sidechainCalls[0]?.tools).toEqual([]);
632
- expect(sidechainCalls[0]?.content).toContain("Generate 5 short");
633
- expect(sidechainCalls[0]?.content).not.toContain("Current time of day:");
634
- expect(sidechainCalls[0]?.content).toContain(
635
- "do not mention the current time",
636
- );
637
- expect(sidechainCalls[0]?.content).toMatch(
638
- /Current user-local time for subtle tone only: morning \(08:15\)\.$/,
639
- );
640
- expect(sidechainCalls[0]?.content).toContain("JSON array");
641
- expect(sidechainCalls[0]?.systemPrompt).toContain(
642
- "Identity sentinel: chartreuse compass.",
643
- );
644
- expect(sidechainCalls[0]?.systemPrompt).toContain(
645
- "Soul sentinel: copper lighthouse.",
646
- );
647
- deferredSidechain.resolve({
648
- text: JSON.stringify([
649
- "Charting the next useful thing?",
650
- "I brought the compass. Where to?",
651
- "Ready to make this lighter.",
652
- "Morning momentum?",
653
- "Five options, one good start.",
654
- "A useful next step?",
655
- ]),
656
- hadTextDeltas: false,
657
- response: { content: [] },
658
- });
659
-
660
- await sidechainResultPromise;
661
- await new Promise((resolve) => setTimeout(resolve, 0));
662
-
663
- sidechainCalls.length = 0;
664
- getConfiguredProviderCalls.length = 0;
665
-
666
- const cachedBody = (await route!.handler({})) as {
667
- greetings: string[];
668
- text: string;
669
- source: string;
670
- refreshing: boolean;
671
- };
672
-
673
- expect(cachedBody).toEqual({
674
- greetings: [
675
- "Charting the next useful thing?",
676
- "I brought the compass. Where to?",
677
- "Ready to make this lighter.",
678
- "Five options, one good start.",
679
- "A useful next step?",
680
- ],
681
- text: "Charting the next useful thing?",
682
- source: "cache",
683
- refreshing: false,
684
- });
685
- expect(getConfiguredProviderCalls).toEqual([]);
686
- expect(sidechainCalls).toEqual([]);
687
- });
688
- });
@@ -173,7 +173,7 @@ describe("inbound invite redemption intercept", () => {
173
173
  // Verify the user is now an active member
174
174
  const result = findContactChannel({
175
175
  channelType: "telegram",
176
- externalUserId: "user-invite-123",
176
+ address: "user-invite-123",
177
177
  });
178
178
  expect(result).not.toBeNull();
179
179
  expect(result!.channel.status).toBe("active");
@@ -203,7 +203,7 @@ describe("inbound invite redemption intercept", () => {
203
203
  // Verify the user was NOT made a member
204
204
  const result = findContactChannel({
205
205
  channelType: "telegram",
206
- externalUserId: "user-invite-123",
206
+ address: "user-invite-123",
207
207
  });
208
208
  expect(result).toBeNull();
209
209
  });
@@ -400,7 +400,7 @@ describe("inbound invite redemption intercept", () => {
400
400
  // its ID as the invite's contactId (satisfies the FK constraint).
401
401
  const existing = findContactChannel({
402
402
  channelType: "telegram",
403
- externalUserId: "user-invite-123",
403
+ address: "user-invite-123",
404
404
  externalChatId: "chat-invite-test",
405
405
  });
406
406
  const targetContactId = existing!.contact.id;
@@ -422,7 +422,7 @@ describe("inbound invite redemption intercept", () => {
422
422
 
423
423
  const result = findContactChannel({
424
424
  channelType: "telegram",
425
- externalUserId: "user-invite-123",
425
+ address: "user-invite-123",
426
426
  externalChatId: "chat-invite-test",
427
427
  });
428
428
  expect(result).not.toBeNull();
@@ -83,7 +83,7 @@ describe("invite-redemption-service", () => {
83
83
 
84
84
  const result = findContactChannel({
85
85
  channelType: "telegram",
86
- externalUserId: "user-1",
86
+ address: "user-1",
87
87
  });
88
88
 
89
89
  expect(result).not.toBeNull();
@@ -112,7 +112,7 @@ describe("invite-redemption-service", () => {
112
112
 
113
113
  const result = findContactChannel({
114
114
  channelType: "telegram",
115
- externalUserId: "code-user-1",
115
+ address: "code-user-1",
116
116
  });
117
117
 
118
118
  expect(result).not.toBeNull();
@@ -325,7 +325,7 @@ describe("invite-redemption-service", () => {
325
325
  // Verify the redeemer's Telegram ID is now bound to Mom's contact
326
326
  const result = findContactChannel({
327
327
  channelType: "telegram",
328
- externalUserId: "guardian-tg-id",
328
+ address: "guardian-tg-id",
329
329
  });
330
330
  expect(result).not.toBeNull();
331
331
  expect(result!.contact.id).toBe(momContact.id);
@@ -417,7 +417,7 @@ describe("invite-redemption-service", () => {
417
417
  // Verify the redeemer's Telegram ID is now bound to Mom's contact
418
418
  const result = findContactChannel({
419
419
  channelType: "telegram",
420
- externalUserId: "guardian-code-id",
420
+ address: "guardian-code-id",
421
421
  });
422
422
  expect(result).not.toBeNull();
423
423
  expect(result!.contact.id).toBe(momContact.id);
@@ -54,9 +54,7 @@ describe("LLM call-site catalog", () => {
54
54
 
55
55
  test("CALL_SITE_DEFAULTS covers every LLMCallSite enum value", () => {
56
56
  const defaultIds = new Set(Object.keys(CALL_SITE_DEFAULTS));
57
- const missing = LLMCallSiteEnum.options.filter(
58
- (id) => !defaultIds.has(id),
59
- );
57
+ const missing = LLMCallSiteEnum.options.filter((id) => !defaultIds.has(id));
60
58
  expect(missing).toEqual([]);
61
59
  });
62
60
 
@@ -68,11 +66,12 @@ describe("LLM call-site catalog", () => {
68
66
  expect(extra).toEqual([]);
69
67
  });
70
68
 
71
- test("every CALL_SITE_DEFAULTS entry has a profile field", () => {
69
+ test("every CALL_SITE_DEFAULTS profile, when present, is a non-empty string", () => {
72
70
  for (const [, config] of Object.entries(CALL_SITE_DEFAULTS)) {
73
- expect(config.profile).toBeDefined();
71
+ // A call site may omit `profile` to inherit the workspace default config.
72
+ if (config.profile === undefined) continue;
74
73
  expect(typeof config.profile).toBe("string");
75
- expect(config.profile!.length).toBeGreaterThan(0);
74
+ expect(config.profile.length).toBeGreaterThan(0);
76
75
  }
77
76
  });
78
77
  });
@@ -30,13 +30,6 @@ function getRepoRoot(): string {
30
30
  }
31
31
 
32
32
  const META_JSON_PATH = join(getRepoRoot(), "meta", "llm-provider-catalog.json");
33
- const SWIFTPM_MIRROR_PATH = join(
34
- getRepoRoot(),
35
- "clients",
36
- "shared",
37
- "Resources",
38
- "llm-provider-catalog.json",
39
- );
40
33
 
41
34
  interface ClientCatalogCredentialsGuide {
42
35
  description: string;
@@ -457,19 +450,4 @@ describe("LLM catalog parity: daemon vs client", () => {
457
450
  longContextMode: "native-model",
458
451
  });
459
452
  });
460
-
461
- // -----------------------------------------------------------------------
462
- // Mirror byte-equality
463
- // -----------------------------------------------------------------------
464
-
465
- test("SwiftPM mirror is byte-identical to meta/ copy", () => {
466
- // `sync-llm-catalog.ts` writes both files from the same serializer; this
467
- // guard catches any case where one copy is regenerated without the other.
468
- // Byte equality is required because SwiftPM bundles the resource verbatim
469
- // into `VellumAssistantShared` while the meta/ JSON is consumed as a
470
- // cross-package artifact (web codegen, etc.).
471
- const metaBytes = readFileSync(META_JSON_PATH);
472
- const swiftPmBytes = readFileSync(SWIFTPM_MIRROR_PATH);
473
- expect(swiftPmBytes.equals(metaBytes)).toBe(true);
474
- });
475
453
  });
@@ -670,30 +670,6 @@ describe("resolveCallSiteConfig", () => {
670
670
  expect(resolved.effort).toBe("medium");
671
671
  });
672
672
 
673
- test("workflowLeaf defaults to the cost-optimized profile", () => {
674
- const llm = LLMSchema.parse({
675
- default: fullDefault,
676
- profiles: {
677
- balanced: {
678
- model: "claude-sonnet-4-7",
679
- effort: "medium",
680
- },
681
- "cost-optimized": {
682
- model: "claude-haiku-4-5-20251001",
683
- effort: "low",
684
- },
685
- },
686
- });
687
-
688
- expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBe(
689
- "cost-optimized",
690
- );
691
- const resolved = resolveCallSiteConfig("workflowLeaf", llm);
692
- expect(resolved.model).toBe("claude-haiku-4-5-20251001");
693
- expect(resolved.effort).toBe("low");
694
- expect(resolved.thinking.enabled).toBe(false);
695
- });
696
-
697
673
  test("explicit callSites config overrides CALL_SITE_DEFAULTS", () => {
698
674
  const llm = LLMSchema.parse({
699
675
  default: fullDefault,
@@ -1437,3 +1413,52 @@ describe("resolveCallSiteConfig logitBias provenance", () => {
1437
1413
  expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBeUndefined();
1438
1414
  });
1439
1415
  });
1416
+
1417
+ describe("resolveCallSiteConfig — workflowLeaf default", () => {
1418
+ test("inherits the workspace default config rather than pinning cost-optimized", () => {
1419
+ const llm = LLMSchema.parse({
1420
+ default: fullDefault,
1421
+ profiles: {
1422
+ "cost-optimized": {
1423
+ provider: "anthropic",
1424
+ model: "claude-haiku-4-5-20251001",
1425
+ },
1426
+ },
1427
+ });
1428
+ const resolved = resolveCallSiteConfig("workflowLeaf", llm);
1429
+ // No pinned profile → the model comes from llm.default, NOT the
1430
+ // `cost-optimized` profile (which is uncredentialed on a BYOK install).
1431
+ expect(resolved.model).toBe("claude-opus-4-7");
1432
+ // Call-site tuning still applies.
1433
+ expect(resolved.effort).toBe("low");
1434
+ expect(resolved.thinking?.enabled).toBe(false);
1435
+ // The call site has no implicit default profile key.
1436
+ expect(resolveDefaultProfileKey("workflowLeaf", llm)).toBeUndefined();
1437
+ });
1438
+
1439
+ test("honors an explicit workflowLeaf call-site override", () => {
1440
+ const llm = LLMSchema.parse({
1441
+ default: fullDefault,
1442
+ profiles: {
1443
+ cheap: { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
1444
+ },
1445
+ callSites: { workflowLeaf: { profile: "cheap" } },
1446
+ });
1447
+ expect(resolveCallSiteConfig("workflowLeaf", llm).model).toBe(
1448
+ "claude-haiku-4-5-20251001",
1449
+ );
1450
+ });
1451
+
1452
+ test("honors a per-call override profile (an explicit per-leaf profile)", () => {
1453
+ const llm = LLMSchema.parse({
1454
+ default: fullDefault,
1455
+ profiles: {
1456
+ fancy: { provider: "anthropic", model: "claude-sonnet-4-7" },
1457
+ },
1458
+ });
1459
+ expect(
1460
+ resolveCallSiteConfig("workflowLeaf", llm, { overrideProfile: "fancy" })
1461
+ .model,
1462
+ ).toBe("claude-sonnet-4-7");
1463
+ });
1464
+ });
@@ -3,17 +3,15 @@ import { describe, expect, test } from "bun:test";
3
3
  import { PROVIDER_SEED_DATA } from "../oauth/seed-providers.js";
4
4
 
5
5
  /**
6
- * Allowed CDN prefixes for ``logoUrl``. Mirrors the source registry in
7
- * ``clients/shared/Resources/integration-logos-manifest.json``:
6
+ * Allowed CDN prefixes for the ``logoUrl`` field on ``PROVIDER_SEED_DATA``
7
+ * (``assistant/src/oauth/seed-providers.ts``):
8
8
  *
9
9
  * - Simple Icons (CC0) is the default for most providers.
10
10
  * - thesvg via jsDelivr is the documented fallback for brands Simple Icons
11
11
  * doesn't host (e.g. Salesforce, which Simple Icons removed for
12
- * trademark reasons). Same source is already used for the bundled PDFs
13
- * of figma/github/gmail/linear/notion/outlook/slack.
12
+ * trademark reasons).
14
13
  *
15
- * Adding another CDN should be a deliberate choice — extend this list
16
- * and update the manifest in tandem.
14
+ * Adding another CDN should be a deliberate choice — extend this list.
17
15
  */
18
16
  const ALLOWED_LOGO_URL_PREFIXES = [
19
17
  "https://cdn.simpleicons.org/",
@@ -59,7 +59,7 @@ mock.module("../util/platform.js", () => ({
59
59
  }));
60
60
 
61
61
  mock.module("../contacts/contact-store.js", () => ({
62
- findContactByChannelExternalId: () => null,
62
+ findContactByAddress: () => null,
63
63
  findGuardianForChannel: (channelType: string) =>
64
64
  channelType === "vellum" ? mockVellumGuardian : null,
65
65
  listGuardianChannels: () => null,
@@ -32,18 +32,14 @@ import {
32
32
  // ── Mock state ────────────────────────────────────────────────────
33
33
 
34
34
  let mockWorkspaceDir: string = "";
35
- let mockVellumGuardian:
36
- | {
37
- contact: { userFile: string | null };
38
- channel: Record<string, unknown>;
39
- }
40
- | null = null;
41
- let mockAnyGuardian:
42
- | {
43
- contact: { userFile: string | null };
44
- channels: Record<string, unknown>[];
45
- }
46
- | null = null;
35
+ let mockVellumGuardian: {
36
+ contact: { userFile: string | null };
37
+ channel: Record<string, unknown>;
38
+ } | null = null;
39
+ let mockAnyGuardian: {
40
+ contact: { userFile: string | null };
41
+ channels: Record<string, unknown>[];
42
+ } | null = null;
47
43
 
48
44
  // ── Mock modules (must precede imports from the module under test) ──
49
45
 
@@ -52,7 +48,7 @@ mock.module("../util/platform.js", () => ({
52
48
  }));
53
49
 
54
50
  mock.module("../contacts/contact-store.js", () => ({
55
- findContactByChannelExternalId: () => null,
51
+ findContactByAddress: () => null,
56
52
  findGuardianForChannel: (channelType: string) =>
57
53
  channelType === "vellum" ? mockVellumGuardian : null,
58
54
  listGuardianChannels: () => mockAnyGuardian,
@@ -138,7 +134,8 @@ describe("ensureGuardianPersonaFile", () => {
138
134
  const userFile = "alice.md";
139
135
  const dir = join(mockWorkspaceDir, "users");
140
136
  const filePath = join(dir, userFile);
141
- const existingContent = "# Existing user notes\n\n- Likes sparkling water\n";
137
+ const existingContent =
138
+ "# Existing user notes\n\n- Likes sparkling water\n";
142
139
 
143
140
  mkdirSync(dir, { recursive: true });
144
141
  writeFileSync(filePath, existingContent, "utf-8");
@@ -0,0 +1,178 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { afterEach, describe, expect, test } from "bun:test";
3
+
4
+ import { invalidateConfigCache } from "../config/loader.js";
5
+ import { getModelProfiles } from "../plugin-api/index.js";
6
+ import { getWorkspaceConfigPath } from "../util/platform.js";
7
+
8
+ /**
9
+ * `getModelProfiles()` — the runtime handle a plugin (e.g. a model router) calls
10
+ * to learn which inference profiles a workspace defines. These tests pin its
11
+ * contract: presentation ordering (`profileOrder` then alphabetical tail), mix
12
+ * profiles included and flagged via `isMix`, disabled profiles included and
13
+ * flagged via `isDisabled`, and the per-field fallbacks (label → key,
14
+ * description → null, isActive → `llm.activeProfile`).
15
+ */
16
+
17
+ function writeFixtureConfig(config: Record<string, unknown>): void {
18
+ writeFileSync(getWorkspaceConfigPath(), JSON.stringify(config), "utf-8");
19
+ invalidateConfigCache();
20
+ }
21
+
22
+ describe("getModelProfiles", () => {
23
+ afterEach(() => {
24
+ invalidateConfigCache();
25
+ });
26
+
27
+ test("orders profiles by profileOrder then the remaining keys alphabetically", () => {
28
+ // GIVEN a workspace whose profileOrder names some keys, with a duplicate
29
+ // ("balanced") and a key that resolves to no profile ("ghost").
30
+ writeFixtureConfig({
31
+ llm: {
32
+ profiles: { zeta: {}, alpha: {}, balanced: {}, "cost-optimized": {} },
33
+ profileOrder: ["balanced", "cost-optimized", "balanced", "ghost"],
34
+ },
35
+ });
36
+ // WHEN the profiles are listed.
37
+ const keys = getModelProfiles().map((p) => p.key);
38
+ // THEN profileOrder keys come first (deduped, existing-only), then the rest
39
+ // alphabetically.
40
+ expect(keys).toEqual(["balanced", "cost-optimized", "alpha", "zeta"]);
41
+ });
42
+
43
+ test("includes mix profiles and flags them via isMix", () => {
44
+ // GIVEN a workspace with a weighted mix profile alongside plain ones.
45
+ writeFixtureConfig({
46
+ llm: {
47
+ profiles: {
48
+ alpha: {},
49
+ beta: {},
50
+ blend: {
51
+ mix: [
52
+ { profile: "alpha", weight: 1 },
53
+ { profile: "beta", weight: 1 },
54
+ ],
55
+ },
56
+ },
57
+ profileOrder: ["alpha", "beta", "blend"],
58
+ },
59
+ });
60
+ // WHEN the profiles are listed.
61
+ const flags = getModelProfiles().map((p) => [p.key, p.isMix]);
62
+ // THEN the mix profile is present and flagged isMix, while plain profiles are not.
63
+ expect(flags).toEqual([
64
+ ["alpha", false],
65
+ ["beta", false],
66
+ ["blend", true],
67
+ ]);
68
+ });
69
+
70
+ test("marks an active mix profile via isActive", () => {
71
+ // GIVEN a workspace whose activeProfile is itself a weighted mix.
72
+ writeFixtureConfig({
73
+ llm: {
74
+ profiles: {
75
+ alpha: {},
76
+ blend: {
77
+ mix: [
78
+ { profile: "alpha", weight: 1 },
79
+ { profile: "beta", weight: 1 },
80
+ ],
81
+ },
82
+ beta: {},
83
+ },
84
+ profileOrder: ["alpha", "blend", "beta"],
85
+ activeProfile: "blend",
86
+ },
87
+ });
88
+ // WHEN the profiles are listed.
89
+ const active = getModelProfiles()
90
+ .filter((p) => p.isActive)
91
+ .map((p) => p.key);
92
+ // THEN the active mix profile is the one flagged isActive.
93
+ expect(active).toEqual(["blend"]);
94
+ });
95
+
96
+ test("includes disabled profiles and flags them via isDisabled", () => {
97
+ // GIVEN a workspace with one active and one disabled profile.
98
+ writeFixtureConfig({
99
+ llm: {
100
+ profiles: {
101
+ alpha: { status: "active" },
102
+ beta: { status: "disabled" },
103
+ },
104
+ profileOrder: ["alpha", "beta"],
105
+ },
106
+ });
107
+ // WHEN the profiles are listed.
108
+ const flags = getModelProfiles().map((p) => [p.key, p.isDisabled]);
109
+ // THEN the disabled profile is present and marked isDisabled.
110
+ expect(flags).toEqual([
111
+ ["alpha", false],
112
+ ["beta", true],
113
+ ]);
114
+ });
115
+
116
+ test("falls back to the key when a profile has no label", () => {
117
+ // GIVEN a workspace where one profile sets a label and one does not.
118
+ writeFixtureConfig({
119
+ llm: {
120
+ profiles: { balanced: { label: "Balanced" }, terse: {} },
121
+ profileOrder: ["balanced", "terse"],
122
+ },
123
+ });
124
+ // WHEN the profiles are listed.
125
+ const byKey = Object.fromEntries(
126
+ getModelProfiles().map((p) => [p.key, p.label]),
127
+ );
128
+ // THEN the unlabeled profile's label falls back to its key.
129
+ expect(byKey.balanced).toBe("Balanced");
130
+ expect(byKey.terse).toBe("terse");
131
+ });
132
+
133
+ test("marks the workspace active profile via isActive", () => {
134
+ // GIVEN a workspace whose activeProfile is "beta".
135
+ writeFixtureConfig({
136
+ llm: {
137
+ profiles: { alpha: {}, beta: {} },
138
+ profileOrder: ["alpha", "beta"],
139
+ activeProfile: "beta",
140
+ },
141
+ });
142
+ // WHEN the profiles are listed.
143
+ const flags = getModelProfiles().map((p) => [p.key, p.isActive]);
144
+ // THEN only the active profile is marked isActive.
145
+ expect(flags).toEqual([
146
+ ["alpha", false],
147
+ ["beta", true],
148
+ ]);
149
+ });
150
+
151
+ test("reports description as null when a profile sets none", () => {
152
+ // GIVEN a workspace where one profile has a description and one does not.
153
+ writeFixtureConfig({
154
+ llm: {
155
+ profiles: {
156
+ documented: { description: "Cheaper models, slower" },
157
+ bare: {},
158
+ },
159
+ profileOrder: ["documented", "bare"],
160
+ },
161
+ });
162
+ // WHEN the profiles are listed.
163
+ const byKey = Object.fromEntries(
164
+ getModelProfiles().map((p) => [p.key, p.description]),
165
+ );
166
+ // THEN a missing description is reported as null.
167
+ expect(byKey.documented).toBe("Cheaper models, slower");
168
+ expect(byKey.bare).toBeNull();
169
+ });
170
+
171
+ test("returns an empty list when the workspace defines no profiles", () => {
172
+ // GIVEN a workspace with no profiles defined.
173
+ writeFixtureConfig({ llm: { profiles: {}, profileOrder: [] } });
174
+ // WHEN the profiles are listed.
175
+ // THEN the result is empty.
176
+ expect(getModelProfiles()).toEqual([]);
177
+ });
178
+ });