@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea

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 (570) hide show
  1. package/Dockerfile +20 -4
  2. package/bun.lock +2 -2
  3. package/docker-entrypoint.sh +4 -2
  4. package/docker-init-apt-root.sh +3 -1
  5. package/docker-kata-apt-env.sh +3 -1
  6. package/docker-kata-runtime-family.sh +12 -0
  7. package/docs/architecture/memory.md +1 -1
  8. package/examples/plugins/echo/README.md +61 -66
  9. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  10. package/examples/plugins/echo/hooks/stop.ts +16 -0
  11. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  12. package/examples/plugins/echo/package.json +1 -2
  13. package/examples/plugins/echo/src/emit.ts +19 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  15. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  16. package/openapi.yaml +3378 -335
  17. package/package.json +2 -2
  18. package/scripts/generate-openapi.ts +68 -41
  19. package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  21. package/src/__tests__/agent-loop.test.ts +37 -87
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  23. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  24. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  25. package/src/__tests__/anthropic-provider.test.ts +95 -2
  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__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  30. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  31. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  32. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  33. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  34. package/src/__tests__/btw-routes.test.ts +62 -3
  35. package/src/__tests__/build-persisted-content.test.ts +184 -0
  36. package/src/__tests__/catalog-files.test.ts +1 -1
  37. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  38. package/src/__tests__/channel-approvals.test.ts +1 -1
  39. package/src/__tests__/clawhub-files.test.ts +1 -1
  40. package/src/__tests__/compaction-circuit.test.ts +258 -0
  41. package/src/__tests__/compaction-direct.test.ts +132 -0
  42. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  43. package/src/__tests__/config-watcher.test.ts +1 -1
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
  48. package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
  49. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  50. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  51. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  52. package/src/__tests__/conversation-pairing.test.ts +4 -31
  53. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
  55. package/src/__tests__/conversation-queue.test.ts +2 -0
  56. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  57. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  58. package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
  59. package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
  60. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  61. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  62. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  63. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  64. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  65. package/src/__tests__/conversation-title-service.test.ts +135 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
  67. package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  71. package/src/__tests__/db-acp-history.test.ts +101 -0
  72. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  73. package/src/__tests__/dm-persistence.test.ts +5 -1
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +304 -0
  76. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  77. package/src/__tests__/file-write-tool.test.ts +63 -0
  78. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  79. package/src/__tests__/gemini-image-service.test.ts +13 -0
  80. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  81. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  82. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  83. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  84. package/src/__tests__/heartbeat-service.test.ts +1 -0
  85. package/src/__tests__/helpers/mock-provider.ts +110 -0
  86. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  87. package/src/__tests__/history-repair-hook.test.ts +1 -0
  88. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  89. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  90. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  91. package/src/__tests__/identity-routes.test.ts +248 -7
  92. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  93. package/src/__tests__/injector-background-turn.test.ts +3 -9
  94. package/src/__tests__/injector-chain.test.ts +139 -275
  95. package/src/__tests__/injector-disk-pressure.test.ts +75 -41
  96. package/src/__tests__/injector-document-comments.test.ts +3 -3
  97. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  98. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  99. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  100. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  101. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  102. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  103. package/src/__tests__/llm-usage-store.test.ts +223 -1
  104. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  105. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  106. package/src/__tests__/native-web-search.test.ts +191 -0
  107. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  108. package/src/__tests__/openai-image-service.test.ts +17 -0
  109. package/src/__tests__/openai-provider.test.ts +31 -1
  110. package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
  111. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  112. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  113. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  114. package/src/__tests__/plugin-api-shim.test.ts +3 -6
  115. package/src/__tests__/plugin-bootstrap.test.ts +14 -40
  116. package/src/__tests__/plugin-registry.test.ts +3 -76
  117. package/src/__tests__/plugin-types.test.ts +0 -193
  118. package/src/__tests__/process-message-display-content.test.ts +6 -2
  119. package/src/__tests__/reaction-persistence.test.ts +1 -1
  120. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  121. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  122. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  123. package/src/__tests__/schedule-routes.test.ts +603 -2
  124. package/src/__tests__/schedule-store.test.ts +41 -0
  125. package/src/__tests__/schedule-tools.test.ts +35 -0
  126. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  127. package/src/__tests__/server-history-render.test.ts +314 -1
  128. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  129. package/src/__tests__/skillssh-files.test.ts +1 -1
  130. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  132. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  133. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  134. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  135. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  136. package/src/__tests__/system-prompt.test.ts +20 -0
  137. package/src/__tests__/task-scheduler.test.ts +162 -1
  138. package/src/__tests__/terminal-tools.test.ts +6 -1
  139. package/src/__tests__/title-generate-hook.test.ts +319 -0
  140. package/src/__tests__/tool-error-hook.test.ts +278 -0
  141. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  142. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  143. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  144. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  145. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  146. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  147. package/src/__tests__/usage-routes.test.ts +285 -1
  148. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  149. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  150. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  151. package/src/acp/__tests__/agent-process.test.ts +161 -0
  152. package/src/acp/__tests__/client-handler.test.ts +40 -0
  153. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  154. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  155. package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
  156. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  157. package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
  158. package/src/acp/agent-process.ts +61 -1
  159. package/src/acp/auto-install.test.ts +196 -0
  160. package/src/acp/auto-install.ts +177 -0
  161. package/src/acp/client-handler.ts +31 -0
  162. package/src/acp/feature-gate.test.ts +48 -0
  163. package/src/acp/feature-gate.ts +34 -0
  164. package/src/acp/prepare-agent-env.ts +83 -29
  165. package/src/acp/resolve-agent.test.ts +320 -7
  166. package/src/acp/resolve-agent.ts +182 -18
  167. package/src/acp/resume-hint.ts +25 -0
  168. package/src/acp/session-manager.ts +495 -73
  169. package/src/acp/types.ts +8 -0
  170. package/src/agent/compaction-circuit.ts +60 -102
  171. package/src/agent/loop.ts +362 -485
  172. package/src/api/events/assistant-thinking-delta.ts +33 -0
  173. package/src/api/events/tool-output-chunk.ts +45 -0
  174. package/src/api/events/tool-use-preview-start.ts +32 -0
  175. package/src/api/events/trace-event.ts +69 -0
  176. package/src/api/index.ts +48 -13
  177. package/src/api/responses/conversation-message.ts +374 -0
  178. package/src/approvals/guardian-request-resolvers.ts +1 -1
  179. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  180. package/src/background-wake/next-wake.ts +1 -0
  181. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  182. package/src/cli/commands/notifications.ts +112 -60
  183. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  184. package/src/config/acp-defaults.test.ts +10 -0
  185. package/src/config/acp-defaults.ts +6 -0
  186. package/src/config/assistant-feature-flags.ts +22 -11
  187. package/src/config/bundled-skills/acp/SKILL.md +83 -31
  188. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  189. package/src/config/bundled-skills/app-builder/SKILL.md +224 -398
  190. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  191. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  192. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  193. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  194. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  195. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  196. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  197. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  198. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  199. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  200. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  201. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  202. package/src/config/bundled-tool-registry.ts +2 -0
  203. package/src/config/feature-flag-cache.ts +3 -3
  204. package/src/config/feature-flag-registry.json +48 -7
  205. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  207. package/src/config/schemas/heartbeat.ts +9 -0
  208. package/src/config/schemas/llm.ts +1 -0
  209. package/src/config/schemas/memory-v2.ts +8 -0
  210. package/src/config/schemas/memory-v3.ts +8 -0
  211. package/src/config/schemas/platform.ts +8 -0
  212. package/src/config/seed-inference-profiles.ts +2 -2
  213. package/src/config/skills.ts +13 -0
  214. package/src/context/compactor.ts +1 -1
  215. package/src/context/strip-injections.ts +128 -0
  216. package/src/context/token-estimator.ts +23 -0
  217. package/src/context/tool-result-truncation.ts +0 -23
  218. package/src/context/window-manager.ts +5 -7
  219. package/src/credential-execution/executable-discovery.ts +16 -0
  220. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  221. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  222. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  223. package/src/daemon/assistant-attachments.ts +1 -1
  224. package/src/daemon/config-watcher.ts +2 -2
  225. package/src/daemon/context-overflow-reducer.ts +0 -1
  226. package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
  227. package/src/daemon/conversation-agent-loop.ts +301 -997
  228. package/src/daemon/conversation-history.ts +5 -4
  229. package/src/daemon/conversation-lifecycle.ts +3 -4
  230. package/src/daemon/conversation-messaging.ts +7 -6
  231. package/src/daemon/conversation-process.ts +11 -16
  232. package/src/daemon/conversation-registry.ts +159 -0
  233. package/src/daemon/conversation-runtime-assembly.ts +218 -398
  234. package/src/daemon/conversation-slash.ts +6 -25
  235. package/src/daemon/conversation-store.ts +9 -90
  236. package/src/daemon/conversation-surfaces.ts +222 -4
  237. package/src/daemon/conversation-tool-setup.ts +2 -29
  238. package/src/daemon/conversation-workspace.ts +17 -0
  239. package/src/daemon/conversation.ts +32 -20
  240. package/src/daemon/external-plugins-bootstrap.ts +17 -18
  241. package/src/daemon/handlers/config-a2a.ts +51 -36
  242. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  243. package/src/daemon/handlers/config-telegram.ts +16 -2
  244. package/src/daemon/handlers/conversations.ts +3 -1
  245. package/src/daemon/handlers/shared.ts +156 -84
  246. package/src/daemon/handlers/skills.ts +42 -10
  247. package/src/daemon/lifecycle.ts +25 -0
  248. package/src/daemon/message-types/apps.ts +1 -29
  249. package/src/daemon/message-types/messages.ts +9 -57
  250. package/src/daemon/message-types/skills.ts +2 -0
  251. package/src/daemon/message-types/surfaces.ts +136 -3
  252. package/src/daemon/now-scratchpad.ts +21 -0
  253. package/src/daemon/orphan-reaper.test.ts +210 -0
  254. package/src/daemon/orphan-reaper.ts +240 -0
  255. package/src/daemon/overflow-reduction-loop.ts +230 -0
  256. package/src/daemon/persist-unsendable-image.ts +117 -0
  257. package/src/daemon/process-message.ts +1 -3
  258. package/src/daemon/server.ts +2 -0
  259. package/src/daemon/trace-emitter.ts +6 -4
  260. package/src/daemon/trust-context.ts +19 -0
  261. package/src/daemon/wake-target-adapter.ts +3 -1
  262. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  263. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  264. package/src/heartbeat/heartbeat-service.ts +26 -0
  265. package/src/home/home-greeting-cache.ts +24 -1
  266. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  267. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  268. package/src/ipc/gateway-client.test.ts +2 -2
  269. package/src/ipc/gateway-client.ts +3 -3
  270. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  271. package/src/ipc/skill-routes/memory.ts +4 -2
  272. package/src/media/gemini-image-service.ts +15 -0
  273. package/src/media/openai-image-service.ts +14 -0
  274. package/src/media/types.ts +34 -0
  275. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  276. package/src/memory/auth-fallback-events-store.ts +94 -0
  277. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  278. package/src/memory/conversation-title-service.ts +65 -41
  279. package/src/memory/db-init.ts +6 -0
  280. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  281. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  282. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  283. package/src/memory/jobs-store.ts +33 -0
  284. package/src/memory/jobs-worker.ts +32 -5
  285. package/src/memory/llm-usage-store.ts +224 -50
  286. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  287. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  288. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  289. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  290. package/src/memory/migrations/index.ts +3 -0
  291. package/src/memory/pkb/autoinject.ts +61 -0
  292. package/src/memory/pkb/context.ts +50 -0
  293. package/src/memory/pkb/types.ts +14 -0
  294. package/src/memory/schedule-attribution-sql.ts +104 -0
  295. package/src/memory/schema/acp.ts +4 -0
  296. package/src/memory/schema/infrastructure.ts +16 -0
  297. package/src/memory/usage-grouped-buckets.ts +6 -1
  298. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
  299. package/src/memory/v2/consolidation-job.ts +14 -5
  300. package/src/notifications/conversation-pairing.ts +8 -15
  301. package/src/notifications/decision-engine.ts +6 -3
  302. package/src/notifications/home-feed-side-effect.ts +12 -1
  303. package/src/permissions/prompter.ts +4 -0
  304. package/src/plugin-api/constants.ts +4 -0
  305. package/src/plugin-api/index.ts +7 -5
  306. package/src/plugin-api/types.ts +151 -1
  307. package/src/plugins/defaults/compaction/compact.ts +59 -0
  308. package/src/plugins/defaults/compaction/package.json +1 -1
  309. package/src/plugins/defaults/compaction/register.ts +8 -19
  310. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  311. package/src/plugins/defaults/empty-response/register.ts +8 -13
  312. package/src/plugins/defaults/index.ts +2 -18
  313. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
  314. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  315. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  316. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
  317. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  318. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +16 -0
  319. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  321. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +48 -12
  322. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
  323. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  324. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  325. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +104 -32
  326. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +96 -30
  328. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
  329. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
  330. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
  331. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  332. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  333. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  334. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  335. package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
  336. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
  337. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
  338. package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
  339. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  340. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
  341. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
  342. package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
  343. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  344. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +51 -45
  345. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  346. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +61 -46
  347. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
  348. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  349. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
  350. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  351. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  352. package/src/plugins/defaults/title-generate/package.json +1 -1
  353. package/src/plugins/defaults/title-generate/register.ts +18 -18
  354. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  355. package/src/plugins/defaults/tool-error/package.json +1 -1
  356. package/src/plugins/defaults/tool-error/register.ts +9 -21
  357. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  358. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  359. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  360. package/src/plugins/external-api.ts +2 -2
  361. package/src/plugins/pipeline.ts +6 -305
  362. package/src/plugins/registry.ts +10 -55
  363. package/src/plugins/types.ts +62 -797
  364. package/src/plugins/user-loader.ts +30 -127
  365. package/src/proactive-artifact/aux-message-injector.ts +4 -4
  366. package/src/proactive-artifact/job.test.ts +8 -13
  367. package/src/prompts/__tests__/system-prompt.test.ts +42 -0
  368. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
  369. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  370. package/src/prompts/templates/system-sections.ts +15 -0
  371. package/src/providers/anthropic/client.ts +37 -29
  372. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  373. package/src/providers/openai/chat-completions-provider.ts +44 -0
  374. package/src/providers/openrouter/client.ts +1 -0
  375. package/src/providers/placeholder-sentinels.ts +35 -0
  376. package/src/runtime/__tests__/agent-wake.test.ts +10 -6
  377. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  378. package/src/runtime/agent-wake.ts +2 -5
  379. package/src/runtime/assistant-event-hub.ts +37 -7
  380. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  381. package/src/runtime/channel-approvals.ts +1 -1
  382. package/src/runtime/http-router.ts +16 -21
  383. package/src/runtime/http-types.ts +16 -70
  384. package/src/runtime/interactive-ui.ts +1 -1
  385. package/src/runtime/pending-interactions.ts +1 -0
  386. package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
  387. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  388. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  389. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  390. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  391. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  392. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  393. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  394. package/src/runtime/routes/acp-routes.test.ts +89 -25
  395. package/src/runtime/routes/acp-routes.ts +81 -29
  396. package/src/runtime/routes/app-management-routes.ts +6 -117
  397. package/src/runtime/routes/app-routes.ts +13 -15
  398. package/src/runtime/routes/approval-routes.ts +1 -1
  399. package/src/runtime/routes/attachment-routes.ts +26 -15
  400. package/src/runtime/routes/avatar-routes.ts +26 -0
  401. package/src/runtime/routes/browser-routes.ts +1 -1
  402. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  403. package/src/runtime/routes/btw-routes.ts +29 -23
  404. package/src/runtime/routes/consolidation-routes.ts +120 -20
  405. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  406. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  407. package/src/runtime/routes/conversation-query-routes.ts +3 -1
  408. package/src/runtime/routes/conversation-routes.ts +372 -185
  409. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  410. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  411. package/src/runtime/routes/documents-routes.ts +4 -0
  412. package/src/runtime/routes/domain-routes.ts +51 -37
  413. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  414. package/src/runtime/routes/events-routes.ts +28 -34
  415. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  416. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  417. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  418. package/src/runtime/routes/host-cu-routes.ts +1 -1
  419. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  420. package/src/runtime/routes/identity-routes.ts +224 -18
  421. package/src/runtime/routes/image-generation-routes.ts +40 -2
  422. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  423. package/src/runtime/routes/index.ts +2 -0
  424. package/src/runtime/routes/integrations/a2a.ts +12 -10
  425. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  426. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  427. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  428. package/src/runtime/routes/integrations/telegram.ts +6 -0
  429. package/src/runtime/routes/integrations/twilio.ts +42 -0
  430. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  431. package/src/runtime/routes/log-export-routes.ts +8 -0
  432. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  433. package/src/runtime/routes/memory-v3-routes.ts +66 -34
  434. package/src/runtime/routes/oauth-apps.ts +66 -12
  435. package/src/runtime/routes/oauth-providers.ts +44 -5
  436. package/src/runtime/routes/platform-routes.ts +81 -5
  437. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  438. package/src/runtime/routes/playground/force-compact.ts +1 -1
  439. package/src/runtime/routes/playground/helpers.ts +1 -1
  440. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  441. package/src/runtime/routes/schedule-routes.ts +152 -42
  442. package/src/runtime/routes/secret-routes.ts +14 -2
  443. package/src/runtime/routes/skills-routes.ts +43 -14
  444. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  445. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  446. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  447. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  448. package/src/runtime/routes/tts-routes.ts +35 -0
  449. package/src/runtime/routes/types.ts +66 -8
  450. package/src/runtime/routes/usage-routes.ts +47 -39
  451. package/src/runtime/routes/webhook-routes.ts +41 -2
  452. package/src/runtime/routes/work-items-routes.ts +2 -4
  453. package/src/runtime/routes/workspace-routes.ts +4 -0
  454. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  455. package/src/runtime/services/analyze-conversation.ts +2 -2
  456. package/src/runtime/services/conversation-serializer.ts +1 -1
  457. package/src/schedule/schedule-store.ts +20 -1
  458. package/src/schedule/schedule-usage-store.ts +83 -0
  459. package/src/schedule/scheduler.ts +12 -5
  460. package/src/signals/cancel.ts +2 -4
  461. package/src/skills/catalog-files.ts +2 -2
  462. package/src/skills/catalog-install.ts +3 -0
  463. package/src/skills/categories-cache.ts +118 -0
  464. package/src/skills/clawhub-files.ts +1 -2
  465. package/src/skills/skillssh-files.ts +1 -2
  466. package/src/subagent/manager.ts +17 -5
  467. package/src/telemetry/types.ts +29 -1
  468. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  469. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  470. package/src/tools/acp/context.ts +20 -0
  471. package/src/tools/acp/list-agents.test.ts +7 -1
  472. package/src/tools/acp/spawn.test.ts +158 -55
  473. package/src/tools/acp/spawn.ts +47 -72
  474. package/src/tools/acp/steer.test.ts +105 -8
  475. package/src/tools/acp/steer.ts +48 -17
  476. package/src/tools/apps/executors.ts +13 -8
  477. package/src/tools/executor.ts +1 -53
  478. package/src/tools/filesystem/write.ts +34 -0
  479. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  480. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  481. package/src/tools/network/web-search-error.test.ts +248 -0
  482. package/src/tools/network/web-search-error.ts +267 -0
  483. package/src/tools/network/web-search.ts +207 -48
  484. package/src/tools/schedule/create.ts +2 -0
  485. package/src/tools/subagent/spawn.ts +2 -4
  486. package/src/tools/terminal/safe-env.ts +10 -1
  487. package/src/tools/ui-surface/definitions.ts +34 -5
  488. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  489. package/src/tts/provider-catalog.ts +76 -1
  490. package/src/util/mutex.ts +47 -0
  491. package/src/workspace/git-service.ts +1 -42
  492. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  493. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  494. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  495. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
  496. package/src/workspace/migrations/registry.ts +6 -0
  497. package/docs/plugins.md +0 -836
  498. package/examples/plugins/echo/register.ts +0 -184
  499. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  500. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  501. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  502. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  503. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  504. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  505. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  506. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  507. package/src/__tests__/pipeline-runner.test.ts +0 -564
  508. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  509. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  510. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  511. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  512. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  513. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  514. package/src/gallery/default-gallery.ts +0 -1359
  515. package/src/gallery/gallery-manifest.ts +0 -28
  516. package/src/home/feature-gate.ts +0 -22
  517. package/src/memory/v3/provider-blocks.ts +0 -16
  518. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  519. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  520. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  521. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  522. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  523. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  524. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  525. package/src/plugins/defaults/injectors/package.json +0 -15
  526. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  527. package/src/plugins/defaults/llm-call/register.ts +0 -45
  528. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  529. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  530. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  531. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  532. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  533. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  534. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  535. package/src/plugins/defaults/persistence/package.json +0 -15
  536. package/src/plugins/defaults/persistence/register.ts +0 -38
  537. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  538. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  539. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  540. package/src/plugins/defaults/token-estimate/package.json +0 -15
  541. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  542. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  543. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  544. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  545. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  546. package/src/plugins/defaults/tool-execute/package.json +0 -15
  547. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  548. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  549. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  550. package/src/skills/category-inference.ts +0 -111
  551. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  552. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  553. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  554. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  555. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  556. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  557. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  558. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +0 -0
  559. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  560. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  561. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  562. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  563. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  564. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  565. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  566. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  567. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  568. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  569. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  570. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
