@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae

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 (475) 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 +263 -121
  12. package/package.json +2 -2
  13. package/src/__tests__/activation-early-marking.test.ts +120 -0
  14. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  15. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  16. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  17. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  18. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  19. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  20. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  21. package/src/__tests__/agent-loop.test.ts +441 -96
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  23. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  24. package/src/__tests__/anthropic-provider.test.ts +23 -8
  25. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  26. package/src/__tests__/app-control-flow.test.ts +1 -1
  27. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  28. package/src/__tests__/app-executors.test.ts +132 -0
  29. package/src/__tests__/approval-cascade.test.ts +6 -5
  30. package/src/__tests__/approval-routes-http.test.ts +4 -1
  31. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  32. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  33. package/src/__tests__/channel-approvals.test.ts +1 -1
  34. package/src/__tests__/compaction-circuit.test.ts +258 -0
  35. package/src/__tests__/compaction-direct.test.ts +146 -0
  36. package/src/__tests__/compaction-events.test.ts +7 -7
  37. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  38. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  39. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  40. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  41. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  42. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  43. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  44. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  45. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  46. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  47. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  48. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  49. package/src/__tests__/conversation-error.test.ts +15 -1
  50. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  52. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  53. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  55. package/src/__tests__/conversation-queue.test.ts +10 -10
  56. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  57. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  58. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  59. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  60. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  61. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  62. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  65. package/src/__tests__/conversation-undo.test.ts +2 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  67. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/corrected-target.test.ts +93 -0
  71. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  72. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  73. package/src/__tests__/db-acp-history.test.ts +101 -0
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +1 -1
  76. package/src/__tests__/file-write-tool.test.ts +63 -0
  77. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  78. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  79. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  80. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  82. package/src/__tests__/heartbeat-service.test.ts +1 -0
  83. package/src/__tests__/history-repair-hook.test.ts +5 -1
  84. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  85. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  86. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  87. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  88. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  89. package/src/__tests__/injector-background-turn.test.ts +13 -23
  90. package/src/__tests__/injector-chain.test.ts +268 -44
  91. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  92. package/src/__tests__/injector-document-comments.test.ts +97 -114
  93. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  94. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  95. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  96. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  97. package/src/__tests__/llm-resolver.test.ts +73 -0
  98. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  99. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  100. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  101. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  102. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  103. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  104. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  105. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  106. package/src/__tests__/plugin-registry.test.ts +3 -49
  107. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  108. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  109. package/src/__tests__/plugin-types.test.ts +0 -70
  110. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  111. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  112. package/src/__tests__/published-app-updater.test.ts +138 -0
  113. package/src/__tests__/reaction-persistence.test.ts +1 -1
  114. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  115. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  116. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  117. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  118. package/src/__tests__/subagent-detail.test.ts +25 -7
  119. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  120. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  121. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  122. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  123. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  124. package/src/__tests__/title-generate-hook.test.ts +5 -1
  125. package/src/__tests__/tool-error-hook.test.ts +1 -1
  126. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  127. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  128. package/src/__tests__/web-fetch.test.ts +45 -0
  129. package/src/acp/__tests__/agent-process.test.ts +161 -0
  130. package/src/acp/__tests__/client-handler.test.ts +40 -0
  131. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  132. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  133. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  134. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  135. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  136. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  137. package/src/acp/agent-process.ts +61 -1
  138. package/src/acp/auto-install.test.ts +280 -0
  139. package/src/acp/auto-install.ts +232 -0
  140. package/src/acp/client-handler.ts +31 -0
  141. package/src/acp/prepare-agent-env.ts +80 -27
  142. package/src/acp/resolve-agent.test.ts +188 -28
  143. package/src/acp/resolve-agent.ts +119 -42
  144. package/src/acp/resume-hint.ts +23 -0
  145. package/src/acp/session-manager.ts +507 -73
  146. package/src/agent/compaction-circuit.ts +60 -102
  147. package/src/agent/loop.ts +403 -251
  148. package/src/api/responses/conversation-message.ts +22 -2
  149. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  150. package/src/approvals/guardian-request-resolvers.ts +27 -1
  151. package/src/background-wake/next-wake.ts +1 -0
  152. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  153. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  154. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  155. package/src/cli/commands/memory-v3.ts +57 -199
  156. package/src/cli/commands/plugins.ts +43 -37
  157. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  158. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  159. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  160. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  161. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  162. package/src/cli/lib/install-from-github.ts +797 -60
  163. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  164. package/src/cli/lib/plugin-details.ts +416 -0
  165. package/src/cli/lib/plugin-marketplace.ts +215 -0
  166. package/src/cli/lib/search-plugins.ts +201 -29
  167. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  168. package/src/config/acp-defaults.test.ts +10 -0
  169. package/src/config/acp-defaults.ts +9 -3
  170. package/src/config/acp-schema.ts +1 -7
  171. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  172. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  173. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  174. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  175. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  176. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  177. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  178. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  180. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  181. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  182. package/src/config/bundled-tool-registry.ts +4 -0
  183. package/src/config/call-site-defaults.ts +0 -3
  184. package/src/config/feature-flag-registry.json +10 -16
  185. package/src/config/llm-resolver.ts +39 -7
  186. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  187. package/src/config/schemas/call-site-catalog.ts +0 -21
  188. package/src/config/schemas/heartbeat.ts +9 -0
  189. package/src/config/schemas/llm.ts +17 -3
  190. package/src/config/schemas/memory-v3.ts +58 -8
  191. package/src/config/seed-inference-profiles.ts +18 -0
  192. package/src/context/compactor.ts +22 -4
  193. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  194. package/src/context/strip-injections.ts +8 -2
  195. package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
  196. package/src/daemon/conversation-agent-loop.ts +308 -1297
  197. package/src/daemon/conversation-error.ts +31 -4
  198. package/src/daemon/conversation-history.ts +1 -1
  199. package/src/daemon/conversation-lifecycle.ts +11 -255
  200. package/src/daemon/conversation-media-retry.ts +19 -6
  201. package/src/daemon/conversation-messaging.ts +17 -0
  202. package/src/daemon/conversation-process.ts +22 -141
  203. package/src/daemon/conversation-queue-manager.ts +8 -0
  204. package/src/daemon/conversation-registry.ts +159 -0
  205. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  206. package/src/daemon/conversation-store.ts +9 -90
  207. package/src/daemon/conversation-surfaces.ts +165 -11
  208. package/src/daemon/conversation-workspace.ts +17 -0
  209. package/src/daemon/conversation.ts +444 -62
  210. package/src/daemon/disk-pressure-policy.ts +0 -1
  211. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  212. package/src/daemon/handlers/config-channels.ts +11 -2
  213. package/src/daemon/handlers/conversations.ts +3 -1
  214. package/src/daemon/handlers/skills.ts +4 -1
  215. package/src/daemon/host-app-control-proxy.ts +72 -57
  216. package/src/daemon/host-proxy-preactivation.ts +1 -3
  217. package/src/daemon/lifecycle.ts +21 -7
  218. package/src/daemon/persist-unsendable-image.ts +62 -25
  219. package/src/daemon/process-message.ts +1 -1
  220. package/src/daemon/server.ts +2 -0
  221. package/src/daemon/tool-side-effects.ts +15 -0
  222. package/src/daemon/wake-conversation-ops.ts +269 -0
  223. package/src/embedded/plugin-api.ts +2 -2
  224. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  225. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  226. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  227. package/src/heartbeat/heartbeat-service.ts +26 -0
  228. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  229. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  230. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  231. package/src/ipc/skill-routes/memory.ts +4 -2
  232. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  233. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  234. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  235. package/src/memory/activation-session-store.ts +43 -0
  236. package/src/memory/conversation-crud.ts +29 -19
  237. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  238. package/src/memory/db-init.ts +6 -0
  239. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  240. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  241. package/src/memory/jobs-worker.ts +25 -1
  242. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  243. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  244. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  245. package/src/memory/migrations/index.ts +3 -0
  246. package/src/memory/onboarding-events-store.ts +66 -18
  247. package/src/memory/schema/acp.ts +4 -0
  248. package/src/memory/schema/infrastructure.ts +13 -0
  249. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  250. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  251. package/src/memory/v2/consolidation-job.ts +2 -63
  252. package/src/memory/v2/types.ts +5 -0
  253. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  254. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  255. package/src/notifications/access-request-copy.ts +298 -0
  256. package/src/notifications/adapters/slack.ts +3 -3
  257. package/src/notifications/adapters/telegram.ts +9 -2
  258. package/src/notifications/copy-composer.ts +49 -267
  259. package/src/notifications/decision-engine.ts +16 -35
  260. package/src/notifications/home-feed-side-effect.ts +1 -6
  261. package/src/plugin-api/constants.ts +4 -0
  262. package/src/plugin-api/index.ts +40 -11
  263. package/src/plugin-api/types.ts +108 -0
  264. package/src/plugins/defaults/compaction/compact.ts +106 -0
  265. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  266. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  267. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  268. package/src/plugins/defaults/compaction/package.json +1 -2
  269. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  270. package/src/plugins/defaults/empty-response/package.json +0 -1
  271. package/src/plugins/defaults/history-repair/package.json +0 -1
  272. package/src/plugins/defaults/index.ts +156 -26
  273. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  274. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  275. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  276. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  277. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  278. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  282. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  283. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  284. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  285. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  286. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  287. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  288. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  289. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  290. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  291. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  293. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  294. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  295. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  296. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  297. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  298. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  299. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  300. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  301. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  302. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  303. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  304. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  306. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  307. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  308. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  309. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  310. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  311. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  312. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  313. package/src/plugins/defaults/title-generate/package.json +0 -1
  314. package/src/plugins/defaults/tool-error/package.json +0 -1
  315. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  316. package/src/plugins/external-api.ts +12 -2
  317. package/src/plugins/pipeline.ts +6 -293
  318. package/src/plugins/registry.ts +9 -37
  319. package/src/plugins/types.ts +76 -381
  320. package/src/plugins/user-loader.ts +30 -127
  321. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  322. package/src/prompts/system-prompt.ts +61 -10
  323. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  324. package/src/providers/anthropic/client.ts +9 -10
  325. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  326. package/src/providers/inference/logit-bias.ts +55 -0
  327. package/src/providers/model-catalog.ts +34 -0
  328. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  329. package/src/providers/openai/chat-completions-provider.ts +44 -1
  330. package/src/providers/retry.ts +22 -0
  331. package/src/providers/types.ts +6 -0
  332. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  333. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  334. package/src/runtime/agent-wake.ts +108 -209
  335. package/src/runtime/assistant-event-hub.ts +1 -1
  336. package/src/runtime/channel-approvals.ts +1 -1
  337. package/src/runtime/interactive-ui.ts +1 -1
  338. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  339. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  340. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  341. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  342. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  343. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  344. package/src/runtime/routes/acp-routes.test.ts +185 -32
  345. package/src/runtime/routes/acp-routes.ts +328 -30
  346. package/src/runtime/routes/app-management-routes.ts +3 -0
  347. package/src/runtime/routes/approval-routes.ts +1 -1
  348. package/src/runtime/routes/browser-routes.ts +1 -1
  349. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  350. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  351. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  352. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  353. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  354. package/src/runtime/routes/conversation-routes.ts +30 -2
  355. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  356. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  357. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  358. package/src/runtime/routes/host-cu-routes.ts +1 -1
  359. package/src/runtime/routes/identity-routes.ts +18 -3
  360. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  361. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  362. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  363. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  364. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  365. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  366. package/src/runtime/routes/playground/helpers.ts +1 -1
  367. package/src/runtime/routes/plugins-routes.ts +337 -35
  368. package/src/runtime/routes/stt-routes.ts +45 -12
  369. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  370. package/src/runtime/routes/work-items-routes.ts +2 -4
  371. package/src/runtime/routes/workspace-routes.ts +50 -15
  372. package/src/runtime/services/conversation-serializer.ts +1 -1
  373. package/src/runtime/verification-outbound-actions.ts +147 -2
  374. package/src/runtime/verification-templates.ts +29 -3
  375. package/src/services/published-app-updater.ts +30 -8
  376. package/src/signals/cancel.ts +2 -4
  377. package/src/subagent/manager.ts +21 -5
  378. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  379. package/src/telemetry/activation-funnel.ts +167 -0
  380. package/src/telemetry/types.ts +13 -0
  381. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  382. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  383. package/src/tools/acp/context.ts +20 -0
  384. package/src/tools/acp/list-agents.test.ts +9 -19
  385. package/src/tools/acp/list-agents.ts +3 -15
  386. package/src/tools/acp/spawn.test.ts +173 -202
  387. package/src/tools/acp/spawn.ts +37 -172
  388. package/src/tools/acp/steer.test.ts +105 -8
  389. package/src/tools/acp/steer.ts +48 -17
  390. package/src/tools/apps/executors.ts +166 -50
  391. package/src/tools/browser/browser-execution.ts +12 -2
  392. package/src/tools/filesystem/write.ts +34 -0
  393. package/src/tools/network/web-fetch.ts +65 -24
  394. package/src/tools/skills/load.ts +1 -1
  395. package/src/tools/subagent/spawn.ts +2 -4
  396. package/src/tools/ui-surface/definitions.ts +32 -5
  397. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  398. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  399. package/docs/plugins.md +0 -832
  400. package/examples/plugins/echo/register.ts +0 -143
  401. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  402. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  403. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  404. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  405. package/src/__tests__/pipeline-runner.test.ts +0 -554
  406. package/src/daemon/wake-target-adapter.ts +0 -253
  407. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  408. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  409. package/src/memory/v3/__tests__/core.test.ts +0 -39
  410. package/src/memory/v3/__tests__/health.test.ts +0 -219
  411. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  412. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  413. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  414. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  415. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  416. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  417. package/src/memory/v3/__tests__/router.test.ts +0 -332
  418. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  419. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  420. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  421. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  422. package/src/memory/v3/assign.ts +0 -268
  423. package/src/memory/v3/capabilities.ts +0 -124
  424. package/src/memory/v3/core.ts +0 -26
  425. package/src/memory/v3/health.ts +0 -0
  426. package/src/memory/v3/maintain-job.ts +0 -314
  427. package/src/memory/v3/needle.ts +0 -115
  428. package/src/memory/v3/orchestrate.ts +0 -126
  429. package/src/memory/v3/page-content.ts +0 -34
  430. package/src/memory/v3/provider-blocks.ts +0 -26
  431. package/src/memory/v3/reconcile.ts +0 -523
  432. package/src/memory/v3/router.ts +0 -190
  433. package/src/memory/v3/selection-log-store.ts +0 -84
  434. package/src/memory/v3/selector.ts +0 -226
  435. package/src/memory/v3/shadow-plugin.ts +0 -411
  436. package/src/memory/v3/snapshot.ts +0 -209
  437. package/src/memory/v3/tree.ts +0 -174
  438. package/src/memory/v3/types.ts +0 -59
  439. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  440. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  441. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  442. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  443. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  444. package/src/plugins/defaults/compaction/register.ts +0 -35
  445. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  446. package/src/plugins/defaults/empty-response/register.ts +0 -23
  447. package/src/plugins/defaults/history-repair/register.ts +0 -24
  448. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  449. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  450. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  451. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  452. package/src/plugins/defaults/title-generate/register.ts +0 -35
  453. package/src/plugins/defaults/tool-error/register.ts +0 -23
  454. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  455. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  456. package/src/proactive-artifact/decision.test.ts +0 -226
  457. package/src/proactive-artifact/decision.ts +0 -165
  458. package/src/proactive-artifact/index.ts +0 -7
  459. package/src/proactive-artifact/job.test.ts +0 -962
  460. package/src/proactive-artifact/job.ts +0 -372
  461. package/src/proactive-artifact/message-copy.ts +0 -58
  462. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  463. package/src/proactive-artifact/trigger-state.ts +0 -123
  464. package/src/util/map-limit.ts +0 -27
  465. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  466. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  467. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  468. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  469. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  470. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  471. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  472. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  473. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  474. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  475. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.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 {
@@ -8,21 +10,22 @@ import {
8
10
  estimateToolsTokens,
9
11
  getCalibrationProviderKey,
10
12
  } from "../context/token-estimator.js";
11
- import type { ContextWindowResult } from "../context/window-manager.js";
13
+ import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
12
14
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
15
+ import type { TrustContext } from "../daemon/trust-context.js";
16
+ import { stripHistoricalWebSearchResults } from "../daemon/web-search-history.js";
13
17
  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
18
  import type {
20
- CompactionArgs,
21
- CompactionCircuitEvent,
22
- CompactionResult,
23
- TurnContext,
24
- } from "../plugins/types.js";
25
- import { PluginTimeoutError } from "../plugins/types.js";
19
+ PostModelCallContext,
20
+ PostToolUseContext,
21
+ PreModelCallContext,
22
+ StopContext,
23
+ } from "../plugin-api/types.js";
24
+ import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
25
+ import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
26
+ import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
27
+ import { runHook } from "../plugins/pipeline.js";
28
+ import type { CompactionCircuitEvent } from "../plugins/types.js";
26
29
  import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
