@vellumai/assistant 0.8.10 → 0.8.11-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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -24,7 +24,11 @@ import {
24
24
  runAssistantDrivenCompaction,
25
25
  runEmergencyCompaction,
26
26
  } from "../../../context/compactor.js";
27
- import { estimatePromptTokens } from "../../../context/token-estimator.js";
27
+ import {
28
+ estimatePromptTokens,
29
+ estimateToolsTokens,
30
+ } from "../../../context/token-estimator.js";
31
+ import type { InjectionMode } from "../../../daemon/conversation-runtime-assembly.js";
28
32
  import type {
29
33
  ContentBlock,
30
34
  Message,
@@ -33,6 +37,15 @@ import type {
33
37
  } from "../../../providers/types.js";
34
38
  import type { TrustClass } from "../../../runtime/actor-trust-resolver.js";
35
39
  import { getLogger } from "../../../util/logger.js";
40
+ import {
41
+ createInitialReducerState,
42
+ reduceContextOverflow,
43
+ type ReducerConfig,
44
+ type ReducerState,
45
+ type ReducerStepResult,
46
+ } from "./context-overflow-reducer.js";
47
+ import { computeCorrectedOverflowTarget } from "./corrected-target.js";
48
+ import { resolveOverflowAction } from "./overflow-policy.js";
36
49
 
37
50
  const log = getLogger("context-window");
38
51
 
@@ -85,6 +98,23 @@ export interface ContextWindowResult {
85
98
  * compactions that did clear the threshold.
86
99
  */
87
100
  exhausted?: boolean;
101
+ /**
102
+ * Runtime-injection volume the overflow reduction ladder settled on for the
103
+ * next provider call. The injection-downgrade rung lowers this to
104
+ * `"minimal"`; every other rung leaves it `"full"`. The agent loop forwards
105
+ * it to the post-compaction re-injection so the reduced prompt keeps the
106
+ * volume the ladder chose. Omitted on the ordinary (non-overflow) compaction
107
+ * path, where re-injection always runs at `"full"`.
108
+ */
109
+ injectionMode?: InjectionMode;
110
+ /**
111
+ * Set when the overflow reduction ladder applied its terminal
112
+ * auto-compress-latest-turn rung. The agent loop reads it to classify the
113
+ * terminal exit when recovery is exhausted: a still-too-large turn after
114
+ * auto-compress ran is a `budget_yield_unrecovered`, without it a
115
+ * `context_too_large`. Omitted on the ordinary compaction path.
116
+ */
117
+ autoCompressApplied?: boolean;
88
118
  }
89
119
 
90
120
  export interface ShouldCompactResult {
@@ -135,6 +165,52 @@ export interface EmergencyCompactOptions {
135
165
  overrideProfile?: string | null;
136
166
  }
137
167
 
168
+ /**
169
+ * Turn-specific inputs for {@link ContextWindowManager.reduceOverflowOneRung}.
170
+ * The manager owns everything the reduction ladder derives from its own state
171
+ * (provider, system prompt, token budgets, conversation id, config, and the
172
+ * running reducer state); the caller supplies only what is specific to the
173
+ * overflow that triggered recovery.
174
+ */
175
+ export interface OverflowRecoveryRungOptions {
176
+ /**
177
+ * Provider-reported token count from the overflow rejection, or `null` when
178
+ * it could not be parsed. Lowers the compaction target in proportion to the
179
+ * estimator's under-count so the reduced history lands under the provider's
180
+ * true ceiling rather than the under-counted estimate.
181
+ */
182
+ actualTokens: number | null;
183
+ /**
184
+ * Whether the terminal auto-compress-latest-turn rung is permitted. The
185
+ * caller resolves this from the overflow policy; the manager never makes the
186
+ * policy call itself.
187
+ */
188
+ allowAutoCompressLatestTurn: boolean;
189
+ /** Per-conversation inference-profile override for the summary call. */
190
+ overrideProfile?: string | null;
191
+ /** Trust class of the actor whose turn triggered overflow recovery. */
192
+ actorTrustClass?: TrustClass;
193
+ }
194
+
195
+ export interface OverflowRecoveryOptions {
196
+ /**
197
+ * Provider-reported token count from the overflow rejection, or `null` when
198
+ * it could not be parsed. Forwarded to the reduction ladder to correct the
199
+ * compaction target against the estimator's under-count.
200
+ */
201
+ actualTokens: number | null;
202
+ /**
203
+ * Whether a human is present this turn. The manager resolves the
204
+ * auto-compress-latest-turn permission from the overflow policy using this
205
+ * flag, so callers signal interactivity rather than the policy verdict.
206
+ */
207
+ isInteractive: boolean;
208
+ /** Per-conversation inference-profile override for the summary call. */
209
+ overrideProfile?: string | null;
210
+ /** Trust class of the actor whose turn triggered overflow recovery. */
211
+ actorTrustClass?: TrustClass;
212
+ }
213
+
138
214
  export interface ContextWindowManagerOptions {
139
215
  provider: Provider;
140
216
  systemPrompt: string | (() => string);
@@ -218,6 +294,26 @@ export class ContextWindowManager {
218
294
  */
219
295
  private _nonPersistedPrefixCount = 0;
220
296
  private _resolvedSystemPrompt: string | undefined;
297
+ /**
298
+ * Reducer state for the in-progress overflow-recovery ladder, held across
299
+ * the successive {@link reduceOverflowOneRung} calls of a single turn so the
300
+ * ladder advances one rung per call. Reset to `undefined` at each turn
301
+ * boundary via {@link resetOverflowRecovery} so a new turn starts the ladder
302
+ * from the emergency rung.
303
+ */
304
+ private _overflowReducerState: ReducerState | undefined;
305
+ /**
306
+ * The corrected compaction target and the prompt-token estimate it was
307
+ * derived from, computed once against the overflowing prompt on the first
308
+ * rung of a turn and reused across that turn's later rungs. The correction
309
+ * captures the estimator error the provider's actual token count revealed at
310
+ * the moment of overflow; re-deriving it against an already-reduced prompt
311
+ * would divide the original actual-token count by a smaller estimate and
312
+ * drive the target ever lower. Reset with {@link resetOverflowRecovery}.
313
+ */
314
+ private _overflowTurnTarget:
315
+ | { targetTokens: number; estimatedInputTokens: number }
316
+ | undefined;
221
317
 
222
318
  constructor(options: ContextWindowManagerOptions) {
223
319
  this.provider = options.provider;
@@ -232,6 +328,16 @@ export class ContextWindowManager {
232
328
  this.config = config;
233
329
  }
234
330
 
331
+ /**
332
+ * Clear the overflow-recovery ladder so the next {@link reduceOverflowOneRung}
333
+ * call starts a fresh ladder from the emergency rung. Called at the turn
334
+ * boundary.
335
+ */
336
+ resetOverflowRecovery(): void {
337
+ this._overflowReducerState = undefined;
338
+ this._overflowTurnTarget = undefined;
339
+ }
340
+
235
341
  /** Leading non-persisted inherited-context messages the compactor preserves. */
236
342
  get nonPersistedPrefixCount(): number {
237
343
  return this._nonPersistedPrefixCount;
@@ -325,6 +431,202 @@ export class ContextWindowManager {
325
431
  }
326
432
  }
327
433
 
434
+ /**
435
+ * Advance the context-overflow reduction ladder by one rung against
436
+ * `messages`, holding the reducer state across the successive calls of a
437
+ * single turn (reset via {@link resetOverflowRecovery}). The compaction
438
+ * target is the manager's overflow preflight budget, lowered in proportion
439
+ * to the estimator error the provider's actual token count reveals, so the
440
+ * reduced history lands under the provider's true ceiling rather than the
441
+ * under-counted estimate.
442
+ */
443
+ async reduceOverflowOneRung(
444
+ messages: Message[],
445
+ options: OverflowRecoveryRungOptions,
446
+ signal?: AbortSignal,
447
+ ): Promise<ReducerStepResult> {
448
+ try {
449
+ return await this._reduceOverflowOneRung(messages, options, signal);
450
+ } finally {
451
+ this.clearSystemPromptCache();
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Drive the context-overflow reduction ladder one rung against `messages`
457
+ * and adapt the rung into a {@link ContextWindowResult} the agent loop's
458
+ * compaction path consumes. Resolves the auto-compress-latest-turn
459
+ * permission from the overflow policy — the manager owns that policy call,
460
+ * the ladder never makes it — and surfaces the rung's injection mode,
461
+ * terminal auto-compress flag, and exhaustion so the loop can re-inject at
462
+ * the chosen volume and classify the terminal exit when recovery runs out.
463
+ */
464
+ async recoverContextOverflow(
465
+ messages: Message[],
466
+ options: OverflowRecoveryOptions,
467
+ signal?: AbortSignal,
468
+ ): Promise<ContextWindowResult> {
469
+ const allowAutoCompressLatestTurn =
470
+ resolveOverflowAction({
471
+ overflowRecovery: this.config.overflowRecovery,
472
+ isInteractive: options.isInteractive,
473
+ }) === "auto_compress_latest_turn";
474
+ const step = await this.reduceOverflowOneRung(
475
+ messages,
476
+ {
477
+ actualTokens: options.actualTokens,
478
+ allowAutoCompressLatestTurn,
479
+ overrideProfile: options.overrideProfile,
480
+ actorTrustClass: options.actorTrustClass,
481
+ },
482
+ signal,
483
+ );
484
+ return this.overflowStepToResult(step, messages);
485
+ }
486
+
487
+ /**
488
+ * Adapt a reduction-ladder {@link ReducerStepResult} into the
489
+ * {@link ContextWindowResult} shape the agent loop's compaction path
490
+ * consumes. A summary rung carries a full compaction result (with the
491
+ * durable-commit and circuit-breaker fields); the non-summary rungs
492
+ * (truncation / media stubbing / injection downgrade) only transform the
493
+ * in-memory history, so they map to a no-op result that still propagates the
494
+ * reduced messages. Both forward the ladder's injection mode, exhaustion, and
495
+ * whether the terminal auto-compress rung was applied.
496
+ */
497
+ private overflowStepToResult(
498
+ step: ReducerStepResult,
499
+ basis: Message[],
500
+ ): ContextWindowResult {
501
+ const autoCompressApplied = step.state.appliedTiers.includes(
502
+ "auto_compress_latest_turn",
503
+ );
504
+ const base =
505
+ step.compactionResult ??
506
+ noopResult(step.messages, step.estimatedTokens, {
507
+ maxInputTokens: this.config.maxInputTokens,
508
+ thresholdTokens: Math.floor(
509
+ this.config.maxInputTokens *
510
+ this.resolveCompactionConfig().autoThreshold,
511
+ ),
512
+ reason: `overflow recovery: ${step.tier}`,
513
+ });
514
+ return {
515
+ ...base,
516
+ messages: step.messages,
517
+ estimatedInputTokens: step.estimatedTokens,
518
+ previousEstimatedInputTokens: this.estimateInputTokens(basis),
519
+ injectionMode: step.state.injectionMode,
520
+ autoCompressApplied,
521
+ exhausted: step.state.exhausted,
522
+ };
523
+ }
524
+
525
+ private async _reduceOverflowOneRung(
526
+ messages: Message[],
527
+ options: OverflowRecoveryRungOptions,
528
+ signal?: AbortSignal,
529
+ ): Promise<ReducerStepResult> {
530
+ if (this.conversationId == null) {
531
+ throw new Error(
532
+ "ContextWindowManager has no conversationId — cannot run overflow recovery",
533
+ );
534
+ }
535
+ if (!this._overflowReducerState) {
536
+ this._overflowReducerState = createInitialReducerState();
537
+ this._overflowTurnTarget = this.deriveOverflowTurnTarget(
538
+ messages,
539
+ options.actualTokens,
540
+ );
541
+ }
542
+ const { targetTokens, estimatedInputTokens } = this._overflowTurnTarget!;
543
+
544
+ const config: ReducerConfig = {
545
+ providerName: this.estimationProviderName,
546
+ systemPrompt: this.systemPrompt,
547
+ contextWindow: this.config,
548
+ targetTokens,
549
+ toolTokenBudget: this.resolveTurnToolTokenBudget(),
550
+ conversationId: this.conversationId,
551
+ overrideProfile: options.overrideProfile ?? null,
552
+ actorTrustClass: options.actorTrustClass,
553
+ previousEstimatedInputTokens: estimatedInputTokens,
554
+ maxMiddleTierAttempts: this.config.overflowRecovery.maxAttempts,
555
+ allowAutoCompressLatestTurn: options.allowAutoCompressLatestTurn,
556
+ };
557
+
558
+ const step = await reduceContextOverflow(
559
+ messages,
560
+ config,
561
+ this._overflowReducerState,
562
+ signal,
563
+ );
564
+ this._overflowReducerState = step.state;
565
+ return step;
566
+ }
567
+
568
+ /**
569
+ * Compute the corrected compaction target for a turn's overflow recovery:
570
+ * the overflow preflight budget lowered in proportion to the estimator error
571
+ * the provider's actual token count reveals, so the reduced history lands
572
+ * under the provider's true ceiling rather than the under-counted estimate.
573
+ */
574
+ private deriveOverflowTurnTarget(
575
+ messages: Message[],
576
+ actualTokens: number | null,
577
+ ): { targetTokens: number; estimatedInputTokens: number } {
578
+ const estimatedInputTokens = estimatePromptTokens(
579
+ messages,
580
+ this.systemPrompt,
581
+ {
582
+ providerName: this.estimationProviderName,
583
+ toolTokenBudget: this.resolveTurnToolTokenBudget(),
584
+ },
585
+ );
586
+ const { targetTokens, estimationErrorRatio } =
587
+ computeCorrectedOverflowTarget({
588
+ preflightBudget: this.resolveOverflowPreflightBudget(messages.length),
589
+ actualTokens,
590
+ estimatedTokens: estimatedInputTokens,
591
+ });
592
+ if (estimationErrorRatio != null) {
593
+ log.warn(
594
+ {
595
+ actualTokens,
596
+ estimatedTokens: estimatedInputTokens,
597
+ estimationErrorRatio: estimationErrorRatio.toFixed(2),
598
+ targetTokens,
599
+ },
600
+ "Adjusting overflow compaction target based on observed estimation error",
601
+ );
602
+ }
603
+ return { targetTokens, estimatedInputTokens };
604
+ }
605
+
606
+ /**
607
+ * Tool-token budget for the current turn's overflow recovery. Prefers the
608
+ * live tool set resolved for the turn — matching what the loop sends to the
609
+ * provider — and falls back to the constructor-time snapshot when no
610
+ * resolver is wired (legacy test paths, ad-hoc instantiation).
611
+ */
612
+ private resolveTurnToolTokenBudget(): number {
613
+ const tools = this.resolveTools?.();
614
+ return tools ? estimateToolsTokens(tools) : this.toolTokenBudget;
615
+ }
616
+
617
+ /**
618
+ * The token budget overflow recovery compacts below, derived from the
619
+ * manager's configured max-input cap and overflow-recovery safety margin.
620
+ * Long histories (> 50 messages) get a wider margin so the reduced prompt
621
+ * keeps clearance under the provider's true ceiling.
622
+ */
623
+ private resolveOverflowPreflightBudget(messageCount: number): number {
624
+ const baseSafetyMargin = this.config.overflowRecovery.safetyMarginRatio;
625
+ const safetyMargin =
626
+ messageCount > 50 ? Math.max(baseSafetyMargin, 0.15) : baseSafetyMargin;
627
+ return Math.floor(this.config.maxInputTokens * (1 - safetyMargin));
628
+ }
629
+
328
630
  private async _maybeCompact(
329
631
  messages: Message[],
330
632
  signal?: AbortSignal,
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Default `post-model-call` hook: when the model yields a turn with no tool
3
+ * calls, decide whether to let the turn end, rewrite it for the user, or
4
+ * re-query the model.
5
+ *
6
+ * Two cases warrant intervention:
7
+ *
8
+ * 1. **Refusal stop.** The provider returned `stopReason === "refusal"` with no
9
+ * visible text (Anthropic's safety classifier zeroed the response) and no
10
+ * earlier turn this run already delivered visible text. The hook rewrites
11
+ * the turn into a plain-text apology (`REFUSAL_FALLBACK_TEXT`) by replacing
12
+ * {@link PostModelCallContext.content} and lets the turn end. A retry is
13
+ * deliberately not attempted: a safety-classifier refusal re-fires on a
14
+ * re-query, so the canned message is the intended terminal response.
15
+ * 2. **Empty turn after tool use.** The turn produced no visible text, follows
16
+ * at least one prior assistant turn this run, and no earlier turn this run
17
+ * already delivered visible text. The hook re-queries the model with
18
+ * `NUDGE_TEXT` (a tool trail exists to summarize, so a retry can recover a
19
+ * real answer). The retry is bounded to one pass per run by a one-shot
20
+ * per-conversation mark this hook sets; the sibling `stop` hook (see
21
+ * `./stop.ts`) clears it when the turn terminates, so the next run nudges
22
+ * afresh.
23
+ *
24
+ * Every other case leaves the decision at `"stop"` (the model said its piece,
25
+ * or there is nothing to act on).
26
+ *
27
+ * Both prior-turn signals are derived from the current response cycle — the
28
+ * messages after the last genuine user prompt (a user turn that isn't purely
29
+ * tool results). Scoping this way keeps prior conversation turns from polluting
30
+ * the signals, and deriving the boundary from history content rather than an
31
+ * index means mid-run compaction (which rewrites the array in place) can't
32
+ * invalidate it. A prior assistant turn this cycle implies a completed tool-use
33
+ * iteration (an empty turn nudges-and-continues without pushing an assistant
34
+ * message), so "a prior assistant turn exists" is the equivalent of "this is
35
+ * not the first model call".
36
+ *
37
+ * Defaults register before any user plugin, so this hook runs at the front of
38
+ * the `post-model-call` chain — later hooks see (and may override) its
39
+ * decision.
40
+ *
41
+ * Only a finalized, no-tool reply is actionable. A provider rejection carries
42
+ * no turn content to assess (a recovery hook like history-repair owns that),
43
+ * and a tool-bearing turn continues naturally — the loop runs the tools and
44
+ * ignores the decision — so the hook returns early for both.
45
+ */
46
+
47
+ import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
48
+
49
+ import type { ContentBlock, Message } from "../../../../providers/types.js";
50
+ import {
51
+ isEmptyResponseNudged,
52
+ markEmptyResponseNudged,
53
+ } from "../nudge-state-store.js";
54
+
55
+ /**
56
+ * Canonical nudge text for an empty turn after tool use. Must stay verbatim so
57
+ * a plugin that wraps the default sees a stable string.
58
+ *
59
+ * Wire-compat note: this is shown to the LLM, not the user. Edits here affect
60
+ * model behavior but not end-user UX directly.
61
+ */
62
+ export const NUDGE_TEXT =
63
+ "<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>";
64
+
65
+ /**
66
+ * User-facing text a refusal turn is rewritten into. Used when the provider
67
+ * stops with `"refusal"` and no visible text — i.e. the safety classifier
68
+ * zeroed the response. Unlike `NUDGE_TEXT` (shown only to the model), this is
69
+ * the message the user actually reads in place of an empty assistant bubble.
70
+ */
71
+ export const REFUSAL_FALLBACK_TEXT =
72
+ "Sorry — I wasn't able to generate a response to that. Please try rephrasing or asking in a different way.";
73
+
74
+ function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
75
+ return content.some(
76
+ (block) => block.type === "text" && block.text.trim().length > 0,
77
+ );
78
+ }
79
+
80
+ function hasToolUse(content: ReadonlyArray<ContentBlock>): boolean {
81
+ return content.some((block) => block.type === "tool_use");
82
+ }
83
+
84
+ function isAssistantTurn(message: Message): boolean {
85
+ return message.role === "assistant";
86
+ }
87
+
88
+ /** A user-role message carrying only tool results, not a fresh prompt. */
89
+ function isToolResultMessage(message: Message): boolean {
90
+ return (
91
+ message.role === "user" &&
92
+ message.content.length > 0 &&
93
+ message.content.every((block) => block.type === "tool_result")
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Messages belonging to the current response cycle: everything after the last
99
+ * genuine user prompt. Falls back to the whole history when none is found.
100
+ */
101
+ function currentCycleMessages(
102
+ messages: ReadonlyArray<Message>,
103
+ ): ReadonlyArray<Message> {
104
+ for (let i = messages.length - 1; i >= 0; i--) {
105
+ const message = messages[i];
106
+ if (message.role === "user" && !isToolResultMessage(message)) {
107
+ return messages.slice(i + 1);
108
+ }
109
+ }
110
+ return messages;
111
+ }
112
+
113
+ const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
114
+ // A provider rejection carries no turn content to assess (a recovery hook
115
+ // owns the rejection); the sibling `stop` hook clears the mark when the turn
116
+ // terminates.
117
+ if (ctx.error) return;
118
+ // A tool-bearing turn continues mid-run — the loop runs the tools — so leave
119
+ // the mark intact to keep the one-nudge-per-run bound across tool iterations.
120
+ if (hasToolUse(ctx.content)) return;
121
+
122
+ const turnHasVisibleText = hasVisibleText(ctx.content);
123
+
124
+ const cycleMessages = currentCycleMessages(ctx.messages);
125
+ const priorAssistantTurns = cycleMessages.filter(isAssistantTurn);
126
+ const hadPriorAssistantTurn = priorAssistantTurns.length > 0;
127
+ const priorAssistantHadVisibleText = priorAssistantTurns.some((message) =>
128
+ hasVisibleText(message.content),
129
+ );
130
+
131
+ // Refusal stop: rewrite the empty turn into a user-facing apology and let it
132
+ // end. Skipped when an earlier turn this run already replied, so the apology
133
+ // never lands beneath a real answer.
134
+ if (
135
+ ctx.stopReason === "refusal" &&
136
+ !turnHasVisibleText &&
137
+ !priorAssistantHadVisibleText
138
+ ) {
139
+ ctx.content = [{ type: "text", text: REFUSAL_FALLBACK_TEXT }];
140
+ return;
141
+ }
142
+
143
+ const isEmptyTurnAfterTools =
144
+ !turnHasVisibleText &&
145
+ hadPriorAssistantTurn &&
146
+ !priorAssistantHadVisibleText;
147
+
148
+ if (isEmptyTurnAfterTools) {
149
+ // Re-query once to recover a real answer. The one-shot per-conversation
150
+ // mark makes the hook self-limiting: a second empty turn this run finds the
151
+ // mark already set and lets the turn end rather than nudging again.
152
+ if (!isEmptyResponseNudged(ctx.conversationId)) {
153
+ markEmptyResponseNudged(ctx.conversationId);
154
+ ctx.messages.push({
155
+ role: "user",
156
+ content: [{ type: "text", text: NUDGE_TEXT }],
157
+ });
158
+ ctx.decision = "continue";
159
+ ctx.logger.warn(
160
+ { plugin: "empty-response", conversationId: ctx.conversationId },
161
+ "Model returned empty response after tool results — retrying",
162
+ );
163
+ return;
164
+ }
165
+
166
+ ctx.logger.error(
167
+ { plugin: "empty-response", conversationId: ctx.conversationId },
168
+ "Model returned empty response after tool results — retries exhausted",
169
+ );
170
+ }
171
+ };
172
+
173
+ export default postModelCall;
@@ -1,126 +1,22 @@
1
1
  /**
2
- * Default `stop` hook: when the model yields a turn with no tool calls, decide
3
- * whether to let the turn end or re-query the model with a nudge.
2
+ * Default `stop` hook: clears the per-conversation empty-response nudge bound
3
+ * when a turn terminates.
4
4
  *
5
- * Two cases warrant a nudge:
6
- *
7
- * 1. **Refusal stop.** The provider returned `stopReason === "refusal"` with no
8
- * visible text (Anthropic's safety classifier zeroed the response). Nudged
9
- * even on the first model call of the run — a refusal there guarantees no
10
- * organic text exists yet, so without intervening the loop would persist an
11
- * empty assistant bubble to the user. Uses `REFUSAL_NUDGE_TEXT`.
12
- * 2. **Empty turn after tool use.** The turn produced no visible text, follows
13
- * at least one prior assistant turn this run, and no earlier turn this run
14
- * already delivered visible text. Uses `NUDGE_TEXT`.
15
- *
16
- * Every other case leaves the decision at `"stop"` (the model said its piece,
17
- * or there is nothing to nudge about). The retry cap is owned by the agent
18
- * loop: this hook always asks to continue when a nudge is warranted, and the
19
- * loop stops anyway once the run's nudge budget is spent.
20
- *
21
- * Both prior-turn signals are derived from the current response cycle — the
22
- * messages after the last genuine user prompt (a user turn that isn't purely
23
- * tool results). Scoping this way keeps prior conversation turns from polluting
24
- * the signals, and deriving the boundary from history content rather than an
25
- * index means mid-run compaction (which rewrites the array in place) can't
26
- * invalidate it. A prior assistant turn this cycle implies a completed tool-use
27
- * iteration (an empty turn nudges-and-continues without pushing an assistant
28
- * message), so "a prior assistant turn exists" is the equivalent of "this is
29
- * not the first model call".
30
- *
31
- * Defaults register before any user plugin, so this hook runs at the front of
32
- * the `stop` chain — later hooks see (and may override) its decision.
5
+ * The `post-model-call` hook (see `./post-model-call.ts`) marks the bound when
6
+ * it re-queries the model after an empty turn. `stop` is the definitive
7
+ * terminal hook — it fires exactly once when the turn is truly ending, after
8
+ * every retry decision has been made — so clearing the bound here
9
+ * unconditionally guarantees the next run nudges afresh, no matter how the turn
10
+ * ended (a finalized reply, an abort, or a retry the loop's per-run backstop
11
+ * refused).
33
12
  */
34
13
 
35
14
  import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
36
15
 
37
- import type { ContentBlock, Message } from "../../../../providers/types.js";
38
-
39
- /**
40
- * Canonical nudge text for an empty turn after tool use. Must stay verbatim so
41
- * a plugin that wraps the default sees a stable string.
42
- *
43
- * Wire-compat note: this is shown to the LLM, not the user. Edits here affect
44
- * model behavior but not end-user UX directly.
45
- */
46
- export const NUDGE_TEXT =
47
- "<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>";
48
-
49
- /**
50
- * Refusal-specific nudge. Used when the provider stops with `"refusal"` and no
51
- * visible text — i.e. the safety classifier zeroed the response. Kept distinct
52
- * from `NUDGE_TEXT` so the model gets context-appropriate guidance (no "summary
53
- * of what you found or did" — there is no tool trail to summarize on a refusal).
54
- *
55
- * Wire-compat note: this is shown to the LLM, not the user. Edits here affect
56
- * retry behavior but not end-user UX directly.
57
- */
58
- export const REFUSAL_NUDGE_TEXT =
59
- '<system_notice>Your previous response was empty because the upstream provider returned stop_reason="refusal". Please answer the user\'s last message directly with a plain-text response. Do not use any tools — just respond with text.</system_notice>';
60
-
61
- function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
62
- return content.some(
63
- (block) => block.type === "text" && block.text.trim().length > 0,
64
- );
65
- }
66
-
67
- function isAssistantTurn(message: Message): boolean {
68
- return message.role === "assistant";
69
- }
70
-
71
- /** A user-role message carrying only tool results, not a fresh prompt. */
72
- function isToolResultMessage(message: Message): boolean {
73
- return (
74
- message.role === "user" &&
75
- message.content.length > 0 &&
76
- message.content.every((block) => block.type === "tool_result")
77
- );
78
- }
79
-
80
- /**
81
- * Messages belonging to the current response cycle: everything after the last
82
- * genuine user prompt. Falls back to the whole history when none is found.
83
- */
84
- function currentCycleMessages(
85
- messages: ReadonlyArray<Message>,
86
- ): ReadonlyArray<Message> {
87
- for (let i = messages.length - 1; i >= 0; i--) {
88
- const message = messages[i];
89
- if (message.role === "user" && !isToolResultMessage(message)) {
90
- return messages.slice(i + 1);
91
- }
92
- }
93
- return messages;
94
- }
16
+ import { clearEmptyResponseNudged } from "../nudge-state-store.js";
95
17
 
96
18
  const stop: PluginHookFn<StopContext> = async (ctx) => {
97
- const turnHasVisibleText = hasVisibleText(ctx.responseContent);
98
-
99
- const appendNudge = (text: string): void => {
100
- ctx.messages.push({ role: "user", content: [{ type: "text", text }] });
101
- ctx.decision = "continue";
102
- };
103
-
104
- if (ctx.stopReason === "refusal" && !turnHasVisibleText) {
105
- appendNudge(REFUSAL_NUDGE_TEXT);
106
- return;
107
- }
108
-
109
- const cycleMessages = currentCycleMessages(ctx.messages);
110
- const priorAssistantTurns = cycleMessages.filter(isAssistantTurn);
111
- const hadPriorAssistantTurn = priorAssistantTurns.length > 0;
112
- const priorAssistantHadVisibleText = priorAssistantTurns.some((message) =>
113
- hasVisibleText(message.content),
114
- );
115
-
116
- const isEmptyTurnAfterTools =
117
- !turnHasVisibleText &&
118
- hadPriorAssistantTurn &&
119
- !priorAssistantHadVisibleText;
120
-
121
- if (isEmptyTurnAfterTools) {
122
- appendNudge(NUDGE_TEXT);
123
- }
19
+ clearEmptyResponseNudged(ctx.conversationId);
124
20
  };
125
21
 
126
22
  export default stop;