@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,510 @@
1
+ # Workflows — Authoring Guide
2
+
3
+ The workflow engine lets the assistant author a short JS/TS script that runs in a
4
+ sandbox and fans work out across many parallel, ephemeral **leaf agents**. A
5
+ workflow is the right tool when a task decomposes into a lot of similar small
6
+ sub-tasks that can run concurrently — score every item in a list, extract a field
7
+ from each of a hundred documents, draft-then-verify a batch — and you want the
8
+ results orchestrated deterministically and reported back when the whole run
9
+ finishes.
10
+
11
+ Workflows are gated behind the `workflows` feature flag (default **off**). The
12
+ `run_workflow` / `manage_workflows` tools are served by the flag-gated `workflows`
13
+ bundled skill rather than as always-on tools — load it with `skill_load` and invoke
14
+ its tools via `skill_execute`. When the flag is off, the skill is absent, the
15
+ management routes 404, and the scheduler rejects `workflow`-mode jobs.
16
+
17
+ - Engine code: `assistant/src/workflows/`
18
+ - Skill (tool surface): `assistant/src/config/bundled-skills/workflows/`
19
+ - Architecture overview: [`ARCHITECTURE.md` § Workflow Orchestration Engine](../../ARCHITECTURE.md#workflow-orchestration-engine)
20
+ - Manual e2e runbook: [`workflows-testing.md`](./workflows-testing.md)
21
+
22
+ ---
23
+
24
+ ## Why it works this way (design rationale)
25
+
26
+ ### The sandbox is hooks-only because scripts may be authored from untrusted input
27
+
28
+ A workflow script can be written by the assistant **after** it has read untrusted
29
+ content — a hostile email, a web page, a shared document. The script must
30
+ therefore be unable to do anything on its own. It runs in a fresh QuickJS-WASM VM
31
+ per run with **no** `fetch`, `XMLHttpRequest`, `WebSocket`, `process`, `Bun`,
32
+ `require`, no dynamic `import()`, no timers, no filesystem, and no network. The
33
+ only way a script affects the outside world is through the host functions the
34
+ engine injects (`agent`, `parallel`, …). There are no ambient capabilities to
35
+ escalate, and the sandbox stops the script from reaching around its declared
36
+ capabilities.
37
+
38
+ ### The capability declaration is the single consent point
39
+
40
+ A run declares **once**, up front, which side-effecting tools and host functions
41
+ its leaves may use and whether they may speak in the assistant's persona. That
42
+ declaration is the only place consent is given for the whole run — there are **no
43
+ per-call permission prompts inside a running workflow**. This is deliberate: a
44
+ run may spawn hundreds of leaves, and prompting per call would be unworkable and
45
+ would defeat the point of unattended fan-out. Leaves always get a curated
46
+ read-only baseline for free; anything that writes, sends, or executes must be
47
+ named in the manifest.
48
+
49
+ ### The runaway guard is the agent cap — by design, no dollar kill-switch
50
+
51
+ The only structural limit on a run is the **agent cap** (`maxAgentsPerRun`,
52
+ default 500): the total number of leaves a single run may spawn. There is
53
+ intentionally **no spend/dollar kill-switch**. The cap bounds the blast radius in
54
+ a way that is deterministic and resume-safe (it counts agents, not wall-clock or
55
+ cost), and leaves default to a cost-optimized model. Concurrency is separately
56
+ bounded by `maxConcurrentLeaves` (default 6).
57
+
58
+ ### Scripts must be deterministic so runs can resume
59
+
60
+ Every leaf call is journaled by a deterministic sequence number and an input
61
+ hash. If the assistant restarts mid-run, resuming the same `runId` **replays
62
+ the unchanged prefix from the journal** instead of re-spawning agents — so a
63
+ long run survives a deploy or crash without redoing (or re-paying for) completed
64
+ work. (Resume is explicit, not automatic — see
65
+ [Recovering a crashed run](#recovering-a-crashed-run).) That guarantee only
66
+ holds if the script is deterministic, so `Date.now()`, `Math.random()`, and
67
+ argless `new Date()` **throw**. Pass any timestamps or seeds in through `args`.
68
+
69
+ ---
70
+
71
+ ## The script model
72
+
73
+ ### Scripts are SYNCHRONOUS — never use `await`
74
+
75
+ This is the single most important authoring fact. Host functions are _asyncified_:
76
+ calling one suspends the entire VM until the host-side promise settles, then
77
+ resumes the VM with the value. From the script's perspective every host call is
78
+ **synchronous** — you call it and the result comes back directly:
79
+
80
+ ```js
81
+ const r = agent("Summarize this thread."); // r is the result, right here
82
+ ```
83
+
84
+ Do **not** write `await agent(...)`, and do **not** make the script `async`.
85
+ Asyncify can only suspend the main evaluation stack, never a promise
86
+ continuation, so an `async`/`await` script would deadlock on its second host
87
+ call. Write plain straight-line code.
88
+
89
+ ### Every script starts with a literal `meta`
90
+
91
+ The first thing in a script must be a pure-literal export — no computed values,
92
+ template strings, or concatenation:
93
+
94
+ ```js
95
+ export const meta = {
96
+ name: "triage-inbox",
97
+ description: "Triage and label inbox messages",
98
+ };
99
+ ```
100
+
101
+ `meta` is extracted **statically**, without executing the script (the source is
102
+ untrusted), so it must be a plain object literal with string `name` and
103
+ `description`. The `name` is how a saved workflow is referenced by `workflow(name)`
104
+ and the scheduler.
105
+
106
+ The script's result is whatever it `return`s at the top level. End with
107
+ `return <result>;` — the script body runs as a function, so a bare trailing
108
+ expression (e.g. `result;`) is **discarded** and the run finishes with no
109
+ result. Always `return` the value you want surfaced:
110
+
111
+ ```js
112
+ const result = agent(`Write the final summary: ${JSON.stringify(parts)}`);
113
+ return result; // returned as the run result
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Host API reference
119
+
120
+ All functions are synchronous from the script's perspective.
121
+
122
+ | Function | Returns | Notes |
123
+ | ---------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
+ | `agent(prompt, opts?)` | the leaf's result | Runs ONE leaf. **Throws** on leaf failure. |
125
+ | `leaf(prompt, opts?)` | a leaf descriptor | Runs nothing on its own; used inside `parallel`/`map`/`pipeline`. |
126
+ | `parallel(specs)` | `results[]` | Runs an array of `leaf(...)` descriptors concurrently (capped at `maxConcurrentLeaves`), results in input order. A failed leaf becomes `null` (never throws). |
127
+ | `map(items, build)` | `results[]` | `build(item, i)` returns a `leaf(...)` descriptor (or a bare prompt string) per item; runs them like `parallel`. |
128
+ | `pipeline(items, ...stages)` | `results[]` | Each `stage(prev, i)` returns a `leaf(...)` descriptor or a plain value, where `prev` is the prior stage's result at index `i`. Per-stage barrier (see below). |
129
+ | `phase(title)` | — | Marks a named phase; surfaced as a progress event. |
130
+ | `log(msg)` | — | Emits a progress log line. |
131
+ | `usage()` | `{ agentsSpawned, inputTokens, outputTokens }` | Live snapshot so a script can self-moderate. |
132
+ | `workflow(name, args?)` | the child's result | Runs a SAVED workflow inline, depth 1 only (see Nesting). |
133
+ | `args` | the run input | The `args` object passed to `run_workflow`. |
134
+
135
+ ### `agent` vs `parallel` failure semantics
136
+
137
+ `agent(...)` is for a single sequential leaf and **throws** if that leaf fails —
138
+ an unhandled throw fails the whole run. `parallel(...)` is the fan-out primitive
139
+ and **never throws on a single leaf**: a failed leaf is `null` in the results
140
+ array, so a batch survives a few bad items. `map` and `pipeline` are built on
141
+ `parallel` and share that null-on-failure behavior.
142
+
143
+ ### `pipeline` has a per-stage barrier
144
+
145
+ `pipeline(items, stageA, stageB)` runs `stageA` across all items in parallel,
146
+ **waits for all of stage A to finish**, then runs `stageB` across stage A's
147
+ results. There is no cross-stage streaming in v1 — item _n_ does not advance to
148
+ stage B early just because its stage A finished first. Each `stage(prev, i)`
149
+ callback receives the prior stage's result for index `i`. This barrier is a
150
+ consequence of the single-threaded VM and is honest about its cost: a pipeline is
151
+ only as fast as the slowest leaf in each stage.
152
+
153
+ ### Leaf options (`opts` for `agent` / `leaf`)
154
+
155
+ | Option | Type | Effect |
156
+ | --------- | -------------------------- | --------------------------------------------------------------------------------------- |
157
+ | `schema` | JSON Schema object literal | Forces structured output via a tool. A schema leaf runs with **no tools**. |
158
+ | `label` | string | Short display/diagnostic label for the leaf. |
159
+ | `profile` | string | Overrides the model profile. Must exist in `llm.profiles` or the leaf throws. |
160
+ | `persona` | boolean | `true` makes the leaf speak as the assistant (identity + memory). Default is anonymous. |
161
+
162
+ #### `schema` is a JSON Schema literal, not Zod
163
+
164
+ A script runs in the sandbox and cannot hold a host-side Zod object, so a leaf's
165
+ `schema` is a plain **JSON Schema object literal**. The engine builds a forced
166
+ `tool_choice` call whose synthetic tool input is that schema, validates the
167
+ model's output against it, and returns the structured object. A leaf with a
168
+ `schema` is a pure judge/extractor — it gets **no tools**:
169
+
170
+ ```js
171
+ leaf(`Score this option 0-10 for fit: ${opt}`, {
172
+ schema: {
173
+ type: "object",
174
+ properties: { score: { type: "number" } },
175
+ required: ["score"],
176
+ },
177
+ });
178
+ ```
179
+
180
+ #### `persona` vs anonymous leaves, and profile resolution
181
+
182
+ By default a leaf is **anonymous**: a minimal task-scoped system prompt, no
183
+ assistant identity, no memory pipeline. Use anonymous leaves for impartial
184
+ judging, scoring, and extraction of input — the bulk of fan-out work.
185
+
186
+ `persona: true` opts the leaf into **persona mode**: it carries the assistant's
187
+ identity system prompt and runs the same memory-injection pipeline a normal turn
188
+ uses, so its output is authentically the assistant's voice (e.g. drafting a reply
189
+ to be sent). This is the costly path — use it for the small number of leaves whose
190
+ output is meant to be _in the assistant's voice_, not for bulk judging.
191
+
192
+ Model profile resolution:
193
+
194
+ - An explicit `profile` always wins and is validated up front — an unknown
195
+ profile throws (a deliberate, loud failure rather than a silent downgrade).
196
+ - With no explicit `profile`, a **persona** leaf mirrors the main agent: the
197
+ workspace `activeProfile` floats above the call-site default (a deleted/stale
198
+ active profile degrades gracefully to the default).
199
+ - With no explicit `profile`, an **anonymous** leaf uses the shipped
200
+ `workflowLeaf` call-site default (cost-optimized).
201
+
202
+ No leaf — anonymous or persona — ever creates a conversation row, jsonl mirror,
203
+ title job, or turn broadcast. Leaves are ephemeral.
204
+
205
+ ---
206
+
207
+ ## Capability manifest semantics
208
+
209
+ The `capabilities` argument to `run_workflow` is the single consent point:
210
+
211
+ ```jsonc
212
+ {
213
+ "tools": ["file_write", "gmail_send"], // side-effecting tools granted to leaves
214
+ "hostFunctions": [], // host-function names the run may invoke
215
+ "persona": true, // grant leaves persona (identity + memory) access
216
+ }
217
+ ```
218
+
219
+ Resolution: the leaf tool set is the **read-only baseline ∪ declared `tools`**,
220
+ minus a forbidden set.
221
+
222
+ - **Read-only baseline** (always available, no declaration needed): `file_read`,
223
+ `file_list`, `recall`, `web_search`. The baseline is auto-granted with no
224
+ launch approval, so it carries only read-only tools. `web_fetch` is **not**
225
+ here — it is classified as a side-effect tool (its URL can exfiltrate read data
226
+ or trigger external actions), so a run that needs it must declare it (which
227
+ arms the threshold-aware launch approval gate).
228
+ - **Declared tools** must exist in the tool registry — an unknown name is a hard
229
+ authoring error, not a silent drop.
230
+ - **Forbidden tools** can never be granted, even if declared (declaring one is a
231
+ hard error): `subagent_spawn`, `run_workflow`, `manage_workflows`,
232
+ `manage_secure_command_tool`, `run_authenticated_command`,
233
+ `make_authenticated_request`. The first four are recursion vectors or
234
+ human-in-the-loop install paths that must not be delegated to an unattended
235
+ leaf. The two CES tools can return `cesApprovalRequired`, which `ToolExecutor`
236
+ resolves by bridging an interactive approval and retrying with a grant; a leaf
237
+ executes `tool.execute()` directly (bypassing that post-processing), so it
238
+ would see the raw approval-required result as an error. They stay forbidden
239
+ until leaf invocations run the executor's post-processing.
240
+
241
+ Side-effecting tools (`file_write`, sends, shell, …) and `persona` are **not** in
242
+ the baseline; a run must declare them. Once declared, every leaf may use them with
243
+ no further prompting.
244
+
245
+ ### Launch and resume approval are threshold-aware
246
+
247
+ Declaring any side-effecting tool or host function arms a **launch approval**: the
248
+ single point at which the user consents to the whole run. It is threshold-aware —
249
+ at the full-access posture (auto-approve threshold `high`) it does **not** prompt;
250
+ in normal posture it prompts once. A read-only run (no declared side effects)
251
+ never prompts.
252
+
253
+ The same posture gates **resume** of a run whose stored manifest granted side
254
+ effects, since resuming restarts the unfinished side-effecting leaves:
255
+
256
+ - **Conversationally** (`manage_workflows` action `resume`): full access bypasses;
257
+ normal posture re-prompts for fresh approval.
258
+ - **Over HTTP** (`POST /v1/workflows/runs/:id/resume`, and the
259
+ `vellum workflows resume` CLI on top of it): full access proceeds; normal
260
+ posture is **refused** (403) and the caller is directed to resume through the
261
+ assistant, since the route has no prompt channel.
262
+
263
+ A read-only run resumes freely regardless of posture. The check is
264
+ `isFullAccessThreshold` in `assistant/src/permissions/threshold.ts`, applied in
265
+ `permission-checker.ts` and `workflow-routes.ts`.
266
+
267
+ ---
268
+
269
+ ## Worked examples
270
+
271
+ ### Triage a list with `map`
272
+
273
+ Score and label each inbox item in parallel (anonymous schema leaves), then write
274
+ one summary in the assistant's voice (a single persona leaf). The item list is
275
+ passed in via `args` — never fetched inside the script.
276
+
277
+ ```js
278
+ export const meta = {
279
+ name: "triage-inbox",
280
+ description: "Score and summarize inbox items",
281
+ };
282
+
283
+ phase("score");
284
+ const scored = map(args.items, (item) =>
285
+ leaf(
286
+ `Rate this message's urgency 0-10 and give a one-line reason:\n${item.subject}\n${item.body}`,
287
+ {
288
+ label: `score:${item.id}`,
289
+ schema: {
290
+ type: "object",
291
+ properties: {
292
+ urgency: { type: "number" },
293
+ reason: { type: "string" },
294
+ },
295
+ required: ["urgency", "reason"],
296
+ },
297
+ },
298
+ ),
299
+ );
300
+
301
+ phase("summarize");
302
+ const summary = agent(
303
+ `Here are scored inbox items. Write a short triage summary for the user, ` +
304
+ `highlighting anything urgent:\n${JSON.stringify(scored)}`,
305
+ { persona: true },
306
+ );
307
+ return summary;
308
+ ```
309
+
310
+ A failed scoring leaf shows up as `null` in `scored`; the run continues.
311
+
312
+ ### Find, then verify, with `pipeline`
313
+
314
+ A two-stage pipeline with a barrier: stage 1 extracts a candidate answer from each
315
+ document; stage 2 verifies each candidate. Stage 2 only starts once all of stage 1
316
+ has finished.
317
+
318
+ ```js
319
+ export const meta = {
320
+ name: "find-and-verify",
321
+ description: "Extract then verify a fact per document",
322
+ };
323
+
324
+ const verified = pipeline(
325
+ args.documents,
326
+
327
+ // Stage 1: extract a candidate from each document.
328
+ (doc) =>
329
+ leaf(
330
+ `Extract the contract end date from this document, or "unknown":\n${doc.text}`,
331
+ {
332
+ label: `extract:${doc.id}`,
333
+ schema: {
334
+ type: "object",
335
+ properties: { endDate: { type: "string" } },
336
+ required: ["endDate"],
337
+ },
338
+ },
339
+ ),
340
+
341
+ // Stage 2: `prev` is stage 1's result for this index.
342
+ (prev, i) =>
343
+ leaf(
344
+ `A prior pass extracted end date "${prev?.endDate}" from document ` +
345
+ `"${args.documents[i].id}". Confirm or correct it, and rate your confidence 0-1.`,
346
+ {
347
+ label: `verify:${args.documents[i].id}`,
348
+ schema: {
349
+ type: "object",
350
+ properties: {
351
+ endDate: { type: "string" },
352
+ confidence: { type: "number" },
353
+ },
354
+ required: ["endDate", "confidence"],
355
+ },
356
+ },
357
+ ),
358
+ );
359
+
360
+ return verified;
361
+ ```
362
+
363
+ ### Granting a side-effecting tool
364
+
365
+ To let leaves write files, declare the tool in the manifest passed to
366
+ `run_workflow`:
367
+
368
+ ```jsonc
369
+ {
370
+ "script": "...",
371
+ "args": {
372
+ "items": [
373
+ /* ... */
374
+ ],
375
+ },
376
+ "capabilities": { "tools": ["file_write"] },
377
+ }
378
+ ```
379
+
380
+ Inside the script, a leaf that needs to write gets `file_write` automatically (no
381
+ schema, so it runs the tool path):
382
+
383
+ ```js
384
+ agent(`Write a per-item report file for: ${JSON.stringify(item)}`, {
385
+ label: `report:${item.id}`,
386
+ });
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Saved workflows (library) and the scheduler
392
+
393
+ ### Saving and invoking by name
394
+
395
+ A saved workflow is a normal script at `<workspace>/workflows/<name>.workflow.ts`.
396
+ It is resolved by its `meta.name` first, then by filename base. Invoke it by name
397
+ instead of an inline script:
398
+
399
+ - From the tool: `run_workflow({ name: "triage-inbox", args: { … } })`.
400
+ - Inline from another script: `workflow("triage-inbox", { … })`.
401
+
402
+ ### Nesting is depth-1 only
403
+
404
+ A top-level script may call `workflow(name, args)` to run a saved workflow inline;
405
+ the child draws from the **same** seq counter, agent cap, journal, and signal, so
406
+ determinism and resume carry across the boundary. A child workflow may **not**
407
+ call `workflow()` — nesting deeper than one level throws.
408
+
409
+ ### Scheduler `workflow` mode
410
+
411
+ A scheduled job can trigger a saved workflow by name (e.g. "triage the inbox every
412
+ morning"). Each `workflow`-mode schedule carries a **persisted capability manifest**
413
+ (`capabilities_json` on `cron_jobs`), consented to **once at schedule creation**:
414
+ `schedule_create` accepts a `capabilities` manifest, validates it against the same
415
+ forbidden/unknown checks `run_workflow` applies, and — if it grants side effects —
416
+ arms the threshold-aware approval at creation time. Both firing paths (the
417
+ scheduler's auto-fire and the run-now `POST /v1/schedules/:id/run` route) execute
418
+ the run under that stored manifest. Legacy or null-manifest schedules fall back to
419
+ the read-only baseline. The trigger records success once the run starts;
420
+ completion/failure is surfaced out-of-band via workflow events and the completion
421
+ wake.
422
+
423
+ ---
424
+
425
+ ## Tools, routes, and CLI
426
+
427
+ ### Tools (served by the `workflows` skill)
428
+
429
+ Reached via the skill (`skill_load` then `skill_execute`), not as always-on tools.
430
+
431
+ - **`run_workflow`** — `{ script?, name?, args?, capabilities?, label? }` (exactly
432
+ one of `script`/`name`). Returns `{ runId }` immediately; the run is
433
+ asynchronous and you are notified in the conversation when it completes. **Do
434
+ not poll.**
435
+ - **`manage_workflows`** — `{ action: "status" | "abort" | "resume" | "list_runs"
436
+ | "list_profiles", run_id? }`. `status`/`abort`/`resume` require `run_id`.
437
+ `list_profiles` returns `{ profiles, activeProfile }` — the defined LLM profile
438
+ names plus the workspace active profile, used to pick a valid leaf `profile`.
439
+
440
+ ### Routes (read/abort/resume, all 404 when the flag is off)
441
+
442
+ | Method | Path | Purpose |
443
+ | ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
444
+ | `GET` | `/v1/workflows` | List saved (named) workflows. |
445
+ | `GET` | `/v1/workflows/runs` | List recent runs (newest first); `?limit`, `?status`. |
446
+ | `GET` | `/v1/workflows/runs/:id` | Get one run. |
447
+ | `POST` | `/v1/workflows/runs/:id/abort` | Signal an in-flight run to abort. |
448
+ | `POST` | `/v1/workflows/runs/:id/resume` | Resume an interrupted run (refuses a side-effecting run in normal posture; proceeds at full access). |
449
+
450
+ ### CLI
451
+
452
+ ```
453
+ vellum workflows list # saved (named) workflows
454
+ vellum workflows runs # recent runs (--limit, --status)
455
+ vellum workflows show <run-id> # one run's status + counts
456
+ vellum workflows abort <run-id> # abort an in-flight run
457
+ vellum workflows resume <run-id> # resume an interrupted run
458
+ ```
459
+
460
+ All subcommands accept `--assistant <name>` to target a specific instance.
461
+
462
+ ---
463
+
464
+ ## Configuration
465
+
466
+ Engine caps live under `workflows.*` in assistant config:
467
+
468
+ | Key | Default | Meaning |
469
+ | ---------------------- | ------- | ------------------------------------------------------------- |
470
+ | `maxAgentsPerRun` | 500 | Total leaves a single run may spawn (the runaway guard). |
471
+ | `maxConcurrentLeaves` | 6 | Max leaves in flight within one run. |
472
+ | `maxConcurrentRuns` | 3 | Max workflow runs in flight at once. |
473
+ | `journalRetentionDays` | 30 | How long finished runs' journals are retained before pruning. |
474
+
475
+ ---
476
+
477
+ ## Persistence and resume
478
+
479
+ Run state lives in two tables (migration 284):
480
+
481
+ - **`workflow_runs`** — one row per run: status (`running` / `completed` /
482
+ `failed` / `aborted` / `cap_exceeded` / `interrupted`), agent/token counts,
483
+ script source + hash, capability manifest, and the originating conversation.
484
+ - **`workflow_journal`** — append-only `(run_id, seq)` log of every leaf call.
485
+
486
+ Leaf cost is attributed in `llm_usage_events` under `call_site = 'workflowLeaf'`,
487
+ so a run's spend is queryable after the fact.
488
+
489
+ On resume (re-invoking the same `runId`), a journal entry whose `(run_id, seq)`
490
+ and input hash match a completed prior call is replayed from cache without
491
+ re-spawning the leaf — the longest-unchanged-prefix replays, and only changed or
492
+ not-yet-run leaves execute.
493
+
494
+ ### Recovering a crashed run
495
+
496
+ Resume is **not automatic**. If the assistant restarts mid-run, the run row is
497
+ left `running`; at startup the assistant reconciles every such orphaned row to
498
+ `interrupted` (status only — the agent/token accounting is preserved so the agent
499
+ cap still carries across the restart). An `interrupted` run sits there until you
500
+ explicitly resume it:
501
+
502
+ - **From the assistant**: `manage_workflows` with `action: "resume"` and the
503
+ `run_id`.
504
+ - **From the CLI**: `vellum workflows resume <run-id>`.
505
+
506
+ Resuming re-invokes the engine with the same `runId`: it replays the completed
507
+ prefix from the journal and continues from the first unfinished leaf, under the
508
+ run's originally-declared capabilities and the same structural agent cap. Only
509
+ `interrupted` runs are resumable; a `completed` / `failed` / `aborted` run is
510
+ terminal.
@@ -51,7 +51,7 @@ echo/
51
51
  ## Install locally
52
52
 
53
53
  The assistant scans `<workspaceDir>/plugins/*` (e.g.
54
- `~/.vellum/workspace/plugins/`) for subdirectories containing a `package.json`
54
+ `$VELLUM_WORKSPACE_DIR/plugins/`) for subdirectories containing a `package.json`
55
55
  and loads each one during assistant startup. Dropping (or symlinking) this
56
56
  directory in place is enough to enable it.
57
57
 
@@ -60,8 +60,8 @@ directory in place is enough to enable it.
60
60
  From the repo root:
61
61
 
62
62
  ```bash
63
- mkdir -p ~/.vellum/workspace/plugins
64
- ln -s "$(pwd)/assistant/examples/plugins/echo" ~/.vellum/workspace/plugins/echo
63
+ mkdir -p "$VELLUM_WORKSPACE_DIR"/plugins
64
+ ln -s "$(pwd)/assistant/examples/plugins/echo" "$VELLUM_WORKSPACE_DIR"/plugins/echo
65
65
  ```
66
66
 
67
67
  Symlinks let you edit the plugin in-place and restart the assistant to
@@ -69,7 +69,7 @@ pick up changes.
69
69
 
70
70
  ### Option 2 — standalone copy
71
71
 
72
- A plain `cp -R` of this directory into `~/.vellum/workspace/plugins/echo/`
72
+ A plain `cp -R` of this directory into `$VELLUM_WORKSPACE_DIR/plugins/echo/`
73
73
  works as-is. The hooks import their types from the public `@vellumai/plugin-api`
74
74
  specifier, which the daemon materializes as a workspace-level shim before it
75
75
  loads any plugin — so the copied directory resolves it without any path
@@ -109,7 +109,7 @@ You should see one line per hook invocation, similar to:
109
109
  Remove the symlink (or the copied directory) and restart the assistant:
110
110
 
111
111
  ```bash
112
- rm ~/.vellum/workspace/plugins/echo
112
+ rm "$VELLUM_WORKSPACE_DIR"/plugins/echo
113
113
  vellum restart
114
114
  ```
115
115
 
package/knip.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "src/api/index.ts!"
9
9
  ],
10
10
  "project": ["src/**/*.ts!", "src/**/*.tsx!", "scripts/**/*.ts"],
11
+ "ignore": ["src/config/preloaded-apps/**"],
11
12
  "ignoreDependencies": [
12
13
  "@microsoft/api-extractor",
13
14
  "@vellumai/ces-client",
@@ -22,6 +23,7 @@
22
23
  "@resvg/resvg-js-darwin-arm64",
23
24
  "@resvg/resvg-js-darwin-x64",
24
25
  "@vellumai/skill-host-contracts",
26
+ "ajv",
25
27
  "madge"
26
28
  ]
27
29
  }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Gateway → daemon inbound payload contract.
3
+ *
4
+ * Zod schema defining the wire format for messages forwarded from the
5
+ * gateway to the daemon via `POST /v1/channels/inbound`. Both services
6
+ * import from here so the contract is enforced at compile time.
7
+ *
8
+ * The gateway constructs this payload in `forwardToRuntime()` from the
9
+ * normalized `GatewayInboundEvent`; the daemon validates and consumes
10
+ * it in `handleChannelInbound()`.
11
+ */
12
+
13
+ import { z } from "zod";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Command intent (channel-initiated commands, e.g. Telegram /start)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const CommandIntentSchema = z.object({
20
+ type: z.string(),
21
+ payload: z.string().optional(),
22
+ });
23
+
24
+ export type CommandIntent = z.infer<typeof CommandIntentSchema>;
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Source metadata — structured fields forwarded from the gateway's
28
+ // normalized inbound event. Replaces the untyped Record<string, unknown>.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export const SourceMetadataSchema = z
32
+ .object({
33
+ /** Provider-assigned update/event ID. */
34
+ updateId: z.string().optional(),
35
+ /** Provider message ID (e.g. Slack message `ts`). */
36
+ messageId: z.string().optional(),
37
+ /** Provider chat type (e.g. Telegram "private", "group"). */
38
+ chatType: z.string().optional(),
39
+ /** Thread/conversation-group ID (e.g. Slack `thread_ts`). */
40
+ threadId: z.string().optional(),
41
+ /** Channel name (e.g. Slack channel display name). */
42
+ channelName: z.string().optional(),
43
+ /** Actor's language code (e.g. "en", "es"). */
44
+ languageCode: z.string().optional(),
45
+ /** Whether the actor is a bot. */
46
+ isBot: z.boolean().optional(),
47
+ /** Actor's IANA timezone (e.g. "America/Los_Angeles"). */
48
+ timezone: z.string().optional(),
49
+ /** Human-readable timezone label (e.g. "Pacific Daylight Time"). */
50
+ timezoneLabel: z.string().optional(),
51
+ /** UTC offset in seconds. */
52
+ timezoneOffsetSeconds: z.number().optional(),
53
+ /** Slack-specific: actor is from an external workspace (Slack Connect). */
54
+ isStranger: z.boolean().optional(),
55
+ /** Slack-specific: actor is a guest / restricted account. */
56
+ isRestricted: z.boolean().optional(),
57
+ /** Transport-layer hints forwarded from the channel adapter. */
58
+ hints: z.array(z.string()).optional(),
59
+ /** Transport-layer UX brief. */
60
+ uxBrief: z.string().optional(),
61
+ /** Client-provided timezone for date formatting. */
62
+ clientTimezone: z.string().optional(),
63
+ /** Channel command intent (e.g. Telegram /start). */
64
+ commandIntent: CommandIntentSchema.optional(),
65
+ /** Slack-specific: whether the bot was @-mentioned. */
66
+ slackBotMentioned: z.boolean().optional(),
67
+ /** Slack workspace/team ID. */
68
+ account: z.string().optional(),
69
+
70
+ // Email-specific fields
71
+ /** Email subject line. */
72
+ emailSubject: z.string().optional(),
73
+ /** Email recipient address. */
74
+ emailRecipient: z.string().optional(),
75
+ /** Email In-Reply-To header. */
76
+ emailInReplyTo: z.string().optional(),
77
+ /** Email References header. */
78
+ emailReferences: z.string().optional(),
79
+ })
80
+ .passthrough();
81
+
82
+ export type SourceMetadata = z.infer<typeof SourceMetadataSchema>;
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Runtime inbound payload — the full wire format
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export const RuntimeInboundPayloadSchema = z.object({
89
+ sourceChannel: z.string(),
90
+ interface: z.string(),
91
+ conversationExternalId: z.string(),
92
+ externalMessageId: z.string(),
93
+ content: z.string(),
94
+ isEdit: z.boolean().optional(),
95
+ callbackQueryId: z.string().optional(),
96
+ callbackData: z.string().optional(),
97
+ actorDisplayName: z.string().optional(),
98
+ actorExternalId: z.string(),
99
+ actorUsername: z.string().optional(),
100
+ sourceMetadata: SourceMetadataSchema.optional(),
101
+ attachmentIds: z.array(z.string()).optional(),
102
+ replyCallbackUrl: z.string().optional(),
103
+ });
104
+
105
+ export type RuntimeInboundPayload = z.infer<typeof RuntimeInboundPayloadSchema>;