27
30
  import type {
28
31
  ContentBlock,
@@ -308,7 +311,7 @@ export type AgentEvent =
308
311
  * immediately before re-injection — whether or not the pipeline actually
309
312
  * compacted. The daemon's event dispatcher always commits `basis` (the
310
313
  * stripped pre-compaction history) as the conversation's durable message
311
- * state, so re-injection ({@link MidLoopCompaction.reinject}) re-applies
314
+ * state, so re-injection ({@link postCompact}) re-applies
312
315
  * injections onto the stripped base rather than stacking on top of the
313
316
  * still-injected messages. When `result.compacted` is set it
314
317
  * additionally commits the durable compaction result (DB-record fields,
@@ -382,34 +385,17 @@ export function isMaxTokensStopReason(
382
385
  }
383
386
 
384
387
  /**
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.
388
+ * Concatenate the text of an assistant message's `text` blocks (ignoring
389
+ * `tool_use`, `thinking`, and other non-text blocks). Used to re-emit the
390
+ * finalized text as a single `text_delta` when a turn's live output was
391
+ * deferred by a `pre-model-call` hook.
396
392
  */
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
- };
393
+ function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
394
+ let text = "";
395
+ for (const block of content) {
396
+ if (block.type === "text") text += block.text;
397
+ }
398
+ return text;
413
399
  }
