@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
@@ -21,11 +21,7 @@ import {
21
21
  unregisterWorkspaceTool,
22
22
  } from "../tools/registry.js";
23
23
  import { eagerModuleToolNames, explicitTools } from "../tools/tool-manifest.js";
24
- import type {
25
- Tool,
26
- ToolContext,
27
- ToolExecutionResult,
28
- } from "../tools/types.js";
24
+ import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
29
25
 
30
26
  // Clean up global registry after this file completes to prevent
31
27
  // contamination of subsequent test files in combined runs.
@@ -115,12 +111,11 @@ describe("tool manifest", () => {
115
111
  expect(eagerModuleToolNames.length).toBe(11);
116
112
  });
117
113
 
118
- test("explicit tools list includes memory and credential tools", () => {
114
+ test("explicit tools list includes memory tools", () => {
119
115
  const names = explicitTools.map((t) => t.name);
120
116
  expect(names).toContain("recall");
121
117
  expect(names.filter((name) => name === "recall")).toHaveLength(1);
122
118
  expect(names).toContain("remember");
123
- expect(names).toContain("credential_store");
124
119
  });
125
120
 
126
121
  test("registered tool count is at least eager + host", async () => {
@@ -1,10 +1,6 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- // These tests exercise the PATCH-side `workflowName` validation, which only
4
- // runs once the `workflows` feature flag is ON (the flag gate short-circuits
5
- // first when it is off). Mock the flag resolver to ON so the validation path is
6
- // reachable; the sibling schedule-routes.test.ts keeps the flag OFF and asserts
7
- // the flag-gate behavior, so the two files do not conflict.
3
+ // These tests exercise the PATCH-side `workflowName` validation.
8
4
  mock.module("../util/logger.js", () => ({
9
5
  getLogger: () =>
10
6
  new Proxy({} as Record<string, unknown>, {
@@ -12,11 +8,6 @@ mock.module("../util/logger.js", () => ({
12
8
  }),
13
9
  }));
14
10
 
15
- mock.module("../config/assistant-feature-flags.js", () => ({
16
- isAssistantFeatureFlagEnabled: (key: string) => key === "workflows",
17
- getAssistantFeatureFlagValue: (key: string) => key === "workflows",
18
- }));
19
-
20
11
  mock.module("../config/loader.js", () => ({
21
12
  getConfig: () => ({}),
22
13
  invalidateConfigCache: () => {},
@@ -1243,36 +1243,6 @@ describe("POST /schedules — create", () => {
1243
1243
  ).toThrow("Only 'execute' and 'workflow' modes are supported");
1244
1244
  });
1245
1245
 
1246
- test("rejects workflow-mode creation when the workflows flag is off", () => {
1247
- // The mocked getConfig returns no feature flags, so `workflows` is off.
1248
- expect(() =>
1249
- postCreate({
1250
- name: "Triage",
1251
- description: "Triage the inbox every morning",
1252
- expression: "0 9 * * *",
1253
- message: "triage inbox",
1254
- mode: "workflow",
1255
- workflowName: "triage-inbox",
1256
- }),
1257
- ).toThrow("Workflows are not enabled");
1258
- });
1259
-
1260
- test("rejects PATCH switching to workflow mode when the flag is off", () => {
1261
- const schedule = createSchedule({
1262
- name: "Plain execute",
1263
- cronExpression: "0 9 * * *",
1264
- message: "hi",
1265
- syntax: "cron",
1266
- });
1267
- const patch = findRoute("schedules/:id", "PATCH");
1268
- expect(() =>
1269
- patch.handler({
1270
- pathParams: { id: schedule.id },
1271
- body: { mode: "workflow", workflowName: "triage-inbox" },
1272
- }),
1273
- ).toThrow("Workflows are not enabled");
1274
- });
1275
-
1276
1246
  test("rejects an unparseable expression", () => {
1277
1247
  expect(() =>
1278
1248
  postCreate({
@@ -421,7 +421,7 @@ describe("schedule tools — workflow mode", () => {
421
421
  beforeEach(() => {
422
422
  getRawDb().run("DELETE FROM cron_runs");
423
423
  getRawDb().run("DELETE FROM cron_jobs");
424
- setOverridesForTesting({ workflows: true });
424
+ setOverridesForTesting({});
425
425
  });
426
426
  afterAll(() => {
427
427
  setOverridesForTesting({});
@@ -466,22 +466,6 @@ describe("schedule tools — workflow mode", () => {
466
466
  expect(result.content).toContain("workflow_name is required");
467
467
  });
468
468
 
469
- test("rejects workflow mode when the workflows flag is off", async () => {
470
- setOverridesForTesting({}); // flag off
471
- const result = await executeScheduleCreate(
472
- {
473
- name: "Flag off",
474
- expression: "0 8 * * *",
475
- mode: "workflow",
476
- workflow_name: "inbox-triage",
477
- },
478
- ctx,
479
- );
480
-
481
- expect(result.isError).toBe(true);
482
- expect(result.content).toContain("workflows are not enabled");
483
- });
484
-
485
469
  test("update to workflow mode requires a workflow_name", async () => {
486
470
  await executeScheduleCreate(
487
471
  { name: "To workflow", expression: "0 9 * * *", message: "test" },
@@ -544,7 +528,7 @@ describe("schedule_create — workflow capability manifest", () => {
544
528
  beforeEach(() => {
545
529
  getRawDb().run("DELETE FROM cron_runs");
546
530
  getRawDb().run("DELETE FROM cron_jobs");
547
- setOverridesForTesting({ workflows: true });
531
+ setOverridesForTesting({});
548
532
  // Deterministic registry so a declared side-effecting tool resolves.
549
533
  __clearRegistryForTesting();
550
534
  registerTool(makeFakeTool("file_write"));
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { resolveSkillExecuteInput } from "../tools/skills/execute.js";
3
+ import {
4
+ augmentSkillExecuteError,
5
+ resolveSkillExecuteInput,
6
+ } from "../tools/skills/execute.js";
4
7
 
5
8
  describe("resolveSkillExecuteInput", () => {
6
9
  test("returns a correctly nested object unchanged", () => {
@@ -83,3 +86,45 @@ describe("resolveSkillExecuteInput", () => {
83
86
  expect(result).toEqual({ foo: "bar" });
84
87
  });
85
88
  });
89
+
90
+ describe("augmentSkillExecuteError", () => {
91
+ test("appends envelope guidance when an empty-input call errors", () => {
92
+ // The subagent_spawn failure: empty input, tool rejects with a field-level
93
+ // message that says nothing about the skill_execute envelope.
94
+ const result = augmentSkillExecuteError(
95
+ "subagent_spawn",
96
+ {},
97
+ { content: 'Both "label" and "objective" are required.', isError: true },
98
+ );
99
+ expect(result.isError).toBe(true);
100
+ expect(result.content).toContain(
101
+ 'Both "label" and "objective" are required.',
102
+ );
103
+ expect(result.content).toContain("carried no parameters");
104
+ expect(result.content).toContain('"tool": "subagent_spawn"');
105
+ expect(result.content).toContain("inside `input`");
106
+ });
107
+
108
+ test("leaves errors untouched when parameters were resolved", () => {
109
+ // A non-empty resolved input means the model structured the call; any error
110
+ // is a real tool-level failure, not an envelope-shape mistake.
111
+ const original = {
112
+ content: "Subagent quota exceeded.",
113
+ isError: true,
114
+ };
115
+ const result = augmentSkillExecuteError(
116
+ "subagent_spawn",
117
+ { label: "x", objective: "y" },
118
+ original,
119
+ );
120
+ expect(result).toBe(original);
121
+ });
122
+
123
+ test("leaves successful empty-input calls untouched", () => {
124
+ // Tools that legitimately accept no parameters (e.g. subagent_status) must
125
+ // not have guidance appended to their successful results.
126
+ const original = { content: "No subagents running.", isError: false };
127
+ const result = augmentSkillExecuteError("subagent_status", {}, original);
128
+ expect(result).toBe(original);
129
+ });
130
+ });
@@ -9,9 +9,8 @@
9
9
  * branches (macOS `.app` Resources, next-to-binary) key off
10
10
  * `import.meta.dir.startsWith("/$bunfs/")`, so at test time only the
11
11
  * source-mode early-return is exercised here; the compiled branch is
12
- * covered structurally via the signing + packaging step in
13
- * `clients/macos/build.sh` and will be exercised end-to-end by the
14
- * supervisor integration test added in PR 27.
12
+ * covered structurally via the Electron signing + packaging step in
13
+ * `clients/macos/scripts/pack.sh`.
15
14
  */
16
15
 
17
16
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
@@ -6,6 +6,27 @@ import { describe, expect, mock, test } from "bun:test";
6
6
  let mockGetMessages: (
7
7
  conversationId: string,
8
8
  ) => Array<{ role: string; content: string }> | null = () => null;
9
+ const mockProfiles = {
10
+ balanced: {},
11
+ disabled: { status: "disabled" },
12
+ "quality-optimized": {},
13
+ };
14
+ mock.module("../config/loader.js", () => ({
15
+ getConfigReadOnly: () => ({
16
+ llm: { profiles: mockProfiles },
17
+ }),
18
+ getConfig: () => ({
19
+ llm: {
20
+ default: {
21
+ provider: "anthropic",
22
+ provider_connection: "anthropic-managed",
23
+ model: "claude-opus-4-7",
24
+ },
25
+ profiles: mockProfiles,
26
+ },
27
+ rateLimit: { maxRequestsPerMinute: 0 },
28
+ }),
29
+ }));
9
30
  mock.module("../memory/conversation-crud.js", () => ({
10
31
  setConversationOriginChannelIfUnset: () => {},
11
32
  updateConversationContextWindow: () => {},
@@ -140,6 +161,7 @@ describe("Subagent tool definitions", () => {
140
161
  const def = findTool("subagent_spawn");
141
162
  expect(def).toBeDefined();
142
163
  expect(def.input_schema.required).toEqual(["label", "objective"]);
164
+ expect(def.input_schema.properties.inference_profile).toBeDefined();
143
165
  });
144
166
 
145
167
  test("abort tool has correct definition", () => {
@@ -422,6 +444,100 @@ describe("Subagent spawn success and failure", () => {
422
444
  }
423
445
  });
424
446
 
447
+ test("spawn passes explicit inference_profile to manager over inherited override", async () => {
448
+ const manager = getSubagentManager();
449
+ const originalSpawn = manager.spawn.bind(manager);
450
+ let capturedConfig: Record<string, unknown> | undefined;
451
+
452
+ manager.spawn = async (config: Record<string, unknown>) => {
453
+ capturedConfig = config;
454
+ return "profile-subagent-id";
455
+ };
456
+
457
+ try {
458
+ const result = await executeSubagentSpawn(
459
+ {
460
+ label: "Profile test",
461
+ objective: "Do it with a chosen model profile",
462
+ inference_profile: "quality-optimized",
463
+ },
464
+ makeContext("sess-spawn-profile", {
465
+ sendToClient: () => {},
466
+ overrideProfile: "balanced",
467
+ }),
468
+ );
469
+
470
+ expect(result.isError).toBe(false);
471
+ expect(capturedConfig).toBeDefined();
472
+ expect(capturedConfig!.overrideProfile).toBe("quality-optimized");
473
+ expect(capturedConfig!.forceOverrideProfile).toBe(true);
474
+ } finally {
475
+ manager.spawn = originalSpawn;
476
+ }
477
+ });
478
+
479
+ test("spawn returns error for unknown inference_profile", async () => {
480
+ const manager = getSubagentManager();
481
+ const originalSpawn = manager.spawn.bind(manager);
482
+ let spawnCalled = false;
483
+
484
+ manager.spawn = async () => {
485
+ spawnCalled = true;
486
+ return "profile-subagent-id";
487
+ };
488
+
489
+ try {
490
+ const result = await executeSubagentSpawn(
491
+ {
492
+ label: "Bad profile",
493
+ objective: "Do it",
494
+ inference_profile: "does-not-exist",
495
+ },
496
+ makeContext("sess-spawn-bad-profile", { sendToClient: () => {} }),
497
+ );
498
+
499
+ expect(result.isError).toBe(true);
500
+ expect(result.content).toContain(
501
+ 'Inference profile "does-not-exist" is not defined',
502
+ );
503
+ expect(spawnCalled).toBe(false);
504
+ } finally {
505
+ manager.spawn = originalSpawn;
506
+ }
507
+ });
508
+
509
+ test("spawn returns error for disabled inference_profile", async () => {
510
+ const manager = getSubagentManager();
511
+ const originalSpawn = manager.spawn.bind(manager);
512
+ let spawnCalled = false;
513
+
514
+ manager.spawn = async () => {
515
+ spawnCalled = true;
516
+ return "profile-subagent-id";
517
+ };
518
+
519
+ try {
520
+ const result = await executeSubagentSpawn(
521
+ {
522
+ label: "Disabled profile",
523
+ objective: "Do it",
524
+ inference_profile: "disabled",
525
+ },
526
+ makeContext("sess-spawn-disabled-profile", {
527
+ sendToClient: () => {},
528
+ }),
529
+ );
530
+
531
+ expect(result.isError).toBe(true);
532
+ expect(result.content).toContain(
533
+ 'Inference profile "disabled" is disabled',
534
+ );
535
+ expect(spawnCalled).toBe(false);
536
+ } finally {
537
+ manager.spawn = originalSpawn;
538
+ }
539
+ });
540
+
425
541
  test("spawn handles non-Error throws gracefully", async () => {
426
542
  const manager = getSubagentManager();
427
543
  const originalSpawn = manager.spawn.bind(manager);