@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62

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 (490) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/docs/activation-funnel-telemetry.md +310 -0
  4. package/examples/plugins/echo/README.md +61 -60
  5. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  6. package/examples/plugins/echo/hooks/stop.ts +16 -0
  7. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  8. package/examples/plugins/echo/package.json +1 -2
  9. package/examples/plugins/echo/src/emit.ts +19 -0
  10. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  11. package/openapi.yaml +273 -121
  12. package/package.json +2 -2
  13. package/scripts/sync-llm-catalog.ts +2 -0
  14. package/src/__tests__/activation-early-marking.test.ts +120 -0
  15. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  16. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  17. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  18. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  19. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  21. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  22. package/src/__tests__/agent-loop.test.ts +441 -96
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  24. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  25. package/src/__tests__/anthropic-provider.test.ts +23 -8
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  27. package/src/__tests__/app-control-flow.test.ts +1 -1
  28. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  29. package/src/__tests__/app-executors.test.ts +132 -0
  30. package/src/__tests__/approval-cascade.test.ts +6 -5
  31. package/src/__tests__/approval-routes-http.test.ts +4 -1
  32. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  33. package/src/__tests__/build-persisted-content.test.ts +75 -1
  34. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  35. package/src/__tests__/channel-approvals.test.ts +1 -1
  36. package/src/__tests__/compaction-circuit.test.ts +258 -0
  37. package/src/__tests__/compaction-direct.test.ts +146 -0
  38. package/src/__tests__/compaction-events.test.ts +7 -7
  39. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  40. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  41. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  42. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  43. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  48. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  49. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  50. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  51. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  52. package/src/__tests__/conversation-error.test.ts +15 -1
  53. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  54. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  55. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  56. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  57. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  58. package/src/__tests__/conversation-queue.test.ts +10 -10
  59. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  60. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  61. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  62. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  63. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  64. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  65. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  66. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  67. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  68. package/src/__tests__/conversation-undo.test.ts +2 -2
  69. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  70. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  71. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  72. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  73. package/src/__tests__/corrected-target.test.ts +93 -0
  74. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -4
  75. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  76. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  77. package/src/__tests__/db-acp-history.test.ts +101 -0
  78. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  79. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  80. package/src/__tests__/empty-response-hook.test.ts +1 -1
  81. package/src/__tests__/file-write-tool.test.ts +63 -0
  82. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  83. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  84. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  85. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  86. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  87. package/src/__tests__/heartbeat-service.test.ts +1 -0
  88. package/src/__tests__/history-repair-hook.test.ts +5 -1
  89. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  90. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  91. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  92. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  93. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  94. package/src/__tests__/injector-background-turn.test.ts +13 -23
  95. package/src/__tests__/injector-chain.test.ts +268 -44
  96. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  97. package/src/__tests__/injector-document-comments.test.ts +97 -114
  98. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  99. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  100. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  101. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  102. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  103. package/src/__tests__/llm-resolver.test.ts +73 -0
  104. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  105. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  106. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  107. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  108. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  109. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  110. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  111. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  112. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  113. package/src/__tests__/plugin-registry.test.ts +3 -49
  114. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  115. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  116. package/src/__tests__/plugin-types.test.ts +0 -70
  117. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  118. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  119. package/src/__tests__/published-app-updater.test.ts +138 -0
  120. package/src/__tests__/reaction-persistence.test.ts +1 -1
  121. package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
  122. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  123. package/src/__tests__/server-history-render.test.ts +39 -0
  124. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  125. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  126. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  127. package/src/__tests__/subagent-detail.test.ts +25 -7
  128. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  129. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  130. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  131. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  132. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  133. package/src/__tests__/title-generate-hook.test.ts +5 -1
  134. package/src/__tests__/tool-error-hook.test.ts +1 -1
  135. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  136. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  137. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  138. package/src/__tests__/web-fetch.test.ts +45 -0
  139. package/src/acp/__tests__/agent-process.test.ts +161 -0
  140. package/src/acp/__tests__/client-handler.test.ts +40 -0
  141. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  142. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  143. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  144. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  145. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  146. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  147. package/src/acp/agent-process.ts +61 -1
  148. package/src/acp/auto-install.test.ts +280 -0
  149. package/src/acp/auto-install.ts +232 -0
  150. package/src/acp/client-handler.ts +31 -0
  151. package/src/acp/prepare-agent-env.ts +80 -27
  152. package/src/acp/resolve-agent.test.ts +188 -28
  153. package/src/acp/resolve-agent.ts +119 -42
  154. package/src/acp/resume-hint.ts +23 -0
  155. package/src/acp/session-manager.ts +507 -73
  156. package/src/agent/compaction-circuit.ts +60 -102
  157. package/src/agent/loop.ts +403 -251
  158. package/src/api/events/tool-result.ts +9 -0
  159. package/src/api/events/tool-use-start.ts +7 -0
  160. package/src/api/responses/conversation-message.ts +72 -13
  161. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  162. package/src/approvals/guardian-request-resolvers.ts +27 -1
  163. package/src/background-wake/next-wake.ts +1 -0
  164. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  165. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  166. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  167. package/src/cli/commands/memory-v3.ts +57 -199
  168. package/src/cli/commands/plugins.ts +43 -37
  169. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  170. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  171. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  172. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  173. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  174. package/src/cli/lib/install-from-github.ts +797 -60
  175. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  176. package/src/cli/lib/plugin-details.ts +416 -0
  177. package/src/cli/lib/plugin-marketplace.ts +215 -0
  178. package/src/cli/lib/search-plugins.ts +201 -29
  179. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  180. package/src/config/acp-defaults.test.ts +10 -0
  181. package/src/config/acp-defaults.ts +9 -3
  182. package/src/config/acp-schema.ts +1 -7
  183. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  184. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  185. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  186. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  187. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  188. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  189. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  190. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  191. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  192. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  193. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  194. package/src/config/bundled-tool-registry.ts +4 -0
  195. package/src/config/call-site-defaults.ts +0 -3
  196. package/src/config/feature-flag-registry.json +10 -56
  197. package/src/config/llm-resolver.ts +39 -7
  198. package/src/config/loader.ts +5 -22
  199. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  200. package/src/config/schemas/call-site-catalog.ts +0 -21
  201. package/src/config/schemas/heartbeat.ts +9 -0
  202. package/src/config/schemas/llm.ts +17 -3
  203. package/src/config/schemas/memory-v3.ts +58 -8
  204. package/src/config/seed-inference-profiles.ts +18 -0
  205. package/src/context/compactor.ts +22 -4
  206. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  207. package/src/context/strip-injections.ts +8 -2
  208. package/src/daemon/conversation-agent-loop-handlers.ts +120 -42
  209. package/src/daemon/conversation-agent-loop.ts +308 -1297
  210. package/src/daemon/conversation-error.ts +31 -4
  211. package/src/daemon/conversation-history.ts +1 -1
  212. package/src/daemon/conversation-lifecycle.ts +11 -255
  213. package/src/daemon/conversation-media-retry.ts +19 -6
  214. package/src/daemon/conversation-messaging.ts +17 -0
  215. package/src/daemon/conversation-process.ts +22 -141
  216. package/src/daemon/conversation-queue-manager.ts +8 -0
  217. package/src/daemon/conversation-registry.ts +159 -0
  218. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  219. package/src/daemon/conversation-store.ts +9 -90
  220. package/src/daemon/conversation-surfaces.ts +165 -11
  221. package/src/daemon/conversation-workspace.ts +17 -0
  222. package/src/daemon/conversation.ts +444 -62
  223. package/src/daemon/disk-pressure-policy.ts +0 -1
  224. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  225. package/src/daemon/handlers/config-channels.ts +11 -2
  226. package/src/daemon/handlers/conversations.ts +3 -1
  227. package/src/daemon/handlers/shared.ts +9 -1
  228. package/src/daemon/handlers/skills.ts +4 -1
  229. package/src/daemon/host-app-control-proxy.ts +72 -57
  230. package/src/daemon/host-proxy-preactivation.ts +1 -3
  231. package/src/daemon/lifecycle.ts +21 -7
  232. package/src/daemon/persist-unsendable-image.ts +62 -25
  233. package/src/daemon/process-message.ts +1 -1
  234. package/src/daemon/server.ts +2 -0
  235. package/src/daemon/tool-side-effects.ts +15 -0
  236. package/src/daemon/wake-conversation-ops.ts +269 -0
  237. package/src/embedded/plugin-api.ts +2 -2
  238. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  239. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  240. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  241. package/src/heartbeat/heartbeat-service.ts +26 -0
  242. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  243. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  244. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  245. package/src/ipc/skill-routes/memory.ts +4 -2
  246. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  247. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  248. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  249. package/src/memory/activation-session-store.ts +43 -0
  250. package/src/memory/conversation-crud.ts +29 -19
  251. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  252. package/src/memory/db-init.ts +6 -0
  253. package/src/memory/embedding-backend.ts +5 -22
  254. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  255. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  256. package/src/memory/jobs-worker.ts +25 -1
  257. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  258. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  259. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  260. package/src/memory/migrations/index.ts +3 -0
  261. package/src/memory/onboarding-events-store.ts +66 -18
  262. package/src/memory/schema/acp.ts +4 -0
  263. package/src/memory/schema/infrastructure.ts +13 -0
  264. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  265. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  266. package/src/memory/v2/consolidation-job.ts +2 -63
  267. package/src/memory/v2/types.ts +5 -0
  268. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  269. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  270. package/src/notifications/access-request-copy.ts +298 -0
  271. package/src/notifications/adapters/slack.ts +3 -3
  272. package/src/notifications/adapters/telegram.ts +9 -2
  273. package/src/notifications/copy-composer.ts +49 -267
  274. package/src/notifications/decision-engine.ts +16 -35
  275. package/src/notifications/home-feed-side-effect.ts +1 -6
  276. package/src/plugin-api/constants.ts +4 -0
  277. package/src/plugin-api/index.ts +40 -11
  278. package/src/plugin-api/types.ts +108 -0
  279. package/src/plugins/defaults/compaction/compact.ts +106 -0
  280. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  281. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  282. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  283. package/src/plugins/defaults/compaction/package.json +1 -2
  284. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  285. package/src/plugins/defaults/empty-response/package.json +0 -1
  286. package/src/plugins/defaults/history-repair/package.json +0 -1
  287. package/src/plugins/defaults/index.ts +156 -26
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  291. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  297. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  304. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  306. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  307. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  308. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  309. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  310. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  311. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  312. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  313. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  314. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  315. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  316. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  317. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  318. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  319. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  321. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  322. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  323. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  324. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  325. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  326. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  328. package/src/plugins/defaults/title-generate/package.json +0 -1
  329. package/src/plugins/defaults/tool-error/package.json +0 -1
  330. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  331. package/src/plugins/external-api.ts +12 -2
  332. package/src/plugins/pipeline.ts +6 -293
  333. package/src/plugins/registry.ts +9 -37
  334. package/src/plugins/types.ts +76 -381
  335. package/src/plugins/user-loader.ts +30 -127
  336. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  337. package/src/prompts/system-prompt.ts +61 -10
  338. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  339. package/src/providers/anthropic/client.ts +9 -10
  340. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  341. package/src/providers/inference/logit-bias.ts +55 -0
  342. package/src/providers/model-catalog.ts +57 -0
  343. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  344. package/src/providers/openai/chat-completions-provider.ts +44 -1
  345. package/src/providers/retry.ts +57 -7
  346. package/src/providers/types.ts +6 -0
  347. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  348. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  349. package/src/runtime/agent-wake.ts +108 -209
  350. package/src/runtime/assistant-event-hub.ts +1 -1
  351. package/src/runtime/channel-approvals.ts +1 -1
  352. package/src/runtime/interactive-ui.ts +1 -1
  353. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  354. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  355. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  356. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  357. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  358. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  359. package/src/runtime/routes/acp-routes.test.ts +185 -32
  360. package/src/runtime/routes/acp-routes.ts +328 -30
  361. package/src/runtime/routes/app-management-routes.ts +3 -0
  362. package/src/runtime/routes/approval-routes.ts +1 -1
  363. package/src/runtime/routes/browser-routes.ts +1 -1
  364. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  365. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  366. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  367. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  368. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  369. package/src/runtime/routes/conversation-routes.ts +30 -2
  370. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  371. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  372. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  373. package/src/runtime/routes/host-cu-routes.ts +1 -1
  374. package/src/runtime/routes/identity-routes.ts +18 -3
  375. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  376. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  377. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  378. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  379. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  380. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  381. package/src/runtime/routes/playground/helpers.ts +1 -1
  382. package/src/runtime/routes/plugins-routes.ts +337 -35
  383. package/src/runtime/routes/stt-routes.ts +45 -12
  384. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  385. package/src/runtime/routes/work-items-routes.ts +2 -4
  386. package/src/runtime/routes/workspace-routes.ts +50 -15
  387. package/src/runtime/services/conversation-serializer.ts +1 -1
  388. package/src/runtime/verification-outbound-actions.ts +147 -2
  389. package/src/runtime/verification-templates.ts +29 -3
  390. package/src/services/published-app-updater.ts +30 -8
  391. package/src/signals/cancel.ts +2 -4
  392. package/src/subagent/manager.ts +21 -5
  393. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  394. package/src/telemetry/activation-funnel.ts +167 -0
  395. package/src/telemetry/types.ts +13 -0
  396. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  397. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  398. package/src/tools/acp/context.ts +20 -0
  399. package/src/tools/acp/list-agents.test.ts +9 -19
  400. package/src/tools/acp/list-agents.ts +3 -15
  401. package/src/tools/acp/spawn.test.ts +173 -202
  402. package/src/tools/acp/spawn.ts +37 -172
  403. package/src/tools/acp/steer.test.ts +105 -8
  404. package/src/tools/acp/steer.ts +48 -17
  405. package/src/tools/apps/executors.ts +166 -50
  406. package/src/tools/browser/browser-execution.ts +12 -2
  407. package/src/tools/filesystem/write.ts +34 -0
  408. package/src/tools/network/web-fetch.ts +65 -24
  409. package/src/tools/skills/load.ts +1 -1
  410. package/src/tools/subagent/spawn.ts +2 -4
  411. package/src/tools/ui-surface/definitions.ts +32 -5
  412. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  413. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  414. package/docs/plugins.md +0 -832
  415. package/examples/plugins/echo/register.ts +0 -143
  416. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  417. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  418. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  419. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  420. package/src/__tests__/pipeline-runner.test.ts +0 -554
  421. package/src/daemon/wake-target-adapter.ts +0 -253
  422. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  423. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  424. package/src/memory/v3/__tests__/core.test.ts +0 -39
  425. package/src/memory/v3/__tests__/health.test.ts +0 -219
  426. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  427. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  428. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  429. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  430. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  431. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  432. package/src/memory/v3/__tests__/router.test.ts +0 -332
  433. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  434. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  435. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  436. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  437. package/src/memory/v3/assign.ts +0 -268
  438. package/src/memory/v3/capabilities.ts +0 -124
  439. package/src/memory/v3/core.ts +0 -26
  440. package/src/memory/v3/health.ts +0 -0
  441. package/src/memory/v3/maintain-job.ts +0 -314
  442. package/src/memory/v3/needle.ts +0 -115
  443. package/src/memory/v3/orchestrate.ts +0 -126
  444. package/src/memory/v3/page-content.ts +0 -34
  445. package/src/memory/v3/provider-blocks.ts +0 -26
  446. package/src/memory/v3/reconcile.ts +0 -523
  447. package/src/memory/v3/router.ts +0 -190
  448. package/src/memory/v3/selection-log-store.ts +0 -84
  449. package/src/memory/v3/selector.ts +0 -226
  450. package/src/memory/v3/shadow-plugin.ts +0 -411
  451. package/src/memory/v3/snapshot.ts +0 -209
  452. package/src/memory/v3/tree.ts +0 -174
  453. package/src/memory/v3/types.ts +0 -59
  454. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  455. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  456. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  457. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  458. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  459. package/src/plugins/defaults/compaction/register.ts +0 -35
  460. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  461. package/src/plugins/defaults/empty-response/register.ts +0 -23
  462. package/src/plugins/defaults/history-repair/register.ts +0 -24
  463. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  464. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  465. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  466. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  467. package/src/plugins/defaults/title-generate/register.ts +0 -35
  468. package/src/plugins/defaults/tool-error/register.ts +0 -23
  469. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  470. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  471. package/src/proactive-artifact/decision.test.ts +0 -226
  472. package/src/proactive-artifact/decision.ts +0 -165
  473. package/src/proactive-artifact/index.ts +0 -7
  474. package/src/proactive-artifact/job.test.ts +0 -962
  475. package/src/proactive-artifact/job.ts +0 -372
  476. package/src/proactive-artifact/message-copy.ts +0 -58
  477. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  478. package/src/proactive-artifact/trigger-state.ts +0 -123
  479. package/src/util/map-limit.ts +0 -27
  480. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  481. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  482. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  483. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  484. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  485. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  486. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  487. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  488. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  489. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  490. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
