@vellumai/assistant 0.8.8 → 0.8.9-staging.1

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 (340) hide show
  1. package/bun.lock +2 -2
  2. package/examples/plugins/echo/README.md +61 -60
  3. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  4. package/examples/plugins/echo/hooks/stop.ts +16 -0
  5. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  6. package/examples/plugins/echo/package.json +1 -2
  7. package/examples/plugins/echo/src/emit.ts +19 -0
  8. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  9. package/openapi.yaml +236 -6
  10. package/package.json +2 -2
  11. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  12. package/src/__tests__/agent-loop-exit-reason.test.ts +185 -140
  13. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  14. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  15. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  16. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  17. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  18. package/src/__tests__/agent-loop.test.ts +441 -96
  19. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  20. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  21. package/src/__tests__/anthropic-provider.test.ts +1 -1
  22. package/src/__tests__/app-control-flow.test.ts +1 -1
  23. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  24. package/src/__tests__/approval-cascade.test.ts +5 -4
  25. package/src/__tests__/approval-routes-http.test.ts +4 -1
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  27. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  28. package/src/__tests__/channel-approvals.test.ts +1 -1
  29. package/src/__tests__/compaction-circuit.test.ts +258 -0
  30. package/src/__tests__/compaction-direct.test.ts +132 -0
  31. package/src/__tests__/compaction-events.test.ts +5 -5
  32. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  33. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  34. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +59 -103
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +94 -189
  36. package/src/__tests__/conversation-agent-loop.test.ts +249 -490
  37. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  38. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  39. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  40. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  41. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +18 -16
  43. package/src/__tests__/conversation-queue.test.ts +9 -9
  44. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  45. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  46. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  47. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  48. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  49. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  50. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  51. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  53. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  54. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  55. package/src/__tests__/db-acp-history.test.ts +101 -0
  56. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  57. package/src/__tests__/empty-response-hook.test.ts +1 -1
  58. package/src/__tests__/file-write-tool.test.ts +63 -0
  59. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  60. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  61. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  63. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  64. package/src/__tests__/heartbeat-service.test.ts +1 -0
  65. package/src/__tests__/history-repair-hook.test.ts +1 -1
  66. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  67. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  68. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  69. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  70. package/src/__tests__/injector-background-turn.test.ts +13 -23
  71. package/src/__tests__/injector-chain.test.ts +268 -44
  72. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  73. package/src/__tests__/injector-document-comments.test.ts +97 -114
  74. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  75. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  76. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  77. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  78. package/src/__tests__/memory-retrieval-hook.test.ts +73 -6
  79. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  80. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  81. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  82. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  83. package/src/__tests__/plugin-registry.test.ts +3 -49
  84. package/src/__tests__/plugin-types.test.ts +0 -70
  85. package/src/__tests__/reaction-persistence.test.ts +1 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  87. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  88. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  89. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  90. package/src/__tests__/subagent-detail.test.ts +25 -7
  91. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  92. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  93. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  94. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  95. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  96. package/src/__tests__/title-generate-hook.test.ts +1 -1
  97. package/src/__tests__/tool-error-hook.test.ts +1 -1
  98. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  99. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  100. package/src/acp/__tests__/agent-process.test.ts +161 -0
  101. package/src/acp/__tests__/client-handler.test.ts +40 -0
  102. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  103. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  104. package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
  105. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  106. package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
  107. package/src/acp/agent-process.ts +61 -1
  108. package/src/acp/auto-install.test.ts +196 -0
  109. package/src/acp/auto-install.ts +177 -0
  110. package/src/acp/client-handler.ts +31 -0
  111. package/src/acp/feature-gate.test.ts +48 -0
  112. package/src/acp/feature-gate.ts +34 -0
  113. package/src/acp/prepare-agent-env.ts +83 -29
  114. package/src/acp/resolve-agent.test.ts +320 -7
  115. package/src/acp/resolve-agent.ts +182 -18
  116. package/src/acp/resume-hint.ts +25 -0
  117. package/src/acp/session-manager.ts +495 -73
  118. package/src/acp/types.ts +8 -0
  119. package/src/agent/compaction-circuit.ts +60 -102
  120. package/src/agent/loop.ts +390 -240
  121. package/src/api/responses/conversation-message.ts +14 -1
  122. package/src/approvals/guardian-request-resolvers.ts +1 -1
  123. package/src/background-wake/next-wake.ts +1 -0
  124. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  125. package/src/cli/commands/plugins.ts +43 -37
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  127. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  128. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  129. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  130. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  131. package/src/cli/lib/install-from-github.ts +464 -55
  132. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  133. package/src/cli/lib/plugin-details.ts +409 -0
  134. package/src/cli/lib/plugin-marketplace.ts +197 -0
  135. package/src/cli/lib/search-plugins.ts +195 -29
  136. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  137. package/src/config/acp-defaults.test.ts +10 -0
  138. package/src/config/acp-defaults.ts +6 -0
  139. package/src/config/bundled-skills/acp/SKILL.md +83 -31
  140. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  141. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  142. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  143. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  144. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  145. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  146. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  147. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  148. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  149. package/src/config/bundled-tool-registry.ts +2 -0
  150. package/src/config/feature-flag-registry.json +14 -5
  151. package/src/config/schemas/heartbeat.ts +9 -0
  152. package/src/context/strip-injections.ts +8 -2
  153. package/src/context/window-manager.ts +27 -13
  154. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  155. package/src/daemon/conversation-agent-loop.ts +167 -1005
  156. package/src/daemon/conversation-lifecycle.ts +11 -255
  157. package/src/daemon/conversation-process.ts +8 -136
  158. package/src/daemon/conversation-registry.ts +159 -0
  159. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  160. package/src/daemon/conversation-store.ts +9 -90
  161. package/src/daemon/conversation-surfaces.ts +24 -8
  162. package/src/daemon/conversation-workspace.ts +17 -0
  163. package/src/daemon/conversation.ts +383 -56
  164. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  165. package/src/daemon/handlers/conversations.ts +3 -1
  166. package/src/daemon/handlers/skills.ts +4 -1
  167. package/src/daemon/host-proxy-preactivation.ts +1 -3
  168. package/src/daemon/lifecycle.ts +21 -0
  169. package/src/daemon/server.ts +2 -0
  170. package/src/daemon/wake-conversation-ops.ts +269 -0
  171. package/src/embedded/plugin-api.ts +2 -2
  172. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  173. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  174. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  175. package/src/heartbeat/heartbeat-service.ts +26 -0
  176. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  177. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  178. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  179. package/src/ipc/skill-routes/memory.ts +4 -2
  180. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  181. package/src/memory/conversation-crud.ts +29 -19
  182. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  183. package/src/memory/db-init.ts +2 -0
  184. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  185. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  186. package/src/memory/jobs-worker.ts +25 -1
  187. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  188. package/src/memory/migrations/index.ts +1 -0
  189. package/src/memory/schema/acp.ts +4 -0
  190. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  191. package/src/memory/v2/consolidation-job.ts +13 -4
  192. package/src/plugin-api/constants.ts +4 -0
  193. package/src/plugin-api/index.ts +6 -5
  194. package/src/plugin-api/types.ts +75 -0
  195. package/src/plugins/defaults/compaction/compact.ts +59 -0
  196. package/src/plugins/defaults/compaction/manager-store.ts +44 -0
  197. package/src/plugins/defaults/compaction/package.json +1 -2
  198. package/src/plugins/defaults/empty-response/package.json +0 -1
  199. package/src/plugins/defaults/history-repair/package.json +0 -1
  200. package/src/plugins/defaults/index.ts +135 -26
  201. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +103 -38
  202. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +167 -56
  203. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  204. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  205. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  206. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  207. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  208. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  209. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  210. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  211. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  212. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  213. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  214. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  215. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  216. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  217. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  218. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  219. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  220. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  221. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  224. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  232. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  233. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  234. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  235. package/src/plugins/defaults/title-generate/package.json +0 -1
  236. package/src/plugins/defaults/tool-error/package.json +0 -1
  237. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  238. package/src/plugins/pipeline.ts +6 -293
  239. package/src/plugins/registry.ts +9 -37
  240. package/src/plugins/types.ts +76 -381
  241. package/src/plugins/user-loader.ts +30 -127
  242. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  243. package/src/proactive-artifact/job.test.ts +1 -1
  244. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  245. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  246. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  247. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  248. package/src/runtime/agent-wake.ts +108 -209
  249. package/src/runtime/assistant-event-hub.ts +1 -1
  250. package/src/runtime/channel-approvals.ts +1 -1
  251. package/src/runtime/interactive-ui.ts +1 -1
  252. package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
  253. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  254. package/src/runtime/routes/__tests__/plugins-routes.test.ts +398 -73
  255. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  256. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  257. package/src/runtime/routes/acp-routes.test.ts +89 -25
  258. package/src/runtime/routes/acp-routes.ts +81 -29
  259. package/src/runtime/routes/approval-routes.ts +1 -1
  260. package/src/runtime/routes/browser-routes.ts +1 -1
  261. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  262. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  263. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  264. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  265. package/src/runtime/routes/conversation-routes.ts +28 -2
  266. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  267. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  268. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  269. package/src/runtime/routes/host-cu-routes.ts +1 -1
  270. package/src/runtime/routes/identity-routes.ts +18 -3
  271. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  272. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  273. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  274. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  275. package/src/runtime/routes/playground/helpers.ts +1 -1
  276. package/src/runtime/routes/plugins-routes.ts +336 -35
  277. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  278. package/src/runtime/routes/work-items-routes.ts +2 -4
  279. package/src/runtime/services/conversation-serializer.ts +1 -1
  280. package/src/signals/cancel.ts +2 -4
  281. package/src/subagent/manager.ts +21 -5
  282. package/src/tools/acp/context.ts +20 -0
  283. package/src/tools/acp/list-agents.test.ts +7 -1
  284. package/src/tools/acp/spawn.test.ts +158 -55
  285. package/src/tools/acp/spawn.ts +47 -72
  286. package/src/tools/acp/steer.test.ts +105 -8
  287. package/src/tools/acp/steer.ts +48 -17
  288. package/src/tools/apps/executors.ts +13 -8
  289. package/src/tools/filesystem/write.ts +34 -0
  290. package/src/tools/subagent/spawn.ts +2 -4
  291. package/src/tools/ui-surface/definitions.ts +25 -5
  292. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  293. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  294. package/docs/plugins.md +0 -832
  295. package/examples/plugins/echo/register.ts +0 -143
  296. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  297. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  298. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  299. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  300. package/src/__tests__/pipeline-runner.test.ts +0 -554
  301. package/src/__tests__/plugin-external-api.test.ts +0 -68
  302. package/src/daemon/wake-target-adapter.ts +0 -253
  303. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  304. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  305. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  306. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  307. package/src/plugins/defaults/compaction/register.ts +0 -35
  308. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  309. package/src/plugins/defaults/empty-response/register.ts +0 -23
  310. package/src/plugins/defaults/history-repair/register.ts +0 -24
  311. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  312. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  313. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  314. package/src/plugins/defaults/title-generate/register.ts +0 -35
  315. package/src/plugins/defaults/tool-error/register.ts +0 -23
  316. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  317. package/src/plugins/external-api.ts +0 -104
  318. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  319. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  320. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  321. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  322. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  323. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  324. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  325. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  326. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  327. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  328. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  329. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  330. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  331. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  332. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  333. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  334. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  335. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  336. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  337. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  340. /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 {
@@ -8,21 +10,28 @@ 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 {
14
+ ContextWindowManager,
15
+ ContextWindowResult,
16
+ } from "../context/window-manager.js";
17
+ import type { InboundActorContext } from "../daemon/conversation-runtime-assembly.js";
12
18
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
19
+ import type { TrustContext } from "../daemon/trust-context.js";
13
20
  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
21
  import type {
20
- CompactionArgs,
21
- CompactionCircuitEvent,
22
- CompactionResult,
23
- TurnContext,
24
- } from "../plugins/types.js";
25
- import { PluginTimeoutError } from "../plugins/types.js";
22
+ AssistantMessageContext,
23
+ PostToolUseContext,
24
+ PreModelCallContext,
25
+ StopContext,
26
+ } from "../plugin-api/types.js";
27
+ import {
28
+ DEFAULT_COMPACTION_PLUGIN_NAME,
29
+ defaultCompact,
30
+ } from "../plugins/defaults/compaction/compact.js";
31
+ import postCompact from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
32
+ import { runHook } from "../plugins/pipeline.js";
33
+ import type { CompactionCircuitEvent } from "../plugins/types.js";
34
+ import { PluginExecutionError } from "../plugins/types.js";
26
35
  import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
