@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,267 @@
1
+ // Single source of truth for classifying `web_search` provider/backend
2
+ // failures and the user-facing copy we surface for them (ATL-727).
3
+ //
4
+ // This is a pure leaf module: it has NO imports from `daemon/`, `agent/`,
5
+ // `apps/`, or any client/UI package. It may only import the logger and pino
6
+ // types (for the telemetry helper). Every web_search code path — native
7
+ // Anthropic handler, app-side providers, and the web client default — funnels
8
+ // failures through `classifyWebSearchFailure` so the same friendly message
9
+ // propagates to every client via `WebSearchMetadata.errorMessage`.
10
+
11
+ import type { Logger } from "pino";
12
+
13
+ import { isAbortReason } from "../../util/abort-reasons.js";
14
+ import { truncateForLog } from "../../util/logger.js";
15
+ import { isRetryableNetworkError } from "../../util/retry.js";
16
+
17
+ /**
18
+ * Canonical user-facing copy for a recoverable web_search backend failure.
19
+ * This is what propagates to every client via `WebSearchMetadata.errorMessage`.
20
+ *
21
+ * It names the search tool as the thing struggling, offers retry /
22
+ * continue-without-search / paste-details, does not blame the user, does not
23
+ * imply we can fix the provider, does not claim the whole internet or all
24
+ * tools are down, and contains no raw provider details, JSON, stack traces,
25
+ * or exception names.
26
+ */
27
+ export const WEB_SEARCH_BACKEND_FAILURE_MESSAGE =
28
+ "Search is having trouble right now. You can try again in a moment, continue without web search, or paste the relevant details here and I'll use those.";
29
+
30
+ const QUERY_TOO_LONG_MESSAGE =
31
+ "That search query was too long. Try a shorter query.";
32
+
33
+ const MAX_USES_EXCEEDED_MESSAGE =
34
+ "I've hit the web-search limit for this turn. I can continue without more searches, or you can paste the details and I'll use those.";
35
+
36
+ const CONFIG_MESSAGE = "Web search isn't configured.";
37
+
38
+ export type WebSearchFailureCategory =
39
+ | "backend_unavailable"
40
+ | "rate_limited"
41
+ | "query_too_long"
42
+ | "max_uses_exceeded"
43
+ | "config"
44
+ | "no_results"
45
+ | "unknown";
46
+
47
+ export interface WebSearchFailureClassification {
48
+ category: WebSearchFailureCategory;
49
+ isBackendFailure: boolean;
50
+ userMessage: string;
51
+ rawDetail: string;
52
+ }
53
+
54
+ export interface WebSearchFailureInput {
55
+ /** Anthropic `web_search_tool_result_error` code, when present. */
56
+ errorCode?: string;
57
+ /** Thrown error or rejected value from a fetch/provider call. */
58
+ error?: unknown;
59
+ /** HTTP status code from a provider response, when present. */
60
+ statusCode?: number;
61
+ /** Whether the tool result was flagged as an error. */
62
+ isError?: boolean;
63
+ /** Whether a successful call returned any results. */
64
+ hasResults?: boolean;
65
+ }
66
+
67
+ /** Categories we consider a transient backend failure worth the friendly copy. */
68
+ function isBackendFailureCategory(category: WebSearchFailureCategory): boolean {
69
+ return category === "backend_unavailable" || category === "rate_limited";
70
+ }
71
+
72
+ function userMessageFor(category: WebSearchFailureCategory): string {
73
+ switch (category) {
74
+ case "backend_unavailable":
75
+ case "rate_limited":
76
+ return WEB_SEARCH_BACKEND_FAILURE_MESSAGE;
77
+ case "query_too_long":
78
+ return QUERY_TOO_LONG_MESSAGE;
79
+ case "max_uses_exceeded":
80
+ return MAX_USES_EXCEEDED_MESSAGE;
81
+ case "config":
82
+ return CONFIG_MESSAGE;
83
+ case "no_results":
84
+ case "unknown":
85
+ // Neutral passthrough: callers keep their existing behavior.
86
+ return "";
87
+ }
88
+ }
89
+
90
+ /** Map an Anthropic `web_search_tool_result_error` code to a category. */
91
+ function categoryFromErrorCode(
92
+ errorCode: string,
93
+ ): WebSearchFailureCategory | undefined {
94
+ switch (errorCode) {
95
+ case "unavailable":
96
+ case "internal_error":
97
+ case "overloaded_error":
98
+ return "backend_unavailable";
99
+ case "too_many_requests":
100
+ return "rate_limited";
101
+ case "query_too_long":
102
+ return "query_too_long";
103
+ case "max_uses_exceeded":
104
+ return "max_uses_exceeded";
105
+ case "invalid_input":
106
+ // Recoverable, but not a backend failure — let callers handle it.
107
+ return "unknown";
108
+ default:
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ /** Map an HTTP status code to a category. */
114
+ function categoryFromStatusCode(
115
+ statusCode: number,
116
+ ): WebSearchFailureCategory | undefined {
117
+ if (statusCode === 429) return "rate_limited";
118
+ if (statusCode === 401 || statusCode === 403) return "config";
119
+ if (statusCode >= 500) return "backend_unavailable";
120
+ return undefined;
121
+ }
122
+
123
+ /**
124
+ * Classify a thrown error / rejected value. This module is web_search-only, so
125
+ * network-layer failures (fetch failed, connection reset/refused, DNS, and
126
+ * timeouts without an explicit user-abort reason) are treated as backend
127
+ * failures.
128
+ */
129
+ function categoryFromError(error: unknown): WebSearchFailureCategory | undefined {
130
+ if (error == null) return undefined;
131
+
132
+ // A user-initiated abort (Stop/Esc, preemption, dispose) is not a failure.
133
+ // The tagged `AbortReason` may surface directly (`AbortSignal.throwIfAborted`
134
+ // throws `signal.reason` verbatim), via `error.reason`, or — when a provider
135
+ // wrapper erases the `AbortError` name — on `ProviderError.abortReason`. Check
136
+ // all three FIRST, before the transport-retryability and abort/timeout
137
+ // substring heuristics, so a tagged cancellation that ALSO carries a
138
+ // transport-shaped `cause` (e.g. ECONNRESET) short-circuits to "not a
139
+ // failure" instead of being mislabeled a backend outage. A bare
140
+ // AbortError/timeout with no tagged reason still falls through below.
141
+ if (
142
+ isAbortReason(error) ||
143
+ isAbortReason((error as { reason?: unknown }).reason) ||
144
+ isAbortReason((error as { abortReason?: unknown }).abortReason)
145
+ ) {
146
+ return undefined;
147
+ }
148
+
149
+ // Retryable transport failures (ECONNRESET/ECONNREFUSED/ETIMEDOUT, socket
150
+ // hang-ups, including one level of `cause` chain) are backend failures.
151
+ if (isRetryableNetworkError(error)) return "backend_unavailable";
152
+
153
+ const name = typeof (error as { name?: unknown }).name === "string"
154
+ ? (error as { name: string }).name
155
+ : "";
156
+ const haystack = `${name} ${(error as { message?: unknown }).message ?? ""}`
157
+ .toLowerCase();
158
+
159
+ // web_search-only: treat aborts/timeouts/DNS/fetch failures (the cases
160
+ // `isRetryableNetworkError` doesn't cover) as backend failures.
161
+ if (
162
+ name === "AbortError" ||
163
+ haystack.includes("abort") ||
164
+ haystack.includes("timeout") ||
165
+ haystack.includes("timed out") ||
166
+ haystack.includes("fetch failed") ||
167
+ haystack.includes("failed to fetch") ||
168
+ haystack.includes("enotfound")
169
+ ) {
170
+ return "backend_unavailable";
171
+ }
172
+ return undefined;
173
+ }
174
+
175
+ /** Build the internal-only raw detail string (never embedded in userMessage). */
176
+ function buildRawDetail(input: WebSearchFailureInput): string {
177
+ const parts: string[] = [];
178
+ if (input.errorCode) parts.push(`errorCode=${input.errorCode}`);
179
+ if (typeof input.statusCode === "number") {
180
+ parts.push(`statusCode=${input.statusCode}`);
181
+ }
182
+ if (input.error != null) {
183
+ const err = input.error as { message?: unknown };
184
+ const msg =
185
+ typeof err.message === "string" ? err.message : String(input.error);
186
+ if (msg) parts.push(msg);
187
+ }
188
+ return truncateForLog(parts.join(" "), 500);
189
+ }
190
+
191
+ /**
192
+ * Classify a web_search failure into a stable category, a user-facing message,
193
+ * and an internal-only raw detail. Never treats an empty-but-successful result
194
+ * as a failure.
195
+ */
196
+ export function classifyWebSearchFailure(
197
+ input: WebSearchFailureInput,
198
+ ): WebSearchFailureClassification {
199
+ const rawDetail = buildRawDetail(input);
200
+
201
+ // Success-passthrough: nothing went wrong, there were just no results.
202
+ if (!input.isError && input.error == null) {
203
+ return {
204
+ category: "no_results",
205
+ isBackendFailure: false,
206
+ userMessage: userMessageFor("no_results"),
207
+ rawDetail,
208
+ };
209
+ }
210
+
211
+ // Resolution order: Anthropic `errorCode` → explicit HTTP `statusCode` →
212
+ // error-body/network heuristics. An explicit status code is authoritative
213
+ // over substring-sniffing the provider's response body (which can contain
214
+ // misleading keywords like "timeout"/"abort"). Tagged user-aborts carry no
215
+ // status code, so they still flow through `categoryFromError` and
216
+ // short-circuit to a non-failure.
217
+ const category =
218
+ (input.errorCode != null
219
+ ? categoryFromErrorCode(input.errorCode)
220
+ : undefined) ??
221
+ (typeof input.statusCode === "number"
222
+ ? categoryFromStatusCode(input.statusCode)
223
+ : undefined) ??
224
+ categoryFromError(input.error) ??
225
+ "unknown";
226
+
227
+ return {
228
+ category,
229
+ isBackendFailure: isBackendFailureCategory(category),
230
+ userMessage: userMessageFor(category),
231
+ rawDetail,
232
+ };
233
+ }
234
+
235
+ export interface WebSearchBackendFailureMeta {
236
+ provider: string;
237
+ requestId?: string;
238
+ errorCategory: WebSearchFailureCategory;
239
+ rawDetail: string;
240
+ fallbackShown: boolean;
241
+ queryLength?: number;
242
+ }
243
+
244
+ /**
245
+ * Emit a structured warning for a web_search backend failure (ATL-727).
246
+ *
247
+ * Do NOT log raw query text — only `queryLength`. `rawDetail` is internal-only
248
+ * provider/HTTP context and must never be surfaced to users.
249
+ */
250
+ export function logWebSearchBackendFailure(
251
+ log: Logger,
252
+ meta: WebSearchBackendFailureMeta,
253
+ ): void {
254
+ log.warn(
255
+ {
256
+ event: "web_search_backend_failure",
257
+ tool: "web_search",
258
+ provider: meta.provider,
259
+ requestId: meta.requestId,
260
+ errorCategory: meta.errorCategory,
261
+ rawDetail: meta.rawDetail,
262
+ fallbackShown: meta.fallbackShown,
263
+ queryLength: meta.queryLength,
264
+ },
265
+ "web_search backend failure",
266
+ );
267
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  import { RiskLevel } from "../../permissions/types.js";
7
7
  import { getProviderKeyAsync } from "../../security/secure-keys.js";
8
8
  import { wrapUntrustedContent } from "../../security/untrusted-content.js";
9
+ import { isAbortReason } from "../../util/abort-reasons.js";
9
10
  import { faviconUrlForDomain } from "../../util/favicon.js";
10
11
  import { getLogger } from "../../util/logger.js";
11
12
  import {
@@ -22,6 +23,11 @@ import type {
22
23
  } from "../types.js";
23
24
  import { extractDomain } from "./domain-normalize.js";
24
25
  import type { ManagedSearchProxyResult } from "./managed-search-proxy.js";
26
+ import {
27
+ classifyWebSearchFailure,
28
+ logWebSearchBackendFailure,
29
+ WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
30
+ } from "./web-search-error.js";
25
31
 
26
32
  const log = getLogger("web-search");
27
33
 
@@ -381,6 +387,115 @@ function errorResult(
381
387
  };
382
388
  }
383
389
 
390
+ /**
391
+ * Wrap an already-read provider response body so {@link backendFailureResult}
392
+ * forwards it into the classifier's internal-only `rawDetail` (telemetry). The
393
+ * classifier reads `error.message`; `buildRawDetail` truncates to ≤500 chars.
394
+ * Returns `undefined` for an empty body so we don't pad `rawDetail` with noise.
395
+ * The body must NEVER reach user-facing `content`/`errorMessage`.
396
+ */
397
+ function rawBodyDetail(body: unknown): { message: string } | undefined {
398
+ if (body == null) return undefined;
399
+ const text =
400
+ typeof body === "string" ? body : safeStringifyBody(body);
401
+ const trimmed = text.trim();
402
+ return trimmed ? { message: trimmed } : undefined;
403
+ }
404
+
405
+ function safeStringifyBody(body: unknown): string {
406
+ try {
407
+ return JSON.stringify(body);
408
+ } catch {
409
+ return String(body);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Build a {@link ToolExecutionResult} for a genuine backend/transport failure
415
+ * (5xx, post-retry rate-limit, thrown network/timeout error). Routes the raw
416
+ * detail through {@link classifyWebSearchFailure}: when it is a backend failure
417
+ * we surface the friendly recoverable copy (the bare sentence so the model
418
+ * reads it as guidance — retry / continue-without-search / paste-details —
419
+ * rather than fabricating) in both the model-facing `content` and the client
420
+ * `errorMessage`, and log the raw detail via telemetry. Non-backend categories
421
+ * (e.g. an unexpected 4xx) fall back to {@link errorResult} with `fallback`.
422
+ *
423
+ * Raw provider JSON / status text must never reach `content` or `errorMessage`;
424
+ * only `rawDetail` (internal-only) captures it for the log.
425
+ */
426
+ function backendFailureResult(
427
+ query: string,
428
+ provider: WebSearchProvider,
429
+ startedAt: number,
430
+ raw: { error?: unknown; statusCode?: number; errorCode?: string },
431
+ fallback: string,
432
+ ): ToolExecutionResult {
433
+ const classification = classifyWebSearchFailure({
434
+ isError: true,
435
+ error: raw.error,
436
+ statusCode: raw.statusCode,
437
+ errorCode: raw.errorCode,
438
+ });
439
+
440
+ if (!classification.isBackendFailure) {
441
+ return errorResult(query, provider, startedAt, fallback);
442
+ }
443
+
444
+ logWebSearchBackendFailure(log, {
445
+ provider,
446
+ errorCategory: classification.category,
447
+ rawDetail: classification.rawDetail,
448
+ fallbackShown: true,
449
+ queryLength: query.length,
450
+ });
451
+
452
+ return {
453
+ content: WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
454
+ isError: true,
455
+ activityMetadata: {
456
+ webSearch: {
457
+ query,
458
+ provider,
459
+ resultCount: 0,
460
+ durationMs: Date.now() - startedAt,
461
+ results: [],
462
+ errorMessage: WEB_SEARCH_BACKEND_FAILURE_MESSAGE,
463
+ },
464
+ },
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Route a thrown fetch error (network/timeout) through {@link backendFailureResult}
470
+ * as a `backend_unavailable` candidate, falling back to a `Web search failed: …`
471
+ * error for non-backend throws (e.g. a JSON parse error).
472
+ *
473
+ * If the caller aborted the request (`signal.aborted` — the user hit Stop/Esc,
474
+ * or an external caller cancelled), the thrown error is re-thrown so the
475
+ * executor's existing cancellation handling takes over. A user-cancel must NOT
476
+ * surface the friendly backend copy or emit `web_search_backend_failure`
477
+ * telemetry. Internal fetch timeouts (where the caller's signal is not aborted)
478
+ * still route to the friendly backend result.
479
+ */
480
+ function networkFailureResult(
481
+ query: string,
482
+ provider: WebSearchProvider,
483
+ startedAt: number,
484
+ err: unknown,
485
+ signal?: AbortSignal,
486
+ ): ToolExecutionResult {
487
+ if (signal?.aborted || isAbortReason((err as { reason?: unknown })?.reason)) {
488
+ throw err;
489
+ }
490
+ return backendFailureResult(
491
+ query,
492
+ provider,
493
+ startedAt,
494
+ { error: err },
495
+ `Web search failed: ${err instanceof Error ? err.message : String(err)}`,
496
+ );
497
+ }
498
+
384
499
  async function executeBraveSearch(
385
500
  query: string,
386
501
  count: number,
@@ -396,21 +511,26 @@ async function executeBraveSearch(
396
511
  const startedAt = Date.now();
397
512
 
398
513
  for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
399
- const response = await fetch(url, {
400
- headers: {
401
- Accept: "application/json",
402
- "Accept-Encoding": "gzip",
403
- "X-Subscription-Token": apiKey,
404
- },
405
- signal,
406
- });
514
+ let response: Response;
515
+ try {
516
+ response = await fetch(url, {
517
+ headers: {
518
+ Accept: "application/json",
519
+ "Accept-Encoding": "gzip",
520
+ "X-Subscription-Token": apiKey,
521
+ },
522
+ signal,
523
+ });
524
+ } catch (err) {
525
+ return networkFailureResult(query, "brave", startedAt, err, signal);
526
+ }
407
527
 
408
528
  if (response.ok) {
409
529
  const data = (await response.json()) as BraveSearchResponse;
410
530
  return successfulBraveResult(data, query, startedAt);
411
531
  }
412
532
 
413
- await response.text();
533
+ const bodyText = await response.text();
414
534
 
415
535
  if (response.status === 401 || response.status === 403) {
416
536
  return errorResult(
@@ -436,20 +556,22 @@ async function executeBraveSearch(
436
556
  }
437
557
 
438
558
  log.warn({ status: response.status }, "Brave Search API error");
439
- return errorResult(
559
+ return backendFailureResult(
440
560
  query,
441
561
  "brave",
442
562
  startedAt,
563
+ { statusCode: response.status, error: rawBodyDetail(bodyText) },
443
564
  response.status === 429
444
565
  ? "Brave Search rate limit exceeded after retries. Try again shortly."
445
566
  : `Brave Search API returned status ${response.status}`,
446
567
  );
447
568
  }
448
569
 
449
- return errorResult(
570
+ return backendFailureResult(
450
571
  query,
451
572
  "brave",
452
573
  startedAt,
574
+ { statusCode: 429 },
453
575
  "Brave Search rate limit exceeded after retries. Try again shortly.",
454
576
  );
455
577
  }
@@ -478,6 +600,23 @@ async function executeManagedBraveSearch(
478
600
  );
479
601
 
480
602
  if (!proxyResult.ok) {
603
+ // Keep billing/auth/unavailable mapping as specific copy; route genuine
604
+ // platform 5xx (transport-level failures) to the friendly backend helper.
605
+ if (
606
+ proxyResult.kind === "platform-error" &&
607
+ proxyResult.status >= 500
608
+ ) {
609
+ return backendFailureResult(
610
+ query,
611
+ "brave",
612
+ startedAt,
613
+ {
614
+ statusCode: proxyResult.status,
615
+ error: rawBodyDetail(proxyResult.body),
616
+ },
617
+ managedSearchProxyErrorMessage(proxyResult),
618
+ );
619
+ }
481
620
  return errorResult(
482
621
  query,
483
622
  "brave",
@@ -503,12 +642,18 @@ async function executeManagedBraveSearch(
503
642
  );
504
643
  }
505
644
 
506
- if (proxyResult.status === 429) {
507
- return errorResult(
645
+ if (proxyResult.status === 429 || proxyResult.status >= 500) {
646
+ return backendFailureResult(
508
647
  query,
509
648
  "brave",
510
649
  startedAt,
511
- "Managed Brave Search rate limit exceeded. Try again shortly.",
650
+ {
651
+ statusCode: proxyResult.status,
652
+ error: rawBodyDetail(proxyResult.body),
653
+ },
654
+ proxyResult.status === 429
655
+ ? "Managed Brave Search rate limit exceeded. Try again shortly."
656
+ : `Managed Brave Search provider returned status ${proxyResult.status}`,
512
657
  );
513
658
  }
514
659
 
@@ -545,18 +690,23 @@ async function executePerplexitySearch(
545
690
  ): Promise<ToolExecutionResult> {
546
691
  const startedAt = Date.now();
547
692
  for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
548
- const response = await fetch(PERPLEXITY_API_URL, {
549
- method: "POST",
550
- headers: {
551
- "Content-Type": "application/json",
552
- Authorization: `Bearer ${apiKey}`,
553
- },
554
- body: JSON.stringify({
555
- model: "sonar",
556
- messages: [{ role: "user", content: query }],
557
- }),
558
- signal,
559
- });
693
+ let response: Response;
694
+ try {
695
+ response = await fetch(PERPLEXITY_API_URL, {
696
+ method: "POST",
697
+ headers: {
698
+ "Content-Type": "application/json",
699
+ Authorization: `Bearer ${apiKey}`,
700
+ },
701
+ body: JSON.stringify({
702
+ model: "sonar",
703
+ messages: [{ role: "user", content: query }],
704
+ }),
705
+ signal,
706
+ });
707
+ } catch (err) {
708
+ return networkFailureResult(query, "perplexity", startedAt, err, signal);
709
+ }
560
710
 
561
711
  if (response.ok) {
562
712
  const data = (await response.json()) as PerplexityResponse;
@@ -574,7 +724,7 @@ async function executePerplexitySearch(
574
724
  };
575
725
  }
576
726
 
577
- await response.text();
727
+ const bodyText = await response.text();
578
728
 
579
729
  if (response.status === 401 || response.status === 403) {
580
730
  return errorResult(
@@ -600,20 +750,22 @@ async function executePerplexitySearch(
600
750
  }
601
751
 
602
752
  log.warn({ status: response.status }, "Perplexity API error");
603
- return errorResult(
753
+ return backendFailureResult(
604
754
  query,
605
755
  "perplexity",
606
756
  startedAt,
757
+ { statusCode: response.status, error: rawBodyDetail(bodyText) },
607
758
  response.status === 429
608
759
  ? "Perplexity rate limit exceeded after retries. Try again shortly."
609
760
  : `Perplexity API returned status ${response.status}`,
610
761
  );
611
762
  }
612
763
 
613
- return errorResult(
764
+ return backendFailureResult(
614
765
  query,
615
766
  "perplexity",
616
767
  startedAt,
768
+ { statusCode: 429 },
617
769
  "Perplexity rate limit exceeded after retries. Try again shortly.",
618
770
  );
619
771
  }
@@ -638,16 +790,21 @@ async function executeTavilySearch(
638
790
  const startedAt = Date.now();
639
791
 
640
792
  for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
641
- const response = await fetch(TAVILY_API_URL, {
642
- method: "POST",
643
- headers: {
644
- "Content-Type": "application/json",
645
- Authorization: `Bearer ${apiKey}`,
646
- "X-Client-Source": "vellum-assistant",
647
- },
648
- body: JSON.stringify(body),
649
- signal,
650
- });
793
+ let response: Response;
794
+ try {
795
+ response = await fetch(TAVILY_API_URL, {
796
+ method: "POST",
797
+ headers: {
798
+ "Content-Type": "application/json",
799
+ Authorization: `Bearer ${apiKey}`,
800
+ "X-Client-Source": "vellum-assistant",
801
+ },
802
+ body: JSON.stringify(body),
803
+ signal,
804
+ });
805
+ } catch (err) {
806
+ return networkFailureResult(query, "tavily", startedAt, err, signal);
807
+ }
651
808
 
652
809
  if (response.ok) {
653
810
  const data = (await response.json()) as TavilySearchResponse;
@@ -665,7 +822,7 @@ async function executeTavilySearch(
665
822
  };
666
823
  }
667
824
 
668
- await response.text();
825
+ const bodyText = await response.text();
669
826
 
670
827
  if (response.status === 401 || response.status === 403) {
671
828
  return errorResult(
@@ -691,20 +848,22 @@ async function executeTavilySearch(
691
848
  }
692
849
 
693
850
  log.warn({ status: response.status }, "Tavily Search API error");
694
- return errorResult(
851
+ return backendFailureResult(
695
852
  query,
696
853
  "tavily",
697
854
  startedAt,
855
+ { statusCode: response.status, error: rawBodyDetail(bodyText) },
698
856
  response.status === 429
699
857
  ? "Tavily Search rate limit exceeded after retries. Try again shortly."
700
858
  : `Tavily Search API returned status ${response.status}`,
701
859
  );
702
860
  }
703
861
 
704
- return errorResult(
862
+ return backendFailureResult(
705
863
  query,
706
864
  "tavily",
707
865
  startedAt,
866
+ { statusCode: 429 },
708
867
  "Tavily Search rate limit exceeded after retries. Try again shortly.",
709
868
  );
710
869
  }
@@ -844,13 +1003,13 @@ export const webSearchTool = {
844
1003
  signal: context.signal,
845
1004
  });
846
1005
  } catch (err) {
847
- const msg = err instanceof Error ? err.message : String(err);
848
1006
  log.error({ err }, "Managed web search failed");
849
- return errorResult(
1007
+ return networkFailureResult(
850
1008
  query,
851
1009
  "brave",
852
1010
  startedAt,
853
- `Managed web search failed: ${msg}`,
1011
+ err,
1012
+ context.signal,
854
1013
  );
855
1014
  }
856
1015
  }
@@ -897,13 +1056,13 @@ export const webSearchTool = {
897
1056
  signal: context.signal,
898
1057
  });
899
1058
  } catch (err) {
900
- const msg = err instanceof Error ? err.message : String(err);
901
1059
  log.error({ err }, "Web search failed");
902
- return errorResult(
1060
+ return networkFailureResult(
903
1061
  query,
904
1062
  provider,
905
1063
  startedAt,
906
- `Web search failed: ${msg}`,
1064
+ err,
1065
+ context.signal,
907
1066
  );
908
1067
  }
909
1068
  },
@@ -144,6 +144,7 @@ export async function executeScheduleCreate(
144
144
  maxRetries,
145
145
  retryBackoffMs,
146
146
  timeoutMs,
147
+ createdFromConversationId: context.conversationId,
147
148
  });
148
149
 
149
150
  const fireDate = formatLocalDate(job.nextRunAt);
@@ -225,6 +226,7 @@ export async function executeScheduleCreate(
225
226
  maxRetries,
226
227
  retryBackoffMs,
227
228
  timeoutMs,
229
+ createdFromConversationId: context.conversationId,
228
230
  });
229
231
 
230
232
  const scheduleDescription =