@@ -1,16 +1,17 @@
1
1
  /**
2
- * Verifies that the per-turn `mutableLatestUserMessage` plumbed into
3
- * `AgentLoop.run()` surfaces on every `SendMessageOptions.config` the loop
4
- * emits — this is the memory-v3-live cache-anchor signal (the latest user
5
- * message carries a volatile `<memory>` block, so the provider must anchor its
6
- * long-TTL cache breakpoint on the most recent STABLE message instead).
2
+ * Verifies that the `memory-v3-live` cache-anchor signal surfaces on every
3
+ * `SendMessageOptions.config` the loop emits. When the flag is on, the latest
4
+ * user message carries a volatile `<memory>` block, so the loop sets
5
+ * `providerConfig.mutableLatestUserMessage` to tell the provider to anchor its
6
+ * long-TTL cache breakpoint on the most recent STABLE message instead.
7
7
  *
8
- * Default behavior (option unset) must remain unchanged — the field is omitted
9
- * from `providerConfig` rather than carrying `undefined`/`false`, so the wire
10
- * is byte-identical to today when the flag is off.
8
+ * The loop reads the flag directly where it assembles `providerConfig`, so the
9
+ * signal is sourced from the flag rather than a run option. When the flag is
10
+ * off the field is omitted (not `false`/`undefined`) so the wire stays
11
+ * byte-identical to today.
11
12
  */