27
36
  import type {
28
37
  ContentBlock,
@@ -308,7 +317,7 @@ export type AgentEvent =
308
317
  * immediately before re-injection — whether or not the pipeline actually
309
318
  * compacted. The daemon's event dispatcher always commits `basis` (the
310
319
  * stripped pre-compaction history) as the conversation's durable message
311
- * state, so re-injection ({@link MidLoopCompaction.reinject}) re-applies
320
+ * state, so re-injection ({@link postCompact}) re-applies
312
321
  * injections onto the stripped base rather than stacking on top of the
313
322
  * still-injected messages. When `result.compacted` is set it
314
323
  * additionally commits the durable compaction result (DB-record fields,
@@ -382,34 +391,17 @@ export function isMaxTokensStopReason(
382
391
  }
383
392
 
384
393
  /**
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.
394
+ * Concatenate the text of an assistant message's `text` blocks (ignoring
395
+ * `tool_use`, `thinking`, and other non-text blocks). Used to re-emit the
396
+ * finalized text as a single `text_delta` when a turn's live output was
397
+ * deferred by a `pre-model-call` hook.
396
398
  */
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
- };
399
+ function assistantTextOf(content: ReadonlyArray<ContentBlock>): string {
400
+ let text = "";
401
+ for (const block of content) {
402
+ if (block.type === "text") text += block.text;
403
+ }
404
+ return text;
413
405
  }
