@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
@@ -4,6 +4,7 @@ import { describe, expect, test } from "bun:test";
4
4
  import { eq } from "drizzle-orm";
5
5
  import { drizzle } from "drizzle-orm/bun-sqlite";
6
6
 
7
+ import { migrateScheduleSourceConversation } from "../memory/migrations/270-schedule-source-conversation.js";
7
8
  import * as schema from "../memory/schema.js";
8
9
  import { scheduleJobs } from "../memory/schema.js";
9
10
 
@@ -38,6 +39,7 @@ describe("schedule_syntax column migration", () => {
38
39
  retry_count INTEGER NOT NULL DEFAULT 0,
39
40
  max_retries INTEGER NOT NULL DEFAULT 3,
40
41
  retry_backoff_ms INTEGER NOT NULL DEFAULT 60000,
42
+ created_from_conversation_id TEXT,
41
43
  created_by TEXT NOT NULL,
42
44
  mode TEXT NOT NULL DEFAULT 'execute',
43
45
  routing_intent TEXT NOT NULL DEFAULT 'all_channels',
@@ -134,6 +136,7 @@ describe("schedule_syntax column migration", () => {
134
136
  } catch {
135
137
  /* already exists */
136
138
  }
139
+ migrateScheduleSourceConversation(db);
137
140
 
138
141
  const row = db
139
142
  .select()
@@ -197,6 +200,8 @@ describe("schedule_syntax column migration", () => {
197
200
  } catch {
198
201
  /* ok */
199
202
  }
203
+ migrateScheduleSourceConversation(db);
204
+ migrateScheduleSourceConversation(db);
200
205
 
201
206
  const now = Date.now();
202
207
  raw.exec(
@@ -94,10 +94,14 @@ function createSlackTurnContext(): MessagingConversationContext {
94
94
  drain: () => [],
95
95
  size: () => 0,
96
96
  } as unknown as MessageQueue;
97
+ let processing = false;
97
98
  return {
98
99
  conversationId: "conv-dm-test",
99
100
  messages: [],
100
- processing: false,
101
+ isProcessing: () => processing,
102
+ setProcessing: (value: boolean) => {
103
+ processing = value;
104
+ },
101
105
  abortController: null,
102
106
  queue: queueStub,
103
107
  getTurnChannelContext: () => channel,
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Tests for the default `empty-response` plugin's `stop` hook.
3
+ *
4
+ * Covers:
5
+ * - The default hook's binary decision for the canonical cases: empty turn
6
+ * after a prior tool-use turn → continue (with the canonical nudge text);
7
+ * visible text → stop; prior turn already delivered visible text → stop;
8
+ * first model call with no prior turn → stop; provider refusal → continue
9
+ * (with the refusal-specific nudge text); refusal-but-recovered → stop.
10
+ * - The hook scopes its prior-turn signals to the current response cycle (the
11
+ * messages after the last genuine user prompt), so visible text from the
12
+ * inbound conversation does not suppress the nudge.
13
+ * - When the hook continues, it appends the nudge as a `user` message to
14
+ * `messages`.
15
+ * - End-to-end through `runHook` + the registry: registering the default
16
+ * plugin makes the hook fire, and a later-registered user hook chains after
17
+ * it and can read/override the decision.
18
+ *
19
+ * The loop's actual side-effects (retry-budget cap, history splice, log
20
+ * emission) live in `agent/loop.ts` and are covered by integration tests in
21
+ * `conversation-agent-loop.test.ts` / `agent-loop.test.ts`. This file isolates
22
+ * the hook.
23
+ */
24
+
25
+ import { beforeEach, describe, expect, test } from "bun:test";
26
+
27
+ import { HOOKS } from "../plugin-api/constants.js";
28
+ import type { PluginLogger, StopContext } from "../plugin-api/types.js";
29
+ import {
30
+ NUDGE_TEXT,
31
+ REFUSAL_NUDGE_TEXT,
32
+ } from "../plugins/defaults/empty-response/hooks/stop.js";
33
+ import stop from "../plugins/defaults/empty-response/hooks/stop.js";
34
+ import { defaultEmptyResponsePlugin } from "../plugins/defaults/empty-response/register.js";
35
+ import { runHook } from "../plugins/pipeline.js";
36
+ import {
37
+ registerPlugin,
38
+ resetPluginRegistryForTests,
39
+ } from "../plugins/registry.js";
40
+ import type { ContentBlock, Message } from "../providers/types.js";
41
+
42
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
43
+
44
+ const noopLogger: PluginLogger = {
45
+ info: () => {},
46
+ warn: () => {},
47
+ error: () => {},
48
+ debug: () => {},
49
+ };
50
+
51
+ const emptyTextBlock: ContentBlock = { type: "text", text: " " };
52
+
53
+ /** A prior assistant turn that issued a tool call but no visible text. */
54
+ const priorToolUseTurn: Message = {
55
+ role: "assistant",
56
+ content: [{ type: "tool_use", id: "tu_1", name: "read_file", input: {} }],
57
+ };
58
+
59
+ /** A prior assistant turn that delivered visible text to the user. */
60
+ const priorVisibleTextTurn: Message = {
61
+ role: "assistant",
62
+ content: [{ type: "text", text: "here is what I found earlier" }],
63
+ };
64
+
65
+ /** A genuine user prompt — the boundary that opens a new response cycle. */
66
+ function userPrompt(text: string): Message {
67
+ return { role: "user", content: [{ type: "text", text }] };
68
+ }
69
+
70
+ function makeCtx(overrides: Partial<StopContext> = {}): StopContext {
71
+ return {
72
+ conversationId: "conv-stop",
73
+ messages: [],
74
+ responseContent: [],
75
+ stopReason: null,
76
+ decision: "stop",
77
+ logger: noopLogger,
78
+ ...overrides,
79
+ };
80
+ }
81
+
82
+ // ─── Default decisions ───────────────────────────────────────────────────────
83
+
84
+ describe("empty-response stop hook — default decisions", () => {
85
+ test("empty turn after a prior tool-use turn → continue with canonical nudge", async () => {
86
+ // GIVEN a run that already issued a tool call, then returned an empty
87
+ // (whitespace-only) assistant turn with no visible text.
88
+ const ctx = makeCtx({
89
+ messages: [priorToolUseTurn],
90
+ responseContent: [emptyTextBlock],
91
+ });
92
+
93
+ // WHEN the default stop hook runs.
94
+ await stop(ctx);
95
+
96
+ // THEN it asks the loop to continue and appends the canonical nudge.
97
+ expect(ctx.decision).toBe("continue");
98
+ expect(ctx.messages.at(-1)).toEqual({
99
+ role: "user",
100
+ content: [{ type: "text", text: NUDGE_TEXT }],
101
+ });
102
+ });
103
+
104
+ test("turn contains visible text → stop", async () => {
105
+ // GIVEN a turn that delivered visible text.
106
+ const ctx = makeCtx({
107
+ messages: [priorToolUseTurn],
108
+ responseContent: [{ type: "text", text: "here is a summary" }],
109
+ });
110
+
111
+ // WHEN the hook runs.
112
+ await stop(ctx);
113
+
114
+ // THEN the decision stays at stop and nothing is appended.
115
+ expect(ctx.decision).toBe("stop");
116
+ expect(ctx.messages).toEqual([priorToolUseTurn]);
117
+ });
118
+
119
+ test("prior turn already delivered visible text → stop", async () => {
120
+ // GIVEN the model said its piece in an earlier turn this run, then ended
121
+ // with a side-effect tool and returned empty. Nudging would force a
122
+ // verbatim re-send of text the user already saw.
123
+ const ctx = makeCtx({
124
+ messages: [userPrompt("do X"), priorVisibleTextTurn, priorToolUseTurn],
125
+ responseContent: [],
126
+ });
127
+
128
+ // WHEN the hook runs.
129
+ await stop(ctx);
130
+
131
+ // THEN the decision stays at stop.
132
+ expect(ctx.decision).toBe("stop");
133
+ });
134
+
135
+ test("visible text before the last user prompt is ignored → continue", async () => {
136
+ // GIVEN the inbound conversation already contains an assistant turn with
137
+ // visible text, but it precedes this run's user prompt. The current cycle
138
+ // (after that prompt) holds only a tool-use turn, so the earlier text
139
+ // belongs to the prior conversation and must not suppress the nudge.
140
+ const ctx = makeCtx({
141
+ messages: [
142
+ priorVisibleTextTurn,
143
+ userPrompt("do the next thing"),
144
+ priorToolUseTurn,
145
+ ],
146
+ responseContent: [],
147
+ });
148
+
149
+ // WHEN the hook runs.
150
+ await stop(ctx);
151
+
152
+ // THEN it continues — the cycle scope sees only the tool-use turn, not the
153
+ // inbound conversation's visible text.
154
+ expect(ctx.decision).toBe("continue");
155
+ expect(ctx.messages.at(-1)).toEqual({
156
+ role: "user",
157
+ content: [{ type: "text", text: NUDGE_TEXT }],
158
+ });
159
+ });
160
+
161
+ test("first model call with no prior turn → stop", async () => {
162
+ // GIVEN an empty first assistant response with no prior turn this run and
163
+ // no refusal — not the pattern the organic-empty-turn nudge guards against.
164
+ const ctx = makeCtx({ messages: [], responseContent: [] });
165
+
166
+ // WHEN the hook runs.
167
+ await stop(ctx);
168
+
169
+ // THEN the decision stays at stop.
170
+ expect(ctx.decision).toBe("stop");
171
+ });
172
+
173
+ // ─── Refusal stop ──────────────────────────────────────────────────────────
174
+
175
+ test("refusal on the first call with no content → continue with refusal nudge", async () => {
176
+ // GIVEN the canonical failure mode: the provider's safety classifier zeros
177
+ // the response on the very first model call, returning `stopReason:
178
+ // "refusal"` and no visible text.
179
+ const ctx = makeCtx({
180
+ messages: [],
181
+ responseContent: [],
182
+ stopReason: "refusal",
183
+ });
184
+
185
+ // WHEN the hook runs.
186
+ await stop(ctx);
187
+
188
+ // THEN it continues with the refusal-specific nudge, even with no prior turn.
189
+ expect(ctx.decision).toBe("continue");
190
+ expect(ctx.messages.at(-1)).toEqual({
191
+ role: "user",
192
+ content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
193
+ });
194
+ });
195
+
196
+ test("refusal with a thinking-only block still continues", async () => {
197
+ // GIVEN a refusal whose only content is a thinking block — the user sees
198
+ // nothing.
199
+ const ctx = makeCtx({
200
+ messages: [],
201
+ responseContent: [
202
+ { type: "thinking", thinking: "...", signature: "sig" },
203
+ ],
204
+ stopReason: "refusal",
205
+ });
206
+
207
+ // WHEN the hook runs.
208
+ await stop(ctx);
209
+
210
+ // THEN it continues with the refusal-specific nudge.
211
+ expect(ctx.decision).toBe("continue");
212
+ expect(ctx.messages.at(-1)).toEqual({
213
+ role: "user",
214
+ content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
215
+ });
216
+ });
217
+
218
+ test("refusal but visible text present → stop (model recovered)", async () => {
219
+ // GIVEN a refusal that still delivered some visible text before refusing —
220
+ // the user has something to see.
221
+ const ctx = makeCtx({
222
+ responseContent: [{ type: "text", text: "partial answer" }],
223
+ stopReason: "refusal",
224
+ });
225
+
226
+ // WHEN the hook runs.
227
+ await stop(ctx);
228
+
229
+ // THEN the decision stays at stop.
230
+ expect(ctx.decision).toBe("stop");
231
+ });
232
+
233
+ test("refusal beats the post-tool-empty nudge wording", async () => {
234
+ // GIVEN conditions that would trip both the refusal branch and the
235
+ // post-tool-empty branch.
236
+ const ctx = makeCtx({
237
+ messages: [priorToolUseTurn],
238
+ responseContent: [],
239
+ stopReason: "refusal",
240
+ });
241
+
242
+ // WHEN the hook runs.
243
+ await stop(ctx);
244
+
245
+ // THEN refusal wins because its wording is more accurate.
246
+ expect(ctx.decision).toBe("continue");
247
+ expect(ctx.messages.at(-1)).toEqual({
248
+ role: "user",
249
+ content: [{ type: "text", text: REFUSAL_NUDGE_TEXT }],
250
+ });
251
+ });
252
+ });
253
+
254
+ // ─── Via runHook + registry ──────────────────────────────────────────────────
255
+
256
+ describe("empty-response stop hook — via runHook", () => {
257
+ beforeEach(() => {
258
+ resetPluginRegistryForTests();
259
+ });
260
+
261
+ test("registering the default plugin makes the hook continue on empty-after-tools", async () => {
262
+ // GIVEN the default empty-response plugin is registered.
263
+ registerPlugin(defaultEmptyResponsePlugin);
264
+
265
+ // WHEN the stop chain runs over an empty-after-tools context.
266
+ const result = await runHook<StopContext>(
267
+ HOOKS.STOP,
268
+ makeCtx({ messages: [priorToolUseTurn], responseContent: [] }),
269
+ );
270
+
271
+ // THEN the chain settles on continue with the canonical nudge appended.
272
+ expect(result.decision).toBe("continue");
273
+ expect(result.messages.at(-1)).toEqual({
274
+ role: "user",
275
+ content: [{ type: "text", text: NUDGE_TEXT }],
276
+ });
277
+ });
278
+
279
+ test("a later-registered user hook can override the default decision", async () => {
280
+ // GIVEN the default plugin registers first, then a user plugin whose hook
281
+ // observes the default's decision and forces a stop.
282
+ let observedDecision: string | null = null;
283
+ registerPlugin(defaultEmptyResponsePlugin);
284
+ registerPlugin({
285
+ manifest: { name: "force-stop", version: "0.0.1" },
286
+ hooks: {
287
+ stop: async (ctx: StopContext) => {
288
+ observedDecision = ctx.decision;
289
+ ctx.decision = "stop";
290
+ },
291
+ },
292
+ });
293
+
294
+ // WHEN the chain runs over an empty-after-tools context.
295
+ const result = await runHook<StopContext>(
296
+ HOOKS.STOP,
297
+ makeCtx({ messages: [priorToolUseTurn], responseContent: [] }),
298
+ );
299
+
300
+ // THEN the user hook saw the default's continue, and its override wins.
301
+ expect(observedDecision as string | null).toBe("continue");
302
+ expect(result.decision).toBe("stop");
303
+ });
304
+ });
@@ -19,7 +19,7 @@
19
19
  // Mirrors `src/config/feature-flag-cache.ts`. Duplicated by design — see
20
20
  // the "No source-module imports" section above.
21
21
  type FlagSlot = {
22
- overrides: Record<string, boolean> | null;
22
+ overrides: Record<string, boolean | string> | null;
23
23
  fromGateway: boolean;
24
24
  };
25
25
 
@@ -45,7 +45,7 @@ function flagSlot(): FlagSlot {
45
45
  * `clearFeatureFlagOverridesCache()` from `assistant-feature-flags.ts`.
46
46
  */
47
47
  export function setOverridesForTesting(
48
- overrides: Record<string, boolean>,
48
+ overrides: Record<string, boolean | string>,
49
49
  ): void {
50
50
  const s = flagSlot();
51
51
  s.overrides = { ...overrides };
@@ -287,6 +287,19 @@ describe("mapGeminiError", () => {
287
287
  expect(msg).toContain("Rate limit");
288
288
  });
289
289
 
290
+ test("maps 402 status to a non-retryable out-of-credits message", () => {
291
+ const msg = mapGeminiError(new FakeApiError(402, "payment required"));
292
+ expect(msg).toContain("out of credits");
293
+ expect(msg).not.toContain("Please try again");
294
+ });
295
+
296
+ test("maps a managed-proxy 402 failure (plain Error) to out-of-credits message", () => {
297
+ const msg = mapGeminiError(
298
+ new Error("Managed proxy request failed (402): insufficient credits"),
299
+ );
300
+ expect(msg).toContain("out of credits");
301
+ });
302
+
290
303
  test("maps 500 status to server error message", () => {
291
304
  const msg = mapGeminiError(new FakeApiError(500, "internal"));
292
305
  expect(msg).toContain("temporarily unavailable");
@@ -0,0 +1,110 @@
1
+ // Type-only import: this binds the mock to the real `Provider` contract so it
2
+ // can be passed to a live `AgentLoop` without casts. It must stay `import type`
3
+ // — a value import would pull `providers/types.ts`'s runtime exports into this
4
+ // shared helper, which the test-machinery isolation rule forbids.
5
+ import type {
6
+ Message,
7
+ Provider,
8
+ ProviderResponse,
9
+ SendMessageOptions,
10
+ ToolDefinition,
11
+ } from "../../providers/types.js";
12
+
13
+ /**
14
+ * Records the arguments of a single `provider.sendMessage` invocation so tests
15
+ * can assert on what the agent loop sent (messages, tools, system prompt, the
16
+ * resolved options bag).
17
+ */
18
+ export interface RecordedProviderCall {
19
+ messages: Message[];
20
+ tools?: ToolDefinition[];
21
+ systemPrompt?: string;
22
+ options?: SendMessageOptions;
23
+ }
24
+
25
+ /**
26
+ * One scripted provider turn. A `ProviderResponse` is returned normally; an
27
+ * `Error` is thrown to simulate a provider HTTP rejection (e.g. a
28
+ * context-too-large error the orchestrator must recover from).
29
+ */
30
+ export type ScriptedResponse = ProviderResponse | Error;
31
+
32
+ /**
33
+ * A mock provider that returns pre-configured responses in sequence.
34
+ *
35
+ * Drives the real {@link import("../../agent/loop.js").AgentLoop} by mocking
36
+ * only the provider HTTP boundary: each call returns the next scripted
37
+ * `ProviderResponse` (the last response repeats once the script is exhausted)
38
+ * and replays its text blocks as `text_delta` events so the loop streams
39
+ * exactly as it would against a live provider. A scripted `Error` entry is
40
+ * thrown instead of returned, so a rejection can be sequenced before a
41
+ * recovery response.
42
+ */
43
+ export function createMockProvider(
44
+ responses: ScriptedResponse[],
45
+ name = "mock",
46
+ ): {
47
+ provider: Provider;
48
+ calls: RecordedProviderCall[];
49
+ } {
50
+ const calls: RecordedProviderCall[] = [];
51
+ let callIndex = 0;
52
+
53
+ const provider: Provider = {
54
+ name,
55
+ async sendMessage(
56
+ messages: Message[],
57
+ options?: SendMessageOptions,
58
+ ): Promise<ProviderResponse> {
59
+ calls.push({
60
+ messages: [...messages],
61
+ tools: options?.tools,
62
+ systemPrompt: options?.systemPrompt,
63
+ options,
64
+ });
65
+ const scripted = responses[callIndex] ?? responses[responses.length - 1];
66
+ callIndex++;
67
+
68
+ if (scripted instanceof Error) {
69
+ throw scripted;
70
+ }
71
+
72
+ // Replay streaming deltas for text blocks, mirroring a live provider.
73
+ if (options?.onEvent) {
74
+ for (const block of scripted.content) {
75
+ if (block.type === "text") {
76
+ options.onEvent({ type: "text_delta", text: block.text });
77
+ }
78
+ }
79
+ }
80
+
81
+ return scripted;
82
+ },
83
+ };
84
+
85
+ return { provider, calls };
86
+ }
87
+
88
+ /** A scripted assistant turn that ends with a plain text response. */
89
+ export function textResponse(text: string): ProviderResponse {
90
+ return {
91
+ content: [{ type: "text", text }],
92
+ model: "mock-model",
93
+ usage: { inputTokens: 10, outputTokens: 5 },
94
+ stopReason: "end_turn",
95
+ };
96
+ }
97
+
98
+ /** A scripted assistant turn that invokes a single tool. */
99
+ export function toolUseResponse(
100
+ id: string,
101
+ name: string,
102
+ input: Record<string, unknown>,
103
+ ): ProviderResponse {
104
+ return {
105
+ content: [{ type: "tool_use", id, name, input }],
106
+ model: "mock-model",
107
+ usage: { inputTokens: 10, outputTokens: 5 },
108
+ stopReason: "tool_use",
109
+ };
110
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Shared test harness for driving the daemon's native `server_tool_complete`
3
+ * web_search handler in isolation (ATL-727).
4
+ *
5
+ * Both `native-web-search.test.ts` and `web-search-backend-failure.test.ts`
6
+ * exercise the same handler the same way: build a set of mocked
7
+ * `EventHandlerDeps` (capturing emitted `ServerMessage`s and `rlog.warn`
8
+ * records), then drive a `server_tool_start` → `server_tool_complete` pair.
9
+ * This module is the single source of truth for that harness so the two suites
10
+ * cannot drift apart.
11
+ *
12
+ * Note: each consuming test file must still install its own
13
+ * `mock.module(...)` stubs for the daemon collaborators the handler imports at
14
+ * load time (config loader, conversation-crud, llm-request-log-store), because
15
+ * Bun's `mock.module()` is scoped to the file that registers it.
16
+ */
17
+ import type {
18
+ EventHandlerDeps,
19
+ EventHandlerState,
20
+ } from "../../daemon/conversation-agent-loop-handlers.js";
21
+ import { dispatchAgentEvent } from "../../daemon/conversation-agent-loop-handlers.js";
22
+ import type { ServerMessage } from "../../daemon/message-protocol.js";
23
+
24
+ /** A `tool_result` `ServerMessage` emitted by the handler. */
25
+ export type ToolResultEvent = Extract<ServerMessage, { type: "tool_result" }>;
26
+
27
+ /** A captured `rlog.warn(obj, msg)` call. */
28
+ export interface LogRecord {
29
+ obj: Record<string, unknown>;
30
+ msg?: string;
31
+ }
32
+
33
+ export interface HandlerHarness {
34
+ deps: EventHandlerDeps;
35
+ /** Every `ServerMessage` the handler emitted via `onEvent`. */
36
+ events: ServerMessage[];
37
+ /** Every `rlog.warn(obj, msg)` call the handler made. */
38
+ warnings: LogRecord[];
39
+ }
40
+
41
+ /** Build mocked handler deps that capture emitted events and warn logs. */
42
+ export function createHandlerDeps(reqId = "req-web-search"): HandlerHarness {
43
+ const events: ServerMessage[] = [];
44
+ const warnings: LogRecord[] = [];
45
+ const rlog = {
46
+ warn: (obj: Record<string, unknown>, msg?: string) =>
47
+ warnings.push({ obj, msg }),
48
+ info: () => {},
49
+ error: () => {},
50
+ debug: () => {},
51
+ trace: () => {},
52
+ fatal: () => {},
53
+ };
54
+ const deps = {
55
+ ctx: {
56
+ conversationId: "conv-web-search",
57
+ provider: { name: "anthropic" },
58
+ traceEmitter: { emit: () => {} },
59
+ streamThinking: false,
60
+ emitActivityState: () => {},
61
+ markWorkspaceTopLevelDirty: () => {},
62
+ currentTurnSurfaces: [],
63
+ } as unknown as EventHandlerDeps["ctx"],
64
+ onEvent: (msg: ServerMessage) => events.push(msg),
65
+ reqId,
66
+ isFirstMessage: false,
67
+ shouldGenerateTitle: false,
68
+ rlog: rlog as unknown as EventHandlerDeps["rlog"],
69
+ turnChannelContext: {
70
+ userMessageChannel: "vellum",
71
+ assistantMessageChannel: "vellum",
72
+ } as EventHandlerDeps["turnChannelContext"],
73
+ turnInterfaceContext: {
74
+ userMessageInterface: "macos",
75
+ assistantMessageInterface: "macos",
76
+ } as EventHandlerDeps["turnInterfaceContext"],
77
+ applyCompaction: async () => {},
78
+ } as EventHandlerDeps;
79
+ return { deps, events, warnings };
80
+ }
81
+
82
+ /** The `server_tool_complete` payload shape a test supplies. */
83
+ export interface WebSearchCompleteEvent {
84
+ isError: boolean;
85
+ errorCode?: string;
86
+ errorMessage?: string;
87
+ content?: unknown[];
88
+ }
89
+
90
+ /** Drive one native (Anthropic) web_search start → complete pair. */
91
+ export async function completeNativeWebSearch(
92
+ state: EventHandlerState,
93
+ deps: EventHandlerDeps,
94
+ toolUseId: string,
95
+ event: WebSearchCompleteEvent,
96
+ ): Promise<void> {
97
+ await dispatchAgentEvent(state, deps, {
98
+ type: "server_tool_start",
99
+ name: "web_search",
100
+ toolUseId,
101
+ input: { query: "what is the weather" },
102
+ });
103
+ await dispatchAgentEvent(state, deps, {
104
+ type: "server_tool_complete",
105
+ toolUseId,
106
+ isError: event.isError,
107
+ ...(event.errorCode ? { errorCode: event.errorCode } : {}),
108
+ ...(event.errorMessage ? { errorMessage: event.errorMessage } : {}),
109
+ content: event.content ?? [],
110
+ });
111
+ }
112
+
113
+ /** All `tool_result` events emitted so far, in order. */
114
+ export function toolResults(events: ServerMessage[]): ToolResultEvent[] {
115
+ return events.filter((e): e is ToolResultEvent => e.type === "tool_result");
116
+ }
117
+
118
+ /** The most recent `tool_result` event, if any. */
119
+ export function lastToolResult(
120
+ events: ServerMessage[],
121
+ ): ToolResultEvent | undefined {
122
+ const results = toolResults(events);
123
+ return results[results.length - 1];
124
+ }
125
+
126
+ /** The captured `web_search_backend_failure` telemetry warn records. */
127
+ export function backendFailureLogs(warnings: LogRecord[]): LogRecord[] {
128
+ return warnings.filter((w) => w.obj.event === "web_search_backend_failure");
129
+ }
@@ -40,6 +40,7 @@ const noopLogger: PluginLogger = {
40
40
  function makeCtx(messages: Message[]): UserPromptSubmitContext {
41
41
  return {
42
42
  conversationId: "conv-test",
43
+ prompt: "",
43
44
  originalMessages: messages,
44
45
  latestMessages: messages,
45
46
  logger: noopLogger,