package/src/agent/loop.ts CHANGED
@@ -1,45 +1,32 @@
1
1
  import * as Sentry from "@sentry/node";
2
2
 
3
3
  import type { LLMCallSite } from "../config/schemas/llm.js";
4
+ import { stripInjectionsForCompaction } from "../context/strip-injections.js";
4
5
  import {
5
6
  estimatePromptTokensRaw,
7
+ estimatePromptTokensWithTools,
6
8
  estimateToolsTokens,
7
9
  getCalibrationProviderKey,
8
10
  } from "../context/token-estimator.js";
9
- import { calculateMaxToolResultChars } from "../context/tool-result-truncation.js";
10
11
  import type { ContextWindowResult } from "../context/window-manager.js";
11
12
  import type { ToolActivityMetadata } from "../daemon/message-types/web-activity.js";
12
- import { defaultCompactionTerminal } from "../plugins/defaults/compaction/terminal.js";
13
- import { defaultEmptyResponseTerminal } from "../plugins/defaults/empty-response/terminal.js";
14
- import { defaultTokenEstimateTerminal } from "../plugins/defaults/token-estimate/terminal.js";
15
- import { defaultToolErrorTerminal } from "../plugins/defaults/tool-error/terminal.js";
16
- import { defaultToolResultTruncateTerminal } from "../plugins/defaults/tool-result-truncate/terminal.js";
17
- import type {
18
- ToolResultTruncateArgs,
19
- ToolResultTruncateResult,
20
- } from "../plugins/defaults/tool-result-truncate/types.js";
21
- import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
22
- import { getMiddlewaresFor } from "../plugins/registry.js";
23
- import type {
24
- CompactionArgs,
25
- CompactionCircuitEvent,
26
- CompactionResult,
27
- EmptyResponseArgs,
28
- EmptyResponseDecision,
29
- EstimateArgs,
30
- EstimateResult,
31
- LLMCallArgs,
32
- LLMCallResult,
33
- ToolErrorArgs,
34
- ToolErrorDecision,
35
- TurnContext,
36
- } from "../plugins/types.js";
37
- import { PluginTimeoutError } from "../plugins/types.js";
13
+ import { HOOKS } from "../plugin-api/constants.js";
14
+ import type { PostToolUseContext, StopContext } from "../plugin-api/types.js";
15
+ import {
16
+ DEFAULT_COMPACTION_PLUGIN_NAME,
17
+ defaultCompact,
18
+ } from "../plugins/defaults/compaction/compact.js";
19
+ import type { PostCompactionHookInput } from "../plugins/defaults/memory-retrieval/hooks/post-compact.js";
20
+ import { runHook } from "../plugins/pipeline.js";
21
+ import type { CompactionCircuitEvent, TurnContext } from "../plugins/types.js";
22
+ import { PluginExecutionError } from "../plugins/types.js";
38
23
  import { normalizeThinkingConfigForWire } from "../providers/thinking-config.js";
