@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,656 @@
1
+ /**
2
+ * End-to-end engine integration tests.
3
+ *
4
+ * Unlike `engine.test.ts` — which injects a FAKE leaf runner — this suite wires
5
+ * the REAL pieces together: the real QuickJS sandbox, the real {@link runLeaf}
6
+ * leaf runner, the real {@link AgentLoop}, the real journal store, and a real
7
+ * temp DB. Only the LLM provider is mocked, so leaves complete without a
8
+ * network call. This catches integration bugs the fake-runner tests miss:
9
+ * sandbox↔host JSON marshaling, the leaf runner's real agent-loop path, and
10
+ * journal DB round-trips on resume.
11
+ *
12
+ * Most leaves run the REAL **tool path**: the engine forwards `capabilities.tools`
13
+ * to tool leaves, the leaf runner spins up an `AgentLoop`, and the mocked
14
+ * provider returns a single `end_turn` message whose text echoes the prompt
15
+ * (so each leaf's `output` is distinguishable and order-checkable). A leaf that
16
+ * declares a `schema` routes to the leaf runner's SCHEMA path; the engine omits
17
+ * `tools` for it (a schema + tools is a hard error in `runLeaf`), so the schema
18
+ * path works even when the run carries the read-only baseline. A sandboxed
19
+ * script passes a JSON Schema object (it can't hold a host-side Zod object),
20
+ * which the leaf runner duck-types — see the schema-path test below.
21
+ *
22
+ * Covered:
23
+ * - `map`/`parallel` fan-out: each leaf returns its echoed output, aggregated
24
+ * in spec-array order through the real sandbox + leaf runner + agent loop.
25
+ * - Schema path: a script-provided JSON Schema drives the forced-tool call and
26
+ * its validated structured object is returned and journaled.
27
+ * - In-process resume: re-running the SAME runId against the SAME DB replays
28
+ * the unchanged prefix from the journal — ZERO duplicate provider calls —
29
+ * and only the changed/new tail re-runs the provider.
30
+ * - Resume journal rewrite: a changed-input leaf re-runs and its journal row is
31
+ * upserted (new hash + result) rather than left stale.
32
+ * - Cap abort: a low `maxAgentsPerRun` aborts with status `cap_exceeded` and
33
+ * the partial counters are persisted on the run row.
34
+ * - Concurrency high-water: the provider mock records in-flight concurrency
35
+ * and never exceeds `maxConcurrentLeaves`.
36
+ * - Hash stability: the same script run twice journals identical
37
+ * `(seq, call_hash)` sequences.
38
+ */
39
+
40
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
41
+
42
+ import { makeMockLogger } from "../__tests__/helpers/mock-logger.js";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Mocks — defined before importing the module under test.
46
+ //
47
+ // We mock ONLY the LLM provider (no network) and silence the logger. Every
48
+ // other surface — sandbox, leaf runner, agent loop, journal store,
49
+ // capabilities, config — is the real implementation.
50
+ // ---------------------------------------------------------------------------
51
+
52
+ mock.module("../util/logger.js", () => ({
53
+ getLogger: () => makeMockLogger(),
54
+ }));
55
+
56
+ interface SendOptions {
57
+ signal?: AbortSignal;
58
+ }
59
+
60
+ /** Live in-flight provider calls, to assert the concurrency high-water mark. */
61
+ let inFlight = 0;
62
+ let maxInFlight = 0;
63
+ /** Total provider sendMessage invocations (the resume duplicate-call check). */
64
+ let sendCallCount = 0;
65
+ /** Per-call delay (ms) so concurrent fan-out actually overlaps in time. */
66
+ let sendDelayMs = 0;
67
+
68
+ /** Extract the latest user prompt text from a provider message array. */
69
+ function lastUserPrompt(
70
+ messages: Array<{
71
+ role: string;
72
+ content: Array<{ type: string; text?: string }>;
73
+ }>,
74
+ ): string {
75
+ for (let i = messages.length - 1; i >= 0; i--) {
76
+ const m = messages[i];
77
+ if (m?.role !== "user") continue;
78
+ return m.content
79
+ .filter((b) => b.type === "text")
80
+ .map((b) => b.text ?? "")
81
+ .join("");
82
+ }
83
+ return "";
84
+ }
85
+
86
+ interface SendOptionsFull extends SendOptions {
87
+ config?: { tool_choice?: { type: string; name: string } };
88
+ }
89
+
90
+ /**
91
+ * The leaf's real `AgentLoop` (tool path) calls the provider; we return a single
92
+ * `end_turn` message whose text echoes the prompt, so the loop terminates in one
93
+ * turn and the leaf's `output` is `processed:<prompt>`.
94
+ *
95
+ * The SCHEMA path forces a `tool_choice` on the synthetic `emit_result` tool;
96
+ * we detect that and instead return a `tool_use` block whose input echoes the
97
+ * prompt under the schema's `answer` field, so the leaf's structured `output`
98
+ * is `{ answer: "processed:<prompt>" }`.
99
+ */
100
+ const sendMessage = mock(
101
+ async (
102
+ messages: Array<{
103
+ role: string;
104
+ content: Array<{ type: string; text?: string }>;
105
+ }>,
106
+ options: SendOptionsFull,
107
+ ): Promise<{
108
+ content: Array<{
109
+ type: string;
110
+ text?: string;
111
+ name?: string;
112
+ input?: unknown;
113
+ }>;
114
+ model: string;
115
+ usage: { inputTokens: number; outputTokens: number };
116
+ stopReason: string;
117
+ }> => {
118
+ sendCallCount += 1;
119
+ inFlight += 1;
120
+ maxInFlight = Math.max(maxInFlight, inFlight);
121
+ try {
122
+ if (sendDelayMs > 0) {
123
+ await new Promise((resolve) => setTimeout(resolve, sendDelayMs));
124
+ }
125
+ if (options.signal?.aborted) {
126
+ throw new DOMException("Aborted", "AbortError");
127
+ }
128
+ const forcedTool = options.config?.tool_choice;
129
+ if (forcedTool?.type === "tool") {
130
+ return {
131
+ content: [
132
+ {
133
+ type: "tool_use",
134
+ name: forcedTool.name,
135
+ input: { answer: `processed:${lastUserPrompt(messages)}` },
136
+ },
137
+ ],
138
+ model: "test-model",
139
+ usage: { inputTokens: 7, outputTokens: 3 },
140
+ stopReason: "tool_use",
141
+ };
142
+ }
143
+ return {
144
+ content: [
145
+ { type: "text", text: `processed:${lastUserPrompt(messages)}` },
146
+ ],
147
+ model: "test-model",
148
+ usage: { inputTokens: 7, outputTokens: 3 },
149
+ stopReason: "end_turn",
150
+ };
151
+ } finally {
152
+ inFlight -= 1;
153
+ }
154
+ },
155
+ );
156
+
157
+ const getConfiguredProvider = mock(async () => ({
158
+ name: "test-provider",
159
+ sendMessage,
160
+ }));
161
+
162
+ mock.module("../providers/provider-send-message.js", () => ({
163
+ getConfiguredProvider,
164
+ // Re-implement the trivial helpers the leaf runner imports from this module.
165
+ extractToolUse: (response: { content: Array<{ type: string }> }) =>
166
+ response.content.find((b) => b.type === "tool_use"),
167
+ }));
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Module under test (after mocks).
171
+ // ---------------------------------------------------------------------------
172
+
173
+ import { WorkflowsConfigSchema } from "../config/schemas/workflows.js";
174
+ import type { TrustContext } from "../daemon/trust-context.js";
175
+ import { getSqlite } from "../memory/db-connection.js";
176
+ import { initializeDb } from "../memory/db-init.js";
177
+ import { registerTool } from "../tools/registry.js";
178
+ import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
179
+ import { resolveCapabilities } from "./capabilities.js";
180
+ import { executeWorkflow } from "./engine.js";
181
+ import * as journalStore from "./journal-store.js";
182
+ import {
183
+ getJournal,
184
+ getRun,
185
+ markRunningAsInterrupted,
186
+ } from "./journal-store.js";
187
+ import { runLeaf } from "./leaf-runner.js";
188
+
189
+ initializeDb();
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Shared fixtures
193
+ // ---------------------------------------------------------------------------
194
+
195
+ const trustContext: TrustContext = {
196
+ sourceChannel: "vellum",
197
+ trustClass: "guardian",
198
+ };
199
+
200
+ /**
201
+ * A real, no-op registry tool the manifest declares so leaves take the leaf
202
+ * runner's REAL tool path (an `AgentLoop` over the provider). The model never
203
+ * calls it — the provider mock returns final text on the first turn — but its
204
+ * presence in `capabilities.tools` is what routes the leaf to the agent loop.
205
+ */
206
+ const ECHO_TOOL_NAME = "wf_integration_echo";
207
+ registerTool({
208
+ name: ECHO_TOOL_NAME,
209
+ description: "Integration-test no-op tool.",
210
+ category: "test",
211
+ defaultRiskLevel: "low" as never,
212
+ executionTarget: "sandbox",
213
+ input_schema: { type: "object", properties: {}, required: [] },
214
+ async execute(
215
+ _input: Record<string, unknown>,
216
+ _ctx: ToolContext,
217
+ ): Promise<ToolExecutionResult> {
218
+ return { content: "noop", isError: false };
219
+ },
220
+ });
221
+
222
+ const capabilities = resolveCapabilities({
223
+ tools: [ECHO_TOOL_NAME],
224
+ hostFunctions: [],
225
+ persona: false,
226
+ });
227
+
228
+ /** Build a `WorkflowsConfig` from schema defaults with optional overrides. */
229
+ function makeConfig(
230
+ overrides: Partial<{
231
+ maxAgentsPerRun: number;
232
+ maxConcurrentLeaves: number;
233
+ }> = {},
234
+ ) {
235
+ return WorkflowsConfigSchema.parse(overrides);
236
+ }
237
+
238
+ /**
239
+ * Base options shared by every `executeWorkflow` call: the REAL journal store,
240
+ * the REAL leaf runner, real capabilities, real trust context.
241
+ */
242
+ function baseOptions(
243
+ runId: string,
244
+ scriptSource: string,
245
+ caps: typeof capabilities = capabilities,
246
+ ) {
247
+ return {
248
+ runId,
249
+ scriptSource,
250
+ args: {},
251
+ capabilities: caps,
252
+ journal: journalStore,
253
+ leafRunner: runLeaf,
254
+ trustContext,
255
+ };
256
+ }
257
+
258
+ function resetTables(): void {
259
+ getSqlite().exec("DELETE FROM workflow_journal");
260
+ getSqlite().exec("DELETE FROM workflow_runs");
261
+ }
262
+
263
+ beforeEach(() => {
264
+ resetTables();
265
+ sendMessage.mockClear();
266
+ getConfiguredProvider.mockClear();
267
+ inFlight = 0;
268
+ maxInFlight = 0;
269
+ sendCallCount = 0;
270
+ sendDelayMs = 0;
271
+ });
272
+
273
+ afterAll(() => {
274
+ resetTables();
275
+ });
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Tests
279
+ // ---------------------------------------------------------------------------
280
+
281
+ describe("workflow engine integration (real sandbox + leaf runner + agent loop + journal)", () => {
282
+ test("map/parallel fan-out: each leaf returns its echoed output, in order", async () => {
283
+ // The script is SYNCHRONOUS from its own view (asyncify) — no `await`.
284
+ const scriptSource = `
285
+ export const meta = { name: "fanout", description: "map over items" };
286
+ const items = args.items;
287
+ const results = map(items, (it) => leaf("item-" + it, { label: "leaf-" + it }));
288
+ return results;
289
+ `;
290
+ const result = await executeWorkflow({
291
+ ...baseOptions("wf-fanout", scriptSource),
292
+ args: { items: ["a", "b", "c"] },
293
+ config: makeConfig(),
294
+ });
295
+
296
+ expect(result.status).toBe("completed");
297
+ // Each leaf's output is its echoed final text, in spec-array order.
298
+ expect(result.result).toEqual([
299
+ "processed:item-a",
300
+ "processed:item-b",
301
+ "processed:item-c",
302
+ ]);
303
+ expect(result.agentsSpawned).toBe(3);
304
+ // Real provider mock was hit once per leaf through the real agent loop.
305
+ expect(sendCallCount).toBe(3);
306
+ // Usage flowed back from the provider through the leaf runner and engine.
307
+ expect(result.inputTokens).toBe(21);
308
+ expect(result.outputTokens).toBe(9);
309
+
310
+ // The journal persisted one completed agent entry per leaf, in seq order.
311
+ const journal = getJournal("wf-fanout");
312
+ expect(journal.map((e) => e.seq)).toEqual([0, 1, 2]);
313
+ expect(journal.every((e) => e.status === "completed")).toBe(true);
314
+ expect(journal.map((e) => e.result)).toEqual([
315
+ "processed:item-a",
316
+ "processed:item-b",
317
+ "processed:item-c",
318
+ ]);
319
+ });
320
+
321
+ test("in-process resume: unchanged prefix replays from journal with ZERO duplicate provider calls", async () => {
322
+ // A script that journals N sequential agent() calls. `args.tail` is the
323
+ // text of the FINAL call; changing it between runs leaves the prefix
324
+ // unchanged (same prompt → same call_hash) and changes only the last seq.
325
+ const scriptSource = `
326
+ export const meta = { name: "resume", description: "sequential agents" };
327
+ const a = agent("step-1");
328
+ const b = agent("step-2");
329
+ const c = agent(args.tail);
330
+ return [a, b, c];
331
+ `;
332
+ // First run: 3 fresh leaves → 3 provider calls.
333
+ const first = await executeWorkflow({
334
+ ...baseOptions("wf-resume", scriptSource),
335
+ args: { tail: "step-3" },
336
+ config: makeConfig(),
337
+ });
338
+ expect(first.status).toBe("completed");
339
+ expect(sendCallCount).toBe(3);
340
+ const firstJournal = getJournal("wf-resume");
341
+ expect(firstJournal.map((e) => e.seq)).toEqual([0, 1, 2]);
342
+
343
+ // Reset the in-test provider counter so we count ONLY the resume's calls.
344
+ sendCallCount = 0;
345
+ sendMessage.mockClear();
346
+
347
+ // Second run: SAME runId, SAME DB, but the last call's prompt changes.
348
+ // seq 0 and 1 are unchanged (hash match) and MUST replay from the journal
349
+ // without touching the provider; seq 2's hash changes and re-runs.
350
+ const second = await executeWorkflow({
351
+ ...baseOptions("wf-resume", scriptSource),
352
+ args: { tail: "step-3-CHANGED" },
353
+ config: makeConfig(),
354
+ });
355
+
356
+ expect(second.status).toBe("completed");
357
+ // THE HEADLINE ACCEPTANCE CRITERION: exactly ONE provider call — only the
358
+ // changed tail. Zero duplicate provider calls for the unchanged prefix,
359
+ // verified THROUGH the real sandbox + leaf runner + agent loop + journal
360
+ // stack (not a fake runner).
361
+ expect(sendCallCount).toBe(1);
362
+ // The engine returns the correct values: the prefix replayed from the
363
+ // journal, the changed tail re-ran and produced its new output.
364
+ expect(second.result).toEqual([
365
+ "processed:step-1",
366
+ "processed:step-2",
367
+ "processed:step-3-CHANGED",
368
+ ]);
369
+
370
+ // The prefix journal entries are untouched (same call_hash as the first
371
+ // run) and there is still exactly one entry per seq.
372
+ const secondJournal = getJournal("wf-resume");
373
+ expect(secondJournal.map((e) => e.seq)).toEqual([0, 1, 2]);
374
+ expect(secondJournal[0]!.callHash).toBe(firstJournal[0]!.callHash);
375
+ expect(secondJournal[1]!.callHash).toBe(firstJournal[1]!.callHash);
376
+ // NOTE: seq 2's journal row is NOT rewritten on resume — see the
377
+ // `resume rewrites a changed leaf's journal entry` test.todo below for the
378
+ // known `INSERT OR IGNORE` staleness bug. The engine's returned value
379
+ // (asserted above) is correct; only the persisted journal row is stale.
380
+ });
381
+
382
+ test("restart→reconcile→resume: an interrupted run replays its prefix and carries accounting", async () => {
383
+ // Stage 1 — a first pass that journals a 2-leaf prefix, mirroring a run that
384
+ // got partway before the process died.
385
+ const scriptSource = `
386
+ export const meta = { name: "reconcile-resume", description: "sequential agents" };
387
+ const a = agent("step-1");
388
+ const b = agent("step-2");
389
+ const c = agent(args.tail);
390
+ return [a, b, c];
391
+ `;
392
+ const first = await executeWorkflow({
393
+ ...baseOptions("wf-reconcile", scriptSource),
394
+ args: { tail: "step-3" },
395
+ config: makeConfig(),
396
+ });
397
+ expect(first.status).toBe("completed");
398
+ expect(first.agentsSpawned).toBe(3);
399
+ const agentsBefore = getRun("wf-reconcile")!.agentsSpawned;
400
+ expect(agentsBefore).toBe(3);
401
+
402
+ // Simulate a crash: force the persisted row back to `running` (the state a
403
+ // mid-flight run leaves on disk), then run startup reconciliation.
404
+ journalStore.updateRun("wf-reconcile", { status: "running" });
405
+ const reconciled = markRunningAsInterrupted();
406
+ expect(reconciled).toBeGreaterThanOrEqual(1);
407
+ const interrupted = getRun("wf-reconcile")!;
408
+ expect(interrupted.status).toBe("interrupted");
409
+ // Reconciliation is STATUS ONLY — the accounting total is untouched.
410
+ expect(interrupted.agentsSpawned).toBe(agentsBefore);
411
+
412
+ sendCallCount = 0;
413
+ sendMessage.mockClear();
414
+
415
+ // Stage 2 — resume re-invokes the engine with the SAME runId. The unchanged
416
+ // prefix (seq 0,1) replays from the journal with ZERO provider calls; only
417
+ // the changed tail re-runs. Accounting SEEDS from the persisted total (3)
418
+ // and grows — it does not reset to 0.
419
+ const resumed = await executeWorkflow({
420
+ ...baseOptions("wf-reconcile", scriptSource),
421
+ args: { tail: "step-3-CHANGED" },
422
+ config: makeConfig(),
423
+ });
424
+
425
+ expect(resumed.status).toBe("completed");
426
+ // Only the changed tail hit the provider — the prefix replayed.
427
+ expect(sendCallCount).toBe(1);
428
+ expect(resumed.result).toEqual([
429
+ "processed:step-1",
430
+ "processed:step-2",
431
+ "processed:step-3-CHANGED",
432
+ ]);
433
+ // Accounting carried: seeded from 3, the single re-run tail makes it 4.
434
+ expect(resumed.agentsSpawned).toBe(4);
435
+ expect(getRun("wf-reconcile")!.agentsSpawned).toBe(4);
436
+ });
437
+
438
+ test("cap abort: a low maxAgentsPerRun yields cap_exceeded with partials persisted", async () => {
439
+ const scriptSource = `
440
+ export const meta = { name: "cap", description: "more leaves than the cap" };
441
+ const a = agent("c-1");
442
+ const b = agent("c-2");
443
+ const c = agent("c-3");
444
+ return [a, b, c];
445
+ `;
446
+ const result = await executeWorkflow({
447
+ ...baseOptions("wf-cap", scriptSource),
448
+ // Cap at 2 — the third sequential agent() trips the cap before launching.
449
+ config: makeConfig({ maxAgentsPerRun: 2 }),
450
+ });
451
+
452
+ expect(result.status).toBe("cap_exceeded");
453
+ expect(result.result).toBeNull();
454
+ // Exactly 2 leaves ran before the cap tripped on the 3rd.
455
+ expect(result.agentsSpawned).toBe(2);
456
+ expect(sendCallCount).toBe(2);
457
+
458
+ // Partial counters and terminal status are persisted on the run row.
459
+ const run = getRun("wf-cap");
460
+ expect(run?.status).toBe("cap_exceeded");
461
+ expect(run?.agentsSpawned).toBe(2);
462
+ expect(run?.error).toContain("Agent cap of 2 exceeded");
463
+ // Two completed journal entries were persisted before the abort.
464
+ expect(getJournal("wf-cap").length).toBe(2);
465
+ });
466
+
467
+ test("concurrency high-water: never exceeds maxConcurrentLeaves", async () => {
468
+ // Delay each provider call so a wide fan-out genuinely overlaps; the mock
469
+ // tracks the live in-flight count.
470
+ sendDelayMs = 15;
471
+ const scriptSource = `
472
+ export const meta = { name: "concurrency", description: "wide parallel fan-out" };
473
+ const items = args.items;
474
+ const results = parallel(items.map((it) => leaf("p-" + it)));
475
+ return results;
476
+ `;
477
+ const items = Array.from({ length: 12 }, (_, i) => i);
478
+ const result = await executeWorkflow({
479
+ ...baseOptions("wf-conc", scriptSource),
480
+ args: { items },
481
+ config: makeConfig({ maxConcurrentLeaves: 3 }),
482
+ });
483
+
484
+ expect(result.status).toBe("completed");
485
+ expect(result.agentsSpawned).toBe(12);
486
+ expect(sendCallCount).toBe(12);
487
+ // The engine's worker pool capped live concurrency at the configured limit.
488
+ expect(maxInFlight).toBeGreaterThan(1);
489
+ expect(maxInFlight).toBeLessThanOrEqual(3);
490
+ });
491
+
492
+ test("hash stability: the same script run twice journals identical (seq, call_hash) sequences", async () => {
493
+ const scriptSource = `
494
+ export const meta = { name: "stable", description: "deterministic hashes" };
495
+ const items = args.items;
496
+ const results = map(items, (it) => leaf("h-" + it));
497
+ const tail = agent("final");
498
+ return { results, tail };
499
+ `;
500
+ const args = { items: ["x", "y", "z"] };
501
+
502
+ await executeWorkflow({
503
+ ...baseOptions("wf-hash-1", scriptSource),
504
+ args,
505
+ config: makeConfig(),
506
+ });
507
+ await executeWorkflow({
508
+ ...baseOptions("wf-hash-2", scriptSource),
509
+ args,
510
+ config: makeConfig(),
511
+ });
512
+
513
+ const seqHashes = (runId: string) =>
514
+ getJournal(runId).map((e) => [e.seq, e.callHash] as const);
515
+
516
+ const a = seqHashes("wf-hash-1");
517
+ const b = seqHashes("wf-hash-2");
518
+ expect(a.length).toBe(4);
519
+ expect(a).toEqual(b);
520
+ });
521
+
522
+ // ---------------------------------------------------------------------------
523
+ // FIXED ENGINE↔LEAF-RUNNER GAP (schema-path marshaling + tool forwarding).
524
+ //
525
+ // A workflow script declares a per-leaf output `schema` via
526
+ // `leaf(prompt, { schema })`. That schema crosses the QuickJS sandbox boundary
527
+ // as a JSON-marshaled plain object (a JSON Schema). The leaf runner's schema
528
+ // path now duck-types its input: a plain JSON Schema object is used directly as
529
+ // the forced-tool `input_schema` and validated via `z.fromJSONSchema`, while a
530
+ // host-side Zod schema keeps the original behavior.
531
+ //
532
+ // This run uses the FULL capability set (non-empty `capabilities.tools`, which
533
+ // every real run carries via the read-only baseline). A schema leaf must NOT
534
+ // receive those tools — `runLeaf` hard-errors on `schema` + non-empty `tools`.
535
+ // The engine now omits `tools` for schema leaves, so the structured-output path
536
+ // works end-to-end even when the run carries baseline tools (the real case).
537
+ // ---------------------------------------------------------------------------
538
+ test("schema-path leaves run with the read-only baseline present (tools not forwarded)", async () => {
539
+ // The script passes a JSON Schema OBJECT LITERAL (the only shape a sandbox
540
+ // script can produce — Zod is host-side). It marshals across the boundary
541
+ // and reaches the leaf runner verbatim.
542
+ const scriptSource = `
543
+ export const meta = { name: "schema", description: "structured leaf output" };
544
+ const schema = {
545
+ type: "object",
546
+ properties: { answer: { type: "string" } },
547
+ required: ["answer"],
548
+ additionalProperties: false,
549
+ };
550
+ const items = args.items;
551
+ const results = map(items, (it) => leaf("item-" + it, { schema }));
552
+ return results;
553
+ `;
554
+ // `capabilities` carries a non-empty toolset (the echo tool); a real run's
555
+ // resolved baseline is likewise non-empty. The schema leaf still succeeds,
556
+ // proving the engine omits `tools` on the schema path.
557
+ expect(capabilities.tools.length).toBeGreaterThan(0);
558
+ const result = await executeWorkflow({
559
+ ...baseOptions("wf-schema", scriptSource, capabilities),
560
+ args: { items: ["a", "b"] },
561
+ config: makeConfig(),
562
+ });
563
+
564
+ expect(result.status).toBe("completed");
565
+ // Each leaf returned the validated structured object (forced-tool path),
566
+ // NOT the tool-path's plain echoed text.
567
+ expect(result.result).toEqual([
568
+ { answer: "processed:item-a" },
569
+ { answer: "processed:item-b" },
570
+ ]);
571
+ expect(result.agentsSpawned).toBe(2);
572
+ expect(sendCallCount).toBe(2);
573
+
574
+ // The journal persisted the structured outputs.
575
+ const journal = getJournal("wf-schema");
576
+ expect(journal.map((e) => e.result)).toEqual([
577
+ { answer: "processed:item-a" },
578
+ { answer: "processed:item-b" },
579
+ ]);
580
+ });
581
+
582
+ // ---------------------------------------------------------------------------
583
+ // PERSONA MANIFEST GATE (consent-gated, like tools).
584
+ //
585
+ // A leaf may request `persona: true` only if the run's capability manifest
586
+ // declared `persona`. An undeclared request fails the run loudly through the
587
+ // real engine + sandbox stack — it must NOT silently downgrade to anonymous.
588
+ // ---------------------------------------------------------------------------
589
+ test("a persona leaf in a run WITHOUT declared persona fails the run", async () => {
590
+ const scriptSource = `
591
+ export const meta = { name: "persona-denied", description: "undeclared persona" };
592
+ return agent("draft in voice", { persona: true });
593
+ `;
594
+ // `capabilities` (the shared fixture) declares persona:false.
595
+ expect(capabilities.persona).toBe(false);
596
+ const result = await executeWorkflow({
597
+ ...baseOptions("wf-persona-denied", scriptSource),
598
+ config: makeConfig(),
599
+ });
600
+
601
+ expect(result.status).toBe("failed");
602
+ // The gate trips before any provider call — nothing was spawned.
603
+ expect(sendCallCount).toBe(0);
604
+ expect(result.agentsSpawned).toBe(0);
605
+ });
606
+
607
+ // ---------------------------------------------------------------------------
608
+ // FIXED RESUME JOURNAL-STALENESS GAP.
609
+ //
610
+ // On resume, a leaf whose input CHANGED (new `call_hash`) re-runs and the
611
+ // engine returns the new value. `appendJournalEntry` now UPSERTs on
612
+ // `(run_id, seq)`, so the stale first-run row is rewritten with the new hash
613
+ // and result — the persisted journal agrees with the engine's returned value.
614
+ // ---------------------------------------------------------------------------
615
+ test("resume rewrites a changed leaf's journal entry (hash + result) instead of keeping the stale row", async () => {
616
+ const scriptSource = `
617
+ export const meta = { name: "resume-rewrite", description: "sequential agents" };
618
+ const a = agent("step-1");
619
+ const b = agent(args.tail);
620
+ return [a, b];
621
+ `;
622
+ // First run: 2 fresh leaves.
623
+ await executeWorkflow({
624
+ ...baseOptions("wf-resume-rewrite", scriptSource),
625
+ args: { tail: "step-2" },
626
+ config: makeConfig(),
627
+ });
628
+ const firstJournal = getJournal("wf-resume-rewrite");
629
+ expect(firstJournal.map((e) => e.seq)).toEqual([0, 1]);
630
+ const seq1HashBefore = firstJournal[1]!.callHash;
631
+ expect(firstJournal[1]!.result).toBe("processed:step-2");
632
+
633
+ sendCallCount = 0;
634
+ sendMessage.mockClear();
635
+
636
+ // Resume: seq 1's input changes, so it re-runs; seq 0 replays from journal.
637
+ const second = await executeWorkflow({
638
+ ...baseOptions("wf-resume-rewrite", scriptSource),
639
+ args: { tail: "step-2-CHANGED" },
640
+ config: makeConfig(),
641
+ });
642
+ expect(second.status).toBe("completed");
643
+ expect(sendCallCount).toBe(1);
644
+
645
+ // The persisted journal row for seq 1 was REWRITTEN (new hash + result),
646
+ // agreeing with the value the engine returned — not the stale first-run row.
647
+ const secondJournal = getJournal("wf-resume-rewrite");
648
+ expect(secondJournal.map((e) => e.seq)).toEqual([0, 1]);
649
+ expect(secondJournal[1]!.callHash).not.toBe(seq1HashBefore);
650
+ expect(secondJournal[1]!.result).toBe("processed:step-2-CHANGED");
651
+ expect(second.result).toEqual([
652
+ "processed:step-1",
653
+ "processed:step-2-CHANGED",
654
+ ]);
655
+ });
656
+ });