414
406
 
415
407
  /**
@@ -454,29 +446,11 @@ export interface ResolvedSystemPrompt {
454
446
  model?: string;
455
447
  }
456
448
 
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
449
  export interface AgentLoopRunOptions {
450
+ /** Input history the run starts from; the loop appends its output onto a copy. */
451
+ messages: Message[];
452
+ /** Sink the loop streams its {@link AgentEvent}s through as the turn runs. */
453
+ onEvent: (event: AgentEvent) => void | Promise<void>;
480
454
  signal?: AbortSignal;
481
455
  requestId?: string;
482
456
  onCheckpoint?: (
@@ -484,14 +458,24 @@ export interface AgentLoopRunOptions {
484
458
  ) => CheckpointDecision | Promise<CheckpointDecision>;
485
459
  callSite?: LLMCallSite;
486
460
  /**
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.
461
+ * Trust classification and channel identity for the turn's inbound actor,
462
+ * supplied by the caller as the turn-start snapshot. Read only on the
463
+ * mid-loop in-place compaction path — to scope the compactor's image
464
+ * manifest (guardian-only attachments are excluded for untrusted actors) and
465
+ * forwarded to {@link postCompact}. Callers without a meaningful actor (agent
466
+ * wakes, standalone unit tests) pass an `unknown`-class snapshot so the
467
+ * compactor fail-closes to excluding guardian-only attachments.
493
468
  */
494
- turnContext?: TurnContext;
469
+ trust: TrustContext;
470
+ /**
471
+ * The conversation's {@link ContextWindowManager}, supplied by the
472
+ * orchestrator so mid-loop in-place compaction can run against the real
473
+ * per-conversation manager. Absent for callers without a compaction path
474
+ * (agent wakes, standalone unit tests); the loop only reads it when the
475
+ * mid-loop budget gate trips with {@link AgentLoopRunOptions.compactInPlace}
476
+ * enabled.
477
+ */
478
+ contextWindowManager?: ContextWindowManager;
495
479
  /**
496
480
  * Ad-hoc inference-profile override applied to every LLM call the loop
497
481
  * issues. When set, each `SendMessageOptions.config` carries
@@ -514,21 +498,51 @@ export interface AgentLoopRunOptions {
514
498
  overflowRecovery: { enabled: boolean; safetyMarginRatio: number };
515
499
  };
516
500
  /**
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.
501
+ * When `true`, the loop owns turn-start and mid-loop compaction. The pre-call
502
+ * budget gate runs before the very first provider call — subsuming the
503
+ * proactive turn-start compaction the orchestrator would otherwise perform
504
+ * inline before `run()` — as well as before each tool-use re-entry. When the
505
+ * gate trips it compacts the running history in place, re-applying runtime
506
+ * injections via the default post-compaction hook ({@link postCompact}), and
507
+ * continues instead of yielding `exitReason = "budget"`.
508
+ *
509
+ * The first-call pass honors the compaction circuit breaker and proceeds with
510
+ * the call whether or not it compacted (preflight-overflow recovery and the
511
+ * convergence loop remain the escalation path), so it never yields on the
512
+ * first call. Reruns without an inline compaction path (agent wakes,
513
+ * convergence/auto-compress reruns) leave it `false`: they skip the
514
+ * first-call gate and keep yielding for budget on mid-loop re-entries.
515
+ * Defaults to `false` when omitted.
516
+ */
517
+ compactInPlace?: boolean;
518
+ /**
519
+ * Whether the in-flight turn has no human present to answer clarification
520
+ * questions. Resolved once by the orchestrator at turn start and forwarded to
521
+ * {@link postCompact} so post-compaction
522
+ * re-injection uses the turn-start snapshot rather than re-reading mutable
523
+ * client/headless state mid-turn. Defaults to `false` when omitted.
521
524
  */
522
- compaction?: MidLoopCompaction;
525
+ isNonInteractive?: boolean;
523
526
  /**
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.
527
+ * The `model_profile:` turn-context label resolved once by the orchestrator
528
+ * at turn start, or `null` when the active inference profile is unchanged
529
+ * since the last notified one. Forwarded to
530
+ * {@link postCompact} so post-compaction re-injection
531
+ * re-emits the turn-start value rather than re-deriving the change-detected
532
+ * label (which flips once the notification is persisted mid-turn). Defaults to
533
+ * `null` when omitted.
530
534
  */
531
- mutableLatestUserMessage?: boolean;
535
+ modelProfile?: string | null;
536
+ /**
537
+ * Inbound actor identity and trust fields for the unified `<turn_context>`
538
+ * block, or `null` on guardian turns. Resolved once by the orchestrator at
539
+ * turn start via the actor-trust resolver, whose contact/member registry
540
+ * inputs can be mutated mid-turn by contact tools, and forwarded to
541
+ * {@link postCompact} so post-compaction
542
+ * re-injection re-emits the turn-start value rather than re-resolving it.
543
+ * Defaults to `null` when omitted.
544
+ */
545
+ actorContext?: InboundActorContext | null;
532
546
  }
