@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Schedules the onboarding "Day 2 Check-in" calendar event server-side.
3
+ *
4
+ * Flow:
5
+ * 1. Resolve the Google OAuth connection, requiring the calendar.events scope.
6
+ * No connection / missing scope → a `scheduled: false` result (never an
7
+ * error): the web caller treats this as a best-effort skip.
8
+ * 2. List tomorrow's timed events in the 8am–8pm window (user's timezone) to
9
+ * derive busy intervals. Uses events.list rather than freeBusy.query
10
+ * because the onboarding grant is the narrow `calendar.events` scope, which
11
+ * authorizes events.list/insert but not freeBusy.query.
12
+ * 3. Choose the first open 15-minute slot (12pm–5pm, widening to 8am–8pm).
13
+ * 4. Create the event with the locked title + HTML description, sendUpdates=all.
14
+ *
15
+ * Authenticates via `resolveOAuthConnection("google")` + `connection.request()`,
16
+ * the same path the calendar watcher uses — no skill subprocess.
17
+ */
18
+
19
+ import { canonicalizeTimeZone } from "../daemon/date-context.js";
20
+ import type { OAuthConnection } from "../oauth/connection.js";
21
+ import { resolveOAuthConnection } from "../oauth/connection-resolver.js";
22
+ import { getLogger } from "../util/logger.js";
23
+ import {
24
+ buildCheckinDescription,
25
+ buildCheckinTitle,
26
+ checkinAvailabilityWindow,
27
+ type CheckinNames,
28
+ chooseCheckinSlot,
29
+ extractBusyFromEvents,
30
+ type GcalEvent,
31
+ } from "./checkin-event.js";
32
+
33
+ const log = getLogger("onboarding:schedule-checkin");
34
+
35
+ const GOOGLE_CALENDAR_BASE_URL = "https://www.googleapis.com/calendar/v3";
36
+ const GOOGLE_CALENDAR_EVENTS_SCOPE =
37
+ "https://www.googleapis.com/auth/calendar.events";
38
+ /** Calendar shares the Google OAuth connection with Gmail. */
39
+ const GOOGLE_PROVIDER = "google";
40
+ const PRIMARY_CALENDAR_ID = "primary";
41
+
42
+ export interface ScheduleCheckinInput extends CheckinNames {
43
+ /** IANA timezone reported by the client (e.g. "America/New_York"). */
44
+ timeZone?: string;
45
+ /** Override "now" for deterministic tests. Defaults to the current time. */
46
+ nowMs?: number;
47
+ }
48
+
49
+ export type ScheduleCheckinResult =
50
+ | {
51
+ scheduled: true;
52
+ eventId: string;
53
+ htmlLink: string | null;
54
+ /** Event start, ISO 8601 (UTC). */
55
+ start: string;
56
+ /** Event end, ISO 8601 (UTC). */
57
+ end: string;
58
+ timeZone: string;
59
+ }
60
+ | {
61
+ scheduled: false;
62
+ /**
63
+ * Why nothing was booked. `calendar_unavailable` covers both "not
64
+ * connected" and "calendar scope not granted" — the client surfaces a
65
+ * single best-effort skip either way.
66
+ */
67
+ reason: "calendar_unavailable";
68
+ };
69
+
70
+ interface EventsListResponse {
71
+ items?: GcalEvent[];
72
+ }
73
+
74
+ /** Subset of the events.insert response we surface back to the caller. */
75
+ interface CreatedEvent {
76
+ id?: string;
77
+ htmlLink?: string;
78
+ }
79
+
80
+ /**
81
+ * Schedule the Day 2 check-in. Resolves to a `scheduled: false` result when no
82
+ * calendar is connected; throws only on unexpected Calendar API failures (the
83
+ * route handler maps those to a 5xx, the web caller swallows them).
84
+ */
85
+ export async function scheduleOnboardingCheckin(
86
+ input: ScheduleCheckinInput,
87
+ ): Promise<ScheduleCheckinResult> {
88
+ const timeZone =
89
+ canonicalizeTimeZone(input.timeZone) ??
90
+ // Fall back to the daemon host timezone when the client didn't report one.
91
+ Intl.DateTimeFormat().resolvedOptions().timeZone ??
92
+ "UTC";
93
+ const nowMs = input.nowMs ?? Date.now();
94
+
95
+ let connection: OAuthConnection;
96
+ try {
97
+ connection = await resolveOAuthConnection(GOOGLE_PROVIDER, {
98
+ requiredScopes: [GOOGLE_CALENDAR_EVENTS_SCOPE],
99
+ });
100
+ } catch (err) {
101
+ // No active connection or the calendar scope wasn't granted — skip quietly.
102
+ log.info(
103
+ { err: err instanceof Error ? err.message : String(err) },
104
+ "Check-in skipped: Google Calendar not available",
105
+ );
106
+ return { scheduled: false, reason: "calendar_unavailable" };
107
+ }
108
+
109
+ const { timeMinMs, timeMaxMs } = checkinAvailabilityWindow(nowMs, timeZone);
110
+
111
+ const eventsResp = await connection.request({
112
+ method: "GET",
113
+ path: `/calendars/${encodeURIComponent(PRIMARY_CALENDAR_ID)}/events`,
114
+ query: {
115
+ timeMin: new Date(timeMinMs).toISOString(),
116
+ timeMax: new Date(timeMaxMs).toISOString(),
117
+ // Expand recurring events into instances so each occurrence is a concrete
118
+ // busy interval in the window.
119
+ singleEvents: "true",
120
+ orderBy: "startTime",
121
+ maxResults: "250",
122
+ },
123
+ baseUrl: GOOGLE_CALENDAR_BASE_URL,
124
+ headers: { "Content-Type": "application/json" },
125
+ });
126
+ if (eventsResp.status < 200 || eventsResp.status >= 300) {
127
+ throw new Error(
128
+ `Calendar events.list ${eventsResp.status}: ${stringifyBody(eventsResp.body)}`,
129
+ );
130
+ }
131
+
132
+ const busy = extractBusyFromEvents(
133
+ (eventsResp.body as EventsListResponse).items ?? [],
134
+ );
135
+ const slot = chooseCheckinSlot(nowMs, timeZone, busy);
136
+
137
+ const uuid = crypto.randomUUID();
138
+ const eventResp = await connection.request({
139
+ method: "POST",
140
+ path: `/calendars/${encodeURIComponent(PRIMARY_CALENDAR_ID)}/events`,
141
+ query: { sendUpdates: "all" },
142
+ baseUrl: GOOGLE_CALENDAR_BASE_URL,
143
+ headers: { "Content-Type": "application/json" },
144
+ body: {
145
+ summary: buildCheckinTitle(input),
146
+ description: buildCheckinDescription(uuid),
147
+ start: {
148
+ dateTime: new Date(slot.startMs).toISOString(),
149
+ timeZone,
150
+ },
151
+ end: {
152
+ dateTime: new Date(slot.endMs).toISOString(),
153
+ timeZone,
154
+ },
155
+ },
156
+ });
157
+ if (eventResp.status < 200 || eventResp.status >= 300) {
158
+ throw new Error(
159
+ `Calendar event create ${eventResp.status}: ${stringifyBody(eventResp.body)}`,
160
+ );
161
+ }
162
+
163
+ const event = eventResp.body as CreatedEvent;
164
+ if (!event.id) {
165
+ throw new Error("Calendar event create returned no event id");
166
+ }
167
+
168
+ log.info(
169
+ {
170
+ eventId: event.id,
171
+ start: new Date(slot.startMs).toISOString(),
172
+ window: slot.window,
173
+ timeZone,
174
+ },
175
+ "Scheduled onboarding Day 2 check-in",
176
+ );
177
+
178
+ return {
179
+ scheduled: true,
180
+ eventId: event.id,
181
+ htmlLink: event.htmlLink ?? null,
182
+ start: new Date(slot.startMs).toISOString(),
183
+ end: new Date(slot.endMs).toISOString(),
184
+ timeZone,
185
+ };
186
+ }
187
+
188
+ function stringifyBody(body: unknown): string {
189
+ return typeof body === "string" ? body : JSON.stringify(body ?? "");
190
+ }
@@ -9,7 +9,7 @@ import type {
9
9
 
10
10
  // Use a tiny timeout so the setTimeout branch fires quickly in tests
11
11
  const mockConfig = {
12
- timeouts: { permissionTimeoutSec: 0.05 },
12
+ timeouts: { questionResponseTimeoutSec: 0.05 },
13
13
  };
14
14
  // Preserve every other export from the real config/loader so other
15
15
  // tests in the same `bun test` run (which share module-level mocks)
@@ -156,9 +156,12 @@ export interface QuestionBatchMetadata {
156
156
  * the whole batch to `/v1/question-response` when the user is done — no
157
157
  * per-question accumulator, no partial state machine.
158
158
  *
159
- * Timeout reuses `getConfig().timeouts.permissionTimeoutSec` (default 5 min) —
160
- * questions are user-prompts in the same UX family as permission prompts and
161
- * secret prompts, so they share the same idle-timeout knob.
159
+ * The idle timeout is a backstop (`getConfig().timeouts.questionResponseTimeoutSec`,
160
+ * default 30 min), not the primary way a prompt is dismissed. An interactive
161
+ * user who moves on instead of answering enqueues another message, which
162
+ * supersedes the open prompt at the enqueue handler (see conversation-routes.ts);
163
+ * a non-interactive turn resolves immediately at the tool. The backstop only
164
+ * fires when a prompt is left open with no response and no follow-up message.
162
165
  */
163
166
  export class QuestionPrompter {
164
167
  async prompt(params: QuestionPromptParams): Promise<QuestionPromptResult> {
@@ -199,7 +202,7 @@ export class QuestionPrompter {
199
202
  const requestId = uuid();
200
203
 
201
204
  return new Promise<QuestionPromptResult>((resolve, reject) => {
202
- const timeoutMs = getConfig().timeouts.permissionTimeoutSec * 1000;
205
+ const timeoutMs = getConfig().timeouts.questionResponseTimeoutSec * 1000;
203
206
 
204
207
  // Closure-scoped idempotency guard. Every resolution path (timeout,
205
208
  // abort, route resolution via `rpcResolve`/`rpcReject`) routes through
@@ -130,12 +130,12 @@ export type {
130
130
  } from "../runtime/assistant-event-hub.js";
131
131
  export { assistantEventHub } from "../runtime/assistant-event-hub.js";
132
132
  export { getModelProfiles } from "./model-profiles.js";
133
- // Check whether a profile's resolved model can process image input. Resolves
134
- // the effective (provider, model) by merging over the workspace default and
135
- // inferring the provider for model-only profiles, then looks up the model
136
- // catalog's `supportsVision` flag. Handles mix profiles (true if any arm
137
- // supports vision). Fail-open for unknown models. Pair with
138
- // `getModelProfiles()` to inspect the active or candidate profiles.
133
+ // Check whether a model or profile can process image input. Accepts a concrete
134
+ // model id, a profile key, or a `ModelProfileInfo`; a bare string is resolved
135
+ // as a model id first and then as a profile key. Profile resolution merges over
136
+ // the workspace default and infers the provider for model-only profiles, then
137
+ // looks up the model catalog's `supportsVision` flag (mix profiles are
138
+ // vision-capable if any arm is). Returns false when nothing resolves.
139
139
  export { doesSupportVision } from "./vision-support.js";
140
140
  // Resolve a provider for a call site (optionally overriding the profile) so a
141
141
  // plugin can run inference through the workspace's configured profiles and
@@ -72,15 +72,13 @@ export type PluginHookFn<TCtx = unknown> = (
72
72
  // ─── Init context ────────────────────────────────────────────────────────────
73
73
 
74
74
  /**
75
- * Context passed to `Plugin.init()` during bootstrap. Carries resolved
76
- * config/credentials, a pino-compatible logger scoped to the plugin, a
77
- * per-plugin writable data directory, and the assistant's version metadata.
75
+ * Context passed to `Plugin.init()` during bootstrap. Carries the resolved
76
+ * config, a pino-compatible logger scoped to the plugin, a per-plugin
77
+ * writable data directory, and the assistant's version metadata.
78
78
  */
79
79
  export interface PluginInitContext {
80
80
  /** Parsed config for this plugin (may be `unknown` until the manifest validates). */
81
81
  config: unknown;
82
- /** Resolved credential values keyed by the entries of `manifest.requiresCredential`. */
83
- credentials: Record<string, string>;
84
82
  /** Pino-compatible child logger bound to `{ plugin: <name> }`. */
85
83
  logger: PluginLogger;
86
84
  /** Absolute path to `<workspaceDir>/plugins-data/<plugin>/` (created by bootstrap). */
@@ -84,16 +84,16 @@ describe("doesSupportVision", () => {
84
84
  expect(doesSupportVision(profile("text-profile"))).toBe(false);
85
85
  });
86
86
 
87
- test("fails open (true) for an unknown profile key not in config", () => {
87
+ test("returns false for an unknown profile key not in config", () => {
88
88
  setMockConfig({});
89
- expect(doesSupportVision(profile("nonexistent"))).toBe(true);
89
+ expect(doesSupportVision(profile("nonexistent"))).toBe(false);
90
90
  });
91
91
 
92
- test("fails open (true) for an unknown provider/model pair", () => {
92
+ test("returns false for an unknown provider/model pair", () => {
93
93
  setMockConfig({
94
94
  "unknown-model": { provider: "unknown-provider", model: "unknown-model" },
95
95
  });
96
- expect(doesSupportVision(profile("unknown-model"))).toBe(true);
96
+ expect(doesSupportVision(profile("unknown-model"))).toBe(false);
97
97
  });
98
98
 
99
99
  test("inherits provider from llm.default when profile only sets model", () => {
@@ -147,3 +147,27 @@ describe("doesSupportVision", () => {
147
147
  expect(doesSupportVision(profile("mix-profile"))).toBe(false);
148
148
  });
149
149
  });
150
+
151
+ describe("doesSupportVision with a bare string", () => {
152
+ test("returns true for a known vision-capable model id", () => {
153
+ expect(doesSupportVision("claude-opus-4-6")).toBe(true);
154
+ });
155
+
156
+ test("returns false for a known text-only model id", () => {
157
+ expect(doesSupportVision("accounts/fireworks/models/glm-5p2")).toBe(false);
158
+ });
159
+
160
+ test("falls back to resolving the string as a profile key", () => {
161
+ // "vision-profile" is not a catalog model id, so it resolves as a profile
162
+ // key → anthropic/claude-opus-4-6 (vision-capable).
163
+ setMockConfig({
164
+ "vision-profile": { provider: "anthropic", model: "claude-opus-4-6" },
165
+ });
166
+ expect(doesSupportVision("vision-profile")).toBe(true);
167
+ });
168
+
169
+ test("returns false for a string that is neither a model nor a profile", () => {
170
+ setMockConfig({});
171
+ expect(doesSupportVision("some-unknown-string")).toBe(false);
172
+ });
173
+ });
@@ -3,10 +3,17 @@
3
3
  *
4
4
  * A plugin that gates image processing on vision capability (e.g. an
5
5
  * image-to-text fallback for text-only models) calls {@link doesSupportVision}
6
- * instead of hardcoding model names. The function resolves the effective
7
- * (provider, model) for a profile — merging with `llm.default` to fill gaps,
8
- * inferring the provider for model-only profiles via the catalog and then
9
- * looks up `supportsVision` in the model catalog.
6
+ * instead of hardcoding model names. One entry point serves both shapes a
7
+ * caller might hold:
8
+ * - a concrete model id (e.g. the provider-reported model that just ran), and
9
+ * - a profile either a {@link ModelProfileInfo} or a bare profile key —
10
+ * resolved through `llm.profiles` to an effective `(provider, model)`.
11
+ *
12
+ * A bare string is tried as a model id first and then as a profile key, so the
13
+ * two callers share one function. Resolution returns `false` when nothing
14
+ * resolves (rather than failing open): a consumer gating an image→text
15
+ * fallback wants an unknown model treated as "can't show images" — caption it —
16
+ * over silently shipping a raw image to a provider that may reject it.
10
17
  */
11
18
 
12
19
  import { getConfig } from "../config/loader.js";
@@ -17,46 +24,74 @@ import {
17
24
  import type { ModelProfileInfo } from "./types.js";
18
25
 
19
26
  /**
20
- * Whether a profile's resolved model can process image input.
27
+ * Whether the given model or profile can process image input.
21
28
  *
22
- * Resolution mirrors the host's call-site resolver:
23
- * - The profile's `(provider, model)` fields are merged over `llm.default` so
24
- * a profile that only sets `model` (or only `provider`) inherits the other
25
- * from the workspace default.
26
- * - When `provider` is still missing but `model` is a known catalog model,
27
- * the provider is inferred via `getCatalogProviderForModel` (same logic as
28
- * the resolver's `withImpliedProviderForKnownModel`).
29
- * - For a mix profile, returns `true` if any constituent arm supports vision
30
- * (the mix can route to it) and `false` only if every arm is text-only.
31
- * - Unknown `(provider, model)` pairs default to `true` (fail-open), matching
32
- * the config GET route's `enrichProfilesWithVisionFlag`.
29
+ * `modelOrProfile` may be a concrete model id, a profile key, or a
30
+ * {@link ModelProfileInfo}. A bare string is resolved as a model id first and,
31
+ * failing that, as a profile key. Returns `false` when nothing resolves.
32
+ */
33
+ export function doesSupportVision(
34
+ modelOrProfile: ModelProfileInfo | string,
35
+ ): boolean {
36
+ if (typeof modelOrProfile === "string") {
37
+ // Concrete model id first, then fall back to treating it as a profile key.
38
+ return (
39
+ modelVision(modelOrProfile) ?? profileVision(modelOrProfile) ?? false
40
+ );
41
+ }
42
+ return profileVision(modelOrProfile.key) ?? false;
43
+ }
44
+
45
+ /**
46
+ * Catalog vision flag for a concrete model id, or `undefined` when the catalog
47
+ * doesn't know the model. The same model id carries the same capability under
48
+ * every provider that offers it, so the first catalog match wins.
33
49
  */
34
- export function doesSupportVision(profile: ModelProfileInfo): boolean {
50
+ function modelVision(model: string): boolean | undefined {
51
+ for (const provider of PROVIDER_CATALOG) {
52
+ const catalogModel = provider.models.find((m) => m.id === model);
53
+ if (catalogModel != null) return catalogModel.supportsVision ?? false;
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ /**
59
+ * Resolve a profile key through `llm.profiles` to its vision capability, or
60
+ * `undefined` when the key is unknown or resolves to a model the catalog
61
+ * doesn't know. A mix profile resolves to `true` if any arm supports vision
62
+ * (the mix can route to it) and `false` only once every arm is a known
63
+ * text-only model.
64
+ */
65
+ function profileVision(profileKey: string): boolean | undefined {
35
66
  const { llm } = getConfig();
36
- const entry = llm.profiles[profile.key];
37
- if (entry == null) return true;
67
+ const entry = llm.profiles[profileKey];
68
+ if (entry == null) return undefined;
38
69
 
39
- // Mix: fail-open if any arm supports vision.
40
70
  if (entry.mix != null) {
41
- return entry.mix.some((arm) => {
71
+ let sawUnknown = false;
72
+ for (const arm of entry.mix) {
42
73
  const armEntry = llm.profiles[arm.profile];
43
- if (armEntry == null) return true;
44
- return resolveEntrySupportsVision(armEntry, llm);
45
- });
74
+ const armVision =
75
+ armEntry == null ? undefined : resolveEntryVision(armEntry, llm);
76
+ if (armVision === true) return true;
77
+ if (armVision == null) sawUnknown = true;
78
+ }
79
+ return sawUnknown ? undefined : false;
46
80
  }
47
81
 
48
- return resolveEntrySupportsVision(entry, llm);
82
+ return resolveEntryVision(entry, llm);
49
83
  }
50
84
 
51
85
  /**
52
86
  * Resolve whether a concrete (non-mix) profile entry supports vision by
53
- * merging its fields over `llm.default` and inferring the provider when
54
- * only the model is set.
87
+ * merging its fields over `llm.default` and inferring the provider when only
88
+ * the model is set. Returns `undefined` when the effective `(provider, model)`
89
+ * can't be determined or isn't in the catalog.
55
90
  */
56
- function resolveEntrySupportsVision(
91
+ function resolveEntryVision(
57
92
  entry: { provider?: string; model?: string },
58
93
  llm: { default?: { provider?: string; model?: string } },
59
- ): boolean {
94
+ ): boolean | undefined {
60
95
  const provider = entry.provider ?? llm.default?.provider;
61
96
  const model = entry.model ?? llm.default?.model;
62
97
 
@@ -67,12 +102,12 @@ function resolveEntrySupportsVision(
67
102
  (typeof model === "string" ? getCatalogProviderForModel(model) : undefined);
68
103
 
69
104
  if (typeof effectiveProvider !== "string" || typeof model !== "string") {
70
- return true; // fail-open
105
+ return undefined;
71
106
  }
72
107
 
73
108
  const catalogProvider = PROVIDER_CATALOG.find(
74
109
  (p) => p.id === effectiveProvider,
75
110
  );
76
111
  const catalogModel = catalogProvider?.models.find((m) => m.id === model);
77
- return catalogModel?.supportsVision ?? true;
112
+ return catalogModel?.supportsVision;
78
113
  }
@@ -8,12 +8,26 @@ let sendMessageArgs: Record<string, unknown> | null = null;
8
8
  let responseText = "Use a channel-based worker pool; drain on shutdown.";
9
9
  let sendMessageError: Error | null = null;
10
10
  let providerResolves = true;
11
+ let providerSupportsWeb = false;
12
+ let streamDeltas: string[] = [];
13
+ let streamEvents: Array<Record<string, unknown>> = [];
11
14
 
12
15
  const fakeProvider = {
13
16
  name: "mock-advisor-provider",
17
+ get supportsNativeWebSearch() {
18
+ return providerSupportsWeb;
19
+ },
14
20
  async sendMessage(messages: unknown, options: unknown) {
15
21
  sendMessageArgs = { messages, options } as Record<string, unknown>;
16
22
  if (sendMessageError) throw sendMessageError;
23
+ const onEvent = (
24
+ options as { onEvent?: (e: Record<string, unknown>) => void }
25
+ ).onEvent;
26
+ if (onEvent) {
27
+ // Activity (search/thinking) streams before the final advice text.
28
+ for (const ev of streamEvents) onEvent(ev);
29
+ for (const text of streamDeltas) onEvent({ type: "text_delta", text });
30
+ }
17
31
  return {
18
32
  content: [{ type: "text", text: responseText }],
19
33
  model: "mock-model",
@@ -29,6 +43,14 @@ mock.module("../../../../providers/provider-send-message.js", () => ({
29
43
  getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
30
44
  }));
31
45
 
46
+ // Keep the tool tests focused on the consult wiring: stub the context pack so
47
+ // they don't reach into the registry / workspace / memory sources (those have
48
+ // their own coverage). The consult itself never imports this module.
49
+ mock.module("../context-pack.js", () => ({
50
+ buildAdvisorContext: async () => null,
51
+ deriveRecallQuery: () => null,
52
+ }));
53
+
32
54
  const { consultAdvisor } = await import("../consult.js");
33
55
  const advisorTool = (await import("../tools/advisor.js")).default;
34
56
  const { recordSystemPrompt, recordMessages, resetAdvisorStateForTests } =
@@ -49,6 +71,9 @@ beforeEach(() => {
49
71
  responseText = "Use a channel-based worker pool; drain on shutdown.";
50
72
  sendMessageError = null;
51
73
  providerResolves = true;
74
+ providerSupportsWeb = false;
75
+ streamDeltas = [];
76
+ streamEvents = [];
52
77
  resetAdvisorStateForTests();
53
78
  });
54
79
 
@@ -100,6 +125,104 @@ describe("consultAdvisor", () => {
100
125
  expect(options.systemPrompt).toContain("You are a coding agent.");
101
126
  });
102
127
 
128
+ test("stays tool-less when the provider has no native web search", async () => {
129
+ providerSupportsWeb = false;
130
+ await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
131
+ const options = sendMessageArgs?.options as { tools?: unknown };
132
+ expect(options.tools).toBeUndefined();
133
+ expect(optionConfig().tool_choice).toEqual({ type: "none" });
134
+ });
135
+
136
+ test("enables native web search when the provider supports it", async () => {
137
+ providerSupportsWeb = true;
138
+ await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
139
+
140
+ const options = sendMessageArgs?.options as {
141
+ tools?: Array<{ name: string }>;
142
+ };
143
+ expect(options.tools?.map((t) => t.name)).toEqual(["web_search"]);
144
+ // tool_choice must not be `none`, or the provider suppresses its server tool.
145
+ expect(optionConfig().tool_choice).toEqual({ type: "auto" });
146
+ });
147
+
148
+ test("streams web-search activity to onText, not just the final advice", async () => {
149
+ providerSupportsWeb = true;
150
+ streamEvents = [
151
+ { type: "server_tool_start", name: "web_search", toolUseId: "s1", input: {} },
152
+ {
153
+ type: "server_tool_complete",
154
+ toolUseId: "s1",
155
+ isError: false,
156
+ resolvedInput: { query: "vellum streaming" },
157
+ },
158
+ ];
159
+ streamDeltas = ["Here is ", "the advice."];
160
+ const chunks: string[] = [];
161
+
162
+ await consultAdvisor({
163
+ systemPrompt: null,
164
+ messages: [userMsg("hi")],
165
+ onText: (c) => chunks.push(c),
166
+ });
167
+
168
+ const joined = chunks.join("");
169
+ // The drawer isn't silent during the search prefix...
170
+ expect(joined).toContain("Searching the web");
171
+ expect(joined).toContain("Searched: vellum streaming");
172
+ // ...and the advice text still streams.
173
+ expect(joined).toContain("Here is the advice.");
174
+ });
175
+
176
+ test("surfaces a failure note (not 'Searched') when a web search errors", async () => {
177
+ providerSupportsWeb = true;
178
+ streamEvents = [
179
+ { type: "server_tool_start", name: "web_search", toolUseId: "s1", input: {} },
180
+ {
181
+ type: "server_tool_complete",
182
+ toolUseId: "s1",
183
+ isError: true,
184
+ errorCode: "query_too_long",
185
+ resolvedInput: { query: "an overly long query" },
186
+ },
187
+ ];
188
+ streamDeltas = ["Proceeding without search."];
189
+ const chunks: string[] = [];
190
+
191
+ await consultAdvisor({
192
+ systemPrompt: null,
193
+ messages: [userMsg("hi")],
194
+ onText: (c) => chunks.push(c),
195
+ });
196
+
197
+ const joined = chunks.join("");
198
+ expect(joined).toContain("Web search failed");
199
+ expect(joined).not.toContain("🔎 Searched:");
200
+ // The consult still continues and streams its guidance.
201
+ expect(joined).toContain("Proceeding without search.");
202
+ });
203
+
204
+ test("streams the model's reasoning summary to onText", async () => {
205
+ streamEvents = [{ type: "thinking_delta", thinking: "weighing tradeoffs" }];
206
+ const chunks: string[] = [];
207
+ await consultAdvisor({
208
+ systemPrompt: null,
209
+ messages: [userMsg("hi")],
210
+ onText: (c) => chunks.push(c),
211
+ });
212
+ expect(chunks.join("")).toContain("weighing tradeoffs");
213
+ });
214
+
215
+ test("embeds the runtime context in the advisor system prompt", async () => {
216
+ await consultAdvisor({
217
+ systemPrompt: "You are a coding agent.",
218
+ messages: [userMsg("hi")],
219
+ runtimeContext: "## Available tools\n- bash — run commands",
220
+ });
221
+ const options = sendMessageArgs?.options as { systemPrompt: string };
222
+ expect(options.systemPrompt).toContain("<agent_runtime_context>");
223
+ expect(options.systemPrompt).toContain("- bash — run commands");
224
+ });
225
+
103
226
  test("soft-fails when no provider is configured", async () => {
104
227
  providerResolves = false;
105
228
  const advice = await consultAdvisor({
@@ -123,6 +246,29 @@ describe("consultAdvisor", () => {
123
246
  });
124
247
  expect(advice).toContain("no guidance");
125
248
  });
249
+
250
+ test("streams the model's text deltas to `onText` as it generates", async () => {
251
+ streamDeltas = ["Use a ", "channel-based ", "worker pool."];
252
+ const chunks: string[] = [];
253
+
254
+ const advice = await consultAdvisor({
255
+ systemPrompt: null,
256
+ messages: [userMsg("hi")],
257
+ onText: (c) => chunks.push(c),
258
+ });
259
+
260
+ // Each visible delta is forwarded live...
261
+ expect(chunks).toEqual(["Use a ", "channel-based ", "worker pool."]);
262
+ // ...and the complete guidance is still returned.
263
+ expect(advice).toBe(responseText);
264
+ });
265
+
266
+ test("registers no `onEvent` sink when `onText` is absent", async () => {
267
+ streamDeltas = ["x"];
268
+ await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
269
+ const options = sendMessageArgs?.options as { onEvent?: unknown };
270
+ expect(options.onEvent).toBeUndefined();
271
+ });
126
272
  });
127
273
 
128
274
  describe("advisor tool.execute", () => {
@@ -150,4 +296,19 @@ describe("advisor tool.execute", () => {
150
296
  expect(result?.content).toContain("advisor unavailable");
151
297
  expect(result?.content).toContain("kaboom");
152
298
  });
299
+
300
+ test("streams the consult live via `ctx.onOutput`", async () => {
301
+ recordMessages("c3", [userMsg("hi")]);
302
+ streamDeltas = ["plan: ", "do X"];
303
+ const out: string[] = [];
304
+
305
+ const result = await advisorTool.execute?.({}, {
306
+ conversationId: "c3",
307
+ onOutput: (c: string) => out.push(c),
308
+ } as never);
309
+
310
+ expect(out).toEqual(["plan: ", "do X"]);
311
+ expect(result?.isError).toBe(false);
312
+ expect(result?.content).toBe(responseText);
313
+ });
153
314
  });