414
400
 
415
401
  /**
@@ -454,29 +440,11 @@ export interface ResolvedSystemPrompt {
454
440
  model?: string;
455
441
  }
456
442
 
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
443
  export interface AgentLoopRunOptions {
444
+ /** Input history the run starts from; the loop appends its output onto a copy. */
445
+ messages: Message[];
446
+ /** Sink the loop streams its {@link AgentEvent}s through as the turn runs. */
447
+ onEvent: (event: AgentEvent) => void | Promise<void>;
480
448
  signal?: AbortSignal;
481
449
  requestId?: string;
482
450
  onCheckpoint?: (
@@ -484,14 +452,15 @@ export interface AgentLoopRunOptions {
484
452
  ) => CheckpointDecision | Promise<CheckpointDecision>;
485
453
  callSite?: LLMCallSite;
486
454
  /**
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.
455
+ * Trust classification and channel identity for the turn's inbound actor,
456
+ * supplied by the caller as the turn-start snapshot. Read only on the
457
+ * mid-loop in-place compaction path — to scope the compactor's image
458
+ * manifest (guardian-only attachments are excluded for untrusted actors) and
459
+ * forwarded to {@link postCompact}. Callers without a meaningful actor (agent
460
+ * wakes, standalone unit tests) pass an `unknown`-class snapshot so the
461
+ * compactor fail-closes to excluding guardian-only attachments.
493
462
  */
494
- turnContext?: TurnContext;
463
+ trust: TrustContext;
495
464
  /**
496
465
  * Ad-hoc inference-profile override applied to every LLM call the loop
497
466
  * issues. When set, each `SendMessageOptions.config` carries
@@ -514,21 +483,51 @@ export interface AgentLoopRunOptions {
514
483
  overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
515
484
  };
516
485
  /**
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.
486
+ * When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
487
+ * budget gate runs before the very first provider call — subsuming the
488
+ * proactive turn-start compaction the orchestrator would otherwise perform
489
+ * inline before `run()` — as well as before each tool-use re-entry. When the
490
+ * gate trips it compacts the running history in place, re-applying runtime
491
+ * injections via the default post-compaction hook ({@link postCompact}), and
492
+ * continues instead of yielding `exitReason = "budget"`.
493
+ *
494
+ * The first-call pass honors the compaction circuit breaker and proceeds with
495
+ * the call whether or not it compacted (preflight-overflow recovery and the
496
+ * convergence loop remain the escalation path), so it never yields on the
497
+ * first call. Reruns without an inline compaction path (agent wakes,
498
+ * convergence/auto-compress reruns) leave it `false`: they skip the
499
+ * first-call gate and keep yielding for budget on mid-loop re-entries.
500
+ * Defaults to `false` when omitted.
501
+ */
502
+ compactInPlace?: boolean;
503
+ /**
504
+ * Whether the in-flight turn has no human present to answer clarification
505
+ * questions. Resolved once by the orchestrator at turn start and forwarded to
506
+ * {@link postCompact} so post-compaction
507
+ * re-injection uses the turn-start snapshot rather than re-reading mutable
508
+ * client/headless state mid-turn. Defaults to `false` when omitted.
521
509
  */
522
- compaction?: MidLoopCompaction;
510
+ isNonInteractive?: boolean;
523
511
  /**
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.
512
+ * The `model_profile:` turn-context label resolved once by the orchestrator
513
+ * at turn start, or `null` when the active inference profile is unchanged
514
+ * since the last notified one. Forwarded to
515
+ * {@link postCompact} so post-compaction re-injection
516
+ * re-emits the turn-start value rather than re-deriving the change-detected
517
+ * label (which flips once the notification is persisted mid-turn). Defaults to
518
+ * `null` when omitted.
530
519
  */
531
- mutableLatestUserMessage?: boolean;
520
+ modelProfile?: string | null;
521
+ /**
522
+ * Inbound actor identity and trust fields for the unified `<turn_context>`
523
+ * block, or `null` on guardian turns. Resolved once by the orchestrator at
524
+ * turn start via the actor-trust resolver, whose contact/member registry
525
+ * inputs can be mutated mid-turn by contact tools, and forwarded to
526
+ * {@link postCompact} so post-compaction
527
+ * re-injection re-emits the turn-start value rather than re-resolving it.
528
+ * Defaults to `null` when omitted.
529
+ */
530
+ actorContext?: InboundActorContext | null;
532
531
  }