12
13
 
13
- import { describe, expect, mock, test } from "bun:test";
14
+ import { afterEach, describe, expect, mock, test } from "bun:test";
14
15
 
15
16
  import { makeMockLogger } from "./helpers/mock-logger.js";
16
17
 
@@ -19,6 +20,10 @@ mock.module("../util/logger.js", () => ({
19
20
  }));
20
21
 
21
22
  import { AgentLoop } from "../agent/loop.js";
23
+ import {
24
+ clearCachedOverrides,
25
+ setCachedOverrides,
26
+ } from "../config/feature-flag-cache.js";
22
27
  import type {
23
28
  Message,
24
29
  Provider,
@@ -75,13 +80,20 @@ function makeRecordingProvider(responses: ProviderResponse[]): {
75
80
  return { provider, configs: () => configs };
76
81
  }
77
82
 
78
- describe("AgentLoop.run — mutableLatestUserMessage plumbing", () => {
79
- test("forwards mutableLatestUserMessage=true to providerConfig on every LLM call (multi-turn)", async () => {
83
+ describe("AgentLoop.run — mutableLatestUserMessage from memory-v3-live", () => {
84
+ afterEach(() => {
85
+ clearCachedOverrides();
86
+ });
87
+
88
+ test("sets mutableLatestUserMessage on every LLM call when memory-v3-live is on (multi-turn)", async () => {
89
+ // GIVEN memory-v3-live is enabled
90
+ setCachedOverrides({ "memory-v3-live": true }, { fromGateway: true });
91
+
92
+ // AND a provider that records the config of each LLM call across a tool round-trip
80
93
  const { provider, configs } = makeRecordingProvider([
81
94
  toolUseResponse("t1", "echo", { value: "first" }),
82
95
  textResponse("done"),
83
96
  ]);
84
-
85
97
  const dummyTools: ToolDefinition[] = [
86
98
  {
87
99
  name: "echo",
@@ -92,50 +104,53 @@ describe("AgentLoop.run — mutableLatestUserMessage plumbing", () => {
92
104
  },
93
105
  },
94
106
  ];
95
-
96
- const toolExecutor = async () => ({ content: "ok", isError: false });
97
-
98
- const loop = new AgentLoop(provider, "system", {
107
+ const loop = new AgentLoop({
108
+ provider: provider,
109
+ systemPrompt: "system",
110
+ conversationId: "test-conversation",
99
111
  config: { maxTokens: 1024 },
100
112
  tools: dummyTools,
101
- toolExecutor: toolExecutor,
113
+ toolExecutor: async () => ({ content: "ok", isError: false }),
102
114
  });
103
115
 
104
- await loop.run([userMessage], () => {}, {
116
+ // WHEN the loop runs
117
+ await loop.run({
118
+ messages: [userMessage],
119
+ onEvent: () => {},
120
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
105
121
  callSite: "mainAgent",
106
- mutableLatestUserMessage: true,
107
122
  });
108
123
 
109
- // Two sends — initial + one tool round-trip. Every send carries the flag.
124
+ // THEN every send (initial + tool round-trip) carries the cache-anchor signal
110
125
  expect(configs()).toHaveLength(2);
111
126
  for (const cfg of configs()) {
112
127
  expect(cfg?.mutableLatestUserMessage).toBe(true);
113
128
  }
114
129
  });
115
130
 
116
- test("omits mutableLatestUserMessage when unset (default behavior unchanged)", async () => {
117
- const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
118
- const loop = new AgentLoop(provider, "system", {
119
- config: { maxTokens: 1024 },
120
- });
121
-
122
- await loop.run([userMessage], () => {});
131
+ test("omits mutableLatestUserMessage when memory-v3-live is off (flag-off byte-identity)", async () => {
132
+ // GIVEN memory-v3-live is off (no override; registry default is false)
133
+ clearCachedOverrides();
123
134
 
124
- expect(configs()).toHaveLength(1);
125
- expect("mutableLatestUserMessage" in (configs()[0] ?? {})).toBe(false);
126
- });
127
-
128
- test("omits mutableLatestUserMessage when explicitly false (flag-off byte-identity)", async () => {
135
+ // AND a provider that records the config of each LLM call
129
136
  const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
130
- const loop = new AgentLoop(provider, "system", {
137
+ const loop = new AgentLoop({
138
+ provider: provider,
139
+ systemPrompt: "system",
140
+ conversationId: "test-conversation",
131
141
  config: { maxTokens: 1024 },
132
142
  });
133
143
 
134
- await loop.run([userMessage], () => {}, {
144
+ // WHEN the loop runs
145
+ await loop.run({
146
+ messages: [userMessage],
147
+ onEvent: () => {},
148
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
135
149
  callSite: "mainAgent",
136
- mutableLatestUserMessage: false,
137
150
  });
138
151
 
152
+ // THEN the field is omitted entirely, not carried as false/undefined
153
+ expect(configs()).toHaveLength(1);
139
154
  expect("mutableLatestUserMessage" in (configs()[0] ?? {})).toBe(false);
140
155
  });
141
156
  });
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Tests for the `pre-model-call` and `post-model-call` plugin hooks: a plugin
3
+ * can edit the outbound request, transform the finalized assistant message
4
+ * (persisted + streamed), and defer the live stream so the transformed text is
5
+ * emitted once. All hooks here use neutral transforms (redaction / uppercasing).
6
+ */
7
+ import { beforeEach, describe, expect, test } from "bun:test";
8
+
9
+ import type { AgentEvent } from "../agent/loop.js";
10
+ import { AgentLoop } from "../agent/loop.js";
11
+ import type {
12
+ PostModelCallContext,
13
+ PreModelCallContext,
14
+ } from "../plugin-api/types.js";
15
+ import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
16
+ import { registerPlugin } from "../plugins/registry.js";
17
+ import type {
18
+ ContentBlock,
19
+ Message,
20
+ ProviderResponse,
21
+ } from "../providers/types.js";
22
+ import { createMockProvider, textResponse } from "./helpers/mock-provider.js";
23
+
24
+ const userMessage: Message = {
25
+ role: "user",
26
+ content: [{ type: "text", text: "Hello" }],
27
+ };
28
+
29
+ function collect(events: AgentEvent[]): (event: AgentEvent) => void {
30
+ return (event) => events.push(event);
31
+ }
32
+
33
+ function streamedText(events: AgentEvent[]): string {
34
+ return events
35
+ .filter(
36
+ (e): e is Extract<AgentEvent, { type: "text_delta" }> =>
37
+ e.type === "text_delta",
38
+ )
39
+ .map((e) => e.text)
40
+ .join("");
41
+ }
42
+
43
+ function textOf(content: ReadonlyArray<ContentBlock>): string {
44
+ let out = "";
45
+ for (const block of content) if (block.type === "text") out += block.text;
46
+ return out;
47
+ }
48
+
49
+ function lastAssistant(history: Message[]): Message {
50
+ for (let i = history.length - 1; i >= 0; i--) {
51
+ if (history[i].role === "assistant") return history[i];
52
+ }
53
+ throw new Error("no assistant message in history");
54
+ }
55
+
56
+ function registerOutputHookPlugin(hooks: {
57
+ preModelCall?: (ctx: PreModelCallContext) => void;
58
+ postModelCall?: (ctx: PostModelCallContext) => void;
59
+ }): void {
60
+ registerPlugin({
61
+ manifest: { name: "test-output-hooks", version: "0.0.0" },
62
+ hooks: {
63
+ ...(hooks.preModelCall
64
+ ? {
65
+ "pre-model-call": async (ctx: PreModelCallContext) => {
66
+ hooks.preModelCall!(ctx);
67
+ },
68
+ }
69
+ : {}),
70
+ ...(hooks.postModelCall
71
+ ? {
72
+ "post-model-call": async (ctx: PostModelCallContext) => {
73
+ hooks.postModelCall!(ctx);
74
+ },
75
+ }
76
+ : {}),
77
+ },
78
+ });
79
+ }
80
+
81
+ describe("agent loop output hooks", () => {
82
+ beforeEach(() => {
83
+ resetPluginRegistryAndRegisterDefaults();
84
+ });
85
+
86
+ test("post-model-call transforms the persisted message content", async () => {
87
+ registerOutputHookPlugin({
88
+ postModelCall: (ctx) => {
89
+ ctx.content = ctx.content.map((b) =>
90
+ b.type === "text"
91
+ ? { type: "text", text: b.text.replace("secret", "[redacted]") }
92
+ : b,
93
+ );
94
+ },
95
+ });
96
+ const { provider } = createMockProvider([textResponse("my secret value")]);
97
+ const loop = new AgentLoop({
98
+ provider: provider,
99
+ systemPrompt: "system",
100
+ conversationId: "test-conversation",
101
+ });
102
+ const events: AgentEvent[] = [];
103
+ const { history } = await loop.run({
104
+ messages: [userMessage],
105
+ onEvent: collect(events),
106
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
107
+ });
108
+ expect(textOf(lastAssistant(history).content)).toBe("my [redacted] value");
109
+ });
110
+
111
+ test("deferred output: the real stream is suppressed and the transformed text is emitted once", async () => {
112
+ registerOutputHookPlugin({
113
+ preModelCall: (ctx) => {
114
+ ctx.deferAssistantOutput = true;
115
+ },
116
+ postModelCall: (ctx) => {
117
+ ctx.content = [{ type: "text", text: "[filtered]" }];
118
+ },
119
+ });
120
+ const { provider } = createMockProvider([textResponse("my secret value")]);
121
+ const loop = new AgentLoop({
122
+ provider: provider,
123
+ systemPrompt: "system",
124
+ conversationId: "test-conversation",
125
+ });
126
+ const events: AgentEvent[] = [];
127
+ const { history } = await loop.run({
128
+ messages: [userMessage],
129
+ onEvent: collect(events),
130
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
131
+ });
132
+ // Her real words never streamed; only the transformed text did, once.
133
+ expect(streamedText(events)).not.toContain("secret");
134
+ expect(streamedText(events)).toBe("[filtered]");
135
+ expect(textOf(lastAssistant(history).content)).toBe("[filtered]");
136
+ });
137
+
138
+ test("without defer, the real text still streams while storage is transformed", async () => {
139
+ registerOutputHookPlugin({
140
+ postModelCall: (ctx) => {
141
+ ctx.content = [{ type: "text", text: "[stored]" }];
142
+ },
143
+ });
144
+ const { provider } = createMockProvider([textResponse("live text")]);
145
+ const loop = new AgentLoop({
146
+ provider: provider,
147
+ systemPrompt: "system",
148
+ conversationId: "test-conversation",
149
+ });
150
+ const events: AgentEvent[] = [];
151
+ const { history } = await loop.run({
152
+ messages: [userMessage],
153
+ onEvent: collect(events),
154
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
155
+ });
156
+ expect(streamedText(events)).toBe("live text"); // streamed live, untransformed
157
+ expect(textOf(lastAssistant(history).content)).toBe("[stored]"); // storage transformed
158
+ });
159
+
160
+ test("transforms text but leaves tool_use blocks intact", async () => {
161
+ const toolAndText: ProviderResponse = {
162
+ content: [
163
+ { type: "text", text: "calling" },
164
+ { type: "tool_use", id: "t1", name: "noop", input: {} },
165
+ ],
166
+ model: "mock-model",
167
+ usage: { inputTokens: 1, outputTokens: 1 },
168
+ stopReason: "tool_use",
169
+ };
170
+ registerOutputHookPlugin({
171
+ postModelCall: (ctx) => {
172
+ ctx.content = ctx.content.map((b) =>
173
+ b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
174
+ );
175
+ },
176
+ });
177
+ const { provider } = createMockProvider([
178
+ toolAndText,
179
+ textResponse("done"),
180
+ ]);
181
+ const loop = new AgentLoop({
182
+ provider: provider,
183
+ systemPrompt: "system",
184
+ conversationId: "test-conversation",
185
+ tools: [
186
+ {
187
+ name: "noop",
188
+ description: "",
189
+ input_schema: { type: "object", properties: {} },
190
+ },
191
+ ],
192
+ toolExecutor: async () => ({ content: "ok", isError: false }),
193
+ });
194
+ const { history } = await loop.run({
195
+ messages: [userMessage],
196
+ onEvent: collect([]),
197
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
198
+ });
199
+ const toolTurn = history.find(
200
+ (m) =>
201
+ m.role === "assistant" && m.content.some((b) => b.type === "tool_use"),
202
+ )!;
203
+ expect(toolTurn.content.find((b) => b.type === "text")).toEqual({
204
+ type: "text",
205
+ text: "CALLING",
206
+ });
207
+ expect(toolTurn.content.find((b) => b.type === "tool_use")).toMatchObject({
208
+ type: "tool_use",
209
+ id: "t1",
210
+ name: "noop",
211
+ });
212
+ });
213
+
214
+ test("pre-model-call can edit the outbound system prompt", async () => {
215
+ registerOutputHookPlugin({
216
+ preModelCall: (ctx) => {
217
+ ctx.systemPrompt = `${ctx.systemPrompt ?? ""} [EDITED]`;
218
+ },
219
+ });
220
+ const { provider, calls } = createMockProvider([textResponse("hi")]);
221
+ const loop = new AgentLoop({
222
+ provider: provider,
223
+ systemPrompt: "base prompt",
224
+ conversationId: "test-conversation",
225
+ });
226
+ await loop.run({
227
+ messages: [userMessage],
228
+ onEvent: collect([]),
229
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
230
+ });
231
+ expect(calls[0].systemPrompt).toContain("[EDITED]");
232
+ });
233
+
234
+ test("fail-open: a hook that mutates in place AND then throws cannot corrupt the persisted content", async () => {
235
+ // The hook mutates the array it receives before throwing. If the loop
236
+ // handed the hook the real `assistantMessage.content` array, the
237
+ // mid-mutation would survive into history. The loop must clone the
238
+ // content before invoking the hook.
239
+ registerOutputHookPlugin({
240
+ postModelCall: (ctx) => {
241
+ ctx.content.push({ type: "text", text: "[INJECTED]" });
242
+ throw new Error("boom");
243
+ },
244
+ });
245
+ const { provider } = createMockProvider([textResponse("untouched")]);
246
+ const loop = new AgentLoop({
247
+ provider: provider,
248
+ systemPrompt: "system",
249
+ conversationId: "test-conversation",
250
+ });
251
+ const { history } = await loop.run({
252
+ messages: [userMessage],
253
+ onEvent: collect([]),
254
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
255
+ });
256
+ const finalContent = lastAssistant(history).content;
257
+ expect(textOf(finalContent)).toBe("untouched");
258
+ expect(
259
+ finalContent.some((b) => b.type === "text" && b.text === "[INJECTED]"),
260
+ ).toBe(false);
261
+ });
262
+
263
+ test("max_tokens turn: the hook fires and the deferred final text is emitted", async () => {
264
+ // Without this, an output-filter plugin misses the truncated reply; with
265
+ // defer set, the live stream was suppressed and the client would see
266
+ // nothing at all.
267
+ const seen: { calls: number } = { calls: 0 };
268
+ registerOutputHookPlugin({
269
+ preModelCall: (ctx) => {
270
+ ctx.deferAssistantOutput = true;
271
+ },
272
+ postModelCall: (ctx) => {
273
+ seen.calls += 1;
274
+ ctx.content = ctx.content.map((b) =>
275
+ b.type === "text" ? { type: "text", text: b.text.toUpperCase() } : b,
276
+ );
277
+ },
278
+ });
279
+ const truncated: ProviderResponse = {
280
+ content: [{ type: "text", text: "partial answer" }],
281
+ model: "mock-model",
282
+ usage: { inputTokens: 1, outputTokens: 1 },
283
+ stopReason: "max_tokens",
284
+ };
285
+ const { provider } = createMockProvider([truncated]);
286
+ const loop = new AgentLoop({
287
+ provider: provider,
288
+ systemPrompt: "system",
289
+ conversationId: "test-conversation",
290
+ });
291
+ const events: AgentEvent[] = [];
292
+ const { history } = await loop.run({
293
+ messages: [userMessage],
294
+ onEvent: collect(events),
295
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
296
+ });
297
+ expect(seen.calls).toBe(1);
298
+ expect(textOf(lastAssistant(history).content)).toBe("PARTIAL ANSWER");
299
+ // Real stream suppressed; transformed final text emitted once.
300
+ expect(streamedText(events)).toBe("PARTIAL ANSWER");
301
+ });
302
+
303
+ test("deferred final text passes through sensitive-output substitution", async () => {
304
+ // A tool returns a sensitive binding; the next assistant turn uses the
305
+ // placeholder (the persisted message must keep it — model never sees real
306
+ // values on reload), but the deferred final stream must show the real
307
+ // value, just like the normal live stream would.
308
+ registerOutputHookPlugin({
309
+ preModelCall: (ctx) => {
310
+ ctx.deferAssistantOutput = true;
311
+ },
312
+ });
313
+ const placeholder = "VELLUM_ASSISTANT_INVITE_CODE_TESTXXXX";
314
+ const real = "real-invite-code-xyz";
315
+ const toolThenText = [
316
+ {
317
+ content: [{ type: "tool_use", id: "t1", name: "issue", input: {} }],
318
+ model: "mock-model",
319
+ usage: { inputTokens: 1, outputTokens: 1 },
320
+ stopReason: "tool_use",
321
+ } as ProviderResponse,
322
+ textResponse(`Your code is ${placeholder}.`),
323
+ ];
324
+ const { provider } = createMockProvider(toolThenText);
325
+ const loop = new AgentLoop({
326
+ provider: provider,
327
+ systemPrompt: "system",
328
+ conversationId: "test-conversation",
329
+ tools: [
330
+ {
331
+ name: "issue",
332
+ description: "",
333
+ input_schema: { type: "object", properties: {} },
334
+ },
335
+ ],
336
+ toolExecutor: async () => ({
337
+ content: `code=${placeholder}`,
338
+ isError: false,
339
+ sensitiveBindings: [{ kind: "invite_code", placeholder, value: real }],
340
+ }),
341
+ });
342
+ const events: AgentEvent[] = [];
343
+ const { history } = await loop.run({
344
+ messages: [userMessage],
345
+ onEvent: collect(events),
346
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
347
+ });
348
+ // Persisted message keeps the placeholder — model must never see real
349
+ // values on reload.
350
+ expect(textOf(lastAssistant(history).content)).toContain(placeholder);
351
+ expect(textOf(lastAssistant(history).content)).not.toContain(real);
352
+ // Streamed final text shows the substituted real value, matching the
353
+ // behavior of the normal live stream.
354
+ expect(streamedText(events)).toContain(real);
355
+ expect(streamedText(events)).not.toContain(placeholder);
356
+ });
357
+ });
@@ -110,13 +110,19 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
110
110
  _input: Record<string, unknown>,
111
111
  ) => ({ content: "ok", isError: false });
112
112
 
113
- const loop = new AgentLoop(provider, "system", {
113
+ const loop = new AgentLoop({
114
+ provider: provider,
115
+ systemPrompt: "system",
116
+ conversationId: "test-conversation",
114
117
  config: { maxTokens: 1024 },
115
118
  tools: dummyTools,
116
119
  toolExecutor: toolExecutor,
117
120
  });
118
121
 
119
- await loop.run([userMessage], () => {}, {
122
+ await loop.run({
123
+ messages: [userMessage],
124
+ onEvent: () => {},
125
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
120
126
  callSite: "mainAgent",
121
127
  overrideProfile: "fast",
122
128
  });
@@ -130,11 +136,18 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
130
136
 
131
137
  test("omits overrideProfile from providerConfig when unset (default behavior unchanged)", async () => {
132
138
  const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
133
- const loop = new AgentLoop(provider, "system", {
139
+ const loop = new AgentLoop({
140
+ provider: provider,
141
+ systemPrompt: "system",
142
+ conversationId: "test-conversation",
134
143
  config: { maxTokens: 1024 },
135
144
  });
136
145
 
137
- await loop.run([userMessage], () => {});
146
+ await loop.run({
147
+ messages: [userMessage],
148
+ onEvent: () => {},
149
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
150
+ });
138
151
 
139
152
  // Single send, no overrideProfile field at all.
140
153
  expect(configs()).toHaveLength(1);
@@ -148,11 +161,17 @@ describe("AgentLoop.run — overrideProfile plumbing", () => {
148
161
  // receives so a non-existent profile silently falls back at the
149
162
  // provider layer (covered by provider-send-message-override-profile.test.ts).
150
163
  const { provider, configs } = makeRecordingProvider([textResponse("hi")]);
151
- const loop = new AgentLoop(provider, "system", {
164
+ const loop = new AgentLoop({
165
+ provider: provider,
166
+ systemPrompt: "system",
167
+ conversationId: "test-conversation",
152
168
  config: { maxTokens: 1024 },
153
169
  });
154
170
 
155
- await loop.run([userMessage], () => {}, {
171
+ await loop.run({
172
+ messages: [userMessage],
173
+ onEvent: () => {},
174
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
156
175
  callSite: "mainAgent",
157
176
  overrideProfile: "does-not-exist",
158
177
  });
@@ -83,14 +83,19 @@ describe("AgentLoop provider_error event emission", () => {
83
83
  const { provider, calls } = makeThrowingProvider("anthropic", () => thrown);
84
84
 
85
85
  const events: AgentEvent[] = [];
86
- const loop = new AgentLoop(provider, "you are a helpful assistant");
86
+ const loop = new AgentLoop({
87
+ provider: provider,
88
+ systemPrompt: "you are a helpful assistant",
89
+ conversationId: "test-conversation",
90
+ });
87
91
 
88
- await loop.run(
89
- [{ role: "user", content: [{ type: "text", text: "hi" }] }],
90
- (e) => {
92
+ await loop.run({
93
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
94
+ onEvent: (e) => {
91
95
  events.push(e);
92
96
  },
93
- );
97
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
98
+ });
94
99
 
95
100
  expect(calls).toHaveLength(1);
96
101
 
@@ -122,14 +127,19 @@ describe("AgentLoop provider_error event emission", () => {
122
127
  const { provider } = makeThrowingProvider("gemini", () => thrown);
123
128
 
124
129
  const events: AgentEvent[] = [];
125
- const loop = new AgentLoop(provider, "system");
130
+ const loop = new AgentLoop({
131
+ provider: provider,
132
+ systemPrompt: "system",
133
+ conversationId: "test-conversation",
134
+ });
126
135
 
127
- await loop.run(
128
- [{ role: "user", content: [{ type: "text", text: "hi" }] }],
129
- (e) => {
136
+ await loop.run({
137
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
138
+ onEvent: (e) => {
130
139
  events.push(e);
131
140
  },
132
- );
141
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
142
+ });
133
143
 
134
144
  const providerErrorIdx = events.findIndex(
135
145
  (e) => e.type === "provider_error",
@@ -148,14 +158,19 @@ describe("AgentLoop provider_error event emission", () => {
148
158
  const { provider } = makeThrowingProvider("openai", () => thrown);
149
159
 
150
160
  const events: AgentEvent[] = [];
151
- const loop = new AgentLoop(provider, "system");
161
+ const loop = new AgentLoop({
162
+ provider: provider,
163
+ systemPrompt: "system",
164
+ conversationId: "test-conversation",
165
+ });
152
166
 
153
- await loop.run(
154
- [{ role: "user", content: [{ type: "text", text: "hi" }] }],
155
- (e) => {
167
+ await loop.run({
168
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
169
+ onEvent: (e) => {
156
170
  events.push(e);
157
171
  },
158
- );
172
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
173
+ });
159
174
 
160
175
  const providerErrorEvent = events.find((e) => e.type === "provider_error");
161
176
  expect(providerErrorEvent).toBeDefined();
@@ -179,15 +194,20 @@ describe("AgentLoop provider_error event emission", () => {
179
194
  });
180
195
 
181
196
  const events: AgentEvent[] = [];
182
- const loop = new AgentLoop(provider, "system");
197
+ const loop = new AgentLoop({
198
+ provider: provider,
199
+ systemPrompt: "system",
200
+ conversationId: "test-conversation",
201
+ });
183
202
 
184
- await loop.run(
185
- [{ role: "user", content: [{ type: "text", text: "hi" }] }],
186
- (e) => {
203
+ await loop.run({
204
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
205
+ onEvent: (e) => {
187
206
  events.push(e);
188
207
  },
189
- { signal: controller.signal },
190
- );
208
+ trust: { sourceChannel: "vellum", trustClass: "unknown" },
209
+ signal: controller.signal,
210
+ });
191
211
 
192
212
  const providerErrorEvent = events.find((e) => e.type === "provider_error");
193
213
  // Cancellation should never produce a recording row — there's no