@vellumai/assistant 0.8.12 → 0.9.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Unit tests for the ephemeral workflow leaf runner.
3
+ *
4
+ * Covers:
5
+ * - Schema path: validates and extracts forced-tool-choice output + usage.
6
+ * - Profile override: an unknown profile throws WorkflowUnknownProfileError;
7
+ * a known profile is forwarded as `overrideProfile`.
8
+ * - Abort: a pre-aborted signal propagates out of the provider call.
9
+ * - Tool path: executes ONLY the supplied toolset and returns usage + tool-call
10
+ * counts.
11
+ * - No persistence: a leaf run creates no conversation rows and no workspace
12
+ * files.
13
+ */
14
+
15
+ import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ import { z } from "zod";
21
+
22
+ import { makeMockLogger } from "../__tests__/helpers/mock-logger.js";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Mocks — defined before importing the module under test.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ mock.module("../util/logger.js", () => ({
29
+ getLogger: () => makeMockLogger(),
30
+ }));
31
+
32
+ const TEST_PROFILES = { balanced: {}, "cost-optimized": {} };
33
+
34
+ // Mutable config the mocked `getConfig` returns. Tests reassign this to drive
35
+ // the active-profile and memory-enabled branches.
36
+ let testConfig: {
37
+ llm: { profiles: Record<string, unknown>; activeProfile?: string };
38
+ memory: { enabled: boolean };
39
+ } = {
40
+ llm: { profiles: TEST_PROFILES },
41
+ memory: { enabled: false },
42
+ };
43
+
44
+ mock.module("../config/loader.js", () => ({
45
+ getConfig: () => testConfig,
46
+ }));
47
+
48
+ // Persona path — identity system prompt. A sentinel marks the persona prompt
49
+ // so tests can assert identity was assembled (vs. the anonymous task prompt).
50
+ const PERSONA_IDENTITY_PROMPT = "PERSONA_IDENTITY_SYSTEM_PROMPT";
51
+ const buildSystemPrompt = mock(() => PERSONA_IDENTITY_PROMPT);
52
+ mock.module("../prompts/system-prompt.js", () => ({ buildSystemPrompt }));
53
+
54
+ // Persona path — memory-injection pipeline. The mock records construction and
55
+ // `prepareMemory` calls, and prepends a sentinel `<memory>` block so tests can
56
+ // assert the pipeline ran and its output reached the provider.
57
+ const MEMORY_BLOCK_TEXT = "INJECTED_MEMORY_BLOCK";
58
+ let graphMemoryInstances = 0;
59
+ let prepareMemoryCalls = 0;
60
+ let disposeCalls = 0;
61
+ class MockConversationGraphMemory {
62
+ constructor(_conversationId: string) {
63
+ graphMemoryInstances += 1;
64
+ }
65
+ async prepareMemory(
66
+ messages: Array<{ role: string; content: unknown[] }>,
67
+ ): Promise<{ runMessages: unknown[] }> {
68
+ prepareMemoryCalls += 1;
69
+ const [first, ...rest] = messages;
70
+ const injected = {
71
+ role: first?.role ?? "user",
72
+ content: [
73
+ { type: "text", text: MEMORY_BLOCK_TEXT },
74
+ ...(first?.content ?? []),
75
+ ],
76
+ };
77
+ return { runMessages: [injected, ...rest] };
78
+ }
79
+ dispose(): void {
80
+ disposeCalls += 1;
81
+ }
82
+ }
83
+ mock.module("../memory/graph/conversation-graph-memory.js", () => ({
84
+ ConversationGraphMemory: MockConversationGraphMemory,
85
+ }));
86
+
87
+ mock.module("../runtime/assistant-event-hub.js", () => ({
88
+ broadcastMessage: () => {},
89
+ }));
90
+
91
+ // Captures the most recent provider resolution + sendMessage invocation so
92
+ // tests can assert on call site, overrideProfile, tool_choice, and signal.
93
+ interface SendCall {
94
+ messages: unknown;
95
+ options: {
96
+ tools?: Array<{ name: string; input_schema?: unknown }>;
97
+ systemPrompt?: string;
98
+ config?: {
99
+ callSite?: string;
100
+ overrideProfile?: string;
101
+ tool_choice?: { type: string; name: string };
102
+ };
103
+ signal?: AbortSignal;
104
+ };
105
+ }
106
+
107
+ let lastResolveOpts: { overrideProfile?: string } | undefined;
108
+ let lastSendCall: SendCall | undefined;
109
+
110
+ // Programmable provider response. Each test sets this before invoking runLeaf.
111
+ // For multi-turn tool-path runs it can be a queue consumed per call.
112
+ let responseQueue: Array<unknown> = [];
113
+
114
+ const sendMessage = mock(
115
+ async (messages: unknown, options: SendCall["options"]): Promise<unknown> => {
116
+ lastSendCall = { messages, options };
117
+ if (options.signal?.aborted) {
118
+ throw new DOMException("Aborted", "AbortError");
119
+ }
120
+ const next = responseQueue.shift();
121
+ if (next === undefined) {
122
+ throw new Error("test: responseQueue exhausted");
123
+ }
124
+ return next;
125
+ },
126
+ );
127
+
128
+ const getConfiguredProvider = mock(
129
+ async (_callSite: string, opts: { overrideProfile?: string } = {}) => {
130
+ lastResolveOpts = opts;
131
+ return { name: "test-provider", sendMessage };
132
+ },
133
+ );
134
+
135
+ mock.module("../providers/provider-send-message.js", () => ({
136
+ getConfiguredProvider,
137
+ // Real helper re-exported by the module under test; reimplement minimally.
138
+ extractToolUse: (response: { content: Array<{ type: string }> }) =>
139
+ response.content.find((b) => b.type === "tool_use"),
140
+ }));
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Module under test (after mocks).
144
+ // ---------------------------------------------------------------------------
145
+
146
+ import { getDb } from "../memory/db-connection.js";
147
+ import { initializeDb } from "../memory/db-init.js";
148
+ import { conversations } from "../memory/schema.js";
149
+ import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
150
+ import { getWorkspaceDir } from "../util/platform.js";
151
+ import { runLeaf, WorkflowUnknownProfileError } from "./leaf-runner.js";
152
+
153
+ initializeDb();
154
+
155
+ const trustContext = {
156
+ sourceChannel: "vellum" as const,
157
+ trustClass: "guardian" as const,
158
+ };
159
+
160
+ function countConversations(): number {
161
+ return getDb().select().from(conversations).all().length;
162
+ }
163
+
164
+ beforeEach(() => {
165
+ sendMessage.mockClear();
166
+ getConfiguredProvider.mockClear();
167
+ buildSystemPrompt.mockClear();
168
+ lastResolveOpts = undefined;
169
+ lastSendCall = undefined;
170
+ responseQueue = [];
171
+ graphMemoryInstances = 0;
172
+ prepareMemoryCalls = 0;
173
+ disposeCalls = 0;
174
+ testConfig = {
175
+ llm: { profiles: TEST_PROFILES },
176
+ memory: { enabled: false },
177
+ };
178
+ });
179
+
180
+ describe("runLeaf — schema path", () => {
181
+ test("validates and extracts forced-tool-choice output + usage", async () => {
182
+ const schema = z.object({ answer: z.string(), score: z.number() });
183
+ responseQueue = [
184
+ {
185
+ content: [
186
+ {
187
+ type: "tool_use",
188
+ name: "emit_result",
189
+ id: "tu-1",
190
+ input: { answer: "yes", score: 7 },
191
+ },
192
+ ],
193
+ model: "test",
194
+ usage: { inputTokens: 42, outputTokens: 9 },
195
+ stopReason: "tool_use",
196
+ },
197
+ ];
198
+
199
+ const before = countConversations();
200
+ const result = await runLeaf({
201
+ prompt: "Answer the question.",
202
+ schema,
203
+ trustContext,
204
+ });
205
+
206
+ expect(result.output).toEqual({ answer: "yes", score: 7 });
207
+ expect(result.inputTokens).toBe(42);
208
+ expect(result.outputTokens).toBe(9);
209
+ expect(result.toolCallCount).toBe(0);
210
+
211
+ // Forced tool choice on the synthetic tool.
212
+ expect(lastSendCall?.options.config?.tool_choice).toEqual({
213
+ type: "tool",
214
+ name: "emit_result",
215
+ });
216
+ const tool = lastSendCall?.options.tools?.[0];
217
+ expect(tool?.name).toBe("emit_result");
218
+ // input_schema is derived from the Zod schema (no $schema key).
219
+ expect(tool?.input_schema).toBeDefined();
220
+ expect(
221
+ (tool?.input_schema as Record<string, unknown>).$schema,
222
+ ).toBeUndefined();
223
+
224
+ // No conversation rows created.
225
+ expect(countConversations()).toBe(before);
226
+ });
227
+
228
+ test("accepts a plain JSON Schema object (sandbox-marshaled) directly", async () => {
229
+ // A workflow script can't hold a Zod object, so its `schema` reaches
230
+ // runLeaf as a JSON-marshaled JSON Schema object. The runner duck-types it,
231
+ // uses it directly as the forced-tool input_schema, and validates the
232
+ // returned input via z.fromJSONSchema.
233
+ const jsonSchema = {
234
+ type: "object",
235
+ properties: { answer: { type: "string" }, score: { type: "number" } },
236
+ required: ["answer", "score"],
237
+ additionalProperties: false,
238
+ };
239
+ responseQueue = [
240
+ {
241
+ content: [
242
+ {
243
+ type: "tool_use",
244
+ name: "emit_result",
245
+ id: "tu-json",
246
+ input: { answer: "yes", score: 7 },
247
+ },
248
+ ],
249
+ model: "test",
250
+ usage: { inputTokens: 5, outputTokens: 2 },
251
+ stopReason: "tool_use",
252
+ },
253
+ ];
254
+
255
+ const result = await runLeaf({
256
+ prompt: "Answer the question.",
257
+ schema: jsonSchema,
258
+ trustContext,
259
+ });
260
+
261
+ expect(result.output).toEqual({ answer: "yes", score: 7 });
262
+ // The JSON Schema is used verbatim as the synthetic tool's input_schema.
263
+ const tool = lastSendCall?.options.tools?.[0];
264
+ expect(tool?.name).toBe("emit_result");
265
+ expect(tool?.input_schema).toMatchObject({
266
+ type: "object",
267
+ properties: { answer: { type: "string" }, score: { type: "number" } },
268
+ });
269
+ });
270
+
271
+ test("throws when JSON-Schema-path output fails validation", async () => {
272
+ const jsonSchema = {
273
+ type: "object",
274
+ properties: { answer: { type: "string" } },
275
+ required: ["answer"],
276
+ additionalProperties: false,
277
+ };
278
+ responseQueue = [
279
+ {
280
+ content: [
281
+ {
282
+ type: "tool_use",
283
+ name: "emit_result",
284
+ id: "tu-json-bad",
285
+ input: { answer: 5 },
286
+ },
287
+ ],
288
+ model: "test",
289
+ usage: { inputTokens: 1, outputTokens: 1 },
290
+ stopReason: "tool_use",
291
+ },
292
+ ];
293
+
294
+ await expect(
295
+ runLeaf({ prompt: "x", schema: jsonSchema, trustContext }),
296
+ ).rejects.toThrow(/schema validation/i);
297
+ });
298
+
299
+ test("throws when provider output fails schema validation", async () => {
300
+ const schema = z.object({ answer: z.string() });
301
+ responseQueue = [
302
+ {
303
+ content: [
304
+ {
305
+ type: "tool_use",
306
+ name: "emit_result",
307
+ id: "tu",
308
+ input: { answer: 5 },
309
+ },
310
+ ],
311
+ model: "test",
312
+ usage: { inputTokens: 1, outputTokens: 1 },
313
+ stopReason: "tool_use",
314
+ },
315
+ ];
316
+
317
+ await expect(
318
+ runLeaf({ prompt: "x", schema, trustContext }),
319
+ ).rejects.toThrow(/schema validation/i);
320
+ });
321
+ });
322
+
323
+ describe("runLeaf — profile override", () => {
324
+ test("unknown profile throws WorkflowUnknownProfileError", async () => {
325
+ const schema = z.object({ a: z.string() });
326
+ await expect(
327
+ runLeaf({ prompt: "x", schema, profile: "nope", trustContext }),
328
+ ).rejects.toBeInstanceOf(WorkflowUnknownProfileError);
329
+ // Provider never resolved — failed before the call.
330
+ expect(getConfiguredProvider).not.toHaveBeenCalled();
331
+ });
332
+
333
+ test("known profile is forwarded as overrideProfile", async () => {
334
+ const schema = z.object({ a: z.string() });
335
+ responseQueue = [
336
+ {
337
+ content: [
338
+ {
339
+ type: "tool_use",
340
+ name: "emit_result",
341
+ id: "t",
342
+ input: { a: "ok" },
343
+ },
344
+ ],
345
+ model: "test",
346
+ usage: { inputTokens: 1, outputTokens: 1 },
347
+ stopReason: "tool_use",
348
+ },
349
+ ];
350
+
351
+ await runLeaf({
352
+ prompt: "x",
353
+ schema,
354
+ profile: "cost-optimized",
355
+ trustContext,
356
+ });
357
+ expect(lastResolveOpts?.overrideProfile).toBe("cost-optimized");
358
+ // The override must ALSO ride on the per-call send config. The schema path
359
+ // sets `callSite`, and CallSiteConfiguredProvider only injects its stored
360
+ // override when the per-call config omits `callSite` — so without an
361
+ // explicit pass-through the leaf silently resolves the default profile.
362
+ expect(lastSendCall?.options.config?.callSite).toBe("workflowLeaf");
363
+ expect(lastSendCall?.options.config?.overrideProfile).toBe(
364
+ "cost-optimized",
365
+ );
366
+ });
367
+
368
+ test("no profile sends config without an overrideProfile", async () => {
369
+ const schema = z.object({ a: z.string() });
370
+ responseQueue = [
371
+ {
372
+ content: [
373
+ {
374
+ type: "tool_use",
375
+ name: "emit_result",
376
+ id: "t",
377
+ input: { a: "ok" },
378
+ },
379
+ ],
380
+ model: "test",
381
+ usage: { inputTokens: 1, outputTokens: 1 },
382
+ stopReason: "tool_use",
383
+ },
384
+ ];
385
+
386
+ await runLeaf({ prompt: "x", schema, trustContext });
387
+ expect(lastResolveOpts?.overrideProfile).toBeUndefined();
388
+ expect(lastSendCall?.options.config?.overrideProfile).toBeUndefined();
389
+ });
390
+ });
391
+
392
+ describe("runLeaf — abort", () => {
393
+ test("a pre-aborted signal propagates", async () => {
394
+ const schema = z.object({ a: z.string() });
395
+ const controller = new AbortController();
396
+ controller.abort();
397
+
398
+ await expect(
399
+ runLeaf({
400
+ prompt: "x",
401
+ schema,
402
+ signal: controller.signal,
403
+ trustContext,
404
+ }),
405
+ ).rejects.toThrow();
406
+ });
407
+ });
408
+
409
+ describe("runLeaf — tool path", () => {
410
+ test("executes only the supplied toolset and returns usage + tool-call counts", async () => {
411
+ const calls: Array<{ name: string; input: unknown }> = [];
412
+ let toolWorkingDir: string | undefined;
413
+
414
+ const makeTool = (name: string): Tool => ({
415
+ name,
416
+ description: `Tool ${name}`,
417
+ category: "test",
418
+ defaultRiskLevel: "low" as never,
419
+ executionTarget: "sandbox",
420
+ input_schema: { type: "object", properties: {}, required: [] },
421
+ async execute(
422
+ input: Record<string, unknown>,
423
+ ctx: ToolContext,
424
+ ): Promise<ToolExecutionResult> {
425
+ calls.push({ name, input });
426
+ toolWorkingDir = ctx.workingDir;
427
+ return { content: `${name} ran`, isError: false };
428
+ },
429
+ });
430
+
431
+ const allowed = makeTool("allowed_tool");
432
+
433
+ // Turn 1: model calls the allowed tool. Turn 2: model emits final text.
434
+ responseQueue = [
435
+ {
436
+ content: [
437
+ {
438
+ type: "tool_use",
439
+ name: "allowed_tool",
440
+ id: "tu-1",
441
+ input: { q: "hi" },
442
+ },
443
+ ],
444
+ model: "test",
445
+ usage: { inputTokens: 10, outputTokens: 4 },
446
+ stopReason: "tool_use",
447
+ },
448
+ {
449
+ content: [{ type: "text", text: "All done." }],
450
+ model: "test",
451
+ usage: { inputTokens: 6, outputTokens: 3 },
452
+ stopReason: "end_turn",
453
+ },
454
+ ];
455
+
456
+ const before = countConversations();
457
+ const result = await runLeaf({
458
+ prompt: "Use the tool.",
459
+ tools: [allowed],
460
+ trustContext,
461
+ });
462
+
463
+ // Only the supplied tool ran.
464
+ expect(calls).toEqual([{ name: "allowed_tool", input: { q: "hi" } }]);
465
+ expect(result.output).toBe("All done.");
466
+ expect(result.toolCallCount).toBe(1);
467
+ // Usage summed across both provider calls.
468
+ expect(result.inputTokens).toBe(16);
469
+ expect(result.outputTokens).toBe(7);
470
+
471
+ // The loop restricts execution to exactly the supplied tools.
472
+ expect(lastSendCall?.options.tools?.map((t) => t.name)).toEqual([
473
+ "allowed_tool",
474
+ ]);
475
+
476
+ // Leaf file tools must be bound to the WORKSPACE, not the daemon's cwd
477
+ // (which is the install/binary dir) — otherwise sandbox-policy'd file tools
478
+ // resolve/reject workspace paths incorrectly.
479
+ expect(toolWorkingDir).toBe(getWorkspaceDir());
480
+ expect(toolWorkingDir).not.toBe(process.cwd());
481
+
482
+ // No conversation rows created.
483
+ expect(countConversations()).toBe(before);
484
+ });
485
+
486
+ test("a leaf with no schema and an empty tool set runs the tool path (no throw)", async () => {
487
+ // An empty resolved tool set with no schema must NOT fall through to the
488
+ // schema path (where schemaToInputSchema(undefined) throws) — it runs the
489
+ // agent loop with zero tools, i.e. a plain text leaf. This is the
490
+ // momentarily-empty-baseline case (schedule fires before initializeTools()).
491
+ responseQueue = [
492
+ {
493
+ content: [{ type: "text", text: "plain text leaf" }],
494
+ model: "test",
495
+ usage: { inputTokens: 3, outputTokens: 2 },
496
+ stopReason: "end_turn",
497
+ },
498
+ ];
499
+
500
+ const result = await runLeaf({
501
+ prompt: "no tools",
502
+ tools: [],
503
+ trustContext,
504
+ });
505
+ expect(result.output).toBe("plain text leaf");
506
+ expect(result.toolCallCount).toBe(0);
507
+ // The send went out with an empty tool list (the agent-loop path), not a
508
+ // forced-tool-choice schema call.
509
+ expect(lastSendCall?.options.tools ?? []).toEqual([]);
510
+ expect(lastSendCall?.options.config?.tool_choice).toBeUndefined();
511
+ });
512
+ });
513
+
514
+ describe("runLeaf — no persistence", () => {
515
+ test("creates no conversation rows and no workspace files", async () => {
516
+ const tmp = mkdtempSync(join(tmpdir(), "leaf-runner-test-"));
517
+ try {
518
+ const before = countConversations();
519
+ const schema = z.object({ a: z.string() });
520
+ responseQueue = [
521
+ {
522
+ content: [
523
+ {
524
+ type: "tool_use",
525
+ name: "emit_result",
526
+ id: "t",
527
+ input: { a: "ok" },
528
+ },
529
+ ],
530
+ model: "test",
531
+ usage: { inputTokens: 1, outputTokens: 1 },
532
+ stopReason: "tool_use",
533
+ },
534
+ ];
535
+
536
+ await runLeaf({ prompt: "x", schema, trustContext });
537
+
538
+ expect(countConversations()).toBe(before);
539
+ // The leaf must not have written anything into the temp dir.
540
+ expect(existsSync(tmp)).toBe(true);
541
+ expect(readdirSync(tmp)).toEqual([]);
542
+ } finally {
543
+ rmSync(tmp, { recursive: true, force: true });
544
+ }
545
+ });
546
+ });
547
+
548
+ describe("runLeaf — persona path", () => {
549
+ const personaResponse = {
550
+ content: [
551
+ { type: "tool_use", name: "emit_result", id: "p", input: { a: "ok" } },
552
+ ],
553
+ model: "test",
554
+ usage: { inputTokens: 3, outputTokens: 2 },
555
+ stopReason: "tool_use",
556
+ };
557
+
558
+ test("injects identity system prompt and runs the memory pipeline", async () => {
559
+ testConfig = {
560
+ llm: { profiles: TEST_PROFILES, activeProfile: "balanced" },
561
+ memory: { enabled: true },
562
+ };
563
+ const schema = z.object({ a: z.string() });
564
+ responseQueue = [personaResponse];
565
+
566
+ const before = countConversations();
567
+ await runLeaf({
568
+ prompt: "Draft a reply.",
569
+ schema,
570
+ persona: true,
571
+ trustContext,
572
+ });
573
+
574
+ // Identity system prompt assembled and handed to the provider.
575
+ expect(buildSystemPrompt).toHaveBeenCalledTimes(1);
576
+ expect(lastSendCall?.options.systemPrompt).toBe(PERSONA_IDENTITY_PROMPT);
577
+
578
+ // Memory pipeline ran (constructed, queried, and disposed) and its
579
+ // injected block reached the provider as the user message's first block.
580
+ expect(graphMemoryInstances).toBe(1);
581
+ expect(prepareMemoryCalls).toBe(1);
582
+ expect(disposeCalls).toBe(1);
583
+ const messages = lastSendCall?.messages as Array<{
584
+ content: Array<{ type: string; text?: string }>;
585
+ }>;
586
+ expect(messages[0]?.content[0]?.text).toBe(MEMORY_BLOCK_TEXT);
587
+
588
+ // No conversation row — persona keeps the no-persistence guarantee.
589
+ expect(countConversations()).toBe(before);
590
+ });
591
+
592
+ test("resolves the workspace active profile by default", async () => {
593
+ testConfig = {
594
+ llm: { profiles: TEST_PROFILES, activeProfile: "balanced" },
595
+ memory: { enabled: true },
596
+ };
597
+ const schema = z.object({ a: z.string() });
598
+ responseQueue = [personaResponse];
599
+
600
+ await runLeaf({ prompt: "x", schema, persona: true, trustContext });
601
+
602
+ expect(lastResolveOpts?.overrideProfile).toBe("balanced");
603
+ });
604
+
605
+ test("explicit profile beats the persona active-profile default", async () => {
606
+ testConfig = {
607
+ llm: { profiles: TEST_PROFILES, activeProfile: "balanced" },
608
+ memory: { enabled: true },
609
+ };
610
+ const schema = z.object({ a: z.string() });
611
+ responseQueue = [personaResponse];
612
+
613
+ await runLeaf({
614
+ prompt: "x",
615
+ schema,
616
+ persona: true,
617
+ profile: "cost-optimized",
618
+ trustContext,
619
+ });
620
+
621
+ expect(lastResolveOpts?.overrideProfile).toBe("cost-optimized");
622
+ });
623
+
624
+ test("missing active profile falls through (no override)", async () => {
625
+ // No `activeProfile` set → persona resolves no overrideProfile, deferring
626
+ // to the shipped call-site default.
627
+ testConfig = {
628
+ llm: { profiles: TEST_PROFILES },
629
+ memory: { enabled: true },
630
+ };
631
+ const schema = z.object({ a: z.string() });
632
+ responseQueue = [personaResponse];
633
+
634
+ await runLeaf({ prompt: "x", schema, persona: true, trustContext });
635
+
636
+ expect(lastResolveOpts?.overrideProfile).toBeUndefined();
637
+ });
638
+
639
+ test("anonymous leaf carries no identity and skips the memory pipeline", async () => {
640
+ testConfig = {
641
+ llm: { profiles: TEST_PROFILES, activeProfile: "balanced" },
642
+ memory: { enabled: true },
643
+ };
644
+ const schema = z.object({ a: z.string() });
645
+ responseQueue = [personaResponse];
646
+
647
+ await runLeaf({ prompt: "x", schema, trustContext });
648
+
649
+ // Anonymous path is unchanged: minimal task prompt, no identity assembly,
650
+ // no memory pipeline, no active-profile override.
651
+ expect(buildSystemPrompt).not.toHaveBeenCalled();
652
+ expect(graphMemoryInstances).toBe(0);
653
+ expect(prepareMemoryCalls).toBe(0);
654
+ expect(lastSendCall?.options.systemPrompt).not.toBe(
655
+ PERSONA_IDENTITY_PROMPT,
656
+ );
657
+ expect(lastResolveOpts?.overrideProfile).toBeUndefined();
658
+ });
659
+
660
+ test("persona leaf skips the memory pipeline for an untrusted actor", async () => {
661
+ // The personal-memory trust gate (isPersonalMemoryAllowed) blocks a remote
662
+ // untrusted (non-guardian) actor: the persona leaf still gets the identity
663
+ // system prompt, but NO private memory is retrieved — mirroring the main
664
+ // turn's `resolveTrustClass(...) === "guardian"` gate before prepareMemory.
665
+ // Force the HTTP-auth dev bypass off so trustClass actually decides.
666
+ const savedAuth = process.env.DISABLE_HTTP_AUTH;
667
+ delete process.env.DISABLE_HTTP_AUTH;
668
+ try {
669
+ testConfig = {
670
+ llm: { profiles: TEST_PROFILES, activeProfile: "balanced" },
671
+ memory: { enabled: true },
672
+ };
673
+ const schema = z.object({ a: z.string() });
674
+ responseQueue = [personaResponse];
675
+
676
+ await runLeaf({
677
+ prompt: "Draft a reply.",
678
+ schema,
679
+ persona: true,
680
+ // Remote, non-guardian actor → gate blocks personal memory.
681
+ trustContext: { sourceChannel: "slack", trustClass: "unknown" },
682
+ });
683
+
684
+ // Identity is still assembled (the assistant's stable identity is not
685
+ // private user content)…
686
+ expect(buildSystemPrompt).toHaveBeenCalledTimes(1);
687
+ expect(lastSendCall?.options.systemPrompt).toBe(PERSONA_IDENTITY_PROMPT);
688
+ // …but the memory pipeline never ran, so no private memory was injected.
689
+ expect(graphMemoryInstances).toBe(0);
690
+ expect(prepareMemoryCalls).toBe(0);
691
+ const messages = lastSendCall?.messages as Array<{
692
+ content: Array<{ type: string; text?: string }>;
693
+ }>;
694
+ expect(messages[0]?.content[0]?.text).not.toBe(MEMORY_BLOCK_TEXT);
695
+ } finally {
696
+ if (savedAuth === undefined) delete process.env.DISABLE_HTTP_AUTH;
697
+ else process.env.DISABLE_HTTP_AUTH = savedAuth;
698
+ }
699
+ });
700
+ });
701
+
702
+ afterAll(() => {
703
+ mock.restore();
704
+ });