@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Tests that the native Anthropic `server_tool_complete` agent event produces
3
+ * a structured `WebSearchMetadata` payload on the emitted `tool_result` server
4
+ * message while keeping the back-compat `result: string` byte-identical.
5
+ *
6
+ * Mirrors the mocked-dependency collector pattern used in
7
+ * tool-result-metadata-plumbing.test.ts.
8
+ */
9
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ // ── Mock platform (must precede imports that read it) ─────────────────────────
12
+ mock.module("../../util/logger.js", () => ({
13
+ getLogger: () =>
14
+ new Proxy({} as Record<string, unknown>, {
15
+ get: () => () => {},
16
+ }),
17
+ }));
18
+
19
+ mock.module("../../config/loader.js", () => ({
20
+ getConfig: () => ({
21
+ skills: {
22
+ entries: {},
23
+ load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
24
+ install: { nodeManager: "npm" },
25
+ allowBundled: null,
26
+ remoteProviders: {
27
+ skillssh: { enabled: true },
28
+ clawhub: { enabled: true },
29
+ },
30
+ remotePolicy: {
31
+ blockSuspicious: true,
32
+ blockMalware: true,
33
+ maxSkillsShRisk: "medium",
34
+ },
35
+ },
36
+ }),
37
+ loadConfig: () => ({}),
38
+ }));
39
+
40
+ mock.module("../../memory/conversation-crud.js", () => ({
41
+ addMessage: () => ({ id: "mock-msg-id" }),
42
+ getMessageById: () => null,
43
+ updateMessageContent: () => {},
44
+ provenanceFromTrustContext: () => ({}),
45
+ }));
46
+
47
+ mock.module("../../memory/llm-request-log-store.js", () => ({
48
+ recordRequestLog: () => {},
49
+ backfillMessageIdOnLogs: () => {},
50
+ }));
51
+
52
+ // ── Imports (after mocks) ─────────────────────────────────────────────────────
53
+ import type {
54
+ EventHandlerDeps,
55
+ EventHandlerState,
56
+ } from "../conversation-agent-loop-handlers.js";
57
+ import {
58
+ createEventHandlerState,
59
+ dispatchAgentEvent,
60
+ } from "../conversation-agent-loop-handlers.js";
61
+ import type { ServerMessage } from "../message-protocol.js";
62
+
63
+ type ToolResultEvent = Extract<ServerMessage, { type: "tool_result" }>;
64
+
65
+ // ── Helpers ───────────────────────────────────────────────────────────────────
66
+
67
+ function createCollectorDeps(providerName = "anthropic"): {
68
+ deps: EventHandlerDeps;
69
+ events: ServerMessage[];
70
+ } {
71
+ const events: ServerMessage[] = [];
72
+ const deps = {
73
+ ctx: {
74
+ conversationId: "conv-native-meta",
75
+ provider: { name: providerName },
76
+ traceEmitter: { emit: () => {} },
77
+ streamThinking: false,
78
+ emitActivityState: () => {},
79
+ markWorkspaceTopLevelDirty: () => {},
80
+ currentTurnSurfaces: [],
81
+ } as unknown as EventHandlerDeps["ctx"],
82
+ onEvent: (msg: ServerMessage) => events.push(msg),
83
+ reqId: "req-native-meta",
84
+ isFirstMessage: false,
85
+ shouldGenerateTitle: false,
86
+ rlog: new Proxy({} as Record<string, unknown>, {
87
+ get: () => () => {},
88
+ }) as unknown as EventHandlerDeps["rlog"],
89
+ turnChannelContext: {
90
+ userMessageChannel: "vellum",
91
+ assistantMessageChannel: "vellum",
92
+ } as EventHandlerDeps["turnChannelContext"],
93
+ turnInterfaceContext: {
94
+ userMessageInterface: "macos",
95
+ assistantMessageInterface: "macos",
96
+ } as EventHandlerDeps["turnInterfaceContext"],
97
+ } as EventHandlerDeps;
98
+ return { deps, events };
99
+ }
100
+
101
+ // ── Tests ─────────────────────────────────────────────────────────────────────
102
+
103
+ describe("native server_tool_complete metadata", () => {
104
+ let state: EventHandlerState;
105
+
106
+ beforeEach(() => {
107
+ state = createEventHandlerState();
108
+ });
109
+
110
+ test("builds structured WebSearchMetadata and keeps result text byte-identical", async () => {
111
+ const { deps, events } = createCollectorDeps();
112
+ const toolUseId = "tu1";
113
+
114
+ await dispatchAgentEvent(state, deps, {
115
+ type: "server_tool_start",
116
+ name: "web_search",
117
+ toolUseId,
118
+ input: { query: "Air Jordan auction" },
119
+ });
120
+
121
+ await dispatchAgentEvent(state, deps, {
122
+ type: "server_tool_complete",
123
+ toolUseId,
124
+ isError: false,
125
+ content: [
126
+ {
127
+ type: "web_search_result",
128
+ title: "X",
129
+ url: "https://www.cnn.com/a",
130
+ },
131
+ {
132
+ type: "web_search_result",
133
+ title: "Y",
134
+ url: "https://www.nbcnews.com/b",
135
+ },
136
+ ],
137
+ });
138
+
139
+ const toolResultEvent = events.find(
140
+ (e): e is ToolResultEvent => e.type === "tool_result",
141
+ );
142
+ expect(toolResultEvent).toBeDefined();
143
+
144
+ const meta = toolResultEvent?.activityMetadata?.webSearch;
145
+ expect(meta).toBeDefined();
146
+ expect(meta?.query).toBe("Air Jordan auction");
147
+ expect(meta?.provider).toBe("anthropic-native");
148
+ expect(meta?.resultCount).toBe(2);
149
+ expect(meta?.results[0]?.domain).toBe("www.cnn.com");
150
+ expect(meta?.results[1]?.domain).toBe("www.nbcnews.com");
151
+ expect(meta?.results[0]?.rank).toBe(1);
152
+ expect(meta?.results[1]?.rank).toBe(2);
153
+ expect(meta?.durationMs).toBeGreaterThanOrEqual(0);
154
+
155
+ // Favicons are synthesized from the result domain so clients can render
156
+ // a per-result icon without an extra round-trip.
157
+ expect(meta?.results[0]?.faviconUrl).toContain("google.com/s2/favicons");
158
+ expect(meta?.results[1]?.faviconUrl).toContain("google.com/s2/favicons");
159
+
160
+ // Back-compat: result text must be byte-identical to the legacy format.
161
+ expect(toolResultEvent?.result).toBe(
162
+ "X\nhttps://www.cnn.com/a\n\nY\nhttps://www.nbcnews.com/b",
163
+ );
164
+
165
+ // Per-toolUseId scratch maps must be cleaned up after completion.
166
+ expect(state.serverToolStartedAt.has(toolUseId)).toBe(false);
167
+ expect(state.serverToolInputs.has(toolUseId)).toBe(false);
168
+ });
169
+
170
+ test("prefers `resolvedInput.query` over the input captured at server_tool_start", async () => {
171
+ // Mirrors the live Anthropic flow: server_tool_start fires with `input: {}`
172
+ // because the SDK streams server-tool input via `input_json_delta`, then
173
+ // the provider populates `resolvedInput` on `server_tool_complete` from
174
+ // the accumulated JSON. Without this plumb-through, `meta.query` is empty.
175
+ const { deps, events } = createCollectorDeps();
176
+ const toolUseId = "tu_resolved";
177
+
178
+ await dispatchAgentEvent(state, deps, {
179
+ type: "server_tool_start",
180
+ name: "web_search",
181
+ toolUseId,
182
+ input: {},
183
+ });
184
+
185
+ await dispatchAgentEvent(state, deps, {
186
+ type: "server_tool_complete",
187
+ toolUseId,
188
+ isError: false,
189
+ resolvedInput: { query: "OpenAI Dev Day recap" },
190
+ content: [
191
+ {
192
+ type: "web_search_result",
193
+ title: "Recap",
194
+ url: "https://example.com/a",
195
+ },
196
+ ],
197
+ });
198
+
199
+ const toolResultEvent = events.find(
200
+ (e): e is ToolResultEvent => e.type === "tool_result",
201
+ );
202
+ expect(toolResultEvent?.activityMetadata?.webSearch?.query).toBe(
203
+ "OpenAI Dev Day recap",
204
+ );
205
+ });
206
+
207
+ test("fires per-toolUseId for parallel server tool calls with independent queries and timings", async () => {
208
+ const { deps, events } = createCollectorDeps();
209
+
210
+ await dispatchAgentEvent(state, deps, {
211
+ type: "server_tool_start",
212
+ name: "web_search",
213
+ toolUseId: "tu_a",
214
+ input: { query: "alpha" },
215
+ });
216
+ await dispatchAgentEvent(state, deps, {
217
+ type: "server_tool_start",
218
+ name: "web_search",
219
+ toolUseId: "tu_b",
220
+ input: { query: "beta" },
221
+ });
222
+
223
+ // Hold long enough that the two completions can't share a duration.
224
+ await new Promise((r) => setTimeout(r, 20));
225
+
226
+ await dispatchAgentEvent(state, deps, {
227
+ type: "server_tool_complete",
228
+ toolUseId: "tu_a",
229
+ isError: false,
230
+ content: [
231
+ {
232
+ type: "web_search_result",
233
+ title: "A1",
234
+ url: "https://a.example.com/1",
235
+ },
236
+ ],
237
+ });
238
+
239
+ await new Promise((r) => setTimeout(r, 20));
240
+
241
+ await dispatchAgentEvent(state, deps, {
242
+ type: "server_tool_complete",
243
+ toolUseId: "tu_b",
244
+ isError: false,
245
+ content: [
246
+ {
247
+ type: "web_search_result",
248
+ title: "B1",
249
+ url: "https://b.example.com/1",
250
+ },
251
+ ],
252
+ });
253
+
254
+ const toolResults = events.filter(
255
+ (e): e is ToolResultEvent => e.type === "tool_result",
256
+ );
257
+ expect(toolResults).toHaveLength(2);
258
+
259
+ const byId = new Map(
260
+ toolResults.map((r) => [r.toolUseId, r] as const),
261
+ );
262
+ expect(byId.get("tu_a")?.activityMetadata?.webSearch?.query).toBe("alpha");
263
+ expect(byId.get("tu_b")?.activityMetadata?.webSearch?.query).toBe("beta");
264
+
265
+ const durA = byId.get("tu_a")?.activityMetadata?.webSearch?.durationMs ?? 0;
266
+ const durB = byId.get("tu_b")?.activityMetadata?.webSearch?.durationMs ?? 0;
267
+ // Each duration is computed against its own startedAt — tu_b should run
268
+ // longer because we slept once more between its start and its complete.
269
+ expect(durB).toBeGreaterThanOrEqual(durA);
270
+ });
271
+
272
+ test("forwards provider error codes instead of generic 'Search failed'", async () => {
273
+ const { deps, events } = createCollectorDeps();
274
+ const toolUseId = "tu_err_code";
275
+
276
+ await dispatchAgentEvent(state, deps, {
277
+ type: "server_tool_start",
278
+ name: "web_search",
279
+ toolUseId,
280
+ input: { query: "over the limit" },
281
+ });
282
+
283
+ await dispatchAgentEvent(state, deps, {
284
+ type: "server_tool_complete",
285
+ toolUseId,
286
+ isError: true,
287
+ errorCode: "max_uses_exceeded",
288
+ content: [],
289
+ });
290
+
291
+ const toolResultEvent = events.find(
292
+ (e): e is ToolResultEvent => e.type === "tool_result",
293
+ );
294
+ expect(toolResultEvent?.activityMetadata?.webSearch?.errorMessage).toBe(
295
+ "max_uses_exceeded",
296
+ );
297
+ });
298
+
299
+ test("does NOT emit activityMetadata for non-Anthropic providers", async () => {
300
+ // OpenAI's responses provider shares `server_tool_start`/`server_tool_complete`
301
+ // for `web_search_call`, but its results live inside the assistant text
302
+ // stream — emitting "anthropic-native" metadata for an OpenAI search would
303
+ // mis-label the provider and ship an empty `results` array.
304
+ const { deps, events } = createCollectorDeps("openai");
305
+ const toolUseId = "tu_openai";
306
+
307
+ await dispatchAgentEvent(state, deps, {
308
+ type: "server_tool_start",
309
+ name: "web_search",
310
+ toolUseId,
311
+ input: {},
312
+ });
313
+
314
+ await dispatchAgentEvent(state, deps, {
315
+ type: "server_tool_complete",
316
+ toolUseId,
317
+ isError: false,
318
+ });
319
+
320
+ const toolResultEvent = events.find(
321
+ (e): e is ToolResultEvent => e.type === "tool_result",
322
+ );
323
+ expect(toolResultEvent).toBeDefined();
324
+ expect(toolResultEvent?.activityMetadata).toBeUndefined();
325
+ // The back-compat `result: string` channel still fires (empty for OpenAI
326
+ // since the results are woven into the text stream, not structured).
327
+ expect(toolResultEvent?.result).toBe("");
328
+ });
329
+
330
+ test("sets errorMessage when the search errored", async () => {
331
+ const { deps, events } = createCollectorDeps();
332
+ const toolUseId = "tu_err";
333
+
334
+ await dispatchAgentEvent(state, deps, {
335
+ type: "server_tool_start",
336
+ name: "web_search",
337
+ toolUseId,
338
+ input: { query: "broken" },
339
+ });
340
+
341
+ await dispatchAgentEvent(state, deps, {
342
+ type: "server_tool_complete",
343
+ toolUseId,
344
+ isError: true,
345
+ content: [],
346
+ });
347
+
348
+ const toolResultEvent = events.find(
349
+ (e): e is ToolResultEvent => e.type === "tool_result",
350
+ );
351
+ const meta = toolResultEvent?.activityMetadata?.webSearch;
352
+ expect(meta?.errorMessage).toBe("Search failed");
353
+ expect(meta?.resultCount).toBe(0);
354
+ expect(meta?.results).toEqual([]);
355
+ expect(toolResultEvent?.isError).toBe(true);
356
+ });
357
+ });
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Tests that the in-flight activity status text emitted for `web_search`
3
+ * and `web_fetch` tool starts surfaces per-call detail (the query / domain)
4
+ * instead of a generic placeholder. Covers both the Anthropic native
5
+ * `server_tool_start` path and the non-native `tool_use` path.
6
+ */
7
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
8
+
9
+ // ── Mock platform (must precede imports that read it) ─────────────────────────
10
+ mock.module("../../util/logger.js", () => ({
11
+ getLogger: () =>
12
+ new Proxy({} as Record<string, unknown>, {
13
+ get: () => () => {},
14
+ }),
15
+ }));
16
+
17
+ mock.module("../../config/loader.js", () => ({
18
+ getConfig: () => ({
19
+ skills: {
20
+ entries: {},
21
+ load: { extraDirs: [], watch: false, watchDebounceMs: 0 },
22
+ install: { nodeManager: "npm" },
23
+ allowBundled: null,
24
+ remoteProviders: {
25
+ skillssh: { enabled: true },
26
+ clawhub: { enabled: true },
27
+ },
28
+ remotePolicy: {
29
+ blockSuspicious: true,
30
+ blockMalware: true,
31
+ maxSkillsShRisk: "medium",
32
+ },
33
+ },
34
+ }),
35
+ loadConfig: () => ({}),
36
+ }));
37
+
38
+ mock.module("../../memory/conversation-crud.js", () => ({
39
+ addMessage: () => ({ id: "mock-msg-id" }),
40
+ getMessageById: () => null,
41
+ updateMessageContent: () => {},
42
+ provenanceFromTrustContext: () => ({}),
43
+ }));
44
+
45
+ mock.module("../../memory/llm-request-log-store.js", () => ({
46
+ recordRequestLog: () => {},
47
+ backfillMessageIdOnLogs: () => {},
48
+ }));
49
+
50
+ // ── Imports (after mocks) ─────────────────────────────────────────────────────
51
+ import type {
52
+ EventHandlerDeps,
53
+ EventHandlerState,
54
+ } from "../conversation-agent-loop-handlers.js";
55
+ import {
56
+ createEventHandlerState,
57
+ dispatchAgentEvent,
58
+ formatFetchStatusText,
59
+ formatSearchStatusText,
60
+ } from "../conversation-agent-loop-handlers.js";
61
+ import type { ServerMessage } from "../message-protocol.js";
62
+
63
+ // ── Helpers ───────────────────────────────────────────────────────────────────
64
+
65
+ interface ActivityStateCapture {
66
+ phase: string;
67
+ reason: string;
68
+ scope: string;
69
+ reqId: string;
70
+ statusText?: string;
71
+ }
72
+
73
+ function createCollectorDeps(): {
74
+ deps: EventHandlerDeps;
75
+ activityStates: ActivityStateCapture[];
76
+ } {
77
+ const activityStates: ActivityStateCapture[] = [];
78
+ const deps = {
79
+ ctx: {
80
+ conversationId: "conv-status-text",
81
+ provider: { name: "anthropic" },
82
+ traceEmitter: { emit: () => {} },
83
+ streamThinking: false,
84
+ emitActivityState: (
85
+ phase: string,
86
+ reason: string,
87
+ scope: string,
88
+ reqId: string,
89
+ statusText?: string,
90
+ ) => {
91
+ activityStates.push({ phase, reason, scope, reqId, statusText });
92
+ },
93
+ markWorkspaceTopLevelDirty: () => {},
94
+ currentTurnSurfaces: [],
95
+ } as unknown as EventHandlerDeps["ctx"],
96
+ onEvent: (_msg: ServerMessage) => {},
97
+ reqId: "req-status-text",
98
+ isFirstMessage: false,
99
+ shouldGenerateTitle: false,
100
+ rlog: new Proxy({} as Record<string, unknown>, {
101
+ get: () => () => {},
102
+ }) as unknown as EventHandlerDeps["rlog"],
103
+ turnChannelContext: {
104
+ userMessageChannel: "vellum",
105
+ assistantMessageChannel: "vellum",
106
+ } as EventHandlerDeps["turnChannelContext"],
107
+ turnInterfaceContext: {
108
+ userMessageInterface: "macos",
109
+ assistantMessageInterface: "macos",
110
+ } as EventHandlerDeps["turnInterfaceContext"],
111
+ } as EventHandlerDeps;
112
+ return { deps, activityStates };
113
+ }
114
+
115
+ // ── Pure helper tests ─────────────────────────────────────────────────────────
116
+
117
+ describe("formatSearchStatusText", () => {
118
+ test("surfaces the query in quotes", () => {
119
+ expect(formatSearchStatusText("web_search", "foo")).toBe(
120
+ 'Searching "foo"',
121
+ );
122
+ });
123
+
124
+ test("truncates queries longer than 60 chars with an ellipsis", () => {
125
+ const longQuery = "a".repeat(120);
126
+ const result = formatSearchStatusText("web_search", longQuery);
127
+ // 57 chars + "..." = 60 char body, wrapped in 'Searching "..."'
128
+ expect(result).toBe(`Searching "${"a".repeat(57)}..."`);
129
+ });
130
+
131
+ test("preserves 60-char queries as-is (no truncation at the boundary)", () => {
132
+ const sixty = "a".repeat(60);
133
+ expect(formatSearchStatusText("web_search", sixty)).toBe(
134
+ `Searching "${sixty}"`,
135
+ );
136
+ });
137
+
138
+ test("falls back to the generic phrasing when the query is empty", () => {
139
+ expect(formatSearchStatusText("web_search", "")).toBe("Searching the web");
140
+ expect(formatSearchStatusText("web_search", " ")).toBe(
141
+ "Searching the web",
142
+ );
143
+ });
144
+
145
+ test("emits 'Running <tool>' for non-web_search tools", () => {
146
+ expect(formatSearchStatusText("other_tool", "x")).toBe(
147
+ "Running other_tool",
148
+ );
149
+ });
150
+ });
151
+
152
+ describe("formatFetchStatusText", () => {
153
+ test("returns the domain when given a parseable URL", () => {
154
+ expect(formatFetchStatusText("https://example.com/path")).toBe(
155
+ "Reading example.com",
156
+ );
157
+ });
158
+
159
+ test("lowercases the host", () => {
160
+ expect(formatFetchStatusText("https://EXAMPLE.COM/x")).toBe(
161
+ "Reading example.com",
162
+ );
163
+ });
164
+
165
+ test("falls back when the URL is unparseable or missing", () => {
166
+ expect(formatFetchStatusText("not a url")).toBe("Reading a page");
167
+ expect(formatFetchStatusText(undefined)).toBe("Reading a page");
168
+ expect(formatFetchStatusText(42)).toBe("Reading a page");
169
+ });
170
+ });
171
+
172
+ // ── Dispatch-path tests ───────────────────────────────────────────────────────
173
+
174
+ describe("server_tool_start status text", () => {
175
+ let state: EventHandlerState;
176
+
177
+ beforeEach(() => {
178
+ state = createEventHandlerState();
179
+ });
180
+
181
+ test("native web_search emits 'Searching \"<query>\"'", async () => {
182
+ const { deps, activityStates } = createCollectorDeps();
183
+
184
+ await dispatchAgentEvent(state, deps, {
185
+ type: "server_tool_start",
186
+ name: "web_search",
187
+ toolUseId: "tu_native_q",
188
+ input: { query: "foo" },
189
+ });
190
+
191
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
192
+ expect(toolStart?.statusText).toBe('Searching "foo"');
193
+ });
194
+
195
+ test("native web_search with long query truncates", async () => {
196
+ const { deps, activityStates } = createCollectorDeps();
197
+ const longQuery = "a".repeat(120);
198
+
199
+ await dispatchAgentEvent(state, deps, {
200
+ type: "server_tool_start",
201
+ name: "web_search",
202
+ toolUseId: "tu_native_long",
203
+ input: { query: longQuery },
204
+ });
205
+
206
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
207
+ expect(toolStart?.statusText).toBe(`Searching "${"a".repeat(57)}..."`);
208
+ });
209
+
210
+ test("native web_search with missing query falls back to the generic phrasing", async () => {
211
+ const { deps, activityStates } = createCollectorDeps();
212
+
213
+ await dispatchAgentEvent(state, deps, {
214
+ type: "server_tool_start",
215
+ name: "web_search",
216
+ toolUseId: "tu_native_empty",
217
+ input: {},
218
+ });
219
+
220
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
221
+ expect(toolStart?.statusText).toBe("Searching the web");
222
+ });
223
+ });
224
+
225
+ describe("tool_use status text (non-native)", () => {
226
+ let state: EventHandlerState;
227
+
228
+ beforeEach(() => {
229
+ state = createEventHandlerState();
230
+ });
231
+
232
+ test("web_search emits 'Searching \"<query>\"'", async () => {
233
+ const { deps, activityStates } = createCollectorDeps();
234
+
235
+ await dispatchAgentEvent(state, deps, {
236
+ type: "tool_use",
237
+ id: "tu_ws",
238
+ name: "web_search",
239
+ input: { query: "bar" },
240
+ });
241
+
242
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
243
+ expect(toolStart?.statusText).toBe('Searching "bar"');
244
+ });
245
+
246
+ test("web_fetch emits 'Reading <domain>'", async () => {
247
+ const { deps, activityStates } = createCollectorDeps();
248
+
249
+ await dispatchAgentEvent(state, deps, {
250
+ type: "tool_use",
251
+ id: "tu_wf",
252
+ name: "web_fetch",
253
+ input: { url: "https://www.nytimes.com/article" },
254
+ });
255
+
256
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
257
+ expect(toolStart?.statusText).toBe("Reading www.nytimes.com");
258
+ });
259
+
260
+ test("web_fetch with malformed url falls back to a generic phrase", async () => {
261
+ const { deps, activityStates } = createCollectorDeps();
262
+
263
+ await dispatchAgentEvent(state, deps, {
264
+ type: "tool_use",
265
+ id: "tu_wf_bad",
266
+ name: "web_fetch",
267
+ input: { url: "not-a-url" },
268
+ });
269
+
270
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
271
+ expect(toolStart?.statusText).toBe("Reading a page");
272
+ });
273
+
274
+ test("unrelated tools keep the existing 'Running <friendly>' fallback", async () => {
275
+ const { deps, activityStates } = createCollectorDeps();
276
+
277
+ await dispatchAgentEvent(state, deps, {
278
+ type: "tool_use",
279
+ id: "tu_bash",
280
+ name: "bash",
281
+ input: { command: "ls" },
282
+ });
283
+
284
+ const toolStart = activityStates.find((s) => s.reason === "tool_use_start");
285
+ expect(toolStart?.statusText).toBe("Running command");
286
+ });
287
+ });