@vellumai/assistant 0.8.8 → 0.8.9-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 (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
package/src/agent/loop.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as Sentry from "@sentry/node";
2
2
 
3
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
4
+ import { getConfig } from "../config/loader.js";
3
5
  import type { LLMCallSite } from "../config/schemas/llm.js";
4
6
  import { stripInjectionsForCompaction } from "../context/strip-injections.js";
5
7
  import {
@@ -9,20 +11,26 @@ import {
9
11
  getCalibrationProviderKey,
10
12
  } from "../context/token-estimator.js";
11
13
  import type { ContextWindowResult } from "../context/window-manager.js";
14
+ import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
12
15
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
16
+ import type { TrustContext } from "../daemon/trust-context.js";
17
+ import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
13
18
  import { HOOKS } from "../plugin-api/constants.js";
14
- import type { PostToolUseContext, StopContext } from "../plugin-api/types.js";
15
- import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
16
- import type { PostCompactionHookInput } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
17
- import { DEFAULT_TIMEOUTS, runHook, runPipeline } from "../plugins/pipeline.js";
18
- import { getMiddlewaresFor } from "../plugins/registry.js";
19
19
  import type {
20
- CompactionArgs,
21
- CompactionCircuitEvent,
22
- CompactionResult,
23
- TurnContext,
24
- } from "../plugins/types.js";
25
- import { PluginTimeoutError } from "../plugins/types.js";
20
+ AssistantMessageContext,
21
+ PostToolUseContext,
22
+ PreModelCallContext,
23
+ StopContext,
24
+ } from "../plugin-api/types.js";
25
+ import {
26
+ DEFAULT_COMPACTION_PLUGIN_NAME,
27
+ defaultCompact,
28
+ } from "../plugins/defaults/compaction/compact.js";
29
+ import { getContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
30
+ import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
31
+ import { runHook } from "../plugins/pipeline.js";
32
+ import type { CompactionCircuitEvent } from "../plugins/types.js";
33
+ import { PluginExecutionError } from "../plugins/types.js";
26
34
  import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
27
35
  import type {
28
36
  ContentBlock,
@@ -308,7 +316,7 @@ export type AgentEvent =
308
316
  * immediately before re-injection — whether or not the pipeline actually
309
317
  * compacted. The daemon's event dispatcher always commits `basis` (the
310
318
  * stripped pre-compaction history) as the conversation's durable message
311
- * state, so re-injection ({@link MidLoopCompaction.reinject}) re-applies
319
+ * state, so re-injection ({@link postCompact}) re-applies
312
320
  * injections onto the stripped base rather than stacking on top of the
313
321
  * still-injected messages. When `result.compacted` is set it
314
322
  * additionally commits the durable compaction result (DB-record fields,
@@ -382,34 +390,17 @@ export function isMaxTokensStopReason(
382
390
  }
383
391
 
384
392
  /**
385
- * Build a minimal {@link TurnContext} for pipeline invocations inside the
386
- * agent loop. Real production call sites thread a full `TurnContext` into
387
- * `AgentLoop.run()` (see the `turnContext` parameter on
388
- * {@link AgentLoop.run}); this helper is the fallback used only by unit
389
- * tests that construct `AgentLoop` directly without an orchestrator.
390
- *
391
- * When the orchestrator-supplied context is present it is used directly so the
392
- * pipeline sees the real `conversationId`, trust, and `contextWindowManager`.
393
- * In the fallback path the returned context is still useful for pipeline
394
- * logging: `requestId` surfaces in every structured record, and `turnIndex`
395
- * reflects the current tool-use iteration.
393
+ * Concatenate the text of an assistant message's `text` blocks (ignoring
394
+ * `tool_use`, `thinking`, and other non-text blocks). Used to re-emit the
395
+ * finalized text as a single `text_delta` when a turn's live output was
396
+ * deferred by a `pre-model-call` hook.
396
397
  */
397
- function buildLoopTurnContext(
398
- requestId: string | undefined,
399
- turnIndex: number,
400
- ): TurnContext {
401
- return {
402
- requestId: requestId ?? "agent-loop",
403
- // Loop-scoped pipelines do not currently carry a conversation ID; the
404
- // outer orchestrator owns that dimension. Use a fixed sentinel so log
405
- // consumers can filter loop-origin records out of conversation queries.
406
- conversationId: "agent-loop",
407
- turnIndex,
408
- trust: {
409
- sourceChannel: "vellum",
410
- trustClass: "unknown",
411
- },
412
- };
398
+ function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
399
+ let text = "";
400
+ for (const block of content) {
401
+ if (block.type === "text") text += block.text;
402
+ }
403
+ return text;
413
404
  }
414
405
 
415
406
  /**
@@ -454,29 +445,11 @@ export interface ResolvedSystemPrompt {
454
445
  model?: string;
455
446
  }
456
447
 
457
- /**
458
- * Orchestrator-supplied hook the loop invokes when the mid-loop budget gate
459
- * trips and inline compaction runs. The loop owns the trigger, the
460
- * `compaction` pipeline call, the result interpretation (circuit-breaker
461
- * bookkeeping + the exhaustion decision), and the inline continue; this hook
462
- * bridges the injection state the loop is intentionally blind to. Durable
463
- * persistence is signalled out-of-band via the `history_stripped` (marker)
464
- * and `compaction_completed` (basis commit + successful summary) {@link
465
- * AgentEvent}s; the {@link MidLoopCompaction.postCompactionHook} is
466
- * orchestrator-supplied, and its inputs migrate loop-ward as the loop
467
- * subsumes the re-injection ceremony.
468
- */
469
- export interface MidLoopCompaction {
470
- /**
471
- * Re-apply runtime injections onto the post-compaction history and return
472
- * the history to continue from. The loop supplies its own working state via
473
- * {@link PostCompactionHookInput} so the hook re-injects from that rather
474
- * than reading it back from orchestrator state.
475
- */
476
- postCompactionHook: (input: PostCompactionHookInput) => Promise<Message[]>;
477
- }
478
-
479
448
  export interface AgentLoopRunOptions {
449
+ /** Input history the run starts from; the loop appends its output onto a copy. */
450
+ messages: Message[];
451
+ /** Sink the loop streams its {@link AgentEvent}s through as the turn runs. */
452
+ onEvent: (event: AgentEvent) => void | Promise<void>;
480
453
  signal?: AbortSignal;
481
454
  requestId?: string;
482
455
  onCheckpoint?: (
@@ -484,14 +457,15 @@ export interface AgentLoopRunOptions {
484
457
  ) => CheckpointDecision | Promise<CheckpointDecision>;
485
458
  callSite?: LLMCallSite;
486
459
  /**
487
- * Per-turn context supplied by the orchestrator. Every pipeline
488
- * invocation inside the loop clones from this value (overwriting only
489
- * `turnIndex`/`requestId`) so middleware sees the real conversation
490
- * identity, trust class, and `contextWindowManager` rather than the
491
- * `"agent-loop"` sentinel used when the loop is instantiated standalone
492
- * in unit tests.
460
+ * Trust classification and channel identity for the turn's inbound actor,
461
+ * supplied by the caller as the turn-start snapshot. Read only on the
462
+ * mid-loop in-place compaction path — to scope the compactor's image
463
+ * manifest (guardian-only attachments are excluded for untrusted actors) and
464
+ * forwarded to {@link postCompact}. Callers without a meaningful actor (agent
465
+ * wakes, standalone unit tests) pass an `unknown`-class snapshot so the
466
+ * compactor fail-closes to excluding guardian-only attachments.
493
467
  */
494
- turnContext?: TurnContext;
468
+ trust: TrustContext;
495
469
  /**
496
470
  * Ad-hoc inference-profile override applied to every LLM call the loop
497
471
  * issues. When set, each `SendMessageOptions.config` carries
@@ -514,21 +488,51 @@ export interface AgentLoopRunOptions {
514
488
  overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
515
489
  };
516
490
  /**
517
- * Hooks for inline mid-loop compaction. When supplied and the budget gate
518
- * trips, the loop compacts in place and continues instead of yielding
519
- * `exitReason = "budget"`. Callers without a compaction path (agent wakes,
520
- * convergence/auto-compress reruns) omit this and keep yielding for budget.
491
+ * When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
492
+ * budget gate runs before the very first provider call subsuming the
493
+ * proactive turn-start compaction the orchestrator would otherwise perform
494
+ * inline before `run()` as well as before each tool-use re-entry. When the
495
+ * gate trips it compacts the running history in place, re-applying runtime
496
+ * injections via the default post-compaction hook ({@link postCompact}), and
497
+ * continues instead of yielding `exitReason = "budget"`.
498
+ *
499
+ * The first-call pass honors the compaction circuit breaker and proceeds with
500
+ * the call whether or not it compacted (preflight-overflow recovery and the
501
+ * convergence loop remain the escalation path), so it never yields on the
502
+ * first call. Reruns without an inline compaction path (agent wakes,
503
+ * convergence/auto-compress reruns) leave it `false`: they skip the
504
+ * first-call gate and keep yielding for budget on mid-loop re-entries.
505
+ * Defaults to `false` when omitted.
521
506
  */
522
- compaction?: MidLoopCompaction;
507
+ compactInPlace?: boolean;
523
508
  /**
524
- * When true, the latest user message carries a volatile per-turn block
525
- * (e.g. a memory-v3 `<memory>` injection) that varies across otherwise
526
- * identical turns. Forwarded to each `SendMessageOptions.config` so the
527
- * provider anchors its long-TTL cache breakpoint on the most recent STABLE
528
- * user message instead of the volatile latest one, keeping the cached
529
- * prefix reusable. Default unset → existing behavior.
509
+ * Whether the in-flight turn has no human present to answer clarification
510
+ * questions. Resolved once by the orchestrator at turn start and forwarded to
511
+ * {@link postCompact} so post-compaction
512
+ * re-injection uses the turn-start snapshot rather than re-reading mutable
513
+ * client/headless state mid-turn. Defaults to `false` when omitted.
530
514
  */
531
- mutableLatestUserMessage?: boolean;
515
+ isNonInteractive?: boolean;
516
+ /**
517
+ * The `model_profile:` turn-context label resolved once by the orchestrator
518
+ * at turn start, or `null` when the active inference profile is unchanged
519
+ * since the last notified one. Forwarded to
520
+ * {@link postCompact} so post-compaction re-injection
521
+ * re-emits the turn-start value rather than re-deriving the change-detected
522
+ * label (which flips once the notification is persisted mid-turn). Defaults to
523
+ * `null` when omitted.
524
+ */
525
+ modelProfile?: string | null;
526
+ /**
527
+ * Inbound actor identity and trust fields for the unified `<turn_context>`
528
+ * block, or `null` on guardian turns. Resolved once by the orchestrator at
529
+ * turn start via the actor-trust resolver, whose contact/member registry
530
+ * inputs can be mutated mid-turn by contact tools, and forwarded to
531
+ * {@link postCompact} so post-compaction
532
+ * re-injection re-emits the turn-start value rather than re-resolving it.
533
+ * Defaults to `null` when omitted.
534
+ */
535
+ actorContext?: InboundActorContext | null;
532
536
  }
533
537
 
534
538
  /**
@@ -570,17 +574,21 @@ export type LoopToolExecutor = (
570
574
  }>;
571
575
 
572
576
  export interface AgentLoopConstructorOptions {
577
+ /** LLM provider the loop issues every call through. */
578
+ provider: Provider;
579
+ /** Base system prompt, before any per-turn dynamic resolution. */
580
+ systemPrompt: string;
573
581
  config?: Partial<AgentLoopConfig>;
574
582
  tools?: ToolDefinition[];
575
583
  toolExecutor?: LoopToolExecutor;
576
584
  resolveTools?: (history: Message[]) => ToolDefinition[];
577
585
  resolveSystemPrompt?: (history: Message[]) => ResolvedSystemPrompt;
578
586
  /**
579
- * Conversation this loop drives. Used to scope the loop-held compaction
580
- * circuit breaker; defaults to an empty key for test loops that never
581
- * exercise compaction.
587
+ * Conversation this loop drives. Scopes the loop-held compaction circuit
588
+ * breaker and is the source of truth the loop's pipeline contexts and
589
+ * post-compaction re-injection resolve the live conversation through.
582
590
  */
583
- conversationId?: string;
591
+ conversationId: string;
584
592
  }
585
593
 
586
594
  export class AgentLoop {
@@ -594,6 +602,14 @@ export class AgentLoop {
594
602
  | null;
595
603
  private toolExecutor: LoopToolExecutor | null;
596
604
 
605
+ /**
606
+ * Conversation this loop drives. Source of truth for the `conversationId`
607
+ * the loop's pipeline contexts and post-compaction re-injection resolve the
608
+ * live conversation through, so the loop knows its own identity without a
609
+ * threaded-in turn context.
610
+ */
611
+ private readonly conversationId: string;
612
+
597
613
  /**
598
614
  * Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
599
615
  * conversation, so it is the source of truth for the cross-turn failure
@@ -603,19 +619,17 @@ export class AgentLoop {
603
619
  */
604
620
  readonly compactionCircuit: CompactionCircuit;
605
621
 
606
- constructor(
607
- provider: Provider,
608
- systemPrompt: string,
609
- options?: AgentLoopConstructorOptions,
610
- ) {
622
+ constructor(options: AgentLoopConstructorOptions) {
611
623
  const {
624
+ provider,
625
+ systemPrompt,
612
626
  config,
613
627
  tools,
614
628
  toolExecutor,
615
629
  resolveTools,
616
630
  resolveSystemPrompt,
617
631
  conversationId,
618
- } = options ?? {};
632
+ } = options;
619
633
  this.provider = provider;
620
634
  this.systemPrompt = systemPrompt;
621
635
  this.config = { ...DEFAULT_CONFIG, ...config };
@@ -623,7 +637,8 @@ export class AgentLoop {
623
637
  this.resolveTools = resolveTools ?? null;
624
638
  this.resolveSystemPrompt = resolveSystemPrompt ?? null;
625
639
  this.toolExecutor = toolExecutor ?? null;
626
- this.compactionCircuit = new CompactionCircuit(conversationId ?? "");
640
+ this.conversationId = conversationId;
641
+ this.compactionCircuit = new CompactionCircuit(this.conversationId);
627
642
  }
628
643
 
629
644
  /**
@@ -675,23 +690,15 @@ export class AgentLoop {
675
690
  * compaction outcome into a user-visible turn failure.
676
691
  */
677
692
  private async recordCompactionOutcome(
678
- turnContext: TurnContext,
693
+ requestId: string | undefined,
679
694
  summaryFailed: boolean,
680
695
  onEvent: (event: AgentEvent) => void | Promise<void>,
681
696
  ): Promise<void> {
682
697
  try {
683
- await this.compactionCircuit.recordOutcome(
684
- {
685
- currentRequestId: turnContext.requestId,
686
- currentTurnTrustContext: turnContext.trust,
687
- turnCount: turnContext.turnIndex,
688
- },
689
- summaryFailed,
690
- onEvent,
691
- );
698
+ await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
692
699
  } catch (recordError) {
693
700
  log.error(
694
- { err: recordError, requestId: turnContext.requestId },
701
+ { err: recordError, requestId },
695
702
  "Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
696
703
  );
697
704
  }
@@ -700,19 +707,21 @@ export class AgentLoop {
700
707
  /**
701
708
  * Compact the running history in place when the mid-loop budget gate trips.
702
709
  *
703
- * Runs the `compaction` pipeline natively (like {@link estimateTokens}) on
704
- * the stripped history, then re-applies injections via the supplied hooks.
705
- * Returns the history to continue from, or `null` when the compactor timed
706
- * out or exhausted its retry budget so the caller yields
707
- * `exitReason = "budget"` and the orchestrator escalates.
710
+ * Calls the default compaction plugin on the stripped history, then
711
+ * re-applies injections via the supplied hooks. Returns the history to
712
+ * continue from, or `null` when the compactor exhausted its retry budget so
713
+ * the caller yields `exitReason = "budget"` and the orchestrator escalates.
708
714
  */
709
715
  private async compact(
710
716
  history: Message[],
711
- turnContext: TurnContext,
712
- compaction: MidLoopCompaction,
717
+ requestId: string | undefined,
718
+ trust: TrustContext,
713
719
  signal: AbortSignal | undefined,
714
720
  onEvent: (event: AgentEvent) => void | Promise<void>,
715
721
  overrideProfile: string | null,
722
+ isNonInteractive: boolean,
723
+ modelProfile: string | null,
724
+ actorContext: InboundActorContext | null,
716
725
  ): Promise<Message[] | null> {
717
726
  await onEvent({ type: "context_compacting" });
718
727
  // Strip runtime injections so the compactor summarizes the raw persistent
@@ -721,49 +730,38 @@ export class AgentLoop {
721
730
  // Record the history-stripped marker right after stripping, before the
722
731
  // pipeline runs.
723
732
  await onEvent({ type: "history_stripped" });
724
- let result: CompactionResult;
725
- try {
726
- result = await runPipeline<CompactionArgs, CompactionResult>(
727
- "compaction",
728
- getMiddlewaresFor("compaction"),
729
- (args) => defaultCompactionTerminal(args, turnContext),
730
- // The mid-loop budget gate is reached only when this turn decides to
731
- // compact in place, so `force` the pipeline past its auto-threshold
732
- // check. `actorTrustClass` comes from the turn context (the actor whose
733
- // turn triggered compaction) so the compactor's image manifest excludes
734
- // guardian-only attachments for untrusted actors. `overrideProfile` is
735
- // the turn's resolved inference-profile override for the summary call.
736
- {
737
- messages: rawHistory,
738
- signal,
739
- options: {
740
- force: true,
741
- actorTrustClass: turnContext.trust.trustClass,
742
- overrideProfile,
743
- },
744
- },
745
- turnContext,
746
- DEFAULT_TIMEOUTS.compaction,
733
+ // The compaction module owns the per-conversation manager; resolve it from
734
+ // the store rather than holding a handle on the loop. Absent for callers
735
+ // without a compaction path (agent wakes, standalone unit tests), which
736
+ // never reach this gate.
737
+ const manager = getContextWindowManager(this.conversationId);
738
+ if (manager == null) {
739
+ throw new PluginExecutionError(
740
+ `default-compaction: no ContextWindowManager registered for conversation ${this.conversationId} — the compaction store must construct one before compaction runs`,
741
+ DEFAULT_COMPACTION_PLUGIN_NAME,
747
742
  );
748
- } catch (error) {
749
- if (error instanceof PluginTimeoutError) {
750
- // A timeout counts as a compaction failure against the circuit breaker.
751
- await this.recordCompactionOutcome(turnContext, true, onEvent);
752
- return null;
753
- }
754
- throw error;
755
743
  }
756
- // `CompactionResult` is intentionally `unknown` at the plugin boundary so
757
- // plugin consumers don't import the window manager; the loop ran the
758
- // pipeline, so it interprets the concrete result here.
759
- const compactResult = result as ContextWindowResult;
744
+ // The mid-loop budget gate is reached only when this turn decides to
745
+ // compact in place, so `force` past the auto-threshold check.
746
+ // `actorTrustClass` comes from the turn's trust snapshot (the actor whose
747
+ // turn triggered compaction) so the compactor's image manifest excludes
748
+ // guardian-only attachments for untrusted actors. `overrideProfile` is the
749
+ // turn's resolved inference-profile override for the summary call.
750
+ const compactResult = await defaultCompact({
751
+ manager,
752
+ messages: rawHistory,
753
+ signal,
754
+ force: true,
755
+ actorTrustClass: trust.trustClass,
756
+ overrideProfile,
757
+ });
760
758
  // `force: true` bypasses the auto-threshold gate, but early returns
761
759
  // for "no eligible messages" / "insufficient messages" still leave
762
760
  // `summaryFailed` undefined. Only record an outcome when the summary LLM
763
761
  // actually ran.
764
762
  if (compactResult.summaryFailed !== undefined) {
765
763
  await this.recordCompactionOutcome(
766
- turnContext,
764
+ requestId,
767
765
  compactResult.summaryFailed,
768
766
  onEvent,
769
767
  );
@@ -783,29 +781,37 @@ export class AgentLoop {
783
781
  // Re-inject onto the same base the `compaction_completed` dispatch commits:
784
782
  // the compacted messages when the pipeline compacted, the stripped
785
783
  // pre-compaction history otherwise.
786
- return compaction.postCompactionHook({
784
+ const injection = await postCompact({
787
785
  history: compactResult.compacted ? compactResult.messages : rawHistory,
788
- turnContext,
786
+ requestId,
787
+ conversationId: this.conversationId,
788
+ trust,
789
+ isNonInteractive,
790
+ // Mid-loop re-injection always runs at full injection volume.
791
+ mode: "full",
792
+ modelProfile,
793
+ actorContext,
789
794
  });
795
+ return injection.messages;
790
796
  }
791
797
 
792
- async run(
793
- messages: Message[],
794
- onEvent: (event: AgentEvent) => void | Promise<void>,
795
- options?: AgentLoopRunOptions,
796
- ): Promise<AgentLoopRunResult> {
798
+ async run(options: AgentLoopRunOptions): Promise<AgentLoopRunResult> {
797
799
  const {
800
+ messages,
801
+ onEvent,
798
802
  signal,
799
803
  requestId,
800
804
  onCheckpoint,
801
805
  callSite,
802
- turnContext,
806
+ trust,
803
807
  overrideProfile,
804
808
  resolveOverrideProfile,
805
809
  resolveContextWindow,
806
- compaction,
807
- mutableLatestUserMessage,
808
- } = options ?? {};
810
+ compactInPlace = false,
811
+ isNonInteractive = false,
812
+ modelProfile = null,
813
+ actorContext = null,
814
+ } = options;
809
815
  let history = [...messages];
810
816
  // Index into `history` where this run's appended output begins. It starts
811
817
  // after the input and resets to the compacted base whenever the loop
@@ -818,6 +824,13 @@ export class AgentLoop {
818
824
  let lastLlmCallTime = 0;
819
825
  let exitReason: ExitReason | null = null;
820
826
  let appendedNewMessages = false;
827
+ // Armed at the end of a tool-use iteration so the budget gate runs at the
828
+ // top of the NEXT iteration — before that iteration's provider call —
829
+ // instead of after the current one. Stop-hook re-query continues re-enter
830
+ // without arming, so the gate fires on exactly the same occasions as the
831
+ // prior post-call placement, plus the first call when
832
+ // `compactInPlace` is set (the primary run's turn-start compaction).
833
+ let budgetGateArmed = compactInPlace;
821
834
  const rlog = requestId ? log.child({ requestId }) : log;
822
835
 
823
836
  // Resolve the inference-profile override that applies right now. The
@@ -858,6 +871,104 @@ export class AgentLoop {
858
871
  let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
859
872
 
860
873
  try {
874
+ // ── Pre-call budget gate ─────────────────────────────────────
875
+ // When overflow recovery is enabled, estimate the running context
876
+ // size as it approaches the preflight budget before issuing the
877
+ // provider call. With `compactInPlace` the loop compacts in place and
878
+ // proceeds with the call; otherwise it yields (`exitReason =
879
+ // "budget"`) so the orchestrator can recover before the call risks a
880
+ // hard context-too-large rejection. Keyed off the loop's own
881
+ // `history.length` (the messages actually in context this turn,
882
+ // including tool iterations) rather than the durable conversation
883
+ // count.
884
+ //
885
+ // Armed after each tool-use iteration; stop-hook re-query continues
886
+ // skip it. The first call runs it only when `compactInPlace` is set,
887
+ // where it stands in for the orchestrator's turn-start compaction: it
888
+ // honors the compaction circuit breaker and proceeds with the call
889
+ // rather than yielding, since there is no prior turn output to
890
+ // escalate.
891
+ if (budgetGateArmed) {
892
+ budgetGateArmed = false;
893
+ // The gate only re-arms after a completed tool-use iteration
894
+ // (`toolUseTurns` is incremented first), so reaching it with
895
+ // `toolUseTurns === 0` uniquely identifies the first-call pass: it
896
+ // compacts-or-proceeds (never yields) and honors the compaction
897
+ // circuit breaker, matching the orchestrator's turn-start compaction.
898
+ const isFirstCallGate = toolUseTurns === 0;
899
+ const contextWindow = resolveContextWindow?.();
900
+ if (contextWindow?.overflowRecovery.enabled) {
901
+ const { maxInputTokens, overflowRecovery } = contextWindow;
902
+ const safetyMargin =
903
+ history.length > LONG_HISTORY_MESSAGE_THRESHOLD
904
+ ? Math.max(
905
+ overflowRecovery.safetyMarginRatio,
906
+ LONG_HISTORY_SAFETY_MARGIN_FLOOR,
907
+ )
908
+ : overflowRecovery.safetyMarginRatio;
909
+ const preflightBudget = Math.floor(
910
+ maxInputTokens * (1 - safetyMargin),
911
+ );
912
+ const midLoopThreshold =
913
+ preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
914
+ const estimated = this.estimateTokens(history);
915
+ if (estimated > midLoopThreshold) {
916
+ let compactedInPlace = false;
917
+ // The turn-start pass skips compaction while the circuit breaker
918
+ // is open so a run of failed summaries doesn't keep hammering the
919
+ // summary LLM; mid-loop compaction is force-driven and proceeds
920
+ // regardless (it has already committed to compacting in place).
921
+ const compactionAllowed =
922
+ !isFirstCallGate || !(await this.compactionCircuit.isOpen());
923
+ if (compactInPlace && compactionAllowed) {
924
+ rlog.info(
925
+ {
926
+ turn: toolUseTurns,
927
+ estimated,
928
+ threshold: midLoopThreshold,
929
+ },
930
+ "Token estimate approaching budget — compacting in place",
931
+ );
932
+ const compacted = await this.compact(
933
+ history,
934
+ requestId,
935
+ trust,
936
+ signal,
937
+ onEvent,
938
+ resolveEffectiveOverrideProfile() ?? null,
939
+ isNonInteractive,
940
+ modelProfile,
941
+ actorContext,
942
+ );
943
+ if (compacted) {
944
+ history = compacted;
945
+ // The compacted, re-injected array is the new base; output
946
+ // produced after this point is what the orchestrator
947
+ // persists.
948
+ newMessagesStart = history.length;
949
+ compactedInPlace = true;
950
+ }
951
+ }
952
+ // The turn-start gate proceeds with the call whether or not it
953
+ // compacted (preflight-overflow recovery and the convergence loop
954
+ // remain the escalation path); only mid-loop re-entries yield to
955
+ // the orchestrator before the call.
956
+ if (!compactedInPlace && !isFirstCallGate) {
957
+ rlog.warn(
958
+ {
959
+ turn: toolUseTurns,
960
+ estimated,
961
+ threshold: midLoopThreshold,
962
+ },
963
+ "Token estimate approaching budget — yielding for compaction",
964
+ );
965
+ exitReason = "budget";
966
+ break;
967
+ }
968
+ }
969
+ }
970
+ }
971
+
861
972
  // Resolve tools for this turn: use the dynamic resolver if provided,
862
973
  // otherwise fall back to the static tool list.
863
974
  const currentTools = this.resolveTools
@@ -921,12 +1032,14 @@ export class AgentLoop {
921
1032
  providerConfig.cacheTtl = this.config.cacheTtl;
922
1033
  }
923
1034
 
924
- // Cache-anchor signal for volatile latest-user-message turns (e.g.
925
- // memory-v3 injects its `<memory>` block into the latest user
926
- // message). Not part of the call-site schema, so it is always sourced
927
- // from the per-run option regardless of `callSite`. Only set when true
928
- // so the wire/config stays byte-identical when off.
929
- if (mutableLatestUserMessage) {
1035
+ // Cache-anchor signal for volatile latest-user-message turns: when
1036
+ // memory-v3 is live it injects a per-turn `<memory>` block into the
1037
+ // latest user message, so the provider must anchor its long-TTL cache
1038
+ // breakpoint on the most recent STABLE user message instead of the
1039
+ // volatile latest one. Read here alongside the rest of the provider
1040
+ // config; only set when true so the wire/config stays byte-identical
1041
+ // when off.
1042
+ if (isAssistantFeatureFlagEnabled("memory-v3-live", getConfig())) {
930
1043
  providerConfig.mutableLatestUserMessage = true;
931
1044
  }
932
1045
 
@@ -941,14 +1054,14 @@ export class AgentLoop {
941
1054
  providerConfig.callSite = callSite;
942
1055
  providerConfig.usageTracking = "manual";
943
1056
  // Per-conversation seed for deterministic `mix`-profile expansion.
944
- // Sourced from the orchestrator-supplied turn context's
945
- // conversationId so every LLM call in a conversation resolves the
946
- // same mix arm (stable across turns and retries, and across daemon
947
- // restarts since the seed is the durable conversation id). Absent
948
- // for standalone `AgentLoop` instances (unit tests / no turnContext)
949
- // — those fall back to per-call random mix selection.
950
- if (turnContext?.conversationId) {
951
- providerConfig.selectionSeed = turnContext.conversationId;
1057
+ // Sourced from the loop's own conversation id so every LLM call in a
1058
+ // conversation resolves the same mix arm (stable across turns and
1059
+ // retries, and across daemon restarts since the seed is the durable
1060
+ // conversation id). Absent for standalone `AgentLoop` instances
1061
+ // (unit tests constructed without a conversation id) those fall
1062
+ // back to per-call random mix selection.
1063
+ if (this.conversationId) {
1064
+ providerConfig.selectionSeed = this.conversationId;
952
1065
  }
953
1066
  }
954
1067
 
@@ -994,15 +1107,16 @@ export class AgentLoop {
994
1107
  );
995
1108
  rlog.info({ turn: toolUseTurns }, "LLM call start");
996
1109
 
997
- // Strip image contentBlocks from older tool results to prevent
998
- // screenshots from accumulating in the context window. The LLM
999
- // already saw each image on the turn it was captured; keeping
1000
- // base64 blobs in history rapidly exhausts the context budget.
1001
- // Also strip old AX tree snapshots to keep TTFT from growing
1002
- // linearly with step count in computer-use sessions.
1003
- const providerHistory = compactAxTreeHistory(
1004
- stripOldMediaBlocks(history),
1005
- );
1110
+ // Sanitize the outbound history right before sending: drop accumulated
1111
+ // media, collapse old AX-tree snapshots, and convert historical
1112
+ // web-search results to text. See {@link preModelCallSanitize}.
1113
+ const providerHistory = preModelCallSanitize(history);
1114
+
1115
+ // A `pre-model-call` hook (below) can defer this turn's assistant
1116
+ // output; when set, the live text stream is held so an
1117
+ // `assistant-message` hook can emit the finalized (transformed) text
1118
+ // instead. Reset per model call.
1119
+ let deferAssistantOutput = false;
1006
1120
 
1007
1121
  // The `onEvent` wrapping below applies sensitive-output placeholder
1008
1122
  // substitution to streamed text while forwarding every other event
@@ -1013,6 +1127,9 @@ export class AgentLoop {
1013
1127
  config: providerConfig,
1014
1128
  onEvent: (event) => {
1015
1129
  if (event.type === "text_delta") {
1130
+ // Held when the turn's output is deferred — the final text is
1131
+ // emitted once, after the `assistant-message` hook runs.
1132
+ if (deferAssistantOutput) return;
1016
1133
  // Apply sensitive-output placeholder substitution (chunk-safe)
1017
1134
  if (substitutionMap.size > 0) {
1018
1135
  const combined = streamingPending + event.text;
@@ -1068,12 +1185,32 @@ export class AgentLoop {
1068
1185
  signal,
1069
1186
  };
1070
1187
 
1071
- // Per-turn pipeline context. Real call sites thread a full
1072
- // `turnContext` into `run()` and it is used directly; standalone
1073
- // unit-test instantiations that never plumb a context through fall
1074
- // back to a synthesized placeholder scoped to the tool-use iteration.
1075
- const turnCtx =
1076
- turnContext ?? buildLoopTurnContext(requestId, toolUseTurns);
1188
+ // Let plugins edit the outbound request and opt this call into deferred
1189
+ // output streaming. Runs for every provider call; hooks self-gate on
1190
+ // call site / conversation. Fail-open: a throwing hook leaves the
1191
+ // request unchanged and streaming live.
1192
+ try {
1193
+ const preModelCtx: PreModelCallContext = {
1194
+ conversationId: this.conversationId,
1195
+ callSite,
1196
+ systemPrompt: providerOptions.systemPrompt,
1197
+ deferAssistantOutput: false,
1198
+ logger: rlog,
1199
+ };
1200
+ const finalPreModelCtx = await runHook(
1201
+ HOOKS.PRE_MODEL_CALL,
1202
+ preModelCtx,
1203
+ );
1204
+ providerOptions.systemPrompt = finalPreModelCtx.systemPrompt;
1205
+ // The hook owns the policy (it sees `callSite`/conversation and
1206
+ // self-gates); the loop honors whatever it decides.
1207
+ deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
1208
+ } catch (preModelCallError) {
1209
+ rlog.error(
1210
+ { err: preModelCallError },
1211
+ "pre-model-call hook failed — proceeding with the original request",
1212
+ );
1213
+ }
1077
1214
 
1078
1215
  // Announce the LLM-call boundary so downstream handlers (the
1079
1216
  // daemon's persistence pipeline) can reserve an empty assistant row
@@ -1161,12 +1298,52 @@ export class AgentLoop {
1161
1298
  streamingPending = "";
1162
1299
  }
1163
1300
 
1301
+ // Run the `assistant-message` hook on a finalized message and, when
1302
+ // output was deferred, emit the finalized text once (with sensitive-output
1303
+ // substitution applied, matching the live stream). Fail-open: the hook
1304
+ // receives a clone, so a throw — even mid in-place mutation — leaves the
1305
+ // original message intact.
1306
+ const finalizeAssistantMessage = async (
1307
+ message: Message,
1308
+ ): Promise<Message> => {
1309
+ let finalized = message;
1310
+ try {
1311
+ const ctx: AssistantMessageContext = {
1312
+ conversationId: this.conversationId,
1313
+ callSite,
1314
+ content: structuredClone(message.content),
1315
+ stopReason: response.stopReason,
1316
+ logger: rlog,
1317
+ };
1318
+ const result = await runHook(HOOKS.ASSISTANT_MESSAGE, ctx);
1319
+ finalized = { role: "assistant", content: result.content };
1320
+ } catch (assistantMessageError) {
1321
+ rlog.error(
1322
+ { err: assistantMessageError },
1323
+ "assistant-message hook failed — keeping the original content",
1324
+ );
1325
+ finalized = message;
1326
+ }
1327
+ if (deferAssistantOutput) {
1328
+ // The persisted message keeps sensitive-output placeholders; the
1329
+ // stream shows real values — substitute before emitting.
1330
+ const finalText = applySubstitutions(
1331
+ assistantTextOf(finalized.content),
1332
+ substitutionMap,
1333
+ );
1334
+ if (finalText.length > 0) {
1335
+ onEvent({ type: "text_delta", text: finalText });
1336
+ }
1337
+ }
1338
+ return finalized;
1339
+ };
1340
+
1164
1341
  // Build the assistant message with placeholder-only text.
1165
1342
  // Both provider history and persisted conversation store must retain
1166
1343
  // placeholders so the model never sees real sensitive values — neither
1167
1344
  // on subsequent loop turns nor on session reload from the database.
1168
1345
  // Substitution to real values happens only in streamed text_delta events.
1169
- const assistantMessage: Message = {
1346
+ let assistantMessage: Message = {
1170
1347
  role: "assistant",
1171
1348
  content: response.content,
1172
1349
  };
@@ -1195,10 +1372,6 @@ export class AgentLoop {
1195
1372
  block.type !== "server_tool_use" &&
1196
1373
  block.type !== "web_search_tool_result",
1197
1374
  );
1198
- const safeAssistantMessage: Message = {
1199
- role: "assistant",
1200
- content: safeContent,
1201
- };
1202
1375
  rlog.warn(
1203
1376
  {
1204
1377
  turn: toolUseTurns,
@@ -1209,6 +1382,13 @@ export class AgentLoop {
1209
1382
  },
1210
1383
  "LLM response reached output token limit",
1211
1384
  );
1385
+ // Run the hook on the truncated reply so output-filter plugins still
1386
+ // see it, and so a deferred turn gets its synthetic final emit (the
1387
+ // live stream was suppressed; without this the client would see nothing).
1388
+ const safeAssistantMessage = await finalizeAssistantMessage({
1389
+ role: "assistant",
1390
+ content: safeContent,
1391
+ });
1212
1392
  history.push(safeAssistantMessage);
1213
1393
  appendedNewMessages = true;
1214
1394
  await onEvent({
@@ -1241,7 +1421,7 @@ export class AgentLoop {
1241
1421
  // follow-up turn. It receives the full history and, when it asks to
1242
1422
  // continue, appends the follow-up turn itself.
1243
1423
  const stopCtx: StopContext = {
1244
- conversationId: turnCtx.conversationId,
1424
+ conversationId: this.conversationId,
1245
1425
  messages: [...history],
1246
1426
  responseContent: response.content,
1247
1427
  stopReason: response.stopReason,
@@ -1280,6 +1460,12 @@ export class AgentLoop {
1280
1460
  }
1281
1461
  }
1282
1462
 
1463
+ // Run the `assistant-message` hook + emit any deferred final text.
1464
+ // On a no-tool turn this point is reached only after the `stop` hook
1465
+ // resolves to "stop" (a `continue` already re-queried above), so a
1466
+ // re-queried reply is never transformed-then-discarded.
1467
+ assistantMessage = await finalizeAssistantMessage(assistantMessage);
1468
+
1283
1469
  history.push(assistantMessage);
1284
1470
  appendedNewMessages = true;
1285
1471
 
@@ -1430,7 +1616,7 @@ export class AgentLoop {
1430
1616
  continue;
1431
1617
  }
1432
1618
  const postToolUseCtx: PostToolUseContext = {
1433
- conversationId: turnCtx.conversationId,
1619
+ conversationId: this.conversationId,
1434
1620
  toolResponse: block as ToolResultContent,
1435
1621
  messages: history,
1436
1622
  maxInputTokens: contextWindowTokens,
@@ -1508,8 +1694,9 @@ export class AgentLoop {
1508
1694
  history.push({ role: "user", content: resultBlocks });
1509
1695
 
1510
1696
  // Invoke checkpoint callback after tool results are in history.
1511
- // Handoff is offered first so a queued message takes precedence over
1512
- // the mid-loop budget yield below.
1697
+ // Handoff takes precedence over the budget gate: a handoff decision
1698
+ // breaks here and leaves `budgetGateArmed` false, so a queued message
1699
+ // is processed before the next iteration's pre-call budget gate.
1513
1700
  if (onCheckpoint) {
1514
1701
  const decision = await onCheckpoint({
1515
1702
  turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
@@ -1523,61 +1710,11 @@ export class AgentLoop {
1523
1710
  }
1524
1711
  }
1525
1712
 
1526
- // Mid-loop budget gate: when overflow recovery is enabled, estimate
1527
- // the running context size as it approaches the preflight budget.
1528
- // With a `compaction` hook the loop compacts in place and continues;
1529
- // without one it yields (`exitReason = "budget"`) so the orchestrator
1530
- // can recover before the next provider call risks a hard
1531
- // context-too-large rejection. Keyed off the loop's own
1532
- // `history.length` (the messages actually in context this turn,
1533
- // including tool iterations) rather than the durable conversation
1534
- // count.
1535
- const contextWindow = resolveContextWindow?.();
1536
- if (contextWindow?.overflowRecovery.enabled) {
1537
- const { maxInputTokens, overflowRecovery } = contextWindow;
1538
- const safetyMargin =
1539
- history.length > LONG_HISTORY_MESSAGE_THRESHOLD
1540
- ? Math.max(
1541
- overflowRecovery.safetyMarginRatio,
1542
- LONG_HISTORY_SAFETY_MARGIN_FLOOR,
1543
- )
1544
- : overflowRecovery.safetyMarginRatio;
1545
- const preflightBudget = Math.floor(
1546
- maxInputTokens * (1 - safetyMargin),
1547
- );
1548
- const midLoopThreshold =
1549
- preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
1550
- const estimated = this.estimateTokens(history);
1551
- if (estimated > midLoopThreshold) {
1552
- if (compaction) {
1553
- rlog.info(
1554
- { phase: "mid-loop", estimated, threshold: midLoopThreshold },
1555
- "Token estimate approaching budget — compacting in place",
1556
- );
1557
- const compacted = await this.compact(
1558
- history,
1559
- turnCtx,
1560
- compaction,
1561
- signal,
1562
- onEvent,
1563
- resolveEffectiveOverrideProfile() ?? null,
1564
- );
1565
- if (compacted) {
1566
- history = compacted;
1567
- // The compacted, re-injected array is the new base; output
1568
- // produced after this point is what the orchestrator persists.
1569
- newMessagesStart = history.length;
1570
- continue;
1571
- }
1572
- }
1573
- rlog.warn(
1574
- { phase: "mid-loop", estimated, threshold: midLoopThreshold },
1575
- "Token estimate approaching budget — yielding for compaction",
1576
- );
1577
- exitReason = "budget";
1578
- break;
1579
- }
1580
- }
1713
+ // Arm the pre-call budget gate for the next iteration. Placed after
1714
+ // the checkpoint so a handoff yield (which breaks above) leaves it
1715
+ // disarmed; the gate then runs at the top of the next iteration,
1716
+ // before that iteration's provider call.
1717
+ budgetGateArmed = true;
1581
1718
  } catch (error) {
1582
1719
  // Abort errors are expected when user cancels — synthesize
1583
1720
  // cancellation tool_results so the history stays valid for the
@@ -1786,3 +1923,32 @@ function stripOldMediaBlocks(history: Message[]): Message[] {
1786
1923
  };
1787
1924
  });
1788
1925
  }
1926
+
1927
+ /**
1928
+ * Sanitize the outbound history immediately before a provider call, bundling
1929
+ * the pre-send transforms the loop applies to every request:
1930
+ * - {@link stripOldMediaBlocks} drops accumulated screenshot/audio bytes from
1931
+ * older tool results — the model saw the media on the turn it was captured.
1932
+ * - {@link compactAxTreeHistory} collapses all but the most recent few
1933
+ * `<ax-tree>` snapshots so TTFT does not grow linearly with step count.
1934
+ * - {@link stripHistoricalWebSearchResults} converts historical
1935
+ * `web_search_tool_result` blocks to text summaries; Anthropic's opaque
1936
+ * `encrypted_content` tokens expire / are route-scoped, and replaying a stale
1937
+ * one is rejected with `Invalid encrypted_content in search_result block`.
1938
+ *
1939
+ * Transforms the outbound copy only — the durable history keeps the rich
1940
+ * originals and each send re-derives the sanitized projection (every transform
1941
+ * is idempotent). Because it runs unconditionally before every provider call,
1942
+ * it is the single place where oversized media and expired web-search tokens
1943
+ * are guaranteed to be removed from a request.
1944
+ *
1945
+ * This is outbound-request preparation and should eventually move to a default
1946
+ * `pre-model-call` plugin hook ({@link HOOKS.PRE_MODEL_CALL}) once that hook's
1947
+ * context carries the outbound message list; for now it lives inline next to
1948
+ * the provider call it guards.
1949
+ */
1950
+ export function preModelCallSanitize(history: Message[]): Message[] {
1951
+ const mediaStripped = stripOldMediaBlocks(history);
1952
+ const axCompacted = compactAxTreeHistory(mediaStripped);
1953
+ return stripHistoricalWebSearchResults(axCompacted).messages;
1954
+ }