39
24
  import type {
40
25
  ContentBlock,
41
26
  Message,
42
27
  Provider,
28
+ ProviderResponse,
29
+ SendMessageOptions,
43
30
  ToolDefinition,
44
31
  ToolResultContent,
45
32
  } from "../providers/types.js";
@@ -48,7 +35,7 @@ import {
48
35
  applyStreamingSubstitution,
49
36
  applySubstitutions,
50
37
  } from "../tools/sensitive-output-placeholders.js";
51
- import { AssistantError, ErrorCode, ProviderError } from "../util/errors.js";
38
+ import { ProviderError } from "../util/errors.js";
52
39
  import { getLogger } from "../util/logger.js";
53
40
  import { isRetryableNetworkError } from "../util/retry.js";
54
41
  import { CompactionCircuit } from "./compaction-circuit.js";
@@ -95,17 +82,28 @@ export type ExitReason = "handoff" | "budget";
95
82
 
96
83
  export type CheckpointDecision = "continue" | ExitReason;
97
84
 
98
- /**
99
- * Result of {@link AgentLoop.run}.
100
- *
101
- * `exitReason` carries the reason the loop paused at a checkpoint so the
102
- * orchestrator reads the loop's own signal instead of inferring it from
103
- * callback side-effects. It is `null` whenever the loop reached a terminal
104
- * stop (completion, error, abort, or a tool-requested yield-to-user).
105
- */
85
+ /** Result of {@link AgentLoop.run}. */
106
86
  export interface AgentLoopRunResult {
87
+ /** Full conversation history after the run, including everything appended this run. */
107
88
  history: Message[];
89
+ /**
90
+ * Reason the loop paused at a checkpoint, or `null` on a terminal stop
91
+ * (completion, error, abort, or a tool-requested yield-to-user).
92
+ */
108
93
  exitReason: ExitReason | null;
94
+ /**
95
+ * Whether the loop produced at least one new assistant message this run —
96
+ * the forward-progress signal for the ordering-error retry gate and the
97
+ * overflow convergence fold (immune to in-loop compaction shrinking history
98
+ * below a pre-run length).
99
+ */
100
+ appendedNewMessages: boolean;
101
+ /**
102
+ * Slice of `history` appended this run, measured from the loop's input or
103
+ * from the compacted base when it compacts in place. The loop owns this
104
+ * boundary, so it cannot desync the way an externally-held index can.
105
+ */
106
+ newMessages: Message[];
109
107
  }
110
108
 
111
109
  /**
@@ -129,8 +127,6 @@ export interface AgentLoopRunResult {
129
127
  export type AgentLoopExitReason =
130
128
  /** `if (signal?.aborted) break;` at the top of the loop. */
131
129
  | "aborted_pre_call"
132
- /** Empty assistant response after the configured retry budget. */
133
- | "empty_response_exhausted"
134
130
  /** Assistant message has no tool-use blocks (or no tool executor). */
135
131
  | "no_tool_calls"
136
132
  /** Signal aborted while building the user-side tool-results message. */
@@ -209,6 +205,14 @@ export type AgentEvent =
209
205
  approvalReason?: string;
210
206
  riskThreshold?: string;
211
207
  activityMetadata?: ToolActivityMetadata;
208
+ /**
209
+ * Set when the loop synthesizes this result for a tool_use that never
210
+ * executed (a "Cancelled by user" block on abort). The daemon still
211
+ * captures it into `pendingToolResults` and forwards it to the client,
212
+ * but skips the side effects that assume the tool ran — marking the
213
+ * workspace dirty and emitting a post-tool "thinking" activity state.
214
+ */
215
+ cancelled?: boolean;
212
216
  }
213
217
  | { type: "tool_use_preview_start"; toolUseId: string; toolName: string }
