@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,589 @@
1
+ /**
2
+ * Ephemeral leaf runner for the workflow orchestration engine.
3
+ *
4
+ * A workflow script (run in a sandbox by a later PR) fans out to many "leaf"
5
+ * agents; the engine calls {@link runLeaf} once per leaf. A leaf is deliberately
6
+ * cheap and ephemeral — it must NOT create a conversation row, a jsonl mirror, a
7
+ * title-generation job, or broadcast turn lifecycle events. By default a leaf is
8
+ * anonymous: a minimal task-scoped system prompt, no assistant identity, and no
9
+ * memory pipeline.
10
+ *
11
+ * Setting `persona: true` opts a leaf into PERSONA mode: it carries the
12
+ * assistant's identity system prompt (`buildSystemPrompt`) AND runs the same
13
+ * memory-injection pipeline a main-agent turn uses
14
+ * (`ConversationGraphMemory.prepareMemory`), so its output is authentically
15
+ * "the assistant" — e.g. drafting a reply in the assistant's voice. This is the
16
+ * costly path by design. Model resolution mirrors `mainAgent`: with no explicit
17
+ * `profile`, the workspace `activeProfile` is used as the `overrideProfile`.
18
+ * Persona leaves keep the same no-persistence guarantee — no conversation row
19
+ * is ever created.
20
+ *
21
+ * Two leaf shapes are supported:
22
+ *
23
+ * - **Schema path** (`schema` provided, no `tools`): a single forced
24
+ * `tool_choice` provider call whose synthetic tool's input schema is the
25
+ * caller's Zod schema. The returned tool input is Zod-validated and returned
26
+ * as `output`. Copies the proven pattern from `memory/v2/router.ts` and
27
+ * `memory/graph/extraction.ts`.
28
+ * - **Tool path** (`tools` provided, no `schema`): a real {@link AgentLoop} with
29
+ * a minimal task-scoped system prompt and a tool-executor restricted to the
30
+ * supplied registry tools. No conversation is bootstrapped — the loop runs
31
+ * over an in-memory message array and its final text is returned as `output`.
32
+ *
33
+ * Profile overrides are validated up front (a deliberate deviation from the
34
+ * resolver's silent fall-through — an explicit script request must fail loudly)
35
+ * and threaded through as the `overrideProfile` argument so the per-call-site
36
+ * config resolution picks them up. Retry/rate-limit handling is NOT added here:
37
+ * the configured provider is already wrapped by `RetryProvider`/`RateLimitProvider`.
38
+ */
39
+
40
+ import { randomUUID } from "node:crypto";
41
+
42
+ import { z } from "zod";
43
+
44
+ import { type AgentEvent, AgentLoop } from "../agent/loop.js";
45
+ import { getConfig } from "../config/loader.js";
46
+ import type { ServerMessage } from "../daemon/message-protocol.js";
47
+ import {
48
+ isPersonalMemoryAllowed,
49
+ type TrustContext,
50
+ } from "../daemon/trust-context.js";
51
+ import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
52
+ import { buildSystemPrompt } from "../prompts/system-prompt.js";
53
+ import {
54
+ extractToolUse,
55
+ getConfiguredProvider,
56
+ } from "../providers/provider-send-message.js";
57
+ import type { Message, ToolDefinition } from "../providers/types.js";
58
+ import { broadcastMessage } from "../runtime/assistant-event-hub.js";
59
+ import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
60
+ import { getLogger } from "../util/logger.js";
61
+ import { getWorkspaceDir } from "../util/platform.js";
62
+
63
+ const log = getLogger("workflow-leaf-runner");
64
+
65
+ /** Call site every leaf provider call resolves through (added in PR 5). */
66
+ const LEAF_CALL_SITE = "workflowLeaf" as const;
67
+
68
+ /** Tool name forced via `tool_choice` on the schema path. */
69
+ const SCHEMA_TOOL_NAME = "emit_result";
70
+
71
+ /**
72
+ * Minimal anonymous system prompt for the tool path. Deliberately carries NO
73
+ * assistant identity, persona, or workspace context — a leaf is a task-scoped
74
+ * worker, not the assistant.
75
+ */
76
+ const LEAF_SYSTEM_PROMPT =
77
+ "You are a focused task worker. Complete the task described in the user " +
78
+ "message using the available tools, then reply with the final result as " +
79
+ "plain text. Be concise.";
80
+
81
+ /**
82
+ * Resolve the system prompt and the (possibly memory-injected) message array
83
+ * for a leaf. Anonymous leaves use the minimal task prompt and the raw prompt
84
+ * message unchanged. Persona leaves assemble the assistant's identity system
85
+ * prompt and run the main-agent memory-injection pipeline, REUSING
86
+ * `buildSystemPrompt` and `ConversationGraphMemory.prepareMemory` — no fork.
87
+ *
88
+ * The memory pipeline keys off an ephemeral conversation id and is disposed in
89
+ * a finally block; it reads the memory graph and prepends a `<memory>` block to
90
+ * the messages WITHOUT creating any conversation row (consistent with the
91
+ * anonymous path's no-persistence guarantee).
92
+ */
93
+ async function resolveLeafContext(
94
+ opts: RunLeafOptions,
95
+ ): Promise<{ systemPrompt: string; messages: Message[] }> {
96
+ const userMessages: Message[] = [
97
+ { role: "user", content: [{ type: "text", text: opts.prompt }] },
98
+ ];
99
+
100
+ if (!opts.persona) {
101
+ return { systemPrompt: LEAF_SYSTEM_PROMPT, messages: userMessages };
102
+ }
103
+
104
+ const systemPrompt = buildSystemPrompt({
105
+ trustContext: opts.trustContext,
106
+ // A persona leaf is not an onboarding turn, and it must not emit the
107
+ // first-run bootstrap block: keep it to the assistant's stable identity.
108
+ excludeBootstrap: true,
109
+ });
110
+
111
+ const messages = await injectPersonaMemory(opts, userMessages);
112
+ return { systemPrompt, messages };
113
+ }
114
+
115
+ /**
116
+ * Run the main-agent memory-injection pipeline for a persona leaf and return
117
+ * the messages with the retrieved `<memory>` block prepended. Constructs an
118
+ * ephemeral, non-persisting {@link ConversationGraphMemory} handle keyed by a
119
+ * synthetic id (it registers itself in the live-by-conversation map, so it is
120
+ * disposed in `finally` to avoid leaking the handle). Best-effort: a retrieval
121
+ * failure leaves the messages unchanged rather than failing the leaf.
122
+ *
123
+ * Gated by the personal-memory trust check — THE canonical gate
124
+ * ({@link isPersonalMemoryAllowed}, which folds in `resolveTrustClass` and the
125
+ * HTTP-auth-disabled dev bypass). A persona leaf retrieves the same private
126
+ * user content (`<memory>` graph) a main-agent turn does, so it honors the same
127
+ * gate the normal turn applies before `prepareMemory`
128
+ * (`userPromptSubmitMemoryRetrieval`): an untrusted (non-guardian) requester's
129
+ * persona leaf still gets the assistant's identity system prompt but NO personal
130
+ * memory. Without this, a workflow launched by an untrusted actor whose manifest
131
+ * grants `persona` could exfiltrate private memory in its output.
132
+ */
133
+ async function injectPersonaMemory(
134
+ opts: RunLeafOptions,
135
+ messages: Message[],
136
+ ): Promise<Message[]> {
137
+ if (!isPersonalMemoryAllowed(opts.trustContext)) return messages;
138
+
139
+ const config = getConfig();
140
+ if (config.memory?.enabled === false) return messages;
141
+
142
+ const ephemeralConversationId = `workflow-leaf:${randomUUID()}`;
143
+ const graphMemory = new ConversationGraphMemory(ephemeralConversationId);
144
+ // The memory pipeline broadcasts retrieval progress to the shared event hub
145
+ // (matching the main-agent hook); a leaf has no per-turn event callback.
146
+ const onEvent = (msg: ServerMessage): void => {
147
+ broadcastMessage(msg);
148
+ };
149
+ // `prepareMemory` requires a non-aborting signal; reuse the caller's when
150
+ // present so cancellation propagates.
151
+ const signal = opts.signal ?? new AbortController().signal;
152
+ try {
153
+ const result = await graphMemory.prepareMemory(
154
+ messages,
155
+ config,
156
+ signal,
157
+ onEvent,
158
+ );
159
+ return result.runMessages;
160
+ } catch (err) {
161
+ log.warn({ err }, "Persona leaf memory injection failed (non-fatal)");
162
+ return messages;
163
+ } finally {
164
+ graphMemory.dispose();
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Thrown when {@link runLeaf} is asked to use an inference profile that does not
170
+ * exist in `llm.profiles`. A DELIBERATE deviation from the resolver's silent
171
+ * fall-through: an explicit script request must fail loudly rather than be
172
+ * downgraded to the default profile without the author noticing.
173
+ */
174
+ export class WorkflowUnknownProfileError extends Error {
175
+ constructor(readonly profile: string) {
176
+ super(
177
+ `Workflow leaf requested inference profile "${profile}", which is not ` +
178
+ `defined in llm.profiles.`,
179
+ );
180
+ this.name = "WorkflowUnknownProfileError";
181
+ }
182
+ }
183
+
184
+ /** Options for a single leaf run. */
185
+ export interface RunLeafOptions {
186
+ /** The task prompt handed to the leaf as the user message. */
187
+ prompt: string;
188
+ /** Optional human-readable label for logging/diagnostics. */
189
+ label?: string;
190
+ /**
191
+ * When provided (and `tools` is absent), runs the schema path: a single
192
+ * forced-tool-choice call whose returned input is validated against this
193
+ * schema and returned as `output`.
194
+ *
195
+ * Accepts EITHER a host-side Zod schema (host callers) OR a plain JSON Schema
196
+ * object. A workflow script runs inside the QuickJS sandbox and cannot hold a
197
+ * Zod object (Zod is host-side), so a script's `leaf(prompt, { schema })`
198
+ * crosses the sandbox→host boundary as a JSON-marshaled JSON Schema. The
199
+ * runner duck-types the input and handles both shapes (see {@link runSchemaLeaf}).
200
+ */
201
+ schema?: z.ZodType | Record<string, unknown>;
202
+ /**
203
+ * When provided (and `schema` is absent), runs the tool path: an agent loop
204
+ * restricted to exactly these registry tools. These are the resolved
205
+ * {@link Tool} objects from `ResolvedCapabilities.tools`.
206
+ */
207
+ tools?: Tool[];
208
+ /**
209
+ * Optional inference-profile override. Validated against `llm.profiles`;
210
+ * an unknown profile throws {@link WorkflowUnknownProfileError}. An explicit
211
+ * profile ALWAYS wins over the persona default (see {@link persona}).
212
+ */
213
+ profile?: string;
214
+ /**
215
+ * Opt-in persona mode. When `true`, the leaf carries the assistant's identity
216
+ * system prompt (`buildSystemPrompt`) AND runs the same memory-injection
217
+ * pipeline a normal main-agent turn uses (`ConversationGraphMemory.prepareMemory`),
218
+ * so its output is authentically "the assistant" (e.g. for drafting replies in
219
+ * the assistant's voice). This is the costly path by design.
220
+ *
221
+ * Model resolution mirrors `mainAgent`: when no explicit {@link profile} is
222
+ * given, the workspace `activeProfile` is used as the `overrideProfile`.
223
+ *
224
+ * When falsy (the default), the leaf stays ANONYMOUS: minimal task prompt, no
225
+ * identity, no memory pipeline. Persona shares the anonymous path's
226
+ * no-persistence guarantee — it never creates a conversation row.
227
+ */
228
+ persona?: boolean;
229
+ /** Cooperative cancellation signal. */
230
+ signal?: AbortSignal;
231
+ /** Trust/auth context for the run, forwarded to the agent loop. */
232
+ trustContext: TrustContext;
233
+ }
234
+
235
+ /** Result of a single leaf run. */
236
+ export interface LeafResult {
237
+ /**
238
+ * The leaf's output — the Zod-validated object on the schema path, or the
239
+ * final assistant text on the tool path.
240
+ */
241
+ output: unknown;
242
+ /** Total input tokens across the leaf's provider call(s). */
243
+ inputTokens: number;
244
+ /** Total output tokens across the leaf's provider call(s). */
245
+ outputTokens: number;
246
+ /** Number of tool invocations the leaf made (always 0 on the schema path). */
247
+ toolCallCount: number;
248
+ }
249
+
250
+ /**
251
+ * Run a single ephemeral leaf agent. See the module doc for the two shapes and
252
+ * the persistence guarantees (no conversation row, jsonl, title job, or
253
+ * broadcast).
254
+ */
255
+ export async function runLeaf(opts: RunLeafOptions): Promise<LeafResult> {
256
+ if (opts.schema && opts.tools && opts.tools.length > 0) {
257
+ throw new Error(
258
+ "runLeaf: provide either `schema` (structured output) or `tools` " +
259
+ "(agent loop), not both.",
260
+ );
261
+ }
262
+
263
+ const overrideProfile = resolveLeafOverrideProfile(opts);
264
+
265
+ // Dispatch on schema presence, NOT on a non-empty tool set: a schema leaf does
266
+ // forced-tool-choice structured output; everything else runs the agent-loop
267
+ // tool path, which handles an EMPTY tool array as a plain text leaf. Keying off
268
+ // `tools.length > 0` would route a no-schema leaf with an empty resolved
269
+ // toolset (e.g. the read-only baseline is momentarily empty if a schedule
270
+ // fires before `initializeTools()` populates it) into `runSchemaLeaf` with no
271
+ // schema, where `schemaToInputSchema(undefined)` throws.
272
+ if (opts.schema !== undefined) {
273
+ return runSchemaLeaf(opts, overrideProfile);
274
+ }
275
+ return runToolLeaf(opts, overrideProfile);
276
+ }
277
+
278
+ /**
279
+ * Resolve the `overrideProfile` forwarded into the per-call-site config
280
+ * resolution for a leaf.
281
+ *
282
+ * An explicit `opts.profile` is validated and always wins. Otherwise, for a
283
+ * persona leaf, the resolution mirrors `mainAgent`: the workspace
284
+ * `activeProfile` is used as the `overrideProfile` (the active profile reflects
285
+ * the user's chat-model selection). Anonymous leaves with no explicit profile
286
+ * resolve to `undefined` (shipped call-site defaults apply).
287
+ */
288
+ function resolveLeafOverrideProfile(opts: RunLeafOptions): string | undefined {
289
+ if (opts.profile !== undefined) {
290
+ return validateProfile(opts.profile);
291
+ }
292
+ if (opts.persona) {
293
+ // Mirror `mainAgent`: the workspace active profile floats above the
294
+ // call-site default. Unlike an explicit request, a missing/deleted active
295
+ // profile degrades gracefully (it is not statically validated) — a
296
+ // non-existent `activeProfile` falls through to the shipped default rather
297
+ // than throwing, matching the `mainAgent` resolver's tolerance for a stale
298
+ // `activeProfile`.
299
+ const activeProfile = getConfig().llm.activeProfile;
300
+ return activeProfile != null && profileExists(activeProfile)
301
+ ? activeProfile
302
+ : undefined;
303
+ }
304
+ return undefined;
305
+ }
306
+
307
+ /**
308
+ * Validate an explicit profile request against `llm.profiles`. Throws
309
+ * {@link WorkflowUnknownProfileError} for an unknown profile — an explicit
310
+ * script request must fail loudly rather than be silently downgraded.
311
+ */
312
+ function validateProfile(profile: string): string {
313
+ if (!profileExists(profile)) {
314
+ throw new WorkflowUnknownProfileError(profile);
315
+ }
316
+ return profile;
317
+ }
318
+
319
+ function profileExists(profile: string): boolean {
320
+ return profile in (getConfig().llm.profiles ?? {});
321
+ }
322
+
323
+ /**
324
+ * Schema path: one forced-tool-choice provider call returning structured
325
+ * output. Mirrors `runRouterBatch`/`extractGraphDiff`.
326
+ */
327
+ async function runSchemaLeaf(
328
+ opts: RunLeafOptions,
329
+ overrideProfile: string | undefined,
330
+ ): Promise<LeafResult> {
331
+ const schema = opts.schema as z.ZodType | Record<string, unknown>;
332
+
333
+ const { systemPrompt, messages } = await resolveLeafContext(opts);
334
+
335
+ const provider = await getConfiguredProvider(LEAF_CALL_SITE, {
336
+ ...(overrideProfile !== undefined ? { overrideProfile } : {}),
337
+ });
338
+ if (!provider) {
339
+ throw new Error(
340
+ `Workflow leaf provider unavailable for call site "${LEAF_CALL_SITE}".`,
341
+ );
342
+ }
343
+
344
+ const tool: ToolDefinition = {
345
+ name: SCHEMA_TOOL_NAME,
346
+ description:
347
+ "Return the result for this task. Call this tool exactly once with the " +
348
+ "structured result.",
349
+ input_schema: schemaToInputSchema(schema),
350
+ };
351
+
352
+ const response = await provider.sendMessage(messages, {
353
+ tools: [tool],
354
+ systemPrompt,
355
+ config: {
356
+ callSite: LEAF_CALL_SITE,
357
+ tool_choice: { type: "tool" as const, name: SCHEMA_TOOL_NAME },
358
+ // `provider` is a CallSiteConfiguredProvider holding our overrideProfile,
359
+ // but it only injects that stored override when the per-call config omits
360
+ // `callSite` — and we set `callSite` here to drive resolveCallSiteConfig.
361
+ // So pass overrideProfile through explicitly (mirroring how AgentLoop
362
+ // builds its provider config on the tool path), or an explicit/persona
363
+ // profile selection is silently dropped and the leaf runs under the
364
+ // default workflowLeaf profile.
365
+ ...(overrideProfile !== undefined ? { overrideProfile } : {}),
366
+ },
367
+ ...(opts.signal ? { signal: opts.signal } : {}),
368
+ });
369
+
370
+ const toolBlock = extractToolUse(response);
371
+ if (!toolBlock || toolBlock.name !== SCHEMA_TOOL_NAME) {
372
+ throw new Error(
373
+ `Workflow leaf "${opts.label ?? "schema"}" returned no ` +
374
+ `${SCHEMA_TOOL_NAME} tool_use block (stopReason: ${response.stopReason}).`,
375
+ );
376
+ }
377
+
378
+ const parsed = validateSchemaOutput(schema, toolBlock.input);
379
+ if (!parsed.success) {
380
+ throw new Error(
381
+ `Workflow leaf "${opts.label ?? "schema"}" output failed schema ` +
382
+ `validation: ${parsed.error.message}`,
383
+ );
384
+ }
385
+
386
+ return {
387
+ output: parsed.data,
388
+ inputTokens: response.usage.inputTokens,
389
+ outputTokens: response.usage.outputTokens,
390
+ toolCallCount: 0,
391
+ };
392
+ }
393
+
394
+ /**
395
+ * Tool path: a real agent loop with a restricted toolset and no conversation
396
+ * persistence. The loop runs over an in-memory message array; its events are
397
+ * consumed only to accumulate usage/tool-call counts. No DB row, jsonl, title
398
+ * job, or broadcast is created.
399
+ */
400
+ async function runToolLeaf(
401
+ opts: RunLeafOptions,
402
+ overrideProfile: string | undefined,
403
+ ): Promise<LeafResult> {
404
+ const tools = opts.tools ?? [];
405
+
406
+ const { systemPrompt, messages } = await resolveLeafContext(opts);
407
+
408
+ const provider = await getConfiguredProvider(LEAF_CALL_SITE, {
409
+ ...(overrideProfile !== undefined ? { overrideProfile } : {}),
410
+ });
411
+ if (!provider) {
412
+ throw new Error(
413
+ `Workflow leaf provider unavailable for call site "${LEAF_CALL_SITE}".`,
414
+ );
415
+ }
416
+
417
+ // Restrict execution to exactly the supplied tools. The agent loop calls
418
+ // this back per tool_use; anything outside the set is a hard error (the
419
+ // model cannot have been handed an out-of-set tool, so this is defense in
420
+ // depth).
421
+ const toolsByName = new Map<string, Tool>(tools.map((t) => [t.name, t]));
422
+
423
+ // A synthetic conversation id is needed for the loop's internal bookkeeping
424
+ // (compaction circuit breaker keying) but is NEVER persisted — no
425
+ // conversation row is created for it.
426
+ const ephemeralConversationId = `workflow-leaf:${randomUUID()}`;
427
+
428
+ const loop = new AgentLoop({
429
+ provider,
430
+ systemPrompt,
431
+ tools: tools as ToolDefinition[],
432
+ toolExecutor: (name, input, onOutput) =>
433
+ executeLeafTool(toolsByName, name, input, {
434
+ ephemeralConversationId,
435
+ // Bind leaf file tools to the WORKSPACE, not the daemon's cwd: the CLI
436
+ // starts the daemon with `cwd: dirname(daemonBinary)`, so `process.cwd()`
437
+ // is the install dir. File tools resolve relative paths and enforce the
438
+ // sandbox boundary against this dir, so a workspace path would resolve
439
+ // wrong / be rejected as out-of-bounds. Mirrors the scheduled-task path
440
+ // (`runtime/routes/task-routes.ts`), which also uses `getWorkspaceDir()`.
441
+ workingDir: getWorkspaceDir(),
442
+ signal: opts.signal,
443
+ onOutput,
444
+ trustContext: opts.trustContext,
445
+ }),
446
+ conversationId: ephemeralConversationId,
447
+ });
448
+
449
+ let inputTokens = 0;
450
+ let outputTokens = 0;
451
+ let toolCallCount = 0;
452
+ const onEvent = (event: AgentEvent): void => {
453
+ if (event.type === "usage") {
454
+ inputTokens += event.inputTokens;
455
+ outputTokens += event.outputTokens;
456
+ } else if (event.type === "tool_use") {
457
+ toolCallCount += 1;
458
+ }
459
+ };
460
+
461
+ const result = await loop.run({
462
+ messages,
463
+ onEvent,
464
+ requestId: ephemeralConversationId,
465
+ trust: opts.trustContext,
466
+ ...(opts.signal ? { signal: opts.signal } : {}),
467
+ ...(overrideProfile !== undefined ? { overrideProfile } : {}),
468
+ });
469
+
470
+ return {
471
+ output: finalAssistantText(result.history),
472
+ inputTokens,
473
+ outputTokens,
474
+ toolCallCount,
475
+ };
476
+ }
477
+
478
+ /**
479
+ * Execute one tool invocation for the tool path, restricted to the supplied
480
+ * set. Builds a minimal {@link ToolContext} — the leaf is anonymous, runs in
481
+ * the sandbox, and self-approves as the requested trust class (the workflow
482
+ * manifest is the single consent point, resolved by the engine before the run).
483
+ */
484
+ async function executeLeafTool(
485
+ toolsByName: Map<string, Tool>,
486
+ name: string,
487
+ input: Record<string, unknown>,
488
+ ctx: {
489
+ ephemeralConversationId: string;
490
+ workingDir: string;
491
+ signal?: AbortSignal;
492
+ onOutput?: (chunk: string) => void;
493
+ trustContext: TrustContext;
494
+ },
495
+ ): Promise<ToolExecutionResult> {
496
+ const tool = toolsByName.get(name);
497
+ if (!tool) {
498
+ return {
499
+ content: `Tool "${name}" is not available to this workflow leaf.`,
500
+ isError: true,
501
+ };
502
+ }
503
+
504
+ const toolContext: ToolContext = {
505
+ conversationId: ctx.ephemeralConversationId,
506
+ workingDir: ctx.workingDir,
507
+ requestId: ctx.ephemeralConversationId,
508
+ trustClass: ctx.trustContext.trustClass,
509
+ allowedToolNames: new Set(toolsByName.keys()),
510
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
511
+ ...(ctx.onOutput ? { onOutput: ctx.onOutput } : {}),
512
+ };
513
+
514
+ try {
515
+ return await tool.execute(input, toolContext);
516
+ } catch (err) {
517
+ log.warn({ err, tool: name }, "Workflow leaf tool execution threw");
518
+ return {
519
+ content: `Tool "${name}" failed: ${
520
+ err instanceof Error ? err.message : String(err)
521
+ }`,
522
+ isError: true,
523
+ };
524
+ }
525
+ }
526
+
527
+ /** Join the text blocks of the final assistant message in `history`. */
528
+ function finalAssistantText(history: Message[]): string {
529
+ for (let i = history.length - 1; i >= 0; i--) {
530
+ const msg = history[i];
531
+ if (msg.role !== "assistant") continue;
532
+ const text = msg.content
533
+ .filter(
534
+ (b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
535
+ )
536
+ .map((b) => b.text)
537
+ .join("");
538
+ return text.trim();
539
+ }
540
+ return "";
541
+ }
542
+
543
+ /**
544
+ * A leaf's structured-output schema is EITHER a host-side Zod schema (passed by
545
+ * a host caller) or a plain JSON Schema object (a script's `leaf(prompt,
546
+ * { schema })` is JSON-marshaled across the sandbox→host boundary, since the
547
+ * sandbox can't hold a Zod object). Duck-type the input: a Zod schema exposes a
548
+ * `safeParse` method (and the internal `_zod` marker), a JSON Schema object does
549
+ * not.
550
+ */
551
+ function isZodSchema(
552
+ schema: z.ZodType | Record<string, unknown>,
553
+ ): schema is z.ZodType {
554
+ return typeof (schema as { safeParse?: unknown }).safeParse === "function";
555
+ }
556
+
557
+ /**
558
+ * Build the synthetic forced-tool's `input_schema` from the leaf's schema. A Zod
559
+ * schema is converted via Zod 4's native `z.toJSONSchema` (mirrors
560
+ * `scripts/generate-openapi.ts`), stripping the `$schema` key tool definitions
561
+ * don't carry. A plain JSON Schema object is used directly (same `$schema`
562
+ * strip for parity).
563
+ */
564
+ function schemaToInputSchema(
565
+ schema: z.ZodType | Record<string, unknown>,
566
+ ): Record<string, unknown> {
567
+ const converted = isZodSchema(schema)
568
+ ? (z.toJSONSchema(schema, { unrepresentable: "any" }) as Record<
569
+ string,
570
+ unknown
571
+ >)
572
+ : schema;
573
+ const { $schema: _dropped, ...rest } = converted;
574
+ return rest;
575
+ }
576
+
577
+ /**
578
+ * Validate the model's returned tool input against the leaf's schema. A Zod
579
+ * schema validates directly; a plain JSON Schema object is reconstructed into a
580
+ * Zod schema via `z.fromJSONSchema` first. Returns Zod's native `safeParse`
581
+ * result so callers handle both shapes uniformly.
582
+ */
583
+ function validateSchemaOutput(
584
+ schema: z.ZodType | Record<string, unknown>,
585
+ input: unknown,
586
+ ) {
587
+ const zodSchema = isZodSchema(schema) ? schema : z.fromJSONSchema(schema);
588
+ return zodSchema.safeParse(input);
589
+ }