@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
@@ -453,6 +453,9 @@ export class SubagentManager {
453
453
  ...(managed.state.config.overrideProfile
454
454
  ? { overrideProfile: managed.state.config.overrideProfile }
455
455
  : {}),
456
+ ...(managed.state.config.forceOverrideProfile
457
+ ? { forceOverrideProfile: true }
458
+ : {}),
456
459
  });
457
460
 
458
461
  // Agent loop completed successfully.
@@ -641,6 +644,9 @@ export class SubagentManager {
641
644
  ...(managed.state.config.overrideProfile
642
645
  ? { overrideProfile: managed.state.config.overrideProfile }
643
646
  : {}),
647
+ ...(managed.state.config.forceOverrideProfile
648
+ ? { forceOverrideProfile: true }
649
+ : {}),
644
650
  })
645
651
  .catch((err) => {
646
652
  log.error({ subagentId, err }, "Subagent message processing failed");
@@ -69,6 +69,12 @@ export interface SubagentConfig {
69
69
  * profile, every spawned subagent inherits it automatically.
70
70
  */
71
71
  overrideProfile?: string;
72
+ /**
73
+ * When true, the subagent's `overrideProfile` is an explicit spawn-time
74
+ * request and must float above call-site layers. Inherited parent profiles
75
+ * leave this unset so existing call-site precedence stays intact.
76
+ */
77
+ forceOverrideProfile?: boolean;
72
78
  /**
73
79
  * Tool-use id of the `skill_execute` call that spawned this subagent.
74
80
  * Forwarded into the `subagent_spawned` event so the client can anchor the
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## New Non-Skill Tools Are Strongly Discouraged
4
4
 
5
- **Prefer skills over new non-skill tool registrations.** Non-skill tools require approval from Team Jarvis.
5
+ **Prefer skills over new non-skill tool registrations.** Non-skill tools require approval from the core team.
6
6
 
7
7
  Skills are the preferred approach for adding new capabilities — they are progressively disclosed into context, more portable, and can be iterated on independently. New non-skill tool registrations (`class ... implements Tool` + `registerTool()`) carry additional costs:
8
8
 
@@ -48,7 +48,7 @@ See [`assistant/docs/credential-execution-service.md`](../../docs/credential-exe
48
48
 
49
49
  ## If You Have Approval
50
50
 
51
- If Team Jarvis has approved your new tool:
51
+ If the core team has approved your new tool:
52
52
 
53
53
  1. The pre-commit hook will block your commit by default
54
54
  2. Use `git commit --no-verify` to bypass the hook
@@ -56,4 +56,4 @@ If Team Jarvis has approved your new tool:
56
56
 
57
57
  ## Questions?
58
58
 
59
- Contact Team Jarvis before shipping a new tool.
59
+ Contact the core team before shipping a new tool.
@@ -1093,7 +1093,7 @@ export async function executeBrowserNavigate(
1093
1093
  }
1094
1094
  } else {
1095
1095
  // Login / 2FA / OAuth - the agent should handle these itself
1096
- // using browser operations + credential_store. Don't hand off.
1096
+ // using browser operations + stored credentials. Don't hand off.
1097
1097
  lines.push("");
1098
1098
  lines.push(formatAuthChallenge(challenge));
1099
1099
  lines.push("");
@@ -2,7 +2,7 @@
2
2
  // failures and the user-facing copy we surface for them (ATL-727).
3
3
  //
4
4
  // This is a pure leaf module: it has NO imports from `daemon/`, `agent/`,
5
- // `apps/`, or any client/UI package. It may only import the logger and pino
5
+ // `clients/`, or any client/UI package. It may only import the logger and pino
6
6
  // types (for the telemetry helper). Every web_search code path — native
7
7
  // Anthropic handler, app-side providers, and the web client default — funnels
8
8
  // failures through `classifyWebSearchFailure` so the same friendly message
@@ -173,7 +173,7 @@ export class PermissionChecker {
173
173
  if (
174
174
  context.forcePromptSideEffects &&
175
175
  result.decision === "allow" &&
176
- isSideEffectTool(name, input)
176
+ isSideEffectTool(name)
177
177
  ) {
178
178
  result.decision = "prompt";
179
179
  result.reason = "Side-effect tool requires explicit approval";
@@ -1,5 +1,3 @@
1
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
2
- import { getConfig } from "../../config/loader.js";
3
1
  import { validateScheduleInferenceProfile } from "../../schedule/inference-profile.js";
4
2
  import { formatIntegrationSummary } from "../../schedule/integration-status.js";
5
3
  import { validateRruleSetLines } from "../../schedule/recurrence-engine.js";
@@ -121,13 +119,9 @@ export async function executeScheduleCreate(
121
119
  };
122
120
  }
123
121
  } else if (mode === "workflow") {
124
- // Workflow mode is gated by the `workflows` flag (a scheduled run would
125
- // otherwise hard-fail at trigger time) and requires a saved workflow name —
126
- // mirrors the HTTP route's create-side validation so the assistant-facing
127
- // path and the settings route enforce the same shape.
128
- if (!isAssistantFeatureFlagEnabled("workflows", getConfig())) {
129
- return { content: "Error: workflows are not enabled.", isError: true };
130
- }
122
+ // Workflow mode requires a saved workflow name — mirrors the HTTP route's
123
+ // create-side validation so the assistant-facing path and the settings route
124
+ // enforce the same shape.
131
125
  if (!workflowName) {
132
126
  return {
133
127
  content:
@@ -1,5 +1,3 @@
1
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
2
- import { getConfig } from "../../config/loader.js";
3
1
  import { validateScheduleInferenceProfile } from "../../schedule/inference-profile.js";
4
2
  import { validateRruleSetLines } from "../../schedule/recurrence-engine.js";
5
3
  import {
@@ -205,8 +203,8 @@ export async function executeScheduleUpdate(
205
203
  };
206
204
  }
207
205
 
208
- // Mirror the HTTP route: a schedule whose RESULTING mode is `workflow` must be
209
- // flag-enabled and carry a non-empty workflowName. Compute the post-update
206
+ // Mirror the HTTP route: a schedule whose RESULTING mode is `workflow` must
207
+ // carry a non-empty workflowName. Compute the post-update
210
208
  // state (the update's value if present, else the persisted one) so both
211
209
  // "switch to workflow without a name" and "clear the name on a workflow
212
210
  // schedule" are rejected — otherwise the scheduler hits the `!job.workflowName`
@@ -217,12 +215,6 @@ export async function executeScheduleUpdate(
217
215
  const resultingMode =
218
216
  updates.mode !== undefined ? (updates.mode as string) : existing.mode;
219
217
  if (resultingMode === "workflow") {
220
- if (!isAssistantFeatureFlagEnabled("workflows", getConfig())) {
221
- return {
222
- content: "Error: workflows are not enabled.",
223
- isError: true,
224
- };
225
- }
226
218
  const resultingWorkflowName =
227
219
  updates.workflowName !== undefined
228
220
  ? ((updates.workflowName as string | null) ?? "")
@@ -25,22 +25,7 @@ const SIDE_EFFECT_TOOLS: ReadonlySet<string> = new Set([
25
25
  * Returns `true` if the given tool name is classified as having side effects
26
26
  * (i.e. it can modify the filesystem, execute arbitrary commands, or trigger
27
27
  * external actions). Read-only and informational tools return `false`.
28
- *
29
- * For mixed-action tools (e.g. credential_store), the optional
30
- * `input` parameter is inspected to distinguish mutating actions (create,
31
- * update, cancel) from read-only ones (list, get).
32
28
  */
33
- export function isSideEffectTool(
34
- toolName: string,
35
- input?: Record<string, unknown>,
36
- ): boolean {
37
- if (SIDE_EFFECT_TOOLS.has(toolName)) return true;
38
-
39
- // Action-aware checks for mixed-action tools
40
- if (toolName === "credential_store") {
41
- const action = input?.action;
42
- return action === "store" || action === "delete" || action === "prompt";
43
- }
44
-
45
- return false;
29
+ export function isSideEffectTool(toolName: string): boolean {
30
+ return SIDE_EFFECT_TOOLS.has(toolName);
46
31
  }
@@ -62,6 +62,40 @@ export function resolveSkillExecuteInput(
62
62
  return {};
63
63
  }
64
64
 
65
+ /**
66
+ * Augment an inner-tool error with `skill_execute` envelope guidance when the
67
+ * call carried no inner parameters.
68
+ *
69
+ * {@link resolveSkillExecuteInput} rescues *misplaced* parameters (siblings, a
70
+ * JSON-encoded string). It cannot rescue a genuinely empty call — there is
71
+ * nothing to relocate — so the inner tool runs with `{}` and rejects it with a
72
+ * field-level message ("<field> is required") that says nothing about the
73
+ * envelope. Weak models then retry the identical empty shape, oscillating over
74
+ * whether parameters belong under `input`, as siblings, or as a JSON string.
75
+ *
76
+ * Appending the canonical envelope shape gives the next attempt a concrete
77
+ * template. Fires only when the resolved inner input was empty AND the tool
78
+ * errored, so well-formed calls and tools that legitimately accept no
79
+ * parameters are untouched.
80
+ */
81
+ export function augmentSkillExecuteError(
82
+ toolName: string,
83
+ resolvedInput: Record<string, unknown>,
84
+ result: ToolExecutionResult,
85
+ ): ToolExecutionResult {
86
+ if (!result.isError || Object.keys(resolvedInput).length > 0) return result;
87
+
88
+ const guidance =
89
+ `\n\nThis skill_execute call carried no parameters for "${toolName}". ` +
90
+ `Put the tool's parameters inside \`input\` as a JSON object — not as ` +
91
+ `siblings of \`tool\`, and not as a JSON-encoded string. For example: ` +
92
+ `{"tool": "${toolName}", "input": { /* the tool's parameters */ }, ` +
93
+ `"activity": "..."}. The skill's instructions (from skill_load) list ` +
94
+ `"${toolName}"'s required fields.`;
95
+
96
+ return { ...result, content: result.content + guidance };
97
+ }
98
+
65
99
  export const skillExecuteTool = {
66
100
  name: "skill_execute",
67
101
  description:
@@ -1,3 +1,4 @@
1
+ import { validateInferenceProfileKey } from "../../config/inference-profile-validation.js";
1
2
  import { findConversation } from "../../daemon/conversation-registry.js";
2
3
  import { getConversationOverrideProfile } from "../../memory/conversation-crud.js";
3
4
  import type { Message } from "../../providers/types.js";
@@ -14,6 +15,7 @@ export async function executeSubagentSpawn(
14
15
  const extraContext = input.context as string | undefined;
15
16
  const fork = input.fork === true;
16
17
  const role = (input.role as string | undefined) ?? undefined;
18
+ const inferenceProfile = input.inference_profile;
17
19
 
18
20
  // For fork mode, sendResultToUser defaults to false unless explicitly set to true.
19
21
  // For regular mode, sendResultToUser defaults to true (existing behavior).
@@ -28,6 +30,26 @@ export async function executeSubagentSpawn(
28
30
  };
29
31
  }
30
32
 
33
+ let requestedOverrideProfile: string | undefined;
34
+ let forceOverrideProfile = false;
35
+ if (inferenceProfile !== undefined) {
36
+ if (typeof inferenceProfile !== "string") {
37
+ return {
38
+ content: "Error: inference_profile must be a string",
39
+ isError: true,
40
+ };
41
+ }
42
+ const profileError = validateInferenceProfileKey(inferenceProfile);
43
+ if (profileError) {
44
+ return {
45
+ content: `Error: ${profileError}`,
46
+ isError: true,
47
+ };
48
+ }
49
+ requestedOverrideProfile = inferenceProfile;
50
+ forceOverrideProfile = true;
51
+ }
52
+
31
53
  const manager = getSubagentManager();
32
54
  const sendToClient = context.sendToClient as
33
55
  | ((msg: { type: string; [key: string]: unknown }) => void)
@@ -75,16 +97,18 @@ export async function executeSubagentSpawn(
75
97
  // `SubagentManager.spawn` forwards it back into the subagent's
76
98
  // `runAgentLoop` call as `options.overrideProfile`.
77
99
  //
78
- // Prefer the per-turn `context.overrideProfile` (populated by
79
- // `runAgentLoopImpl` from its resolved `turnOverrideProfile`) over a
80
- // row read so nested spawns inherit correctly. The current subagent's
81
- // own conversation row never has `inferenceProfile` set — its override
82
- // arrived via in-memory `SubagentConfig.overrideProfile` — and even if it
83
- // were set, `getConversationOverrideProfile` short-circuits for
84
- // background conversations and returns `undefined`. Falling back to the
85
- // row read preserves behavior for tool calls that originate outside an
86
- // agent-loop turn.
100
+ // Prefer an explicit spawn-time profile, then the per-turn
101
+ // `context.overrideProfile` (populated by `runAgentLoopImpl` from its
102
+ // resolved `turnOverrideProfile`) over a row read so nested spawns inherit
103
+ // correctly. The current subagent's own conversation row never has
104
+ // `inferenceProfile` set — its override arrived via in-memory
105
+ // `SubagentConfig.overrideProfile` — and even if it were set,
106
+ // `getConversationOverrideProfile` short-circuits for background
107
+ // conversations and returns `undefined`. Falling back to the row read
108
+ // preserves behavior for tool calls that originate outside an agent-loop
109
+ // turn.
87
110
  const inheritedOverrideProfile =
111
+ requestedOverrideProfile ??
88
112
  context.overrideProfile ??
89
113
  getConversationOverrideProfile(context.conversationId);
90
114
 
@@ -102,6 +126,7 @@ export async function executeSubagentSpawn(
102
126
  ...(inheritedOverrideProfile
103
127
  ? { overrideProfile: inheritedOverrideProfile }
104
128
  : {}),
129
+ ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
105
130
  ...(context.toolUseId ? { parentToolUseId: context.toolUseId } : {}),
106
131
  ...forkFields,
107
132
  },
@@ -157,7 +157,6 @@ const UI_SURFACE_TOOLS = new Set(["ui_show", "ui_update", "ui_dismiss"]);
157
157
 
158
158
  function requiresGuardianApprovalForActor(
159
159
  toolName: string,
160
- input: Record<string, unknown>,
161
160
  executionTarget: ExecutionTarget,
162
161
  ): boolean {
163
162
  // UI surface tools are passive, user-visible operations (cards, forms,
@@ -170,7 +169,7 @@ function requiresGuardianApprovalForActor(
170
169
  // Side-effect tools always require guardian approval for untrusted actors.
171
170
  // Read-only host execution is also blocked because it can leak sensitive
172
171
  // local information (e.g. shell/file reads).
173
- return isSideEffectTool(toolName, input) || executionTarget === "host";
172
+ return isSideEffectTool(toolName) || executionTarget === "host";
174
173
  }
175
174
 
176
175
  function guardianApprovalDeniedMessage(
@@ -320,7 +319,6 @@ export class ToolApprovalHandler {
320
319
 
321
320
  const guardianApprovalRequired = requiresGuardianApprovalForActor(
322
321
  name,
323
- input,
324
322
  executionTarget,
325
323
  );
326
324
 
@@ -15,7 +15,6 @@ import { askQuestionTool } from "./ask-question/ask-question-tool.js";
15
15
  import { makeAuthenticatedRequestTool } from "./credential-execution/make-authenticated-request.js";
16
16
  import { manageSecureCommandTool } from "./credential-execution/manage-secure-command-tool.js";
17
17
  import { runAuthenticatedCommandTool } from "./credential-execution/run-authenticated-command.js";
18
- import { credentialStoreTool } from "./credentials/vault.js";
19
18
  import { fileEditTool } from "./filesystem/edit.js";
20
19
  import { fileListTool } from "./filesystem/list.js";
21
20
  import { fileReadTool } from "./filesystem/read.js";
@@ -90,7 +89,6 @@ export const explicitTools: ToolDefinition[] = [
90
89
  // Always-explicit tools
91
90
  rememberTool,
92
91
  recallTool,
93
- credentialStoreTool,
94
92
  notifyParentTool,
95
93
  askQuestionTool,
96
94
  // NOTE: external skill tools (registered via registerExternalTools in
@@ -59,6 +59,14 @@ function proxyExecute(toolName: string) {
59
59
  };
60
60
  }
61
61
 
62
+ if (toolName === "ui_update" && isEmptyUpdate(input)) {
63
+ return {
64
+ content:
65
+ 'Error: ui_update received an empty `data` payload, so the surface was unchanged — the user still sees its previous state. The provided data is merged into the surface\'s current data, and merging nothing is a no-op. To advance a task_progress card, send the full step list: ui_update { surface_id: "<id>", data: { templateData: { steps: [{ label: "<step>", status: "completed" }, { label: "<step>", status: "in_progress" }] } } }. Resend ui_update with the fields you intend to change under `data`.',
66
+ isError: true,
67
+ };
68
+ }
69
+
62
70
  if (!context.proxyToolResolver) {
63
71
  return {
64
72
  content: `No proxy resolver configured for proxy tool "${toolName}". This tool requires an external resolver (e.g. a connected macOS client).`,
@@ -98,6 +106,36 @@ function isTaskProgressCardShow(input: Record<string, unknown>): boolean {
98
106
  return data?.template === "task_progress";
99
107
  }
100
108
 
109
+ /**
110
+ * A `ui_update` whose `data` merge would change nothing: missing, not an
111
+ * object, or containing only (recursively) empty objects. Merging such a
112
+ * payload is a silent no-op — the surface keeps its prior state while the
113
+ * client still reports "Surface updated" — so the model never learns its
114
+ * update was hollow and a live card (e.g. task_progress) appears frozen.
115
+ * Arrays (e.g. `templateData.steps`) and any non-empty primitive leaf count
116
+ * as content.
117
+ */
118
+ function isEmptyUpdate(input: Record<string, unknown>): boolean {
119
+ const data = asRecord(input.data);
120
+ return data === null || !hasContent(data);
121
+ }
122
+
123
+ function hasContent(value: unknown): boolean {
124
+ if (value === null || value === undefined) {
125
+ return false;
126
+ }
127
+ if (Array.isArray(value)) {
128
+ return value.length > 0;
129
+ }
130
+ if (typeof value === "object") {
131
+ return Object.values(value).some(hasContent);
132
+ }
133
+ if (typeof value === "string") {
134
+ return value.trim().length > 0;
135
+ }
136
+ return true;
137
+ }
138
+
101
139
  function isEmptyDynamicPage(input: Record<string, unknown>): boolean {
102
140
  if (input.surface_type !== "dynamic_page") {
103
141
  return false;
@@ -311,7 +349,7 @@ export const uiShowTool = {
311
349
  // ui_update
312
350
  // ---------------------------------------------------------------------------
313
351
 
314
- const uiUpdateTool = {
352
+ export const uiUpdateTool = {
315
353
  name: "ui_update",
316
354
  description:
317
355
  "Update an existing surface's data. The provided data object is merged into the surface's current data.\n" +
@@ -11,11 +11,9 @@ mock.module("../../util/logger.js", () => ({
11
11
  }));
12
12
 
13
13
  // ── Mutable mock state ────────────────────────────────────────────────
14
- // `flagEnabled` toggles the `workflows` feature flag; `configThrows`
15
- // simulates config not yet loaded (test-setup race). The run-manager mock
16
- // records the args of the last `start()` call for assertion.
14
+ // `configThrows` simulates config not yet loaded (test-setup race). The
15
+ // run-manager mock records the args of the last `start()` call for assertion.
17
16
 
18
- let flagEnabled = true;
19
17
  let configThrows = false;
20
18
 
21
19
  const realLoader = await import("../../config/loader.js");
@@ -35,13 +33,6 @@ mock.module("../../config/loader.js", () => ({
35
33
  }) as unknown as ReturnType<typeof realLoader.loadConfig>,
36
34
  }));
37
35
 
38
- const realFlags = await import("../../config/assistant-feature-flags.js");
39
- mock.module("../../config/assistant-feature-flags.js", () => ({
40
- ...realFlags,
41
- isAssistantFeatureFlagEnabled: (key: string) =>
42
- key === "workflows" ? flagEnabled : false,
43
- }));
44
-
45
36
  // No live conversation in tests — the tool falls back to a synthetic trust
46
37
  // context built from the tool context's trustClass.
47
38
  const realRegistry = await import("../../daemon/conversation-registry.js");
@@ -92,7 +83,6 @@ function makeContext(): Parameters<typeof executeRunWorkflow>[1] {
92
83
  }
93
84
 
94
85
  beforeEach(() => {
95
- flagEnabled = true;
96
86
  configThrows = false;
97
87
  startThrows = null;
98
88
  lastStartArgs = null;
@@ -105,10 +95,10 @@ beforeEach(() => {
105
95
  });
106
96
 
107
97
  describe("workflow tools are served by the workflows skill", () => {
108
- // The cutover moved run_workflow / manage_workflows out of the always-on tool
109
- // manifest into the flag-gated `workflows` bundled skill (loaded via
110
- // skill_load, invoked via skill_execute). The skill manifest is the
111
- // source of truth; assert it declares both as in-process host tools.
98
+ // run_workflow / manage_workflows are served by the `workflows` bundled skill
99
+ // (loaded via skill_load, invoked via skill_execute), not the always-on tool
100
+ // manifest. The skill manifest is the source of truth; assert it declares both
101
+ // as in-process host tools.
112
102
  test("the workflows skill TOOLS.json declares both host-executed tools", async () => {
113
103
  const { readFileSync } = await import("node:fs");
114
104
  const { join } = await import("node:path");
@@ -211,13 +201,13 @@ describe("run_workflow launch", () => {
211
201
  });
212
202
 
213
203
  test("surfaces a run-manager start error as a tool error", async () => {
214
- startThrows = new Error("Workflows are not enabled.");
204
+ startThrows = new Error("run manager exploded");
215
205
  const res = await executeRunWorkflow(
216
206
  { script: "export const meta = {};" },
217
207
  makeContext(),
218
208
  );
219
209
  expect(res.isError).toBe(true);
220
- expect(res.content).toContain("Workflows are not enabled.");
210
+ expect(res.content).toContain("run manager exploded");
221
211
  });
222
212
  });
223
213
 
@@ -402,8 +402,8 @@ export function getSkillRuntimePath(
402
402
  *
403
403
  * Resolution order:
404
404
  *
405
- * 1. macOS `.app` bundle: `Contents/Resources/bun` — shipped by
406
- * `clients/macos/build.sh` at a version that matches `.tool-versions`.
405
+ * 1. macOS `.app` bundle: `Contents/Resources/bun` — bundled at a version
406
+ * that matches `.tool-versions`.
407
407
  * 2. Next-to-binary: `<execDir>/bun` for Docker/generic compiled layouts
408
408
  * that stage a bun binary alongside the daemon (PR 29 wires this up).
409
409
  *
@@ -29,9 +29,8 @@
29
29
  * (If host execution is ever wanted, the deliberate path is to thread the
30
30
  * originating tool context through the engine — not to relax this gate.)
31
31
  *
32
- * This module is pure logic: it performs no feature-flag checks and no I/O
33
- * beyond the synchronous tool-registry lookup. The `workflows` flag gates the
34
- * callers, not this code.
32
+ * This module is pure logic: it performs no I/O beyond the synchronous
33
+ * tool-registry lookup.
35
34
  */
36
35
 
37
36
  import { z } from "zod";
@@ -22,7 +22,6 @@ import {
22
22
  WorkflowRunCapError,
23
23
  WorkflowRunManager,
24
24
  type WorkflowRunManagerDeps,
25
- WorkflowsDisabledError,
26
25
  } from "./run-manager.js";
27
26
 
28
27
  const TRUST: TrustContext = {
@@ -144,7 +143,6 @@ interface EngineResult {
144
143
  * deterministically), an in-memory journal, and spies for broadcast + wake.
145
144
  */
146
145
  function makeHarness(opts?: {
147
- flagEnabled?: boolean;
148
146
  maxConcurrentRuns?: number;
149
147
  /** Custom engine impl; defaults to the deferred-resolver fake. */
150
148
  engine?: WorkflowRunManagerDeps["executeWorkflow"];
@@ -190,7 +188,6 @@ function makeHarness(opts?: {
190
188
  }) as unknown as WorkflowRunManagerDeps["leafRunner"],
191
189
  journal: fake.journal,
192
190
  getConfig: () => makeConfig(opts?.maxConcurrentRuns),
193
- isFlagEnabled: () => opts?.flagEnabled ?? true,
194
191
  wake: (async (wakeOpts) => {
195
192
  wakes.push({
196
193
  conversationId: wakeOpts.conversationId,
@@ -227,22 +224,6 @@ function makeHarness(opts?: {
227
224
  };
228
225
  }
229
226
 
230
- describe("WorkflowRunManager.start — flag gate", () => {
231
- test("flag off → start throws and the engine is never invoked", () => {
232
- const h = makeHarness({ flagEnabled: false });
233
- expect(() =>
234
- h.manager.start({
235
- scriptSource: "export const meta = { name: 'x', description: 'y' }",
236
- args: {},
237
- manifest: { tools: [], hostFunctions: [], persona: false },
238
- trustContext: TRUST,
239
- }),
240
- ).toThrow(WorkflowsDisabledError);
241
- expect(h.executeCalls).toHaveLength(0);
242
- expect(h.fake.rows.size).toBe(0);
243
- });
244
- });
245
-
246
227
  describe("WorkflowRunManager.start — concurrent-run cap", () => {
247
228
  test("the (N+1)th concurrent start is rejected", () => {
248
229
  const h = makeHarness({ maxConcurrentRuns: 2 });
@@ -702,10 +683,4 @@ describe("WorkflowRunManager.resume", () => {
702
683
 
703
684
  expect(() => h.manager.resume("run-x")).toThrow(WorkflowRunCapError);
704
685
  });
705
-
706
- test("resume is rejected when the flag is off", () => {
707
- const h = makeHarness({ flagEnabled: false });
708
- seedRun(h, { id: "run-x", status: "interrupted" });
709
- expect(() => h.manager.resume("run-x")).toThrow(WorkflowsDisabledError);
710
- });
711
686
  });
@@ -5,8 +5,6 @@
5
5
  * and routes (later PRs) drive. It owns everything the raw {@link executeWorkflow}
6
6
  * engine deliberately does NOT:
7
7
  *
8
- * - **Feature-flag gate.** `start` hard-fails with {@link WorkflowsDisabledError}
9
- * when the `workflows` flag is off, BEFORE any engine code path is reachable.
10
8
  * - **Concurrent-run cap.** At most `config.workflows.maxConcurrentRuns` runs
11
9
  * may be in flight; the (N+1)th `start` throws {@link WorkflowRunCapError}.
12
10
  * - **Async launch.** `start` resolves capabilities, creates the journal run
@@ -28,7 +26,6 @@
28
26
 
29
27
  import { createHash, randomUUID } from "node:crypto";
30
28
 
31
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
32
29
  import { getConfig } from "../config/loader.js";
33
30
  import type { AssistantConfig } from "../config/schema.js";
34
31
  import {
@@ -58,15 +55,6 @@ const log = getLogger("workflow-run-manager");
58
55
  /** Source tag for the completion wake (shows up in the wake's structured log). */
59
56
  const WORKFLOW_WAKE_SOURCE = "workflow_completed";
60
57
 
61
- /** Thrown by `start` when the `workflows` feature flag is disabled. */
62
- export class WorkflowsDisabledError extends Error {
63
- readonly code = "workflows_disabled" as const;
64
- constructor() {
65
- super("Workflows are not enabled.");
66
- this.name = "WorkflowsDisabledError";
67
- }
68
- }
69
-
70
58
  /** Thrown by `start` when the concurrent-run cap is already reached. */
71
59
  export class WorkflowRunCapError extends Error {
72
60
  readonly code = "workflow_run_cap_exceeded" as const;
@@ -141,7 +129,6 @@ export interface WorkflowRunManagerDeps {
141
129
  leafRunner: typeof runLeaf;
142
130
  journal: typeof journalStore;
143
131
  getConfig: () => AssistantConfig;
144
- isFlagEnabled: (config: AssistantConfig) => boolean;
145
132
  wake: typeof wakeAgentForOpportunity;
146
133
  broadcast: typeof broadcastMessage;
147
134
  newRunId: () => string;
@@ -155,8 +142,6 @@ function defaultDeps(): WorkflowRunManagerDeps {
155
142
  leafRunner: runLeaf,
156
143
  journal: journalStore,
157
144
  getConfig,
158
- isFlagEnabled: (config) =>
159
- isAssistantFeatureFlagEnabled("workflows", config),
160
145
  wake: wakeAgentForOpportunity,
161
146
  broadcast: broadcastMessage,
162
147
  newRunId: () => randomUUID(),
@@ -178,18 +163,14 @@ export class WorkflowRunManager {
178
163
  }
179
164
 
180
165
  /**
181
- * Launch a workflow run. Gates on the `workflows` flag and the concurrent-run
182
- * cap (both throw before any engine code is reachable), resolves capabilities,
166
+ * Launch a workflow run. Gates on the concurrent-run cap (which throws before
167
+ * any engine code is reachable), resolves capabilities,
183
168
  * creates the journal run row, and kicks off {@link executeWorkflow}
184
169
  * asynchronously. Returns the `runId` immediately — completion is surfaced via
185
170
  * events and a conversation wake.
186
171
  */
187
172
  start(opts: StartWorkflowOptions): { runId: string } {
188
173
  const config = this.deps.getConfig();
189
- if (!this.deps.isFlagEnabled(config)) {
190
- throw new WorkflowsDisabledError();
191
- }
192
-
193
174
  const limit = config.workflows.maxConcurrentRuns;
194
175
  if (this.inflight.size >= limit) {
195
176
  throw new WorkflowRunCapError(limit);
@@ -282,9 +263,6 @@ export class WorkflowRunManager {
282
263
  */
283
264
  resume(runId: string): { runId: string } {
284
265
  const config = this.deps.getConfig();
285
- if (!this.deps.isFlagEnabled(config)) {
286
- throw new WorkflowsDisabledError();
287
- }
288
266
 
289
267
  if (this.inflight.has(runId)) {
290
268
  throw new WorkflowResumeNotPossibleError(runId, "in_flight");