533
547
 
534
548
  /**
@@ -570,17 +584,21 @@ export type LoopToolExecutor = (
570
584
  }>;
571
585
 
572
586
  export interface AgentLoopConstructorOptions {
587
+ /** LLM provider the loop issues every call through. */
588
+ provider: Provider;
589
+ /** Base system prompt, before any per-turn dynamic resolution. */
590
+ systemPrompt: string;
573
591
  config?: Partial<AgentLoopConfig>;
574
592
  tools?: ToolDefinition[];
575
593
  toolExecutor?: LoopToolExecutor;
576
594
  resolveTools?: (history: Message[]) => ToolDefinition[];
577
595
  resolveSystemPrompt?: (history: Message[]) => ResolvedSystemPrompt;
578
596
  /**
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.
597
+ * Conversation this loop drives. Scopes the loop-held compaction circuit
598
+ * breaker and is the source of truth the loop's pipeline contexts and
599
+ * post-compaction re-injection resolve the live conversation through.
582
600
  */
583
- conversationId?: string;
601
+ conversationId: string;
584
602
  }
585
603
 
586
604
  export class AgentLoop {
@@ -594,6 +612,14 @@ export class AgentLoop {
594
612
  | null;
595
613
  private toolExecutor: LoopToolExecutor | null;
596
614
 
615
+ /**
616
+ * Conversation this loop drives. Source of truth for the `conversationId`
617
+ * the loop's pipeline contexts and post-compaction re-injection resolve the
618
+ * live conversation through, so the loop knows its own identity without a
619
+ * threaded-in turn context.
620
+ */
621
+ private readonly conversationId: string;
622
+
597
623
  /**
598
624
  * Loop-held compaction circuit breaker. The loop has a 1:1 lifetime with its
599
625
  * conversation, so it is the source of truth for the cross-turn failure
@@ -603,19 +629,17 @@ export class AgentLoop {
603
629
  */
604
630
  readonly compactionCircuit: CompactionCircuit;
605
631
 
606
- constructor(
607
- provider: Provider,
608
- systemPrompt: string,
609
- options?: AgentLoopConstructorOptions,
610
- ) {
632
+ constructor(options: AgentLoopConstructorOptions) {
611
633
  const {
634
+ provider,
635
+ systemPrompt,
612
636
  config,
613
637
  tools,
614
638
  toolExecutor,
615
639
  resolveTools,
616
640
  resolveSystemPrompt,
617
641
  conversationId,
618
- } = options ?? {};
642
+ } = options;
619
643
  this.provider = provider;
620
644
  this.systemPrompt = systemPrompt;
621
645
  this.config = { ...DEFAULT_CONFIG, ...config };
@@ -623,7 +647,8 @@ export class AgentLoop {
623
647
  this.resolveTools = resolveTools ?? null;
624
648
  this.resolveSystemPrompt = resolveSystemPrompt ?? null;
625
649
  this.toolExecutor = toolExecutor ?? null;
626
- this.compactionCircuit = new CompactionCircuit(conversationId ?? "");
650
+ this.conversationId = conversationId;
651
+ this.compactionCircuit = new CompactionCircuit(this.conversationId);
627
652
  }
628
653
 
629
654
  /**
@@ -675,23 +700,15 @@ export class AgentLoop {
675
700
  * compaction outcome into a user-visible turn failure.
676
701
  */
677
702
  private async recordCompactionOutcome(
678
- turnContext: TurnContext,
703
+ requestId: string | undefined,
679
704
  summaryFailed: boolean,
680
705
  onEvent: (event: AgentEvent) => void | Promise<void>,
681
706
  ): Promise<void> {
682
707
  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
- );
708
+ await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
692
709
  } catch (recordError) {
693
710
  log.error(
694
- { err: recordError, requestId: turnContext.requestId },
711
+ { err: recordError, requestId },
695
712
  "Recording a compaction outcome against the circuit breaker failed; suppressing to keep the agent loop alive",
696
713
  );
697
714
  }
@@ -700,19 +717,22 @@ export class AgentLoop {
700
717
  /**
701
718
  * Compact the running history in place when the mid-loop budget gate trips.
702
719
  *
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.
720
+ * Calls the default compaction plugin on the stripped history, then
721
+ * re-applies injections via the supplied hooks. Returns the history to
722
+ * continue from, or `null` when the compactor exhausted its retry budget so
723
+ * the caller yields `exitReason = "budget"` and the orchestrator escalates.
708
724
  */
709
725
  private async compact(
710
726
  history: Message[],
711
- turnContext: TurnContext,
712
- compaction: MidLoopCompaction,
727
+ requestId: string | undefined,
728
+ trust: TrustContext,
729
+ manager: ContextWindowManager | undefined,
713
730
  signal: AbortSignal | undefined,
714
731
  onEvent: (event: AgentEvent) => void | Promise<void>,
715
732
  overrideProfile: string | null,
733
+ isNonInteractive: boolean,
734
+ modelProfile: string | null,
735
+ actorContext: InboundActorContext | null,
716
736
  ): Promise<Message[] | null> {
717
737
  await onEvent({ type: "context_compacting" });
718
738
  // Strip runtime injections so the compactor summarizes the raw persistent
@@ -721,49 +741,33 @@ export class AgentLoop {
721
741
  // Record the history-stripped marker right after stripping, before the
722
742
  // pipeline runs.
723
743
  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,
744
+ if (manager == null) {
745
+ throw new PluginExecutionError(
746
+ "default-compaction: contextWindowManager run option is missing — orchestrator must supply it before invoking compaction",
747
+ DEFAULT_COMPACTION_PLUGIN_NAME,
747
748
  );
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
749
  }
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;
750
+ // The mid-loop budget gate is reached only when this turn decides to
751
+ // compact in place, so `force` past the auto-threshold check.
752
+ // `actorTrustClass` comes from the turn's trust snapshot (the actor whose
753
+ // turn triggered compaction) so the compactor's image manifest excludes
754
+ // guardian-only attachments for untrusted actors. `overrideProfile` is the
755
+ // turn's resolved inference-profile override for the summary call.
756
+ const compactResult = await defaultCompact({
757
+ manager,
758
+ messages: rawHistory,
759
+ signal,
760
+ force: true,
761
+ actorTrustClass: trust.trustClass,
762
+ overrideProfile,
763
+ });
760
764
  // `force: true` bypasses the auto-threshold gate, but early returns
761
765
  // for "no eligible messages" / "insufficient messages" still leave
762
766
  // `summaryFailed` undefined. Only record an outcome when the summary LLM
763
767
  // actually ran.
764
768
  if (compactResult.summaryFailed !== undefined) {
765
769
  await this.recordCompactionOutcome(
766
- turnContext,
770
+ requestId,
767
771
  compactResult.summaryFailed,
768
772
  onEvent,
769
773
  );
@@ -783,29 +787,38 @@ export class AgentLoop {
783
787
  // Re-inject onto the same base the `compaction_completed` dispatch commits:
784
788
  // the compacted messages when the pipeline compacted, the stripped
785
789
  // pre-compaction history otherwise.
786
- return compaction.postCompactionHook({
790
+ const injection = await postCompact({
787
791
  history: compactResult.compacted ? compactResult.messages : rawHistory,
788
- turnContext,
792
+ requestId,
793
+ conversationId: this.conversationId,
794
+ trust,
795
+ isNonInteractive,
796
+ // Mid-loop re-injection always runs at full injection volume.
797
+ mode: "full",
798
+ modelProfile,
799
+ actorContext,
789
800
  });
801
+ return injection.messages;
790
802
  }
791
803
 
792
- async run(
793
- messages: Message[],
794
- onEvent: (event: AgentEvent) => void | Promise<void>,
795
- options?: AgentLoopRunOptions,
796
- ): Promise<AgentLoopRunResult> {
804
+ async run(options: AgentLoopRunOptions): Promise<AgentLoopRunResult> {
797
805
  const {
806
+ messages,
807
+ onEvent,
798
808
  signal,
799
809
  requestId,
800
810
  onCheckpoint,
801
811
  callSite,
802
- turnContext,
812
+ trust,
803
813
  overrideProfile,
804
814
  resolveOverrideProfile,
805
815
  resolveContextWindow,
806
- compaction,
807
- mutableLatestUserMessage,
808
- } = options ?? {};
816
+ compactInPlace = false,
817
+ contextWindowManager,
818
+ isNonInteractive = false,
819
+ modelProfile = null,
820
+ actorContext = null,
821
+ } = options;
809
822
  let history = [...messages];
810
823
  // Index into `history` where this run's appended output begins. It starts
811
824
  // after the input and resets to the compacted base whenever the loop
@@ -818,6 +831,13 @@ export class AgentLoop {
818
831
  let lastLlmCallTime = 0;
819
832
  let exitReason: ExitReason | null = null;
820
833
  let appendedNewMessages = false;
834
+ // Armed at the end of a tool-use iteration so the budget gate runs at the
835
+ // top of the NEXT iteration — before that iteration's provider call —
836
+ // instead of after the current one. Stop-hook re-query continues re-enter
837
+ // without arming, so the gate fires on exactly the same occasions as the
838
+ // prior post-call placement, plus the first call when
839
+ // `compactInPlace` is set (the primary run's turn-start compaction).
840
+ let budgetGateArmed = compactInPlace;
821
841
  const rlog = requestId ? log.child({ requestId }) : log;
822
842
 
823
843
  // Resolve the inference-profile override that applies right now. The
@@ -858,6 +878,105 @@ export class AgentLoop {
858
878
  let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
859
879
 
860
880
  try {
881
+ // ── Pre-call budget gate ─────────────────────────────────────
882
+ // When overflow recovery is enabled, estimate the running context
883
+ // size as it approaches the preflight budget before issuing the
884
+ // provider call. With `compactInPlace` the loop compacts in place and
885
+ // proceeds with the call; otherwise it yields (`exitReason =
886
+ // "budget"`) so the orchestrator can recover before the call risks a
887
+ // hard context-too-large rejection. Keyed off the loop's own
888
+ // `history.length` (the messages actually in context this turn,
889
+ // including tool iterations) rather than the durable conversation
890
+ // count.
891
+ //
892
+ // Armed after each tool-use iteration; stop-hook re-query continues
893
+ // skip it. The first call runs it only when `compactInPlace` is set,
894
+ // where it stands in for the orchestrator's turn-start compaction: it
895
+ // honors the compaction circuit breaker and proceeds with the call
896
+ // rather than yielding, since there is no prior turn output to
897
+ // escalate.
898
+ if (budgetGateArmed) {
899
+ budgetGateArmed = false;
900
+ // The gate only re-arms after a completed tool-use iteration
901
+ // (`toolUseTurns` is incremented first), so reaching it with
902
+ // `toolUseTurns === 0` uniquely identifies the first-call pass: it
903
+ // compacts-or-proceeds (never yields) and honors the compaction
904
+ // circuit breaker, matching the orchestrator's turn-start compaction.
905
+ const isFirstCallGate = toolUseTurns === 0;
906
+ const contextWindow = resolveContextWindow?.();
907
+ if (contextWindow?.overflowRecovery.enabled) {
908
+ const { maxInputTokens, overflowRecovery } = contextWindow;
909
+ const safetyMargin =
910
+ history.length > LONG_HISTORY_MESSAGE_THRESHOLD
911
+ ? Math.max(
912
+ overflowRecovery.safetyMarginRatio,
913
+ LONG_HISTORY_SAFETY_MARGIN_FLOOR,
914
+ )
915
+ : overflowRecovery.safetyMarginRatio;
916
+ const preflightBudget = Math.floor(
917
+ maxInputTokens * (1 - safetyMargin),
918
+ );
919
+ const midLoopThreshold =
920
+ preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
921
+ const estimated = this.estimateTokens(history);
922
+ if (estimated > midLoopThreshold) {
923
+ let compactedInPlace = false;
924
+ // The turn-start pass skips compaction while the circuit breaker
925
+ // is open so a run of failed summaries doesn't keep hammering the
926
+ // summary LLM; mid-loop compaction is force-driven and proceeds
927
+ // regardless (it has already committed to compacting in place).
928
+ const compactionAllowed =
929
+ !isFirstCallGate || !(await this.compactionCircuit.isOpen());
930
+ if (compactInPlace && compactionAllowed) {
931
+ rlog.info(
932
+ {
933
+ turn: toolUseTurns,
934
+ estimated,
935
+ threshold: midLoopThreshold,
936
+ },
937
+ "Token estimate approaching budget — compacting in place",
938
+ );
939
+ const compacted = await this.compact(
940
+ history,
941
+ requestId,
942
+ trust,
943
+ contextWindowManager,
944
+ signal,
945
+ onEvent,
946
+ resolveEffectiveOverrideProfile() ?? null,
947
+ isNonInteractive,
948
+ modelProfile,
949
+ actorContext,
950
+ );
951
+ if (compacted) {
952
+ history = compacted;
953
+ // The compacted, re-injected array is the new base; output
954
+ // produced after this point is what the orchestrator
955
+ // persists.
956
+ newMessagesStart = history.length;
957
+ compactedInPlace = true;
958
+ }
959
+ }
960
+ // The turn-start gate proceeds with the call whether or not it
961
+ // compacted (preflight-overflow recovery and the convergence loop
962
+ // remain the escalation path); only mid-loop re-entries yield to
963
+ // the orchestrator before the call.
964
+ if (!compactedInPlace && !isFirstCallGate) {
965
+ rlog.warn(
966
+ {
967
+ turn: toolUseTurns,
968
+ estimated,
969
+ threshold: midLoopThreshold,
970
+ },
971
+ "Token estimate approaching budget — yielding for compaction",
972
+ );
973
+ exitReason = "budget";
974
+ break;
975
+ }
976
+ }
977
+ }
978
+ }
979
+
861
980
  // Resolve tools for this turn: use the dynamic resolver if provided,
862
981
  // otherwise fall back to the static tool list.
863
982
  const currentTools = this.resolveTools
@@ -921,12 +1040,14 @@ export class AgentLoop {
921
1040
  providerConfig.cacheTtl = this.config.cacheTtl;
922
1041
  }
923
1042
 
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) {
1043
+ // Cache-anchor signal for volatile latest-user-message turns: when
1044
+ // memory-v3 is live it injects a per-turn `<memory>` block into the
1045
+ // latest user message, so the provider must anchor its long-TTL cache
1046
+ // breakpoint on the most recent STABLE user message instead of the
1047
+ // volatile latest one. Read here alongside the rest of the provider
1048
+ // config; only set when true so the wire/config stays byte-identical
1049
+ // when off.
1050
+ if (isAssistantFeatureFlagEnabled("memory-v3-live", getConfig())) {
930
1051
  providerConfig.mutableLatestUserMessage = true;
931
1052
  }
932
1053
 
@@ -941,14 +1062,14 @@ export class AgentLoop {
941
1062
  providerConfig.callSite = callSite;
942
1063
  providerConfig.usageTracking = "manual";
943
1064
  // 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;
1065
+ // Sourced from the loop's own conversation id so every LLM call in a
1066
+ // conversation resolves the same mix arm (stable across turns and
1067
+ // retries, and across daemon restarts since the seed is the durable
1068
+ // conversation id). Absent for standalone `AgentLoop` instances
1069
+ // (unit tests constructed without a conversation id) — those fall
1070
+ // back to per-call random mix selection.
1071
+ if (this.conversationId) {
1072
+ providerConfig.selectionSeed = this.conversationId;
952
1073
  }
953
1074
  }
954
1075
 
@@ -1004,6 +1125,12 @@ export class AgentLoop {
1004
1125
  stripOldMediaBlocks(history),
1005
1126
  );
1006
1127
 
1128
+ // A `pre-model-call` hook (below) can defer this turn's assistant
1129
+ // output; when set, the live text stream is held so an
1130
+ // `assistant-message` hook can emit the finalized (transformed) text
1131
+ // instead. Reset per model call.
1132
+ let deferAssistantOutput = false;
1133
+
1007
1134
  // The `onEvent` wrapping below applies sensitive-output placeholder
1008
1135
  // substitution to streamed text while forwarding every other event
1009
1136
  // type through unchanged.
@@ -1013,6 +1140,9 @@ export class AgentLoop {
1013
1140
  config: providerConfig,
1014
1141
  onEvent: (event) => {
1015
1142
  if (event.type === "text_delta") {
1143
+ // Held when the turn's output is deferred — the final text is
1144
+ // emitted once, after the `assistant-message` hook runs.
1145
+ if (deferAssistantOutput) return;
1016
1146
  // Apply sensitive-output placeholder substitution (chunk-safe)
1017
1147
  if (substitutionMap.size > 0) {
1018
1148
  const combined = streamingPending + event.text;
@@ -1068,12 +1198,32 @@ export class AgentLoop {
1068
1198
  signal,
1069
1199
  };
1070
1200
 
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);
1201
+ // Let plugins edit the outbound request and opt this call into deferred
1202
+ // output streaming. Runs for every provider call; hooks self-gate on
1203
+ // call site / conversation. Fail-open: a throwing hook leaves the
1204
+ // request unchanged and streaming live.
1205
+ try {
1206
+ const preModelCtx: PreModelCallContext = {
1207
+ conversationId: this.conversationId,
1208
+ callSite,
1209
+ systemPrompt: providerOptions.systemPrompt,
1210
+ deferAssistantOutput: false,
1211
+ logger: rlog,
1212
+ };
1213
+ const finalPreModelCtx = await runHook(
1214
+ HOOKS.PRE_MODEL_CALL,
1215
+ preModelCtx,
1216
+ );
1217
+ providerOptions.systemPrompt = finalPreModelCtx.systemPrompt;
1218
+ // The hook owns the policy (it sees `callSite`/conversation and
1219
+ // self-gates); the loop honors whatever it decides.
1220
+ deferAssistantOutput = finalPreModelCtx.deferAssistantOutput;
1221
+ } catch (preModelCallError) {
1222
+ rlog.error(
1223
+ { err: preModelCallError },
1224
+ "pre-model-call hook failed — proceeding with the original request",
1225
+ );
1226
+ }
1077
1227
 
1078
1228
  // Announce the LLM-call boundary so downstream handlers (the
1079
1229
  // daemon's persistence pipeline) can reserve an empty assistant row
@@ -1161,12 +1311,52 @@ export class AgentLoop {
1161
1311
  streamingPending = "";
1162
1312
  }
1163
1313
 
1314
+ // Run the `assistant-message` hook on a finalized message and, when
1315
+ // output was deferred, emit the finalized text once (with sensitive-output
1316
+ // substitution applied, matching the live stream). Fail-open: the hook
1317
+ // receives a clone, so a throw — even mid in-place mutation — leaves the
1318
+ // original message intact.
1319
+ const finalizeAssistantMessage = async (
1320
+ message: Message,
1321
+ ): Promise<Message> => {
1322
+ let finalized = message;
1323
+ try {
1324
+ const ctx: AssistantMessageContext = {
1325
+ conversationId: this.conversationId,
1326
+ callSite,
1327
+ content: structuredClone(message.content),
1328
+ stopReason: response.stopReason,
1329
+ logger: rlog,
1330
+ };
1331
+ const result = await runHook(HOOKS.ASSISTANT_MESSAGE, ctx);
1332
+ finalized = { role: "assistant", content: result.content };
1333
+ } catch (assistantMessageError) {
1334
+ rlog.error(
1335
+ { err: assistantMessageError },
1336
+ "assistant-message hook failed — keeping the original content",
1337
+ );
1338
+ finalized = message;
1339
+ }
1340
+ if (deferAssistantOutput) {
1341
+ // The persisted message keeps sensitive-output placeholders; the
1342
+ // stream shows real values — substitute before emitting.
1343
+ const finalText = applySubstitutions(
1344
+ assistantTextOf(finalized.content),
1345
+ substitutionMap,
1346
+ );
1347
+ if (finalText.length > 0) {
1348
+ onEvent({ type: "text_delta", text: finalText });
1349
+ }
1350
+ }
1351
+ return finalized;
1352
+ };
1353
+
1164
1354
  // Build the assistant message with placeholder-only text.
1165
1355
  // Both provider history and persisted conversation store must retain
1166
1356
  // placeholders so the model never sees real sensitive values — neither
1167
1357
  // on subsequent loop turns nor on session reload from the database.
1168
1358
  // Substitution to real values happens only in streamed text_delta events.
1169
- const assistantMessage: Message = {
1359
+ let assistantMessage: Message = {
1170
1360
  role: "assistant",
1171
1361
  content: response.content,
1172
1362
  };
@@ -1195,10 +1385,6 @@ export class AgentLoop {
1195
1385
  block.type !== "server_tool_use" &&
1196
1386
  block.type !== "web_search_tool_result",
1197
1387
  );
1198
- const safeAssistantMessage: Message = {
1199
- role: "assistant",
1200
- content: safeContent,
1201
- };
1202
1388
  rlog.warn(
1203
1389
  {
1204
1390
  turn: toolUseTurns,
@@ -1209,6 +1395,13 @@ export class AgentLoop {
1209
1395
  },
1210
1396
  "LLM response reached output token limit",
1211
1397
  );
1398
+ // Run the hook on the truncated reply so output-filter plugins still
1399
+ // see it, and so a deferred turn gets its synthetic final emit (the
1400
+ // live stream was suppressed; without this the client would see nothing).
1401
+ const safeAssistantMessage = await finalizeAssistantMessage({
1402
+ role: "assistant",
1403
+ content: safeContent,
1404
+ });
1212
1405
  history.push(safeAssistantMessage);
1213
1406
  appendedNewMessages = true;
1214
1407
  await onEvent({
@@ -1241,7 +1434,7 @@ export class AgentLoop {
1241
1434
  // follow-up turn. It receives the full history and, when it asks to
1242
1435
  // continue, appends the follow-up turn itself.
1243
1436
  const stopCtx: StopContext = {
1244
- conversationId: turnCtx.conversationId,
1437
+ conversationId: this.conversationId,
1245
1438
  messages: [...history],
1246
1439
  responseContent: response.content,
1247
1440
  stopReason: response.stopReason,
@@ -1280,6 +1473,12 @@ export class AgentLoop {
1280
1473
  }
1281
1474
  }
1282
1475
 
1476
+ // Run the `assistant-message` hook + emit any deferred final text.
1477
+ // On a no-tool turn this point is reached only after the `stop` hook
1478
+ // resolves to "stop" (a `continue` already re-queried above), so a
1479
+ // re-queried reply is never transformed-then-discarded.
1480
+ assistantMessage = await finalizeAssistantMessage(assistantMessage);
1481
+
1283
1482
  history.push(assistantMessage);
1284
1483
  appendedNewMessages = true;
1285
1484
 
@@ -1430,7 +1629,7 @@ export class AgentLoop {
1430
1629
  continue;
1431
1630
  }
1432
1631
  const postToolUseCtx: PostToolUseContext = {
1433
- conversationId: turnCtx.conversationId,
1632
+ conversationId: this.conversationId,
1434
1633
  toolResponse: block as ToolResultContent,
1435
1634
  messages: history,
1436
1635
  maxInputTokens: contextWindowTokens,
@@ -1508,8 +1707,9 @@ export class AgentLoop {
1508
1707
  history.push({ role: "user", content: resultBlocks });
1509
1708
 
1510
1709
  // 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.
1710
+ // Handoff takes precedence over the budget gate: a handoff decision
1711
+ // breaks here and leaves `budgetGateArmed` false, so a queued message
1712
+ // is processed before the next iteration's pre-call budget gate.
1513
1713
  if (onCheckpoint) {
1514
1714
  const decision = await onCheckpoint({
1515
1715
  turnIndex: toolUseTurns - 1, // 0-based (toolUseTurns was already incremented)
@@ -1523,61 +1723,11 @@ export class AgentLoop {
1523
1723
  }
1524
1724
  }
1525
1725
 
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
- }
1726
+ // Arm the pre-call budget gate for the next iteration. Placed after
1727
+ // the checkpoint so a handoff yield (which breaks above) leaves it
1728
+ // disarmed; the gate then runs at the top of the next iteration,
1729
+ // before that iteration's provider call.
1730
+ budgetGateArmed = true;
1581
1731
  } catch (error) {
1582
1732
  // Abort errors are expected when user cancels — synthesize
1583
1733
  // cancellation tool_results so the history stays valid for the