@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -675,10 +675,7 @@ describe("buildSystemPrompt", () => {
675
675
  // so the bundled body must not leak into the rendered output. This is
676
676
  // the explicit "user silenced this section" path.
677
677
  mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
678
- writeFileSync(
679
- PARALLEL_FILE,
680
- "---\nenabled: false\n---\nIgnored body.\n",
681
- );
678
+ writeFileSync(PARALLEL_FILE, "---\nenabled: false\n---\nIgnored body.\n");
682
679
  const result = buildSystemPrompt();
683
680
  expect(result).not.toContain("<use_parallel_tool_calls>");
684
681
  expect(result).not.toContain("Batch independent tool calls");
@@ -713,7 +710,10 @@ describe("buildSystemPrompt", () => {
713
710
  });
714
711
 
715
712
  describe("containerized section (slot 02)", () => {
716
- const CONTAINERIZED_FILE = join(SYSTEM_PROMPTS_DIR, "02-containerized.md");
713
+ const CONTAINERIZED_FILE = join(
714
+ SYSTEM_PROMPTS_DIR,
715
+ "02-containerized.md",
716
+ );
717
717
 
718
718
  // The runtime gate is `isContainerized` on the render context, sourced
719
719
  // from `getIsContainerized()` which reads `process.env.IS_CONTAINERIZED`.
@@ -1093,10 +1093,7 @@ describe("buildSystemPrompt", () => {
1093
1093
  });
1094
1094
 
1095
1095
  describe("external-content section (slot 07)", () => {
1096
- const EXTERNAL_FILE = join(
1097
- SYSTEM_PROMPTS_DIR,
1098
- "07-external-content.md",
1099
- );
1096
+ const EXTERNAL_FILE = join(SYSTEM_PROMPTS_DIR, "07-external-content.md");
1100
1097
 
1101
1098
  test("workspace external-content file is rendered into the static block", () => {
1102
1099
  mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
@@ -1139,69 +1136,6 @@ describe("buildSystemPrompt", () => {
1139
1136
  });
1140
1137
  });
1141
1138
 
1142
- describe("background-conversation section (slot 08)", () => {
1143
- const BACKGROUND_FILE = join(
1144
- SYSTEM_PROMPTS_DIR,
1145
- "08-background-conversation.md",
1146
- );
1147
-
1148
- test("bundled default renders when isBackgroundConversation is true", () => {
1149
- mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1150
- const result = buildSystemPrompt({ isBackgroundConversation: true });
1151
- expect(result).toContain("## Background Conversation");
1152
- expect(result).toContain("non-interactive background job");
1153
- expect(result).toContain("`notifications` skill");
1154
- });
1155
-
1156
- test("bundled default is gated out when isBackgroundConversation is false", () => {
1157
- // Mustache `{{#isBackgroundConversation}}...{{/isBackgroundConversation}}`
1158
- // wraps the entire heading + body so the slot interpolates to empty
1159
- // in foreground conversations and the renderer drops it.
1160
- mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1161
- const result = buildSystemPrompt({ isBackgroundConversation: false });
1162
- expect(result).not.toContain("## Background Conversation");
1163
- });
1164
-
1165
- test("bundled default is gated out when isBackgroundConversation is omitted", () => {
1166
- // `ctx.isBackgroundConversation` is normalized to `false` when the
1167
- // caller omits the flag, so the gated section drops out cleanly.
1168
- mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1169
- const result = buildSystemPrompt();
1170
- expect(result).not.toContain("## Background Conversation");
1171
- });
1172
-
1173
- test("workspace override is also gated by isBackgroundConversation", () => {
1174
- // Workspace overrides flow through the same mustache interpolation,
1175
- // so authors can rely on the same `{{#isBackgroundConversation}}`
1176
- // gate in their custom body.
1177
- mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1178
- writeFileSync(
1179
- BACKGROUND_FILE,
1180
- "{{#isBackgroundConversation}}## Background Conversation\n\nWorkspace override marker COMET_3K.\n{{/isBackgroundConversation}}\n",
1181
- );
1182
-
1183
- const offResult = buildSystemPrompt({ isBackgroundConversation: false });
1184
- expect(offResult).not.toContain("## Background Conversation");
1185
- expect(offResult).not.toContain("Workspace override marker COMET_3K.");
1186
-
1187
- const onResult = buildSystemPrompt({ isBackgroundConversation: true });
1188
- expect(onResult).toContain("## Background Conversation");
1189
- expect(onResult).toContain("Workspace override marker COMET_3K.");
1190
- });
1191
-
1192
- test("renders after the external-content section when both render", () => {
1193
- // Numeric prefix `08-` > `07-` so the background-conversation
1194
- // section trails the external-content section in the static block.
1195
- mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1196
- const result = buildSystemPrompt({ isBackgroundConversation: true });
1197
- const externalIdx = result.indexOf("## External Content");
1198
- const backgroundIdx = result.indexOf("## Background Conversation");
1199
- expect(externalIdx).toBeGreaterThan(-1);
1200
- expect(backgroundIdx).toBeGreaterThan(-1);
1201
- expect(externalIdx).toBeLessThan(backgroundIdx);
1202
- });
1203
- });
1204
-
1205
1139
  test("unresolved {{variable}} is left as a literal in the body", () => {
1206
1140
  mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1207
1141
  writeFileSync(
@@ -1211,7 +1145,6 @@ describe("buildSystemPrompt", () => {
1211
1145
  const result = buildSystemPrompt();
1212
1146
  expect(result).toContain("Has {{somethingMissing}} in body.");
1213
1147
  });
1214
-
1215
1148
  });
1216
1149
  });
1217
1150
 
@@ -0,0 +1,228 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { memoryRouterBalancedProfileMigration } from "../workspace/migrations/087-memory-router-balanced-profile.js";
13
+
14
+ let workspaceDir: string;
15
+
16
+ function freshWorkspace(): void {
17
+ workspaceDir = join(
18
+ tmpdir(),
19
+ `vellum-migration-087-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
20
+ );
21
+ mkdirSync(workspaceDir, { recursive: true });
22
+ }
23
+
24
+ function writeConfig(data: Record<string, unknown>): void {
25
+ writeFileSync(
26
+ join(workspaceDir, "config.json"),
27
+ JSON.stringify(data, null, 2) + "\n",
28
+ );
29
+ }
30
+
31
+ function readConfig(): Record<string, unknown> {
32
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
33
+ }
34
+
35
+ function configPath(): string {
36
+ return join(workspaceDir, "config.json");
37
+ }
38
+
39
+ beforeEach(() => {
40
+ freshWorkspace();
41
+ delete process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
42
+ });
43
+
44
+ afterEach(() => {
45
+ delete process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH;
46
+ if (existsSync(workspaceDir)) {
47
+ rmSync(workspaceDir, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ describe("087-memory-router-balanced-profile migration", () => {
52
+ test("has correct migration id", () => {
53
+ expect(memoryRouterBalancedProfileMigration.id).toBe(
54
+ "087-memory-router-balanced-profile",
55
+ );
56
+ });
57
+
58
+ test("replaces seeded Sonnet 4.6 + 1M context with balanced profile", () => {
59
+ writeConfig({
60
+ llm: {
61
+ default: { provider: "anthropic" },
62
+ callSites: {
63
+ memoryRouter: {
64
+ model: "claude-sonnet-4-6",
65
+ contextWindow: { maxInputTokens: 1_000_000 },
66
+ },
67
+ },
68
+ },
69
+ });
70
+
71
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
72
+
73
+ const config = readConfig() as {
74
+ llm: { callSites: Record<string, Record<string, unknown>> };
75
+ };
76
+ expect(config.llm.callSites.memoryRouter).toEqual({ profile: "balanced" });
77
+ });
78
+
79
+ test("creates the call site when missing", () => {
80
+ writeConfig({
81
+ llm: { default: { provider: "anthropic" } },
82
+ });
83
+
84
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
85
+
86
+ const config = readConfig() as {
87
+ llm: { callSites: Record<string, Record<string, unknown>> };
88
+ };
89
+ expect(config.llm.callSites.memoryRouter).toEqual({ profile: "balanced" });
90
+ });
91
+
92
+ test("writes a fresh starter config when config.json is absent", () => {
93
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
94
+
95
+ expect(existsSync(configPath())).toBe(true);
96
+ const config = readConfig() as {
97
+ llm: { callSites: Record<string, Record<string, unknown>> };
98
+ };
99
+ expect(config.llm.callSites.memoryRouter).toEqual({ profile: "balanced" });
100
+ });
101
+
102
+ test("skips BYOK / non-Anthropic workspaces to avoid breaking memoryRouter", () => {
103
+ // `balanced` resolves to the managed Anthropic connection, which BYOK
104
+ // installs disable. Rewriting to `balanced` there would silently disable
105
+ // memory injection (getConfiguredProvider returns null). Preserve whatever
106
+ // memoryRouter config the workspace already has — including absence.
107
+ writeConfig({
108
+ llm: {
109
+ default: { provider: "openai", model: "gpt-5.4" },
110
+ callSites: {
111
+ memoryRouter: { model: "gpt-5.4" },
112
+ },
113
+ },
114
+ });
115
+
116
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
117
+
118
+ const config = readConfig() as {
119
+ llm: { callSites: Record<string, Record<string, unknown>> };
120
+ };
121
+ expect(config.llm.callSites.memoryRouter).toEqual({ model: "gpt-5.4" });
122
+ });
123
+
124
+ test("skips BYOK workspaces with no existing memoryRouter entry", () => {
125
+ writeConfig({
126
+ llm: { default: { provider: "gemini" } },
127
+ });
128
+
129
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
130
+
131
+ const config = readConfig() as {
132
+ llm: { callSites?: Record<string, Record<string, unknown>> };
133
+ };
134
+ expect(config.llm.callSites?.memoryRouter).toBeUndefined();
135
+ });
136
+
137
+ test("preserves user customizations on memoryRouter (non-077-seeded shape)", () => {
138
+ writeConfig({
139
+ llm: {
140
+ default: { provider: "anthropic" },
141
+ callSites: {
142
+ memoryRouter: {
143
+ model: "claude-haiku-4-5-20251001",
144
+ effort: "low",
145
+ },
146
+ },
147
+ },
148
+ });
149
+
150
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
151
+
152
+ const config = readConfig() as {
153
+ llm: { callSites: Record<string, Record<string, unknown>> };
154
+ };
155
+ expect(config.llm.callSites.memoryRouter).toEqual({
156
+ model: "claude-haiku-4-5-20251001",
157
+ effort: "low",
158
+ });
159
+ });
160
+
161
+ test("is idempotent — second run does not change the call site", () => {
162
+ writeConfig({
163
+ llm: {
164
+ default: { provider: "anthropic" },
165
+ callSites: {
166
+ memoryRouter: {
167
+ model: "claude-sonnet-4-6",
168
+ contextWindow: { maxInputTokens: 1_000_000 },
169
+ },
170
+ },
171
+ },
172
+ });
173
+
174
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
175
+ const afterFirst = readFileSync(configPath(), "utf-8");
176
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
177
+ const afterSecond = readFileSync(configPath(), "utf-8");
178
+
179
+ expect(afterSecond).toBe(afterFirst);
180
+ });
181
+
182
+ test("skips when VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH is set", () => {
183
+ process.env.VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH = "/tmp/overlay.json";
184
+ writeConfig({
185
+ llm: {
186
+ default: { provider: "anthropic" },
187
+ callSites: {
188
+ memoryRouter: { model: "claude-sonnet-4-6" },
189
+ },
190
+ },
191
+ });
192
+
193
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
194
+
195
+ const config = readConfig() as {
196
+ llm: { callSites: Record<string, Record<string, unknown>> };
197
+ };
198
+ expect(config.llm.callSites.memoryRouter).toEqual({
199
+ model: "claude-sonnet-4-6",
200
+ });
201
+ });
202
+
203
+ test("preserves sibling call-site entries", () => {
204
+ writeConfig({
205
+ llm: {
206
+ default: { provider: "anthropic" },
207
+ callSites: {
208
+ mainAgent: { model: "claude-opus-4-7", maxTokens: 32000 },
209
+ memoryRouter: {
210
+ model: "claude-sonnet-4-6",
211
+ contextWindow: { maxInputTokens: 1_000_000 },
212
+ },
213
+ },
214
+ },
215
+ });
216
+
217
+ memoryRouterBalancedProfileMigration.run(workspaceDir);
218
+
219
+ const config = readConfig() as {
220
+ llm: { callSites: Record<string, Record<string, unknown>> };
221
+ };
222
+ expect(config.llm.callSites.mainAgent).toEqual({
223
+ model: "claude-opus-4-7",
224
+ maxTokens: 32000,
225
+ });
226
+ expect(config.llm.callSites.memoryRouter).toEqual({ profile: "balanced" });
227
+ });
228
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { buildAgentCard } from "../agent-card.js";
4
+
5
+ describe("buildAgentCard", () => {
6
+ const BASE_PARAMS = {
7
+ assistantName: "Alice",
8
+ baseUrl: "https://example.com",
9
+ };
10
+
11
+ test("includes all required top-level fields", () => {
12
+ const card = buildAgentCard(BASE_PARAMS);
13
+
14
+ expect(card.name).toBe("Alice");
15
+ expect(card.description).toBeDefined();
16
+ expect(card.version).toBe("1.0.0");
17
+ expect(card.supported_interfaces).toBeDefined();
18
+ expect(card.capabilities).toBeDefined();
19
+ expect(card.default_input_modes).toBeDefined();
20
+ expect(card.default_output_modes).toBeDefined();
21
+ expect(card.skills).toBeDefined();
22
+ });
23
+
24
+ test("interface URL is baseUrl + /a2a/message:send", () => {
25
+ const card = buildAgentCard(BASE_PARAMS);
26
+
27
+ expect(card.supported_interfaces).toHaveLength(1);
28
+ expect(card.supported_interfaces[0].url).toBe(
29
+ "https://example.com/a2a/message:send",
30
+ );
31
+ expect(card.supported_interfaces[0].protocol_binding).toBe("JSONRPC");
32
+ expect(card.supported_interfaces[0].protocol_version).toBe("1.0");
33
+ });
34
+
35
+ test("push_notifications capability is true", () => {
36
+ const card = buildAgentCard(BASE_PARAMS);
37
+
38
+ expect(card.capabilities.push_notifications).toBe(true);
39
+ });
40
+
41
+ test("streaming capability is false", () => {
42
+ const card = buildAgentCard(BASE_PARAMS);
43
+
44
+ expect(card.capabilities.streaming).toBe(false);
45
+ });
46
+
47
+ test("extended_agent_card capability is false", () => {
48
+ const card = buildAgentCard(BASE_PARAMS);
49
+
50
+ expect(card.capabilities.extended_agent_card).toBe(false);
51
+ });
52
+
53
+ test("defaults description when omitted", () => {
54
+ const card = buildAgentCard(BASE_PARAMS);
55
+
56
+ expect(card.description).toBe("Alice — a Vellum AI assistant");
57
+ });
58
+
59
+ test("uses explicit description when provided", () => {
60
+ const card = buildAgentCard({
61
+ ...BASE_PARAMS,
62
+ assistantDescription: "A specialized research assistant",
63
+ });
64
+
65
+ expect(card.description).toBe("A specialized research assistant");
66
+ });
67
+
68
+ test("advertises text/plain as default input and output mode", () => {
69
+ const card = buildAgentCard(BASE_PARAMS);
70
+
71
+ expect(card.default_input_modes).toEqual(["text/plain"]);
72
+ expect(card.default_output_modes).toEqual(["text/plain"]);
73
+ });
74
+
75
+ test("includes a conversation skill", () => {
76
+ const card = buildAgentCard(BASE_PARAMS);
77
+
78
+ expect(card.skills).toHaveLength(1);
79
+ expect(card.skills[0]).toEqual({
80
+ id: "conversation",
81
+ name: "General conversation",
82
+ description: "Send a message and receive a response",
83
+ tags: ["chat"],
84
+ });
85
+ });
86
+
87
+ test("constructs correct interface URL with trailing-slash base", () => {
88
+ const card = buildAgentCard({
89
+ ...BASE_PARAMS,
90
+ baseUrl: "https://example.com/",
91
+ });
92
+
93
+ // The URL is constructed by simple concatenation; callers normalize the base.
94
+ expect(card.supported_interfaces[0].url).toBe(
95
+ "https://example.com//a2a/message:send",
96
+ );
97
+ });
98
+ });