@vellumai/assistant 0.8.7 → 0.8.8

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 (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Default `memoryRetrieval` post-compaction hook.
3
+ *
4
+ * After the agent loop compacts a conversation mid-turn it must re-apply the
5
+ * runtime injections compaction stripped — the NOW.md scratchpad, PKB context,
6
+ * memory-v2 static block, workspace top-level context, and Slack chronological
7
+ * snapshot — onto the compacted history before the turn continues. This hook
8
+ * is the memory system's home for that transform: it receives the message
9
+ * history plus the resolved runtime-injection options and returns the edited
10
+ * history (and the blocks it captured), with no dependency on the agent loop's
11
+ * closure state.
12
+ *
13
+ * It re-applies the runtime injections via {@link applyRuntimeInjections},
14
+ * re-tracks the memory graph's cached nodes against the re-injected history,
15
+ * and converts now-historical `web_search_tool_result` blocks to text so their
16
+ * expired `encrypted_content` tokens are not replayed. The remaining
17
+ * orchestrator-side step (the post-injection bookkeeping the loop records) is
18
+ * expected to migrate here as the hook subsumes the loop's re-injection
19
+ * ceremony.
20
+ *
21
+ * The memory graph handle is sourced internally from the plugin's own
22
+ * conversation-keyed registry ({@link getLiveGraphMemory}) rather than being
23
+ * threaded in by the loop — it is memory-retrieval-specific state, not
24
+ * something the generic loop or the shared {@link TurnContext} should carry.
25
+ */
26
+
27
+ import {
28
+ applyRuntimeInjections,
29
+ type RuntimeInjectionOptions,
30
+ type RuntimeInjectionResult,
31
+ } from "../../../../daemon/conversation-runtime-assembly.js";
32
+ import { stripHistoricalWebSearchResults } from "../../../../daemon/web-search-history.js";
33
+ import { getLiveGraphMemory } from "../../../../memory/graph/conversation-graph-memory.js";
34
+ import type { PluginLogger } from "../../../../plugin-api/types.js";
35
+ import type { Message } from "../../../../providers/types.js";
36
+ import type { TurnContext } from "../../../types.js";
37
+
38
+ /**
39
+ * The slice of the hook's context the agent loop supplies from its own working
40
+ * state. Re-injection inputs migrate loop-ward by growing this type; the loop
41
+ * hands the hook an object of this shape via
42
+ * {@link MidLoopCompaction.postCompactionHook}.
43
+ */
44
+ export interface PostCompactionHookInput {
45
+ /** Compacted message history to re-inject onto. */
46
+ history: Message[];
47
+ /** Per-turn conversation context forwarded to the injector chain. */
48
+ turnContext?: TurnContext;
49
+ }
50
+
51
+ /**
52
+ * Everything the hook needs in a single context: the loop-supplied
53
+ * {@link PostCompactionHookInput}, the resolved {@link RuntimeInjectionOptions}
54
+ * (spread top-level so each field stays individually addressable), and the
55
+ * conversation-scoped state the options bag cannot carry (actor trust and a
56
+ * turn-scoped logger). The memory graph handle is not part of this context —
57
+ * the hook sources it internally via {@link getLiveGraphMemory}.
58
+ */
59
+ export interface PostCompactContext
60
+ extends RuntimeInjectionOptions, PostCompactionHookInput {
61
+ /** True when the actor for this turn is trusted (guardian-class). */
62
+ isTrustedActor: boolean;
63
+ /** Turn-scoped logger for diagnostics emitted while re-injecting. */
64
+ logger: PluginLogger;
65
+ }
66
+
67
+ export default async function postCompactReinject(
68
+ ctx: PostCompactContext,
69
+ ): Promise<RuntimeInjectionResult> {
70
+ const { history, isTrustedActor, logger, ...options } = ctx;
71
+ const result = await applyRuntimeInjections(history, options);
72
+ // Re-track the nodes the memory graph last injected so they survive against
73
+ // the re-injected history. Untrusted actors and minimal-mode turns never
74
+ // received a memory-graph injection, so there is nothing to re-track. The
75
+ // live graph handle is looked up from the plugin's own registry by the
76
+ // turn's conversation id — the same instance the turn's retrieval mutated,
77
+ // so re-tracking sees the real cached-node state.
78
+ if (isTrustedActor && options.mode !== "minimal") {
79
+ getLiveGraphMemory(
80
+ options.turnContext?.conversationId,
81
+ )?.retrackCachedNodes();
82
+ }
83
+ const strip = stripHistoricalWebSearchResults(result.messages);
84
+ if (strip.stats.blocksStripped > 0) {
85
+ logger.info(
86
+ { phase: "mid-loop-compact", ...strip.stats },
87
+ "Converted historical web_search_tool_result blocks to text summaries",
88
+ );
89
+ }
90
+ return { ...result, messages: strip.messages };
91
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Default `user-prompt-submit-temp` hook: runs the memory-graph retrieval the
3
+ * agent loop needs before building a turn's runtime-injection block.
4
+ *
5
+ * **Memory graph** via {@link ConversationGraphMemory.prepareMemory} —
6
+ * dispatches to context-load or per-turn retrieval depending on initialization
7
+ * state; gated on the actor being trusted (guardian).
8
+ *
9
+ * The hook also owns the retrieval's side effects — persisting the injected
10
+ * block onto the user message's metadata, writing the recall log, and emitting
11
+ * the `memory_recalled` event — so the loop only consumes the turn-scoped
12
+ * `latestMessages` written back onto the context. The PKB query-vector pair is
13
+ * recorded on the conversation's graph handle for the PKB-reminder injector to
14
+ * read back. PKB context and NOW.md are sourced directly by their injectors
15
+ * (gated on block presence), not produced here.
16
+ *
17
+ * This fires at the early "prompt submitted, before context assembly" moment,
18
+ * distinct from the canonical late `user-prompt-submit` hook (history repair,
19
+ * title): memory's outputs feed the injection and overflow-reduction transforms
20
+ * that run between the two moments. The `-temp` suffix marks this as a
21
+ * transitional staging point that folds into `user-prompt-submit` once
22
+ * compaction is cleared from the gap between the two call sites.
23
+ */
24
+
25
+ import type { PluginHookFn } from "@vellumai/plugin-api";
26
+ import type { Logger } from "pino";
27
+
28
+ import type { AssistantConfig } from "../../../../config/schema.js";
29
+ import type { ServerMessage } from "../../../../daemon/message-protocol.js";
30
+ import type { MemoryRecalled } from "../../../../daemon/message-types/memory.js";
31
+ import { updateMessageMetadata } from "../../../../memory/conversation-crud.js";
32
+ import type { ConversationGraphMemory } from "../../../../memory/graph/conversation-graph-memory.js";
33
+ import { recordMemoryRecallLog } from "../../../../memory/memory-recall-log-store.js";
34
+ import type { Message } from "../../../../providers/types.js";
35
+ import type { GraphMemoryResult } from "../../../types.js";
36
+
37
+ /**
38
+ * Context threaded through the `user-prompt-submit-temp` hook. The readonly
39
+ * fields carry the conversation-scoped state the retriever needs (graph
40
+ * handle, event sink, abort signal); the output fields are populated by the
41
+ * hook and read back by the agent loop. `latestMessages` straddles both: the
42
+ * loop seeds it with the pre-injection array and the hook overwrites it with
43
+ * the injected result.
44
+ */
45
+ export interface MemoryRetrievalHookContext {
46
+ /** Per-conversation memory graph handle. */
47
+ readonly graphMemory: ConversationGraphMemory;
48
+ /** Assistant config snapshot. */
49
+ readonly config: AssistantConfig;
50
+ /** Event sink used by the graph retriever and `memory_recalled` emission. */
51
+ readonly onEvent: (msg: ServerMessage) => void;
52
+ /** True when the actor for this turn is trusted (guardian-class). */
53
+ readonly isTrustedActor: boolean;
54
+ /** Conversation the turn belongs to — keys the recall-log row. */
55
+ readonly conversationId: string;
56
+ /** User message the injected memory block is persisted onto. */
57
+ readonly userMessageId: string;
58
+ /** Turn-scoped logger for non-fatal persistence warnings. */
59
+ readonly logger: Logger;
60
+ /**
61
+ * Per-turn abort signal forwarded to `prepareMemory`. An external cancel
62
+ * aborts the underlying retrieval instead of letting it run to completion.
63
+ */
64
+ readonly signal: AbortSignal;
65
+ /**
66
+ * Working message list for the turn. Seeded by the loop with the
67
+ * pre-injection messages and consumed as the retrieval input; the hook
68
+ * overwrites it with the memory-graph block injected, or leaves it
69
+ * unchanged when no graph retrieval ran (untrusted actor, or a no-op
70
+ * retrieval). Read back by the loop.
71
+ */
72
+ latestMessages: Message[];
73
+ }
74
+
75
+ /**
76
+ * Persist and broadcast the retrieval's side effects: the injected block on
77
+ * the user message's metadata (so it survives reloads), a recall-log row, and
78
+ * the `memory_recalled` debug event. All three are best-effort — a failure to
79
+ * persist must not abort the turn.
80
+ */
81
+ function recordRecallSideEffects(
82
+ graphResult: GraphMemoryResult,
83
+ ctx: MemoryRetrievalHookContext,
84
+ ): void {
85
+ // Persist the injected block text in message metadata so it survives
86
+ // conversation reloads (eviction, restart, fork). loadFromDb re-injects
87
+ // from metadata.
88
+ if (graphResult.injectedBlockText) {
89
+ try {
90
+ updateMessageMetadata(ctx.userMessageId, {
91
+ memoryInjectedBlock: graphResult.injectedBlockText,
92
+ });
93
+ } catch (err) {
94
+ ctx.logger.warn(
95
+ { err },
96
+ "Failed to persist memory injection to metadata (non-fatal)",
97
+ );
98
+ }
99
+ }
100
+
101
+ const m = graphResult.metrics;
102
+
103
+ try {
104
+ recordMemoryRecallLog({
105
+ conversationId: ctx.conversationId,
106
+ enabled: true,
107
+ degraded: false,
108
+ provider: m?.embeddingProvider ?? undefined,
109
+ model: m?.embeddingModel ?? undefined,
110
+ semanticHits: m?.semanticHits ?? 0,
111
+ mergedCount: m?.mergedCount ?? 0,
112
+ selectedCount: m?.selectedCount ?? 0,
113
+ tier1Count: m?.tier1Count ?? 0,
114
+ tier2Count: m?.tier2Count ?? 0,
115
+ hybridSearchLatencyMs: m?.hybridSearchLatencyMs ?? 0,
116
+ sparseVectorUsed: m?.sparseVectorUsed ?? false,
117
+ injectedTokens: graphResult.injectedTokens,
118
+ latencyMs: graphResult.latencyMs,
119
+ topCandidatesJson: (m?.topCandidates ?? []).map((c) => ({
120
+ key: c.nodeId,
121
+ type: c.type,
122
+ kind: "graph",
123
+ finalScore: c.score,
124
+ semantic: c.semanticSimilarity,
125
+ recency: c.recencyBoost,
126
+ })),
127
+ injectedText: graphResult.injectedBlockText ?? undefined,
128
+ reason: `graph:${graphResult.mode}`,
129
+ queryContext: m?.queryContext ?? undefined,
130
+ });
131
+ } catch (err) {
132
+ ctx.logger.warn({ err }, "Failed to persist memory recall log (non-fatal)");
133
+ }
134
+
135
+ if (m) {
136
+ const memoryRecalledEvent: MemoryRecalled = {
137
+ type: "memory_recalled",
138
+ provider: m.embeddingProvider ?? "unknown",
139
+ model: m.embeddingModel ?? "unknown",
140
+ semanticHits: m.semanticHits,
141
+ mergedCount: m.mergedCount,
142
+ selectedCount: m.selectedCount,
143
+ tier1Count: m.tier1Count,
144
+ tier2Count: m.tier2Count,
145
+ hybridSearchLatencyMs: m.hybridSearchLatencyMs,
146
+ sparseVectorUsed: m.sparseVectorUsed,
147
+ injectedTokens: graphResult.injectedTokens,
148
+ latencyMs: graphResult.latencyMs,
149
+ topCandidates: m.topCandidates.map((c) => ({
150
+ key: c.nodeId,
151
+ type: c.type,
152
+ kind: "graph",
153
+ finalScore: c.score,
154
+ semantic: c.semanticSimilarity,
155
+ recency: c.recencyBoost,
156
+ })),
157
+ };
158
+ ctx.onEvent(memoryRecalledEvent);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Run the default retrieval, writing `latestMessages` back onto the context and
164
+ * recording the PKB query-vector pair on the graph handle. Skips the
165
+ * memory-graph call entirely (leaving `latestMessages` as the seeded input
166
+ * messages and no query pair recorded) when the actor is not trusted.
167
+ *
168
+ * Memory retrieval blocks the turn — there is no soft timeout here. Memory is
169
+ * critical context, and silently dropping it produces a worse outcome than a
170
+ * slower turn. Cancellation still works via `ctx.signal`, which is threaded
171
+ * into `prepareMemory`.
172
+ */
173
+ const userPromptSubmitMemoryRetrieval: PluginHookFn<
174
+ MemoryRetrievalHookContext
175
+ > = async (ctx) => {
176
+ if (!ctx.isTrustedActor) {
177
+ // Untrusted actors skip memory-graph retrieval entirely.
178
+ return;
179
+ }
180
+
181
+ const graphResult = await ctx.graphMemory.prepareMemory(
182
+ ctx.latestMessages,
183
+ ctx.config,
184
+ ctx.signal,
185
+ ctx.onEvent,
186
+ );
187
+
188
+ recordRecallSideEffects(graphResult, ctx);
189
+
190
+ ctx.latestMessages = graphResult.runMessages;
191
+ // Select dense+sparse as a matched pair so RRF fusion combines two signals
192
+ // aligned to the same query text:
193
+ // 1. Context-load with a user query: user-query dense + user-query sparse
194
+ // — the cleanest pairing.
195
+ // 2. Otherwise (context-load without a user query, or per-turn): whatever
196
+ // `queryVector` / `sparseVector` the retriever produced, which are
197
+ // themselves co-aligned (both summary-derived in context-load, both
198
+ // user-last-message-derived in per-turn).
199
+ // Never pair a user-query dense with a summary-aligned sparse.
200
+ // The PKB-reminder injector reads this pair back off the same graph handle
201
+ // (looked up by conversation id) rather than receiving it threaded through
202
+ // the agent loop.
203
+ if (graphResult.userQueryVector) {
204
+ ctx.graphMemory.recordPkbQueryVectors(
205
+ graphResult.userQueryVector,
206
+ graphResult.userQuerySparseVector,
207
+ );
208
+ } else {
209
+ ctx.graphMemory.recordPkbQueryVectors(
210
+ graphResult.queryVector,
211
+ graphResult.sparseVector,
212
+ );
213
+ }
214
+ };
215
+
216
+ export default userPromptSubmitMemoryRetrieval;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The assembled runtime injector chain.
3
+ *
4
+ * Injection is not a plugin contribution: the first-party injectors are
5
+ * imported directly and sorted once, by ascending `order`, into the single
6
+ * sequence `applyRuntimeInjections` walks each turn. This co-locates the
7
+ * chain with the memory-retrieval domain it serves, rather than aggregating
8
+ * injectors out of the plugin registry.
9
+ *
10
+ * The chain combines the default injectors ({@link defaultInjectors}) with the
11
+ * memory-v3 injector ({@link memoryV3Injector}). The sort mirrors the previous
12
+ * registry aggregation (`Array.prototype.sort` is stable, so injectors sharing
13
+ * an `order` keep their listed order), so the produced sequence — and therefore
14
+ * the injected content — is identical.
15
+ *
16
+ * The chain is assembled lazily on first access and memoized, so the sort runs
17
+ * once per process rather than per turn and module evaluation stays free of any
18
+ * ordering assumptions about when `defaultInjectors` finishes initializing.
19
+ */
20
+
21
+ import { memoryV3Injector } from "../../../memory/v3/shadow-plugin.js";
22
+ import type { Injector } from "../../types.js";
23
+ import { defaultInjectors } from "../injectors/register.js";
24
+
25
+ let cachedChain: Injector[] | null = null;
26
+
27
+ /** The order-sorted runtime injector chain, assembled once and memoized. */
28
+ export function getInjectorChain(): Injector[] {
29
+ if (cachedChain === null) {
30
+ cachedChain = [...defaultInjectors, memoryV3Injector].sort(
31
+ (a, b) => a.order - b.order,
32
+ );
33
+ }
34
+ return cachedChain;
35
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Default `stop` hook: triggers the second-pass conversation-title
3
+ * regeneration once a conversation has accumulated enough context.
4
+ *
5
+ * The first title is generated from the opening prompt alone (see
6
+ * `./user-prompt-submit.ts`). After a few exchanges the conversation's real
7
+ * topic is usually clearer, so a single second pass re-titles using the most
8
+ * recent messages. This hook is the trigger — it fires the regeneration when
9
+ * the conversation reaches its third user turn — and delegates the title
10
+ * itself to the service (`memory/conversation-title-service.ts`), which
11
+ * re-checks that the title is still auto-generated, resolves the title
12
+ * provider, persists, and broadcasts the `conversation_title_updated` /
13
+ * `sync_changed` events.
14
+ *
15
+ * Turn count is read from history rather than an external counter: the number
16
+ * of genuine user prompts — user-role messages that aren't purely tool results
17
+ * — is the conversation's turn number. Deriving it from history keeps the hook
18
+ * stateless and means a mid-run array rewrite (compaction) can't invalidate it.
19
+ */
20
+
21
+ import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
22
+
23
+ import { getConfig } from "../../../../config/loader.js";
24
+ import { queueRegenerateConversationTitle } from "../../../../memory/conversation-title-service.js";
25
+ import type { Message } from "../../../../providers/types.js";
26
+
27
+ /**
28
+ * User turn at which the second title pass fires. Matches the
29
+ * `conversations.skipAutoRetitling` opt-out, documented as skipping the
30
+ * regeneration "that fires after the third user turn".
31
+ */
32
+ const SECOND_PASS_USER_TURN = 3;
33
+
34
+ /** A user-role message carrying only tool results, not a fresh prompt. */
35
+ function isToolResultMessage(message: Message): boolean {
36
+ return (
37
+ message.role === "user" &&
38
+ message.content.length > 0 &&
39
+ message.content.every((block) => block.type === "tool_result")
40
+ );
41
+ }
42
+
43
+ /** Count of genuine user prompts in history — the conversation's turn number. */
44
+ function countUserTurns(messages: ReadonlyArray<Message>): number {
45
+ let turns = 0;
46
+ for (const message of messages) {
47
+ if (message.role === "user" && !isToolResultMessage(message)) turns++;
48
+ }
49
+ return turns;
50
+ }
51
+
52
+ const stop: PluginHookFn<StopContext> = async (ctx) => {
53
+ // Only re-title at a genuine turn end. A `"continue"` decision means an
54
+ // earlier hook is re-querying the model (e.g. an empty-response nudge), so
55
+ // defer to the eventual terminal stop.
56
+ if (ctx.decision !== "stop") return;
57
+
58
+ if (getConfig().conversations.skipAutoRetitling) return;
59
+
60
+ if (countUserTurns(ctx.messages) !== SECOND_PASS_USER_TURN) return;
61
+
62
+ const { conversationId } = ctx;
63
+ // Deferred to a later macrotask so the just-completed turn lands first. The
64
+ // hook fires at the stop boundary, before the loop appends the turn's
65
+ // assistant reply to history and emits `message_complete` (which persists
66
+ // it). The service regenerates from the most recent stored messages, so it
67
+ // must run after the reply is persisted to reflect it. The service is itself
68
+ // fire-and-forget and re-checks replaceability, owning provider resolution,
69
+ // persistence, and the resulting broadcast.
70
+ setTimeout(() => {
71
+ queueRegenerateConversationTitle({ conversationId });
72
+ }, 0);
73
+ };
74
+
75
+ export default stop;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Default `user-prompt-submit` hook: kicks off conversation-title generation
3
+ * from the submitted prompt.
4
+ *
5
+ * Title generation is a self-contained side effect that only needs the user's
6
+ * prompt as context, so it belongs at the prompt-submit boundary rather than
7
+ * threaded through the agent loop. The hook is a pure trigger — it schedules
8
+ * the work and returns; persistence and the resulting
9
+ * `conversation_title_updated` / `sync_changed` broadcast are owned by the
10
+ * title service (see `memory/conversation-title-service.ts`).
11
+ */
12
+
13
+ import type {
14
+ PluginHookFn,
15
+ UserPromptSubmitContext,
16
+ } from "@vellumai/plugin-api";
17
+
18
+ import { queueGenerateConversationTitle } from "../../../../memory/conversation-title-service.js";
19
+
20
+ const userPromptSubmit: PluginHookFn<UserPromptSubmitContext> = async (ctx) => {
21
+ // Deferred to a later macrotask so the main agent-loop LLM request is
22
+ // issued first; on strict single-slot provider configs this keeps the
23
+ // background title call from claiming the rate-limit slot ahead of the
24
+ // user-visible response. The title service is itself fire-and-forget and
25
+ // re-checks title replaceability before making any LLM call, so an
26
+ // already-titled conversation incurs no generation.
27
+ setTimeout(() => {
28
+ queueGenerateConversationTitle({
29
+ conversationId: ctx.conversationId,
30
+ userMessage: ctx.prompt,
31
+ });
32
+ }, 0);
33
+ };
34
+
35
+ export default userPromptSubmit;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "default-title-generate",
3
3
  "version": "1.0.0",
4
- "description": "First-party default plugin wrapping the assistant's built-in title-generate pipeline with a passthrough implementation.",
4
+ "description": "First-party default plugin that triggers conversation-title generation from the user-prompt-submit hook.",
5
5
  "private": true,
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -1,11 +1,17 @@
1
1
  /**
2
- * Default `titleGenerate` pipeline plugin.
2
+ * Default `title-generate` plugin.
3
3
  *
4
- * Declares no middleware the terminal handler in `./terminal.ts` is wired in
5
- * as the pipeline's `terminal` argument by the `runPipeline` call site in
6
- * `daemon/conversation-agent-loop.ts`. This plugin exists purely to negotiate
7
- * the `titleGenerateApi` capability so bootstrap has a record that the
8
- * assistant runtime exposes this pipeline.
4
+ * Contributes two hooks, both pure triggers that delegate the title work to
5
+ * the service (`memory/conversation-title-service.ts`):
6
+ *
7
+ * - `user-prompt-submit` (`./hooks/user-prompt-submit.ts`) first-pass title
8
+ * generation from the submitted prompt.
9
+ * - `stop` (`./hooks/stop.ts`) — second-pass regeneration once the
10
+ * conversation reaches its third user turn, for a title that reflects the
11
+ * established topic.
12
+ *
13
+ * Persistence and the resulting `conversation_title_updated` / `sync_changed`
14
+ * broadcast are owned by the title service.
9
15
  *
10
16
  * Registered via a side-effect import from
11
17
  * `daemon/external-plugins-bootstrap.ts` so it is present in the registry
@@ -13,23 +19,17 @@
13
19
  */
14
20
 
15
21
  import { type Plugin } from "../../types.js";
22
+ import stop from "./hooks/stop.js";
23
+ import userPromptSubmit from "./hooks/user-prompt-submit.js";
16
24
  import pkg from "./package.json" with { type: "json" };
17
25
 
18
- /**
19
- * Default titleGenerate plugin. Declares no middleware — it exists purely
20
- * to negotiate the `titleGenerateApi` capability so bootstrap has a record
21
- * that the assistant runtime exposes this pipeline.
22
- *
23
- * The terminal handler (`./terminal.ts`) is supplied at the call site in
24
- * `conversation-agent-loop.ts` rather than through `middleware.titleGenerate`,
25
- * because a default middleware would short-circuit user-registered middleware
26
- * by always running first in onion order. Keeping the terminal outside the
27
- * middleware chain lets user plugins observe/transform/short-circuit the
28
- * call without competing with an assistant-owned default middleware.
29
- */
30
26
  export const defaultTitleGeneratePlugin: Plugin = {
31
27
  manifest: {
32
28
  name: pkg.name,
33
29
  version: pkg.version,
34
30
  },
31
+ hooks: {
32
+ "user-prompt-submit": userPromptSubmit,
33
+ stop,
34
+ },
35
35
  };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Default `post-tool-use` hook: when a tool result carries `is_error`, set
3
+ * `additionalContext` with a system-notice that coaches the model to either
4
+ * retry with corrected parameters (for recoverable errors) or report the
5
+ * failure to the user (for unrecoverable ones).
6
+ *
7
+ * The coaching is delivered via `additionalContext`, not by mutating the tool
8
+ * result's `content`. The loop appends it to the provider-bound history as a
9
+ * separate block after the tool_result event is emitted, so the model sees the
10
+ * guidance while the client-facing and persisted tool output stay the tool's
11
+ * actual result. This mirrors how Claude Code (`additionalContext`) and Codex
12
+ * (`additional_contexts`) surface PostToolUse feedback as separate context
13
+ * rather than rewriting the tool response.
14
+ *
15
+ * The coaching is bounded per tool: once a single tool has failed
16
+ * `MAX_CONSECUTIVE_ERROR_NUDGES` times in a row the notice is dropped — the
17
+ * error is likely not something the model can fix on its own, and continuing
18
+ * to coach a retry only burns tokens. The consecutive-failure count is derived
19
+ * from the conversation history (the trailing run of error results for this
20
+ * tool name, plus the current one) rather than a loop-held counter, so the
21
+ * guard survives mid-run compaction rewriting the history array. A successful
22
+ * result for the tool resets its streak.
23
+ */
24
+
25
+ import type { PluginHookFn, PostToolUseContext } from "@vellumai/plugin-api";
26
+
27
+ import type { Message } from "../../../../providers/types.js";
28
+
29
+ /**
30
+ * Canonical tool-error coaching text. Kept as a module-level constant so tests
31
+ * and plugins that wrap the default can match it without duplicating the
32
+ * string.
33
+ *
34
+ * This is shown to the model as provider-only context, not the user. Edits
35
+ * here affect retry behavior but not end-user UX directly.
36
+ */
37
+ export const TOOL_ERROR_NUDGE_TEXT =
38
+ "<system_notice>This tool call returned an error. If the error looks recoverable (e.g. missing or invalid parameters), fix the parameters and retry. If the error is clearly unrecoverable (e.g. a service is down, a resource does not exist, or a permission is permanently denied), report it to the user.</system_notice>";
39
+
40
+ /**
41
+ * Number of back-to-back failures of a single tool to coach before giving up.
42
+ * Coaching fires on the 1st through Nth consecutive failure and is dropped from
43
+ * the (N+1)th onward.
44
+ */
45
+ const MAX_CONSECUTIVE_ERROR_NUDGES = 3;
46
+
47
+ /** Map every `tool_use` block id in history to the tool name it invoked. */
48
+ function toolNamesById(messages: ReadonlyArray<Message>): Map<string, string> {
49
+ const names = new Map<string, string>();
50
+ for (const message of messages) {
51
+ if (message.role !== "assistant") continue;
52
+ for (const block of message.content) {
53
+ if (block.type === "tool_use") names.set(block.id, block.name);
54
+ }
55
+ }
56
+ return names;
57
+ }
58
+
59
+ /**
60
+ * Trailing run of consecutive error results for `toolName` already in history.
61
+ * Walks the tool's results in chronological order and counts back from the most
62
+ * recent until a successful result breaks the streak. The current result is not
63
+ * yet in history, so callers add it themselves.
64
+ */
65
+ function priorConsecutiveErrors(
66
+ messages: ReadonlyArray<Message>,
67
+ toolName: string,
68
+ namesById: ReadonlyMap<string, string>,
69
+ ): number {
70
+ const isErrorByOrder: boolean[] = [];
71
+ for (const message of messages) {
72
+ if (message.role !== "user") continue;
73
+ for (const block of message.content) {
74
+ if (block.type !== "tool_result") continue;
75
+ if (namesById.get(block.tool_use_id) !== toolName) continue;
76
+ isErrorByOrder.push(block.is_error === true);
77
+ }
78
+ }
79
+
80
+ let streak = 0;
81
+ for (let i = isErrorByOrder.length - 1; i >= 0; i--) {
82
+ if (!isErrorByOrder[i]) break;
83
+ streak++;
84
+ }
85
+ return streak;
86
+ }
87
+
88
+ const postToolUse: PluginHookFn<PostToolUseContext> = async (ctx) => {
89
+ if (ctx.toolResponse.is_error !== true) return;
90
+
91
+ const namesById = toolNamesById(ctx.messages);
92
+ const toolName = namesById.get(ctx.toolResponse.tool_use_id);
93
+
94
+ // Prior failures of this tool plus the current one. An unresolved name (the
95
+ // current turn's tool_use is always in history, so this is defensive) falls
96
+ // back to coaching this lone failure.
97
+ const consecutiveErrors =
98
+ (toolName === undefined
99
+ ? 0
100
+ : priorConsecutiveErrors(ctx.messages, toolName, namesById)) + 1;
101
+
102
+ if (consecutiveErrors > MAX_CONSECUTIVE_ERROR_NUDGES) {
103
+ ctx.logger.info(
104
+ {
105
+ plugin: "tool-error",
106
+ toolName,
107
+ toolUseId: ctx.toolResponse.tool_use_id,
108
+ consecutiveErrors,
109
+ },
110
+ "Skipping tool-error coaching after repeated consecutive failures",
111
+ );
112
+ return;
113
+ }
114
+
115
+ ctx.additionalContext = TOOL_ERROR_NUDGE_TEXT;
116
+ };
117
+
118
+ export default postToolUse;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "default-tool-error",
3
3
  "version": "1.0.0",
4
- "description": "First-party default plugin wrapping the assistant's built-in tool-error pipeline with a passthrough implementation.",
4
+ "description": "First-party default plugin contributing a post-tool-use hook that coaches the model to retry or report a failed tool call.",
5
5
  "private": true,
6
6
  "license": "MIT",
7
7
  "type": "module",