533
532
 
534
533
  /**
@@ -570,17 +569,21 @@ export type LoopToolExecutor = (
570
569
  }>;
571
570
 
572
571
  export interface AgentLoopConstructorOptions {
572
+ /** LLM provider the loop issues every call through. */
573
+ provider: Provider;
574
+ /** Base system prompt, before any per-turn dynamic resolution. */
575
+ systemPrompt: string;
573
576
  config?: Partial<AgentLoopConfig>;
574
577
  tools?: ToolDefinition[];
575
578
  toolExecutor?: LoopToolExecutor;
576
579
  resolveTools?: (history: Message[]) => ToolDefinition[];
577
580
  resolveSystemPrompt?: (history: Message[]) => ResolvedSystemPrompt;
578
581
  /**
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.
582
+ * Conversation this loop drives. Scopes the loop-held compaction circuit
583
+ * breaker and is the source of truth the loop's pipeline contexts and
584
+ * post-compaction re-injection resolve the live conversation through.
582
585
  */
583
- conversationId?: string;
586
+ conversationId: string;
584
587
  }
585
588
 
586
589
  export class AgentLoop {
@@ -594,6 +597,14 @@ export class AgentLoop {
594
597
  | null;
595
598
  private toolExecutor: LoopToolExecutor | null;
596
599
 
600
+ /**
601
+ * Conversation this loop drives. Source of truth for the `conversationId`
602
+ * the loop's pipeline contexts and post-compaction re-injection resolve the
603
+ * live conversation through, so the loop knows its own identity without a
604
+ * threaded-in turn context.
605
+ */
606
+ private readonly conversationId: string;
607
+
597
608
  /**
598
609
  * Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
599
610
  * conversation, so it is the source of truth for the cross-turn failure
@@ -603,19 +614,17 @@ export class AgentLoop {
603
614
  */
604
615
  readonly compactionCircuit: CompactionCircuit;
605
616
 
606
- constructor(
607
- provider: Provider,
608
- systemPrompt: string,
609
- options?: AgentLoopConstructorOptions,
610
- ) {
617
+ constructor(options: AgentLoopConstructorOptions) {
611
618
  const {
619
+ provider,
620
+ systemPrompt,
612
621
  config,
613
622
  tools,
614
623
  toolExecutor,
615
624
  resolveTools,
616
625
  resolveSystemPrompt,
617
626
  conversationId,
618
- } = options ?? {};
627
+ } = options;
619
628
  this.provider = provider;
620
629
  this.systemPrompt = systemPrompt;
621
630
  this.config = { ...DEFAULT_CONFIG, ...config };
@@ -623,7 +632,8 @@ export class AgentLoop {
623
632
  this.resolveTools = resolveTools ?? null;
624
633
  this.resolveSystemPrompt = resolveSystemPrompt ?? null;
625
634
  this.toolExecutor = toolExecutor ?? null;
626
- this.compactionCircuit = new CompactionCircuit(conversationId ?? "");
635
+ this.conversationId = conversationId;
636
+ this.compactionCircuit = new CompactionCircuit(this.conversationId);
627
637
  }
628
638
 
629
639
  /**
@@ -675,23 +685,15 @@ export class AgentLoop {
675
685
  * compaction outcome into a user-visible turn failure.
676
686
  */
677
687
  private async recordCompactionOutcome(
678
- turnContext: TurnContext,
688
+ requestId: string | undefined,
679
689
  summaryFailed: boolean,
680
690
  onEvent: (event: AgentEvent) => void | Promise<void>,
681
691
  ): Promise<void> {
682
692
  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
- );
693
+ await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
692
694
  } catch (recordError) {
693
695
  log.error(
694
- { err: recordError, requestId: turnContext.requestId },
696
+ { err: recordError, requestId },
695
697
  "Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
696
698
  );
697
699
  }
@@ -700,19 +702,21 @@ export class AgentLoop {
700
702
  /**
701
703
  * Compact the running history in place when the mid-loop budget gate trips.
702
704
  *
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.
705
+ * Calls the default compaction plugin on the stripped history, then
706
+ * re-applies injections via the supplied hooks. Returns the history to
707
+ * continue from, or `null` when the compactor exhausted its retry budget so
708
+ * the caller yields `exitReason = "budget"` and the orchestrator escalates.
708
709
  */
709
710
  private async compact(
710
711
  history: Message[],
711
- turnContext: TurnContext,
712
- compaction: MidLoopCompaction,
712
+ requestId: string | undefined,
713
+ trust: TrustContext,
713
714
  signal: AbortSignal | undefined,
714
715
  onEvent: (event: AgentEvent) => void | Promise<void>,
715
716
  overrideProfile: string | null,
717
+ isNonInteractive: boolean,
718
+ modelProfile: string | null,
719
+ actorContext: InboundActorContext | null,
716
720
  ): Promise<Message[] | null> {
717
721
  await onEvent({ type: "context_compacting" });
718
722
  // Strip runtime injections so the compactor summarizes the raw persistent
@@ -721,49 +725,29 @@ export class AgentLoop {
721
725
  // Record the history-stripped marker right after stripping, before the
722
726
  // pipeline runs.
723
727
  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,
747
- );
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
- }
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;
728
+ // The compaction module owns the per-conversation manager; pass the
729
+ // conversation id and let `defaultCompact` resolve it from the store.
730
+ // The mid-loop budget gate is reached only when this turn decides to
731
+ // compact in place, so `force` past the auto-threshold check.
732
+ // `actorTrustClass` comes from the turn's trust snapshot (the actor whose
733
+ // turn triggered compaction) so the compactor's image manifest excludes
734
+ // guardian-only attachments for untrusted actors. `overrideProfile` is the
735
+ // turn's resolved inference-profile override for the summary call.
736
+ const compactResult = await defaultCompact({
737
+ conversationId: this.conversationId,
738
+ messages: rawHistory,
739
+ signal,
740
+ force: true,
741
+ actorTrustClass: trust.trustClass,
742
+ overrideProfile,
743
+ });
760
744
  // `force: true` bypasses the auto-threshold gate, but early returns
761
745
  // for "no eligible messages" / "insufficient messages" still leave
762
746
  // `summaryFailed` undefined. Only record an outcome when the summary LLM
763
747
  // actually ran.
764
748
  if (compactResult.summaryFailed !== undefined) {
765
749
  await this.recordCompactionOutcome(
766
- turnContext,
750
+ requestId,
767
751
  compactResult.summaryFailed,
768
752
  onEvent,
769
753
  );
@@ -783,29 +767,37 @@ export class AgentLoop {
783
767
  // Re-inject onto the same base the `compaction_completed` dispatch commits:
784
768
  // the compacted messages when the pipeline compacted, the stripped
785
769
  // pre-compaction history otherwise.
786
- return compaction.postCompactionHook({
770
+ const injection = await postCompact({
787
771
  history: compactResult.compacted ? compactResult.messages : rawHistory,
788
- turnContext,
772
+ requestId,
773
+ conversationId: this.conversationId,
774
+ trust,
775
+ isNonInteractive,
776
+ // Mid-loop re-injection always runs at full injection volume.
777
+ mode: "full",
778
+ modelProfile,
779
+ actorContext,
789
780
  });
781
+ return injection.messages;
790
782
  }
791
783
 
792
- async run(
793
- messages: Message[],
794
- onEvent: (event: AgentEvent) => void | Promise<void>,
795
- options?: AgentLoopRunOptions,
796
- ): Promise<AgentLoopRunResult> {
784
+ async run(options: AgentLoopRunOptions): Promise<AgentLoopRunResult> {
797
785
  const {
786
+ messages,
787
+ onEvent,
798
788
  signal,
799
789
  requestId,
800
790
  onCheckpoint,
801
791
  callSite,
802
- turnContext,
792
+ trust,
803
793
  overrideProfile,
804
794
  resolveOverrideProfile,
805
795
  resolveContextWindow,
806
- compaction,
807
- mutableLatestUserMessage,
808
- } = options ?? {};
796
+ compactInPlace = false,
797
+ isNonInteractive = false,
798
+ modelProfile = null,
799
+ actorContext = null,
800
+ } = options;
809
801
  let history = [...messages];
810
802
  // Index into `history` where this run's appended output begins. It starts
811
803
  // after the input and resets to the compacted base whenever the loop
@@ -818,6 +810,13 @@ export class AgentLoop {
818
810
  let lastLlmCallTime = 0;
819
811
  let exitReason: ExitReason | null = null;
820
812
  let appendedNewMessages = false;
813
+ // Armed at the end of a tool-use iteration so the budget gate runs at the
814
+ // top of the NEXT iteration — before that iteration's provider call —
815
+ // instead of after the current one. Stop-hook re-query continues re-enter
816
+ // without arming, so the gate fires on exactly the same occasions as the
817
+ // prior post-call placement, plus the first call when
818
+ // `compactInPlace` is set (the primary run's turn-start compaction).
819
+ let budgetGateArmed = compactInPlace;
821
820
  const rlog = requestId ? log.child({ requestId }) : log;
822
821
 
823
822
  // Resolve the inference-profile override that applies right now. The
@@ -858,6 +857,104 @@ export class AgentLoop {
858
857
  let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
859
858
 
860
859
  try {
860
+ // ── Pre-call budget gate ─────────────────────────────────────
861
+ // When overflow recovery is enabled, estimate the running context
862
+ // size as it approaches the preflight budget before issuing the
863
+ // provider call. With `compactInPlace` the loop compacts in place and
864
+ // proceeds with the call; otherwise it yields (`exitReason =
865
+ // "budget"`) so the orchestrator can recover before the call risks a
866
+ // hard context-too-large rejection. Keyed off the loop's own
867
+ // `history.length` (the messages actually in context this turn,
868
+ // including tool iterations) rather than the durable conversation
869
+ // count.
870
+ //
871
+ // Armed after each tool-use iteration; stop-hook re-query continues
872
+ // skip it. The first call runs it only when `compactInPlace` is set,
873
+ // where it stands in for the orchestrator's turn-start compaction: it
874
+ // honors the compaction circuit breaker and proceeds with the call
875
+ // rather than yielding, since there is no prior turn output to
876
+ // escalate.
877
+ if (budgetGateArmed) {
878
+ budgetGateArmed = false;
879
+ // The gate only re-arms after a completed tool-use iteration
880
+ // (`toolUseTurns` is incremented first), so reaching it with
881
+ // `toolUseTurns === 0` uniquely identifies the first-call pass: it
882
+ // compacts-or-proceeds (never yields) and honors the compaction
883
+ // circuit breaker, matching the orchestrator's turn-start compaction.
884
+ const isFirstCallGate = toolUseTurns === 0;
885
+ const contextWindow = resolveContextWindow?.();
886
+ if (contextWindow?.overflowRecovery.enabled) {
887
+ const { maxInputTokens, overflowRecovery } = contextWindow;
888
+ const safetyMargin =
889
+ history.length > LONG_HISTORY_MESSAGE_THRESHOLD
890
+ ? Math.max(
891
+ overflowRecovery.safetyMarginRatio,
892
+ LONG_HISTORY_SAFETY_MARGIN_FLOOR,
893
+ )
894
+ : overflowRecovery.safetyMarginRatio;
895
+ const preflightBudget = Math.floor(
896
+ maxInputTokens * (1 - safetyMargin),
897
+ );
898
+ const midLoopThreshold =
899
+ preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
900
+ const estimated = this.estimateTokens(history);
901
+ if (estimated > midLoopThreshold) {
902
+ let compactedInPlace = false;
903
+ // The turn-start pass skips compaction while the circuit breaker
904
+ // is open so a run of failed summaries doesn't keep hammering the
905
+ // summary LLM; mid-loop compaction is force-driven and proceeds
906
+ // regardless (it has already committed to compacting in place).
907
+ const compactionAllowed =
908
+ !isFirstCallGate || !(await this.compactionCircuit.isOpen());
909
+ if (compactInPlace && compactionAllowed) {
910
+ rlog.info(
911
+ {
912
+ turn: toolUseTurns,
913
+ estimated,
914
+ threshold: midLoopThreshold,
915
+ },
916
+ "Token estimate approaching budget — compacting in place",
917
+ );
918
+ const compacted = await this.compact(
919
+ history,
920
+ requestId,
921
+ trust,
922
+ signal,
923
+ onEvent,
924
+ resolveEffectiveOverrideProfile() ?? null,
925
+ isNonInteractive,
926
+ modelProfile,
927
+ actorContext,
928
+ );
929
+ if (compacted) {
930
+ history = compacted;
931
+ // The compacted, re-injected array is the new base; output
932
+ // produced after this point is what the orchestrator
933
+ // persists.
934
+ newMessagesStart = history.length;
935
+ compactedInPlace = true;
936
+ }
937
+ }
938
+ // The turn-start gate proceeds with the call whether or not it
939
+ // compacted (preflight-overflow recovery and the convergence loop
940
+ // remain the escalation path); only mid-loop re-entries yield to
941
+ // the orchestrator before the call.
942
+ if (!compactedInPlace && !isFirstCallGate) {
943
+ rlog.warn(
944
+ {
945
+ turn: toolUseTurns,
946
+ estimated,
947
+ threshold: midLoopThreshold,
948
+ },
949
+ "Token estimate approaching budget — yielding for compaction",
950
+ );
951
+ exitReason = "budget";
952
+ break;
953
+ }
954
+ }
955
+ }
956
+ }
957
+
861
958
  // Resolve tools for this turn: use the dynamic resolver if provided,
862
959
  // otherwise fall back to the static tool list.
863
960
  const currentTools = this.resolveTools
@@ -921,12 +1018,14 @@ export class AgentLoop {
921
1018
  providerConfig.cacheTtl = this.config.cacheTtl;
922
1019
  }
923
1020
 
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) {
1021
+ // Cache-anchor signal for volatile latest-user-message turns: when
1022
+ // memory-v3 is live it injects a per-turn `<memory>` block into the
1023
+ // latest user message, so the provider must anchor its long-TTL cache
1024
+ // breakpoint on the most recent STABLE user message instead of the
1025
+ // volatile latest one. Read here alongside the rest of the provider
1026
+ // config; only set when true so the wire/config stays byte-identical
1027
+ // when off.
1028
+ if (isAssistantFeatureFlagEnabled("memory-v3-live", getConfig())) {
930
1029
  providerConfig.mutableLatestUserMessage = true;
931
1030
  }
932
1031
 
@@ -941,14 +1040,14 @@ export class AgentLoop {
941
1040
  providerConfig.callSite = callSite;
942
1041
  providerConfig.usageTracking = "manual";
943
1042
  // 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;
1043
+ // Sourced from the loop's own conversation id so every LLM call in a
1044
+ // conversation resolves the same mix arm (stable across turns and
1045
+ // retries, and across daemon restarts since the seed is the durable
1046
+ // conversation id). Absent for standalone `AgentLoop` instances
1047
+ // (unit tests constructed without a conversation id) — those fall
1048
+ // back to per-call random mix selection.
1049
+ if (this.conversationId) {
1050
+ providerConfig.selectionSeed = this.conversationId;
952
1051
  }
953
1052
  }
954
1053
 
@@ -994,15 +1093,16 @@ export class AgentLoop {
994
1093
  );
995
1094
  rlog.info({ turn: toolUseTurns }, "LLM call start");
996
1095
 
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
- );
1096
+ // Sanitize the outbound history right before sending: drop accumulated
1097
+ // media, collapse old AX-tree snapshots, and convert historical
1098
+ // web-search results to text. See {@link preModelCallSanitize}.
1099
+ const providerHistory = preModelCallSanitize(history);
1100
+
1101
+ // A `pre-model-call` hook (below) can defer this turn's assistant
1102
+ // output; when set, the live text stream is held so an
1103
+ // `post-model-call` hook can emit the finalized (transformed) text
1104
+ // instead. Reset per model call.
1105
+ let deferAssistantOutput = false;
1006
1106
 
1007
1107
  // The `onEvent` wrapping below applies sensitive-output placeholder
1008
1108
  // substitution to streamed text while forwarding every other event
@@ -1013,6 +1113,9 @@ export class AgentLoop {
1013
1113
  config: providerConfig,
1014
1114
  onEvent: (event) => {
1015
1115
  if (event.type === "text_delta") {
1116
+ // Held when the turn's output is deferred — the final text is
1117
+ // emitted once, after the `post-model-call` hook runs.
1118
+ if (deferAssistantOutput) return;
1016
1119
  // Apply sensitive-output placeholder substitution (chunk-safe)
1017
1120
  if (substitutionMap.size > 0) {
1018
1121
  const combined = streamingPending + event.text;
@@ -1068,12 +1171,32 @@ export class AgentLoop {
1068
1171
  signal,
1069
1172
  };
1070
1173
 
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);
1174
+ // Let plugins edit the outbound request and opt this call into deferred
1175
+ // output streaming. Runs for every provider call; hooks self-gate on
1176
+ // call site / conversation. Fail-open: a throwing hook leaves the
1177
+ // request unchanged and streaming live.
1178
+ try {
1179
+ const preModelCtx: PreModelCallContext = {
1180
+ conversationId: this.conversationId,
1181
+ callSite,
1182
+ systemPrompt: providerOptions.systemPrompt,
1183
+ deferAssistantOutput: false,
1184
+ logger: rlog,
1185
+ };
1186
+ const finalPreModelCtx = await runHook(
1187
+ HOOKS.PRE_MODEL_CALL,
1188
+ preModelCtx,
1189
+ );
1190
+ providerOptions.systemPrompt = finalPreModelCtx.systemPrompt;
1191
+ // The hook owns the policy (it sees `callSite`/conversation and
1192
+ // self-gates); the loop honors whatever it decides.
1193
+ deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
1194
+ } catch (preModelCallError) {
1195
+ rlog.error(
1196
+ { err: preModelCallError },
1197
+ "pre-model-call hook failed — proceeding with the original request",
1198
+ );
1199
+ }
1077
1200
 
1078
1201
  // Announce the LLM-call boundary so downstream handlers (the
1079
1202
  // daemon's persistence pipeline) can reserve an empty assistant row
@@ -1161,12 +1284,52 @@ export class AgentLoop {
1161
1284
  streamingPending = "";
1162
1285
  }
1163
1286
 
1287
+ // Run the `post-model-call` hook on a finalized message and, when
1288
+ // output was deferred, emit the finalized text once (with sensitive-output
1289
+ // substitution applied, matching the live stream). Fail-open: the hook
1290
+ // receives a clone, so a throw — even mid in-place mutation — leaves the
1291
+ // original message intact.
1292
+ const finalizeAssistantMessage = async (
1293
+ message: Message,
1294
+ ): Promise<Message> => {
1295
+ let finalized = message;
1296
+ try {
1297
+ const ctx: PostModelCallContext = {
1298
+ conversationId: this.conversationId,
1299
+ callSite,
1300
+ content: structuredClone(message.content),
1301
+ stopReason: response.stopReason,
1302
+ logger: rlog,
1303
+ };
1304
+ const result = await runHook(HOOKS.POST_MODEL_CALL, ctx);
1305
+ finalized = { role: "assistant", content: result.content };
1306
+ } catch (assistantMessageError) {
1307
+ rlog.error(
1308
+ { err: assistantMessageError },
1309
+ "post-model-call hook failed — keeping the original content",
1310
+ );
1311
+ finalized = message;
1312
+ }
1313
+ if (deferAssistantOutput) {
1314
+ // The persisted message keeps sensitive-output placeholders; the
1315
+ // stream shows real values — substitute before emitting.
1316
+ const finalText = applySubstitutions(
1317
+ assistantTextOf(finalized.content),
1318
+ substitutionMap,
1319
+ );
1320
+ if (finalText.length > 0) {
1321
+ onEvent({ type: "text_delta", text: finalText });
1322
+ }
1323
+ }
1324
+ return finalized;
1325
+ };
1326
+
1164
1327
  // Build the assistant message with placeholder-only text.
1165
1328
  // Both provider history and persisted conversation store must retain
1166
1329
  // placeholders so the model never sees real sensitive values — neither
1167
1330
  // on subsequent loop turns nor on session reload from the database.
1168
1331
  // Substitution to real values happens only in streamed text_delta events.
1169
- const assistantMessage: Message = {
1332
+ let assistantMessage: Message = {
1170
1333
  role: "assistant",
1171
1334
  content: response.content,
1172
1335
  };
@@ -1195,10 +1358,6 @@ export class AgentLoop {
1195
1358
  block.type !== "server_tool_use" &&
1196
1359
  block.type !== "web_search_tool_result",
1197
1360
  );
1198
- const safeAssistantMessage: Message = {
1199
- role: "assistant",
1200
- content: safeContent,
1201
- };
1202
1361
  rlog.warn(
1203
1362
  {
1204
1363
  turn: toolUseTurns,
@@ -1209,6 +1368,13 @@ export class AgentLoop {
1209
1368
  },
1210
1369
  "LLM response reached output token limit",
1211
1370
  );
1371
+ // Run the hook on the truncated reply so output-filter plugins still
1372
+ // see it, and so a deferred turn gets its synthetic final emit (the
1373
+ // live stream was suppressed; without this the client would see nothing).
1374
+ const safeAssistantMessage = await finalizeAssistantMessage({
1375
+ role: "assistant",
1376
+ content: safeContent,
1377
+ });
1212
1378
  history.push(safeAssistantMessage);
1213
1379
  appendedNewMessages = true;
1214
1380
  await onEvent({
@@ -1241,7 +1407,7 @@ export class AgentLoop {
1241
1407
  // follow-up turn. It receives the full history and, when it asks to
1242
1408
  // continue, appends the follow-up turn itself.
1243
1409
  const stopCtx: StopContext = {
1244
- conversationId: turnCtx.conversationId,
1410
+ conversationId: this.conversationId,
1245
1411
  messages: [...history],
1246
1412
  responseContent: response.content,
1247
1413
  stopReason: response.stopReason,
@@ -1280,6 +1446,12 @@ export class AgentLoop {
1280
1446
  }
1281
1447
  }
1282
1448
 
1449
+ // Run the `post-model-call` hook + emit any deferred final text.
1450
+ // On a no-tool turn this point is reached only after the `stop` hook
1451
+ // resolves to "stop" (a `continue` already re-queried above), so a
1452
+ // re-queried reply is never transformed-then-discarded.
1453
+ assistantMessage = await finalizeAssistantMessage(assistantMessage);
1454
+
1283
1455
  history.push(assistantMessage);
1284
1456
  appendedNewMessages = true;
1285
1457
 
@@ -1430,7 +1602,7 @@ export class AgentLoop {
1430
1602
  continue;
1431
1603
  }
1432
1604
  const postToolUseCtx: PostToolUseContext = {
1433
- conversationId: turnCtx.conversationId,
1605
+ conversationId: this.conversationId,
1434
1606
  toolResponse: block as ToolResultContent,
1435
1607
  messages: history,
1436
1608
  maxInputTokens: contextWindowTokens,
@@ -1508,8 +1680,9 @@ export class AgentLoop {
1508
1680
  history.push({ role: "user", content: resultBlocks });
1509
1681
 
1510
1682
  // 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.
1683
+ // Handoff takes precedence over the budget gate: a handoff decision
1684
+ // breaks here and leaves `budgetGateArmed` false, so a queued message
1685
+ // is processed before the next iteration's pre-call budget gate.
1513
1686
  if (onCheckpoint) {
1514
1687
  const decision = await onCheckpoint({
1515
1688
  turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
@@ -1523,61 +1696,11 @@ export class AgentLoop {
1523
1696
  }
1524
1697
  }
1525
1698
 
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
- }
1699
+ // Arm the pre-call budget gate for the next iteration. Placed after
1700
+ // the checkpoint so a handoff yield (which breaks above) leaves it
1701
+ // disarmed; the gate then runs at the top of the next iteration,
1702
+ // before that iteration's provider call.
1703
+ budgetGateArmed = true;
1581
1704
  } catch (error) {
1582
1705
  // Abort errors are expected when user cancels — synthesize
1583
1706
  // cancellation tool_results so the history stays valid for the
@@ -1786,3 +1909,32 @@ function stripOldMediaBlocks(history: Message[]): Message[] {
1786
1909
  };
1787
1910
  });
1788
1911
  }
1912
+
1913
+ /**
1914
+ * Sanitize the outbound history immediately before a provider call, bundling
1915
+ * the pre-send transforms the loop applies to every request:
1916
+ * - {@link stripOldMediaBlocks} drops accumulated screenshot/audio bytes from
1917
+ * older tool results — the model saw the media on the turn it was captured.
1918
+ * - {@link compactAxTreeHistory} collapses all but the most recent few
1919
+ * `<ax-tree>` snapshots so TTFT does not grow linearly with step count.
1920
+ * - {@link stripHistoricalWebSearchResults} converts historical
1921
+ * `web_search_tool_result` blocks to text summaries; Anthropic's opaque
1922
+ * `encrypted_content` tokens expire / are route-scoped, and replaying a stale
1923
+ * one is rejected with `Invalid encrypted_content in search_result block`.
1924
+ *
1925
+ * Transforms the outbound copy only — the durable history keeps the rich
1926
+ * originals and each send re-derives the sanitized projection (every transform
1927
+ * is idempotent). Because it runs unconditionally before every provider call,
1928
+ * it is the single place where oversized media and expired web-search tokens
1929
+ * are guaranteed to be removed from a request.
1930
+ *
1931
+ * This is outbound-request preparation and should eventually move to a default
1932
+ * `pre-model-call` plugin hook ({@link HOOKS.PRE_MODEL_CALL}) once that hook's
1933
+ * context carries the outbound message list; for now it lives inline next to
1934
+ * the provider call it guards.
1935
+ */
1936
+ export function preModelCallSanitize(history: Message[]): Message[] {
1937
+ const mediaStripped = stripOldMediaBlocks(history);
1938
+ const axCompacted = compactAxTreeHistory(mediaStripped);
1939
+ return stripHistoricalWebSearchResults(axCompacted).messages;
1940
+ }