214
218
  | {
@@ -243,7 +247,7 @@ export type AgentEvent =
243
247
  | { type: "error"; error: Error }
244
248
  | {
245
249
  /**
246
- * Emitted when the `llmCall` pipeline throws — i.e. the provider
250
+ * Emitted when the provider call throws — i.e. the provider
247
251
  * rejected the request before returning a usable response. Carries
248
252
  * the loop-level raw request we attempted to send (messages, tools,
249
253
  * system prompt, provider-agnostic config) plus the thrown error.
@@ -295,6 +299,42 @@ export type AgentEvent =
295
299
  */
296
300
  type: "context_compacting";
297
301
  }
302
+ | {
303
+ /**
304
+ * Emitted after the loop's inline mid-loop compaction pipeline runs,
305
+ * immediately before re-injection — whether or not the pipeline actually
306
+ * compacted. The daemon's event dispatcher always commits `basis` (the
307
+ * stripped pre-compaction history) as the conversation's durable message
308
+ * state, so re-injection ({@link MidLoopCompaction.reinject}) re-applies
309
+ * injections onto the stripped base rather than stacking on top of the
310
+ * still-injected messages. When `result.compacted` is set it
311
+ * additionally commits the durable compaction result (DB-record fields,
312
+ * graph-memory side effects, SSE) and flips the per-turn re-injection
313
+ * guards on the handler state.
314
+ *
315
+ * Treated as a critical event: a failed durable commit re-throws so the
316
+ * turn aborts rather than re-injecting against half-applied state.
317
+ *
318
+ * `basis` is the stripped pre-compaction history the summary was built
319
+ * from; the dispatcher uses it to project Slack provenance onto the
320
+ * compacted result.
321
+ */
322
+ type: "compaction_completed";
323
+ result: ContextWindowResult;
324
+ basis: Message[];
325
+ }
326
+ | {
327
+ /**
328
+ * Emitted right after the loop strips runtime injections from the
329
+ * running history, before the compaction pipeline runs. The daemon's
330
+ * event dispatcher records the history-stripped marker — a Conversation
331
+ * DB-record field read back at load time to strip embedded injection
332
+ * prefixes from pre-strip messages. Best-effort: a transient marker
333
+ * write must not abort the turn, so unlike `compaction_completed` this
334
+ * event is not treated as critical.
335
+ */
336
+ type: "history_stripped";
337
+ }
298
338
  /**
299
339
  * Circuit-breaker transitions emitted when auto-compaction is paused
300
340
  * (`compaction_circuit_open`, after three consecutive summary-LLM
@@ -324,8 +364,7 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
324
364
  minTurnIntervalMs: 150,
325
365
  };
326
366
 
327
- const MAX_CONSECUTIVE_ERROR_NUDGES = 3;
328
- const MAX_EMPTY_RESPONSE_RETRIES = 1;
367
+ const MAX_STOP_CONTINUE_RETRIES = 1;
329
368
  const MAX_TOKENS_STOP_REASONS = new Set([
330
369
  "length",
331
370
  "max_output_tokens",
@@ -346,12 +385,11 @@ export function isMaxTokensStopReason(
346
385
  * {@link AgentLoop.run}); this helper is the fallback used only by unit
347
386
  * tests that construct `AgentLoop` directly without an orchestrator.
348
387
  *
349
- * When the orchestrator-supplied context is present, {@link resolveLoopTurnContext}
350
- * is used instead of this helper so the pipeline sees the real
351
- * `conversationId`, trust, and `contextWindowManager`. In the fallback path
352
- * the returned context is still useful for pipeline logging: `requestId`
353
- * surfaces in every structured record, and `turnIndex` reflects the
354
- * current tool-use iteration.
388
+ * When the orchestrator-supplied context is present it is used directly so the
389
+ * pipeline sees the real `conversationId`, trust, and `contextWindowManager`.
390
+ * In the fallback path the returned context is still useful for pipeline
391
+ * logging: `requestId` surfaces in every structured record, and `turnIndex`
392
+ * reflects the current tool-use iteration.
355
393
  */
356
394
  function buildLoopTurnContext(
357
395
  requestId: string | undefined,
@@ -371,29 +409,6 @@ function buildLoopTurnContext(
371
409
  };
372
410
  }
373
411
 
374
- /**
375
- * Produce a `TurnContext` for a pipeline call inside {@link AgentLoop.run}.
376
- *
377
- * When the orchestrator supplied a `turnContext`, clone it and overwrite
378
- * `requestId` + `turnIndex` with the loop-scoped values so plugin log
379
- * records correctly attribute the call to the current tool-use iteration
380
- * while preserving the real `conversationId`, trust context, and
381
- * `contextWindowManager` the orchestrator assembled for the turn. Without
382
- * an orchestrator context (unit tests that instantiate `AgentLoop` with no
383
- * `turnContext`), fall back to {@link buildLoopTurnContext}'s synthesized
384
- * placeholder.
385
- */
386
- function resolveLoopTurnContext(
387
- base: TurnContext | undefined,
388
- requestId: string | undefined,
389
- turnIndex: number,
390
- ): TurnContext {
391
- if (base) {
392
- return { ...base, requestId: requestId ?? base.requestId, turnIndex };
393
- }
394
- return buildLoopTurnContext(requestId, turnIndex);
395
- }
396
-
397
412
  /**
398
413
  * User-config HTTP status codes that should never page the on-call: billing
399
414
  * exhaustion (402), invalid credentials (401), and forbidden/plan-gated (403).
@@ -437,28 +452,25 @@ export interface ResolvedSystemPrompt {
437
452
  }
438
453
 
439
454
  /**
440
- * Orchestrator-supplied hooks the loop invokes when the mid-loop budget gate
455
+ * Orchestrator-supplied hook the loop invokes when the mid-loop budget gate
441
456
  * trips and inline compaction runs. The loop owns the trigger, the
442
- * `compaction` pipeline call, the result interpretation (circuit-breaker
443
- * bookkeeping + the exhaustion decision), and the inline continue; these hooks
444
- * bridge the durable / injection state the loop is intentionally blind to.
445
- * Durable persistence ({@link applyResult}) and re-injection
446
- * ({@link reinject}) remain orchestrator-supplied for now and are expected to
447
- * move into the loop in a future change.
457
+ * compaction call, the result interpretation (circuit-breaker
458
+ * bookkeeping + the exhaustion decision), and the inline continue; this hook
459
+ * bridges the injection state the loop is intentionally blind to. Durable
460
+ * persistence is signalled out-of-band via the `history_stripped` (marker)
461
+ * and `compaction_completed` (basis commit + successful summary) {@link
462
+ * AgentEvent}s; the {@link MidLoopCompaction.postCompactionHook} is
463
+ * orchestrator-supplied, and its inputs migrate loop-ward as the loop
464
+ * subsumes the re-injection ceremony.
448
465
  */
449
466
  export interface MidLoopCompaction {
450
- /** Strip runtime injections, commit stripped messages, and resolve pipeline options. */
451
- prepare: (history: Message[]) => {
452
- rawHistory: Message[];
453
- options: CompactionArgs["options"];
454
- };
455
- /** Commit a successful compaction result to durable state. */
456
- applyResult: (
457
- result: ContextWindowResult,
458
- rawHistory: Message[],
459
- ) => Promise<void>;
460
- /** Re-apply runtime injections and return the history to continue from. */
461
- reinject: () => Promise<Message[]>;
467
+ /**
468
+ * Re-apply runtime injections onto the post-compaction history and return
469
+ * the history to continue from. The loop supplies its own working state via
470
+ * {@link PostCompactionHookInput} so the hook re-injects from that rather
471
+ * than reading it back from orchestrator state.
472
+ */
473
+ postCompactionHook: (input: PostCompactionHookInput) => Promise<Message[]>;
462
474
  }
463
475
 
464
476
  export interface AgentLoopRunOptions {
@@ -518,21 +530,12 @@ export interface AgentLoopRunOptions {
518
530
 
519
531
  /**
520
532
  * Callback shape the loop uses to execute a tool invocation.
521
- *
522
- * The trailing `turnContext` is optional so in-process tests that wire the
523
- * callback without an orchestrator keep working. Production sites (the
524
- * `Conversation`'s `createToolExecutor`) forward the supplied context into
525
- * `ToolExecutor.execute` so the `toolExecute` pipeline sees the orchestrator's
526
- * real conversation identity/trust/contextWindowManager instead of the
527
- * synthesized placeholder `ToolExecutor` would otherwise build from the
528
- * `ToolContext` alone.
529
533
  */
530
534
  export type LoopToolExecutor = (
531
535
  name: string,
532
536
  input: Record<string, unknown>,
533
537
  onOutput?: (chunk: string) => void,
534
538
  toolUseId?: string,
535
- turnContext?: TurnContext,
536
539
  ) => Promise<{
537
540
  content: string;
538
541
  isError: boolean;
@@ -624,10 +627,9 @@ export class AgentLoop {
624
627
  * Resolve the tool definitions sent to the provider for the given turn.
625
628
  *
626
629
  * Mirrors the logic of {@link getToolTokenBudget} but returns the tool
627
- * array itself — callers that need to thread the tool set into a plugin
628
- * pipeline (e.g. `tokenEstimate`, where the pipeline's args include
629
- * `tools`) use this rather than re-implementing the dynamic-vs-static
630
- * resolver fork.
630
+ * array itself — callers that need to thread the tool set into the token
631
+ * estimate (`estimatePromptTokensWithTools`, whose args include `tools`)
632
+ * use this rather than re-implementing the dynamic-vs-static resolver fork.
631
633
  */
632
634
  getResolvedTools(history?: Message[]): ToolDefinition[] {
633
635
  return history && this.resolveTools
@@ -648,28 +650,15 @@ export class AgentLoop {
648
650
  }
649
651
 
650
652
  /**
651
- * Estimate total prompt tokens for `history` via the `tokenEstimate`
652
- * pipeline. Args are shallow-frozen so a mutating middleware cannot strip
653
- * context from the loop's live `history`.
653
+ * Calibrated prompt-token estimate for `history`, including the
654
+ * resolved-tool budget for the turn.
654
655
  */
655
- private estimateTokens(
656
- history: Message[],
657
- turnContext: TurnContext,
658
- ): Promise<EstimateResult> {
659
- return runPipeline<EstimateArgs, EstimateResult>(
660
- "tokenEstimate",
661
- getMiddlewaresFor("tokenEstimate"),
662
- defaultTokenEstimateTerminal,
663
- {
664
- history: Object.freeze([...history]) as Message[],
665
- systemPrompt: this.systemPrompt,
666
- tools: Object.freeze([
667
- ...this.getResolvedTools(history),
668
- ]) as ToolDefinition[],
669
- providerName: getCalibrationProviderKey(this.provider),
670
- },
671
- turnContext,
672
- DEFAULT_TIMEOUTS.tokenEstimate,
656
+ private estimateTokens(history: Message[]): number {
657
+ return estimatePromptTokensWithTools(
658
+ history,
659
+ this.systemPrompt,
660
+ this.getResolvedTools(history),
661
+ getCalibrationProviderKey(this.provider),
673
662
  );
674
663
  }
675
664
 
@@ -688,15 +677,7 @@ export class AgentLoop {
688
677
  onEvent: (event: AgentEvent) => void | Promise<void>,
689
678
  ): Promise<void> {
690
679
  try {
691
- await this.compactionCircuit.recordOutcome(
692
- {
693
- currentRequestId: turnContext.requestId,
694
- currentTurnTrustContext: turnContext.trust,
695
- turnCount: turnContext.turnIndex,
696
- },
697
- summaryFailed,
698
- onEvent,
699
- );
680
+ await this.compactionCircuit.recordOutcome(summaryFailed, onEvent);
700
681
  } catch (recordError) {
701
682
  log.error(
702
683
  { err: recordError, requestId: turnContext.requestId },
@@ -708,11 +689,10 @@ export class AgentLoop {
708
689
  /**
709
690
  * Compact the running history in place when the mid-loop budget gate trips.
710
691
  *
711
- * Runs the `compaction` pipeline natively (like {@link estimateTokens}) on
712
- * the stripped history, then re-applies injections via the supplied hooks.
713
- * Returns the history to continue from, or `null` when the compactor timed
714
- * out or exhausted its retry budget so the caller yields
715
- * `exitReason = "budget"` and the orchestrator escalates.
692
+ * Calls the default compaction plugin on the stripped history, then
693
+ * re-applies injections via the supplied hooks. Returns the history to
694
+ * continue from, or `null` when the compactor exhausted its retry budget so
695
+ * the caller yields `exitReason = "budget"` and the orchestrator escalates.
716
696
  */
717
697
  private async compact(
718
698
  history: Message[],
@@ -720,32 +700,37 @@ export class AgentLoop {
720
700
  compaction: MidLoopCompaction,
721
701
  signal: AbortSignal | undefined,
722
702
  onEvent: (event: AgentEvent) => void | Promise<void>,
703
+ overrideProfile: string | null,
723
704
  ): Promise<Message[] | null> {
724
705
  await onEvent({ type: "context_compacting" });
725
- const { rawHistory, options } = compaction.prepare(history);
726
- let result: CompactionResult;
727
- try {
728
- result = await runPipeline<CompactionArgs, CompactionResult>(
729
- "compaction",
730
- getMiddlewaresFor("compaction"),
731
- (args) => defaultCompactionTerminal(args, turnContext),
732
- { messages: rawHistory, signal, options },
733
- turnContext,
734
- DEFAULT_TIMEOUTS.compaction,
706
+ // Strip runtime injections so the compactor summarizes the raw persistent
707
+ // messages.
708
+ const rawHistory = stripInjectionsForCompaction(history);
709
+ // Record the history-stripped marker right after stripping, before the
710
+ // pipeline runs.
711
+ await onEvent({ type: "history_stripped" });
712
+ const manager = turnContext.contextWindowManager;
713
+ if (manager == null) {
714
+ throw new PluginExecutionError(
715
+ "default-compaction: turnContext.contextWindowManager is missing — orchestrator must attach it before invoking compaction",
716
+ DEFAULT_COMPACTION_PLUGIN_NAME,
735
717
  );
736
- } catch (error) {
737
- if (error instanceof PluginTimeoutError) {
738
- // A timeout counts as a compaction failure against the circuit breaker.
739
- await this.recordCompactionOutcome(turnContext, true, onEvent);
740
- return null;
741
- }
742
- throw error;
743
718
  }
744
- // `CompactionResult` is intentionally `unknown` at the plugin boundary so
745
- // plugin consumers don't import the window manager; the loop ran the
746
- // pipeline, so it interprets the concrete result here.
747
- const compactResult = result as ContextWindowResult;
748
- // `force: true` bypasses the cooldown/threshold gates, but early returns
719
+ // The mid-loop budget gate is reached only when this turn decides to
720
+ // compact in place, so `force` past the auto-threshold check.
721
+ // `actorTrustClass` comes from the turn context (the actor whose turn
722
+ // triggered compaction) so the compactor's image manifest excludes
723
+ // guardian-only attachments for untrusted actors. `overrideProfile` is the
724
+ // turn's resolved inference-profile override for the summary call.
725
+ const compactResult = await defaultCompact({
726
+ manager,
727
+ messages: rawHistory,
728
+ signal,
729
+ force: true,
730
+ actorTrustClass: turnContext.trust.trustClass,
731
+ overrideProfile,
732
+ });
733
+ // `force: true` bypasses the auto-threshold gate, but early returns
749
734
  // for "no eligible messages" / "insufficient messages" still leave
750
735
  // `summaryFailed` undefined. Only record an outcome when the summary LLM
751
736
  // actually ran.
@@ -756,13 +741,25 @@ export class AgentLoop {
756
741
  onEvent,
757
742
  );
758
743
  }
759
- if (compactResult.compacted) {
760
- await compaction.applyResult(compactResult, rawHistory);
761
- }
744
+ // Emit unconditionally: the dispatcher commits the stripped `basis` as the
745
+ // durable message base whether or not the pipeline compacted (re-injection
746
+ // reads it), and runs the durable compaction commit only when
747
+ // `result.compacted`.
748
+ await onEvent({
749
+ type: "compaction_completed",
750
+ result: compactResult,
751
+ basis: rawHistory,
752
+ });
762
753
  if (compactResult.exhausted ?? false) {
763
754
  return null;
764
755
  }
765
- return compaction.reinject();
756
+ // Re-inject onto the same base the `compaction_completed` dispatch commits:
757
+ // the compacted messages when the pipeline compacted, the stripped
758
+ // pre-compaction history otherwise.
759
+ return compaction.postCompactionHook({
760
+ history: compactResult.compacted ? compactResult.messages : rawHistory,
761
+ turnContext,
762
+ });
766
763
  }
767
764
 
768
765
  async run(
@@ -783,26 +780,36 @@ export class AgentLoop {
783
780
  mutableLatestUserMessage,
784
781
  } = options ?? {};
785
782
  let history = [...messages];
783
+ // Index into `history` where this run's appended output begins. It starts
784
+ // after the input and resets to the compacted base whenever the loop
785
+ // compacts in place, so `history.slice(newMessagesStart)` is always exactly
786
+ // what the loop produced since the last (re-injected) base.
787
+ let newMessagesStart = history.length;
786
788
  let producedVisibleTextThisRun = false;
787
789
  let toolUseTurns = 0;
788
- let consecutiveErrorTurns = 0;
789
- let emptyResponseRetries = 0;
790
+ let stopContinueRetries = 0;
790
791
  let lastLlmCallTime = 0;
791
792
  let exitReason: ExitReason | null = null;
793
+ let appendedNewMessages = false;
792
794
  const rlog = requestId ? log.child({ requestId }) : log;
793
795
 
796
+ // Resolve the inference-profile override that applies right now. The
797
+ // optional resolver lets a turn observe a confirmed mid-turn profile switch
798
+ // before the next model call; absent a resolver the turn-start value holds.
799
+ const resolveEffectiveOverrideProfile = (): string | undefined =>
800
+ resolveOverrideProfile ? resolveOverrideProfile() : overrideProfile;
801
+
794
802
  // Per-run substitution map for sensitive output placeholders.
795
803
  // Bindings are accumulated from tool results; placeholders are
796
804
  // resolved in streamed deltas and final assistant message text.
797
805
  const substitutionMap = new Map<string, string>();
798
806
  let streamingPending = "";
799
807
 
800
- // Idempotency guard for `emitExit`. Used so the throw path in the
801
- // empty-response branch can stamp its reason ("empty_response_exhausted")
802
- // before throwing the catch handler that observes the rethrow will
803
- // then attempt to stamp "error" and harmlessly no-op, preserving the
804
- // more specific reason. Also defends against accidental future
805
- // double-emits if a new break site is added without checking this.
808
+ // Idempotency guard for `emitExit`: the first reason stamped wins. A break
809
+ // site that stamps a specific reason before unwinding into the catch
810
+ // handler keeps that reason instead of the generic "error", and the guard
811
+ // also defends against accidental double-emits if a new break site is
812
+ // added without checking this.
806
813
  let exitReasonEmitted = false;
807
814
  const emitExit = async (reason: AgentLoopExitReason): Promise<void> => {
808
815
  if (exitReasonEmitted) return;
@@ -923,12 +930,8 @@ export class AgentLoop {
923
930
  // `activeProfile` and any call-site named profile. Threading it on
924
931
  // every send (rather than once at construction) keeps subagents that
925
932
  // share an `AgentLoop` instance but ought to inherit a different
926
- // profile correct — and matches how `callSite` is plumbed. The
927
- // optional resolver lets a turn observe an explicitly confirmed
928
- // profile-session switch before the next model call.
929
- const effectiveOverrideProfile = resolveOverrideProfile
930
- ? resolveOverrideProfile()
931
- : overrideProfile;
933
+ // profile correct — and matches how `callSite` is plumbed.
934
+ const effectiveOverrideProfile = resolveEffectiveOverrideProfile();
932
935
  if (effectiveOverrideProfile) {
933
936
  providerConfig.overrideProfile = effectiveOverrideProfile;
934
937
  }
@@ -974,95 +977,76 @@ export class AgentLoop {
974
977
  stripOldMediaBlocks(history),
975
978
  );
976
979
 
977
- // Wrap the provider call in the `llmCall` pipeline so middleware
978
- // contributed by plugins may observe, rewrite, short-circuit, or
979
- // post-process every LLM request. The terminal below is the real
980
- // `provider.sendMessage(...)` call; middleware reach it by calling
981
- // `next(args)`. The default `defaultLlmCallPlugin` contributes a
982
- // passthrough middleware that forwards to `next(args)` — it
983
- // registers at module load and sits at the outermost onion layer,
984
- // so it must yield to keep user-registered `llmCall` middleware
985
- // reachable. Timeout is `null` (`DEFAULT_TIMEOUTS.llmCall`) — the
986
- // provider layer already enforces its own HTTP-level budgets.
987
- //
988
- // The `onEvent` wrapping is kept inside `args.options` so substitution
989
- // and streaming behavior exactly match the pre-pipeline call site.
990
- const llmCallArgs: LLMCallArgs = {
991
- provider: this.provider,
992
- messages: providerHistory,
993
- options: {
994
- tools: currentTools.length > 0 ? currentTools : undefined,
995
- systemPrompt: turnSystemPrompt,
996
- config: providerConfig,
997
- onEvent: (event) => {
998
- if (event.type === "text_delta") {
999
- // Apply sensitive-output placeholder substitution (chunk-safe)
1000
- if (substitutionMap.size > 0) {
1001
- const combined = streamingPending + event.text;
1002
- const { emit, pending } = applyStreamingSubstitution(
1003
- combined,
1004
- substitutionMap,
1005
- );
1006
- streamingPending = pending;
1007
- if (emit.length > 0) {
1008
- onEvent({ type: "text_delta", text: emit });
1009
- }
1010
- } else {
1011
- onEvent({ type: "text_delta", text: event.text });
980
+ // The `onEvent` wrapping below applies sensitive-output placeholder
981
+ // substitution to streamed text while forwarding every other event
982
+ // type through unchanged.
983
+ const providerOptions: SendMessageOptions = {
984
+ tools: currentTools.length > 0 ? currentTools : undefined,
985
+ systemPrompt: turnSystemPrompt,
986
+ config: providerConfig,
987
+ onEvent: (event) => {
988
+ if (event.type === "text_delta") {
989
+ // Apply sensitive-output placeholder substitution (chunk-safe)
990
+ if (substitutionMap.size > 0) {
991
+ const combined = streamingPending + event.text;
992
+ const { emit, pending } = applyStreamingSubstitution(
993
+ combined,
994
+ substitutionMap,
995
+ );
996
+ streamingPending = pending;
997
+ if (emit.length > 0) {
998
+ onEvent({ type: "text_delta", text: emit });
1012
999
  }
1013
- } else if (event.type === "thinking_delta") {
1014
- onEvent({ type: "thinking_delta", thinking: event.thinking });
1015
- } else if (event.type === "tool_use_preview_start") {
1016
- onEvent({
1017
- type: "tool_use_preview_start",
1018
- toolUseId: event.toolUseId,
1019
- toolName: event.toolName,
1020
- });
1021
- } else if (event.type === "input_json_delta") {
1022
- onEvent({
1023
- type: "input_json_delta",
1024
- toolName: event.toolName,
1025
- toolUseId: event.toolUseId,
1026
- accumulatedJson: event.accumulatedJson,
1027
- });
1028
- } else if (event.type === "server_tool_start") {
1029
- onEvent({
1030
- type: "server_tool_start",
1031
- name: event.name,
1032
- toolUseId: event.toolUseId,
1033
- input: event.input,
1034
- });
1035
- } else if (event.type === "server_tool_complete") {
1036
- onEvent({
1037
- type: "server_tool_complete",
1038
- toolUseId: event.toolUseId,
1039
- isError: event.isError,
1040
- ...(event.content ? { content: event.content } : {}),
1041
- ...(event.resolvedInput
1042
- ? { resolvedInput: event.resolvedInput }
1043
- : {}),
1044
- ...(event.errorCode ? { errorCode: event.errorCode } : {}),
1045
- ...(event.errorMessage
1046
- ? { errorMessage: event.errorMessage }
1047
- : {}),
1048
- });
1000
+ } else {
1001
+ onEvent({ type: "text_delta", text: event.text });
1049
1002
  }
1050
- },
1051
- signal,
1003
+ } else if (event.type === "thinking_delta") {
1004
+ onEvent({ type: "thinking_delta", thinking: event.thinking });
1005
+ } else if (event.type === "tool_use_preview_start") {
1006
+ onEvent({
1007
+ type: "tool_use_preview_start",
1008
+ toolUseId: event.toolUseId,
1009
+ toolName: event.toolName,
1010
+ });
1011
+ } else if (event.type === "input_json_delta") {
1012
+ onEvent({
1013
+ type: "input_json_delta",
1014
+ toolName: event.toolName,
1015
+ toolUseId: event.toolUseId,
1016
+ accumulatedJson: event.accumulatedJson,
1017
+ });
1018
+ } else if (event.type === "server_tool_start") {
1019
+ onEvent({
1020
+ type: "server_tool_start",
1021
+ name: event.name,
1022
+ toolUseId: event.toolUseId,
1023
+ input: event.input,
1024
+ });
1025
+ } else if (event.type === "server_tool_complete") {
1026
+ onEvent({
1027
+ type: "server_tool_complete",
1028
+ toolUseId: event.toolUseId,
1029
+ isError: event.isError,
1030
+ ...(event.content ? { content: event.content } : {}),
1031
+ ...(event.resolvedInput
1032
+ ? { resolvedInput: event.resolvedInput }
1033
+ : {}),
1034
+ ...(event.errorCode ? { errorCode: event.errorCode } : {}),
1035
+ ...(event.errorMessage
1036
+ ? { errorMessage: event.errorMessage }
1037
+ : {}),
1038
+ });
1039
+ }
1052
1040
  },
1041
+ signal,
1053
1042
  };
1054
1043
 
1055
- // Per-turn pipeline context. When the orchestrator threaded a full
1056
- // `turnContext` into `run()`, use it (overwriting `turnIndex` with
1057
- // the loop-scoped tool-use iteration) so middleware sees the real
1058
- // conversation identity, trust, and `contextWindowManager`. The
1059
- // synthesized fallback is only reached by standalone unit-test
1060
- // instantiations that never plumb a context through.
1061
- const turnCtx = resolveLoopTurnContext(
1062
- turnContext,
1063
- requestId,
1064
- toolUseTurns,
1065
- );
1044
+ // Per-turn pipeline context. Real call sites thread a full
1045
+ // `turnContext` into `run()` and it is used directly; standalone
1046
+ // unit-test instantiations that never plumb a context through fall
1047
+ // back to a synthesized placeholder scoped to the tool-use iteration.
1048
+ const turnCtx =
1049
+ turnContext ?? buildLoopTurnContext(requestId, toolUseTurns);
1066
1050
 
1067
1051
  // Announce the LLM-call boundary so downstream handlers (the
1068
1052
  // daemon's persistence pipeline) can reserve an empty assistant row
@@ -1085,15 +1069,11 @@ export class AgentLoop {
1085
1069
  // `llm_request_logs` row, then re-throw so the existing outer catch
1086
1070
  // continues to handle abort sync, Sentry capture, the `error` event,
1087
1071
  // and the loop break unchanged.
1088
- let response: LLMCallResult;
1072
+ let response: ProviderResponse;
1089
1073
  try {
1090
- response = await runPipeline<LLMCallArgs, LLMCallResult>(
1091
- "llmCall",
1092
- getMiddlewaresFor("llmCall"),
1093
- (args) => args.provider.sendMessage(args.messages, args.options),
1094
- llmCallArgs,
1095
- turnCtx,
1096
- DEFAULT_TIMEOUTS.llmCall,
1074
+ response = await this.provider.sendMessage(
1075
+ providerHistory,
1076
+ providerOptions,
1097
1077
  );
1098
1078
  } catch (llmCallError) {
1099
1079
  // Skip recording on abort — the user cancelled the request and
@@ -1111,10 +1091,10 @@ export class AgentLoop {
1111
1091
  // misrepresent both.
1112
1092
  const rawRequest = {
1113
1093
  provider: this.provider.name,
1114
- messages: llmCallArgs.messages,
1115
- tools: llmCallArgs.options?.tools,
1116
- systemPrompt: llmCallArgs.options?.systemPrompt,
1117
- config: llmCallArgs.options?.config,
1094
+ messages: providerHistory,
1095
+ tools: providerOptions.tools,
1096
+ systemPrompt: providerOptions.systemPrompt,
1097
+ config: providerOptions.config,
1118
1098
  };
1119
1099
  onEvent({
1120
1100
  type: "provider_error",
@@ -1203,6 +1183,7 @@ export class AgentLoop {
1203
1183
  "LLM response reached output token limit",
1204
1184
  );
1205
1185
  history.push(safeAssistantMessage);
1186
+ appendedNewMessages = true;
1206
1187
  await onEvent({
1207
1188
  type: "max_tokens_reached",
1208
1189
  stopReason: response.stopReason,
@@ -1215,126 +1196,65 @@ export class AgentLoop {
1215
1196
  break;
1216
1197
  }
1217
1198
 
1218
- // Detect empty responses: no user-visible text and no tool calls.
1219
- // This can happen when the model fails to produce output after
1220
- // receiving a large tool result. Retry once with a nudge before
1221
- // the message is persisted.
1222
- //
1223
- // Only nudge when the model hasn't already delivered text to the user
1224
- // earlier in this tool-use chain. If a prior assistant turn in history
1225
- // contained visible text (e.g. the model said its piece before calling
1226
- // a side-effect tool like `remember`), an empty follow-up is the model
1227
- // correctly ending its turn — nudging would mislead it into thinking
1228
- // its earlier text didn't land and cause a verbatim re-send.
1229
- //
1230
- // Note: we check ANY prior assistant turn from this run()
1231
- // invocation, not just the most recent one. In multi-step tool-use
1232
- // chains (say-something → call-tool → call-another-tool → end),
1233
- // the "say-something" text lives on an earlier assistant turn while
1234
- // the most recent assistant turn is a pure tool_use with no text.
1235
- // Restricting the check to the most recent assistant turn would
1236
- // falsely nudge in that case and trigger a duplicate re-send of
1237
- // text the user already saw.
1238
- //
1239
- // Scope the scan to messages appended during this run() call only.
1240
- // Assistant text from prior conversation turns (earlier run()
1241
- // invocations passed in via `messages`) must NOT suppress the
1242
- // nudge — those turns completed long ago and have no bearing on
1243
- // whether the current tool-use chain has delivered text yet.
1244
- //
1245
- // The actual decision (nudge vs. accept vs. error) is delegated to
1246
- // the `emptyResponse` plugin pipeline. The pipeline returns a
1247
- // decision; the loop carries out the side-effect (pushing the nudge
1248
- // or surfacing the error). See `plugins/defaults/empty-response/register.ts`
1249
- // for the default decision logic.
1199
+ // The model's "stop" moment: a response with no tool calls is about to
1200
+ // yield to the user. The `stop` hook (below) decides whether to accept
1201
+ // the turn or re-query with a follow-up; `priorAssistantHadVisibleText`
1202
+ // gates the ops log for the post-tool empty case.
1250
1203
  const hasVisibleText = response.content.some(
1251
1204
  (block) => block.type === "text" && block.text.trim().length > 0,
1252
1205
  );
1253
- // Track whether the model produced visible text earlier in this
1254
- // run() invocation. Run-scoped rather than derived from `history` so
1255
- // it survives inline compaction rewriting the message array: an empty
1256
- // completion after a compaction must not be nudged into re-sending
1257
- // text the user already saw.
1258
1206
  const priorAssistantHadVisibleText = producedVisibleTextThisRun;
1259
1207
  if (hasVisibleText) {
1260
1208
  producedVisibleTextThisRun = true;
1261
1209
  }
1262
1210
 
1263
- const emptyResponseArgs: EmptyResponseArgs = {
1264
- responseContent: response.content,
1265
- toolUseBlocksLength: toolUseBlocks.length,
1266
- toolUseTurns,
1267
- emptyResponseRetries,
1268
- maxEmptyResponseRetries: MAX_EMPTY_RESPONSE_RETRIES,
1269
- priorAssistantHadVisibleText,
1270
- stopReason: response.stopReason,
1271
- };
1272
- const emptyResponseCtx = resolveLoopTurnContext(
1273
- turnContext,
1274
- requestId,
1275
- toolUseTurns,
1276
- );
1277
- const emptyResponseDecision: EmptyResponseDecision = await runPipeline(
1278
- "emptyResponse",
1279
- getMiddlewaresFor("emptyResponse"),
1280
- async (args) => defaultEmptyResponseTerminal(args),
1281
- emptyResponseArgs,
1282
- emptyResponseCtx,
1283
- DEFAULT_TIMEOUTS.emptyResponse,
1284
- );
1285
-
1286
- if (emptyResponseDecision.action === "nudge") {
1287
- // Fall back to the canonical nudge text if the plugin returned
1288
- // `action: "nudge"` but forgot `nudgeText`. Keeps a misbehaving
1289
- // plugin from silently breaking the loop invariant that the
1290
- // model sees a coherent prompt.
1291
- const nudgeText =
1292
- emptyResponseDecision.nudgeText ??
1293
- "<system_notice>Your previous response was empty. You must respond to the user with a summary of what you found or did. Do not use any tools — just respond with text.</system_notice>";
1294
- emptyResponseRetries++;
1295
- rlog.warn(
1296
- { turn: toolUseTurns, retry: emptyResponseRetries },
1297
- "Model returned empty response after tool results — retrying",
1298
- );
1299
- history.push({
1300
- role: "user",
1301
- content: [{ type: "text", text: nudgeText }],
1302
- });
1303
- continue;
1304
- }
1305
-
1306
- if (emptyResponseDecision.action === "error") {
1307
- rlog.error(
1308
- { turn: toolUseTurns, retries: emptyResponseRetries },
1309
- "emptyResponse pipeline requested error surface",
1310
- );
1311
- // Stamp the specific exit reason *before* throwing. The catch
1312
- // handler below will see the rethrown error and attempt to stamp
1313
- // "error" — guarded by `exitReasonEmitted`, that becomes a no-op
1314
- // and the more specific reason wins.
1315
- await emitExit("empty_response_exhausted");
1316
- throw new AssistantError(
1317
- "Model returned empty response after tool results",
1318
- ErrorCode.INTERNAL_ERROR,
1319
- );
1320
- }
1211
+ if (toolUseBlocks.length === 0) {
1212
+ // The model stopped requesting tools — the run's stop boundary. The
1213
+ // `stop` hook decides whether to let the turn end or re-query with a
1214
+ // follow-up turn. It receives the full history and, when it asks to
1215
+ // continue, appends the follow-up turn itself.
1216
+ const stopCtx: StopContext = {
1217
+ conversationId: turnCtx.conversationId,
1218
+ messages: [...history],
1219
+ responseContent: response.content,
1220
+ stopReason: response.stopReason,
1221
+ decision: "stop",
1222
+ logger: rlog,
1223
+ };
1224
+ const finalStopCtx = await runHook(HOOKS.STOP, stopCtx);
1225
+
1226
+ if (finalStopCtx.decision === "continue") {
1227
+ // The loop owns the retry budget: a hook always asks to continue
1228
+ // when a nudge is warranted, and the loop stops anyway once the
1229
+ // budget is spent. This bounds the hook-driven re-query loop.
1230
+ if (stopContinueRetries < MAX_STOP_CONTINUE_RETRIES) {
1231
+ stopContinueRetries++;
1232
+ rlog.warn(
1233
+ { turn: toolUseTurns, retry: stopContinueRetries },
1234
+ "Model returned empty response after tool results — retrying",
1235
+ );
1236
+ history = finalStopCtx.messages;
1237
+ continue;
1238
+ }
1321
1239
 
1322
- // action === "accept" fall through. Emit a dedicated log line for
1323
- // the specific "empty turn after tool results, retries exhausted"
1324
- // case so ops dashboards that grep on this line keep working.
1325
- if (
1326
- !hasVisibleText &&
1327
- toolUseBlocks.length === 0 &&
1328
- toolUseTurns > 0 &&
1329
- !priorAssistantHadVisibleText
1330
- ) {
1331
- rlog.error(
1332
- { turn: toolUseTurns, retries: emptyResponseRetries },
1333
- "Model returned empty response after tool results — retries exhausted",
1334
- );
1240
+ // Budget spent accept the empty turn. Emit a dedicated log line
1241
+ // for the post-tool empty case so ops dashboards that grep on it
1242
+ // keep working.
1243
+ if (
1244
+ !hasVisibleText &&
1245
+ toolUseTurns > 0 &&
1246
+ !priorAssistantHadVisibleText
1247
+ ) {
1248
+ rlog.error(
1249
+ { turn: toolUseTurns, retries: stopContinueRetries },
1250
+ "Model returned empty response after tool results — retries exhausted",
1251
+ );
1252
+ }
1253
+ }
1335
1254
  }
1336
1255
 
1337
1256
  history.push(assistantMessage);
1257
+ appendedNewMessages = true;
1338
1258
 
1339
1259
  await onEvent({ type: "message_complete", message: assistantMessage });
1340
1260
 
@@ -1364,6 +1284,15 @@ export class AgentLoop {
1364
1284
  }),
1365
1285
  );
1366
1286
  history.push({ role: "user", content: cancelledBlocks });
1287
+ for (const toolUse of toolUseBlocks) {
1288
+ await onEvent({
1289
+ type: "tool_result",
1290
+ toolUseId: toolUse.id,
1291
+ content: "Cancelled by user",
1292
+ isError: true,
1293
+ cancelled: true,
1294
+ });
1295
+ }
1367
1296
  await emitExit("aborted_post_response");
1368
1297
  break;
1369
1298
  }
@@ -1393,14 +1322,6 @@ export class AgentLoop {
1393
1322
  });
1394
1323
  },
1395
1324
  toolUse.id,
1396
- // Forward the loop's resolved `TurnContext` through the
1397
- // executor callback so `ToolExecutor.execute` can thread the
1398
- // real orchestrator context into the `toolExecute` pipeline.
1399
- // Standalone tests that don't wire a `turnContext` into
1400
- // `AgentLoop.run()` pass `undefined` here and the executor
1401
- // falls back to the synthesized placeholder — preserving the
1402
- // existing unit-test behavior.
1403
- turnCtx,
1404
1325
  );
1405
1326
 
1406
1327
  return { toolUse, result };
@@ -1464,60 +1385,39 @@ export class AgentLoop {
1464
1385
  }),
1465
1386
  );
1466
1387
 
1467
- // Pre-emptively truncate oversized tool results to prevent context
1468
- // overflow. The work is delegated to the `toolResultTruncate`
1469
- // plugin pipeline so downstream plugins can swap in a smarter
1470
- // truncation strategy (e.g. a summariser) while the default
1471
- // middleware preserves the historical tail-drop behaviour.
1388
+ // Run the `post-tool-use` hook once per tool result, after the tool
1389
+ // returns and before the result joins the provider-bound history.
1390
+ // The default tool-result-truncate plugin tail-drops oversized output
1391
+ // to fit the context window; user hooks can swap in a smarter strategy
1392
+ // (e.g. a summariser) or observe results for side effects.
1472
1393
  const contextWindowTokens =
1473
1394
  resolveContextWindow?.().maxInputTokens ??
1474
1395
  this.config.maxInputTokens ??
1475
1396
  180_000;
1476
- const maxChars = calculateMaxToolResultChars(contextWindowTokens);
1477
- const truncateMiddlewares = getMiddlewaresFor("toolResultTruncate");
1478
1397
 
1479
- let truncatedCount = 0;
1480
- const truncatedBlocks: ContentBlock[] = [];
1398
+ const resultBlocks: ContentBlock[] = [];
1399
+ const additionalContextBlocks: ContentBlock[] = [];
1481
1400
  for (const block of rawResultBlocks) {
1482
1401
  if (block.type !== "tool_result") {
1483
- truncatedBlocks.push(block);
1402
+ resultBlocks.push(block);
1484
1403
  continue;
1485
1404
  }
1486
- const toolBlock = block as ToolResultContent;
1487
- if (
1488
- typeof toolBlock.content !== "string" ||
1489
- toolBlock.content.length <= maxChars
1490
- ) {
1491
- truncatedBlocks.push(block);
1492
- continue;
1493
- }
1494
- const pipelineResult = await runPipeline<
1495
- ToolResultTruncateArgs,
1496
- ToolResultTruncateResult
1497
- >(
1498
- "toolResultTruncate",
1499
- truncateMiddlewares,
1500
- async (args) => defaultToolResultTruncateTerminal(args),
1501
- { content: toolBlock.content, maxChars },
1502
- turnCtx,
1503
- DEFAULT_TIMEOUTS.toolResultTruncate,
1504
- );
1505
- if (pipelineResult.truncated) {
1506
- truncatedCount++;
1507
- truncatedBlocks.push({
1508
- ...toolBlock,
1509
- content: pipelineResult.content,
1405
+ const postToolUseCtx: PostToolUseContext = {
1406
+ conversationId: turnCtx.conversationId,
1407
+ toolResponse: block as ToolResultContent,
1408
+ messages: history,
1409
+ maxInputTokens: contextWindowTokens,
1410
+ logger: rlog,
1411
+ };
1412
+ const finalCtx = await runHook(HOOKS.POST_TOOL_USE, postToolUseCtx);
1413
+ resultBlocks.push(finalCtx.toolResponse);
1414
+ if (finalCtx.additionalContext !== undefined) {
1415
+ additionalContextBlocks.push({
1416
+ type: "text",
1417
+ text: finalCtx.additionalContext,
1510
1418
  });
1511
- } else {
1512
- truncatedBlocks.push(block);
1513
1419
  }
1514
1420
  }
1515
- const resultBlocks = truncatedBlocks;
1516
- if (truncatedCount > 0) {
1517
- log.warn(
1518
- `Truncated ${truncatedCount} oversized tool result(s) to prevent context overflow`,
1519
- );
1520
- }
1521
1421
 
1522
1422
  // Emit tool_result events AFTER truncation so downstream consumers
1523
1423
  // (e.g. session persistence) receive the truncated content.
@@ -1569,54 +1469,15 @@ export class AgentLoop {
1569
1469
 
1570
1470
  toolUseTurns++;
1571
1471
 
1572
- // When any tool returned an error, nudge the LLM to retry with
1573
- // corrected parameters instead of ending its turn. Skip the nudge
1574
- // after MAX_CONSECUTIVE_ERROR_NUDGES consecutive error turns
1575
- // (the error is likely unrecoverable at that point). The nudge
1576
- // decision is delegated to the `toolError` plugin pipeline so user
1577
- // plugins can change the text, observe the event, or suppress it.
1578
- const hasToolError = toolResults.some(({ result }) => result.isError);
1579
- if (hasToolError) {
1580
- consecutiveErrorTurns++;
1581
- } else {
1582
- consecutiveErrorTurns = 0;
1583
- }
1584
- const toolErrorArgs: ToolErrorArgs = {
1585
- hasToolError,
1586
- consecutiveErrorTurns,
1587
- maxConsecutiveErrorNudges: MAX_CONSECUTIVE_ERROR_NUDGES,
1588
- };
1589
- const toolErrorCtx: TurnContext = resolveLoopTurnContext(
1590
- turnContext,
1591
- requestId,
1592
- toolUseTurns - 1,
1593
- );
1594
- const toolErrorDecision = await runPipeline<
1595
- ToolErrorArgs,
1596
- ToolErrorDecision
1597
- >(
1598
- "toolError",
1599
- getMiddlewaresFor("toolError"),
1600
- // Terminal: the canonical nudge decision. The default plugin's
1601
- // middleware is a passthrough (so later-registered user plugins
1602
- // aren't shadowed), so this terminal is what actually produces
1603
- // the decision when no user plugin overrides it. Wiring the
1604
- // decision here also ensures the nudge fires for direct
1605
- // AgentLoop callers (tests, benchmarks) that skip
1606
- // `bootstrapPlugins()` and therefore never register the default.
1607
- async (args) => defaultToolErrorTerminal(args),
1608
- toolErrorArgs,
1609
- toolErrorCtx,
1610
- DEFAULT_TIMEOUTS.toolError,
1611
- );
1612
- if (toolErrorDecision.action === "nudge") {
1613
- resultBlocks.push({
1614
- type: "text",
1615
- text: toolErrorDecision.nudgeText,
1616
- });
1617
- }
1472
+ // Append any guidance a post-tool-use hook surfaced via
1473
+ // `additionalContext` (e.g. tool-error retry coaching) as separate
1474
+ // blocks. They join the provider-bound history below but were not part
1475
+ // of the tool_result events emitted above, so the model sees the
1476
+ // guidance while the client-facing and persisted tool output stay the
1477
+ // tool's actual result.
1478
+ resultBlocks.push(...additionalContextBlocks);
1618
1479
 
1619
- // Add tool results as a user message and continue the loop
1480
+ // Add tool results as a user message and continue the loop.
1620
1481
  history.push({ role: "user", content: resultBlocks });
1621
1482
 
1622
1483
  // Invoke checkpoint callback after tool results are in history.
@@ -1659,7 +1520,7 @@ export class AgentLoop {
1659
1520
  );
1660
1521
  const midLoopThreshold =
1661
1522
  preflightBudget * MID_LOOP_YIELD_THRESHOLD_RATIO;
1662
- const estimated = await this.estimateTokens(history, turnCtx);
1523
+ const estimated = this.estimateTokens(history);
1663
1524
  if (estimated > midLoopThreshold) {
1664
1525
  if (compaction) {
1665
1526
  rlog.info(
@@ -1672,9 +1533,13 @@ export class AgentLoop {
1672
1533
  compaction,
1673
1534
  signal,
1674
1535
  onEvent,
1536
+ resolveEffectiveOverrideProfile() ?? null,
1675
1537
  );
1676
1538
  if (compacted) {
1677
1539
  history = compacted;
1540
+ // The compacted, re-injected array is the new base; output
1541
+ // produced after this point is what the orchestrator persists.
1542
+ newMessagesStart = history.length;
1678
1543
  continue;
1679
1544
  }
1680
1545
  }
@@ -1701,6 +1566,15 @@ export class AgentLoop {
1701
1566
  }),
1702
1567
  );
1703
1568
  history.push({ role: "user", content: cancelledBlocks });
1569
+ for (const toolUse of toolUseBlocks) {
1570
+ await onEvent({
1571
+ type: "tool_result",
1572
+ toolUseId: toolUse.id,
1573
+ content: "Cancelled by user",
1574
+ isError: true,
1575
+ cancelled: true,
1576
+ });
1577
+ }
1704
1578
  }
1705
1579
  await emitExit("aborted_via_error");
1706
1580
  break;
@@ -1714,11 +1588,9 @@ export class AgentLoop {
1714
1588
  Sentry.captureException(err);
1715
1589
  }
1716
1590
  onEvent({ type: "error", error: err });
1717
- // Catch-block fallback. If the rethrow came from the
1718
- // empty-response throw path above, `emitExit("error")` no-ops
1719
- // because `emitExit("empty_response_exhausted")` already ran
1720
- // before the throw. Otherwise, this is the genuine
1721
- // unhandled-error exit.
1591
+ // Catch-block fallback. A break site that stamped a more specific
1592
+ // reason before unwinding here keeps it; the guard makes this a no-op.
1593
+ // Otherwise this is the genuine unhandled-error exit.
1722
1594
  await emitExit("error");
1723
1595
  break;
1724
1596
  }
@@ -1733,7 +1605,12 @@ export class AgentLoop {
1733
1605
  "Agent loop exited",
1734
1606
  );
1735
1607
 
1736
- return { history, exitReason };
1608
+ return {
1609
+ history,
1610
+ exitReason,
1611
+ appendedNewMessages,
1612
+ newMessages: history.slice(newMessagesStart),
1613
+ };
1737
1614
  }
1738
1615
  }
1739
1616